اشتراکها
من این تغییرات رو اعمال کردم
ولی بعد از دومین بار اجرا، توی ایونتها این خطارو دارم
به نظرتون جایی از کد رو اشتباه نوشتم؟
public static Task RunAndDispose(Func<Task> action) { try { action(); } finally { System.Diagnostics.Trace.WriteLine("Finaly"); if (!HttpContextLifecycle.HasContext()) { new HybridLifecycle().FindCache(null).DisposeAndClear(); } } return Task.FromResult(0); }
و متد RunAsync
public override async Task RunAsync() { await SemaphoreSlim.WaitAsync(); try { if (this.IsShuttingDown || this.Pause) return; await IoCWrapper.RunAndDispose(async () => { var draftsService = IoCWrapper.GetInstance<IBlogPostDraftsService>(); await draftsService.RunConvertDraftsToPostsJobAsync(); }); } finally { SemaphoreSlim.Release(); } }
ActivatedEventTimeDurationThread Activated Historical Code ContextException thrown: 'System.ObjectDisposedException' in mscorlib.dll ("The ObjectContext instance has been disposed and can no longer be used for operations that require a connection.") Exception thrown: 'System.ObjectDisposedException' in mscorlib.dll ("The ObjectContext instance has been disposed and can no longer be used for operations that require a connection.") Hyperlink: Activate Historical Debugging346.49s[14880] Worker Thread
به نظرتون جایی از کد رو اشتباه نوشتم؟
یکی دیگر از قابلیتهای جذاب نسخهی جدید سیشارپ، عملگر nameof است. هدف اصلی آن ارائه کدهایی با قابلیت Refactoring بهتر است؛ زیرا به جای نوشتن نام فیلدها و یا متدها در صورت نیاز به صورت hard-coded، میتوانیم از این عملگر استفاده کنیم. به عنوان مثال در زمان صدور استثناءیی از نوع ArgumentNullException باید نام آرگومان را به سازندهی این کلاس پاس دهیم. متاسفانه یکی از مشکلاتی که با رشتهها در حالت کلی وجود دارد این است که امکان دیباگ در زمان کامپایل را از دست خواهیم داد و با تغییر هر المنت، تغییرات به صورت خودکار به رشته پاس داده شده، به سازندهی کلاس ArgumentNullException اعمال نخواهد شد:
اگر ReSharper را نصب کرده باشید، به شما پیشنهاد میدهد که از nameof به جای یک رشتهی جادویی (magic string) استفاده نمائید:
اما با کمک عملگر nameof میتوانیم قسمت فراخوانی متد OnPropertyChanged را به اینصورت نیز بازنویسی کنیم:
برای آزمایش عملگر nameof میتوانیم یک تست را در حالتهای زیر بنویسیم:
public static void DoWork(string name) { if (name == null) { throw new ArgumentNullException("name"); } }
اما با استفاده از عملگر nameof، کد امنتری را خواهیم داشت؛ زیرا همیشه نام واقعی آرگومان به سازندهی کلاس ArgumentNullException پاس داده میشود:
public static void DoWork(string name) { if (name == null) { throw new ArgumentNullException(nameof(name)); } }
یک مثال دیگر میتواند در زمان فراخوانی رخدادهای مربوط به OnPropertyChanged باشد. در اینجا باید نام خصوصیتی را که تغییر یافته است، به آن پاس دهیم:
public string Name { get { return _name; } set { _name = value; OnPropertyChanged("Name"); } }
OnPropertyChanged(nameof(Name));
ممکن است عنوان کنید قبلاً در سیشارپ 5 هم میتوانستیم از ویژگی [CallerMemberName] استفاده کنیم، پس دیگر نیازی به استفاده از عملگر nameof نخواهد بود. اما تفاوت کلیدی این است که CallerMemberName در زمان اجرا نام فیلد فراخوان را دریافت میکند (run time)، در حالیکه با استفاده از عملگر nameof میتوانید در زمان کامپایل به نام فیلد دسترسی داشته باشید (compile time).
محدودیتهای عملگر nameof
این عملگر حالتهایی را که مشاهده میکنید، فعلاً پشتیبانی نخواهد کرد:
nameof(f()); // where f is a method - you could use nameof(f) instead nameof(c._Age); // where c is a different class and _Age is private. Nameof can't break accessor rules. nameof(List<>); // List<> isn't valid C# anyway, so this won't work nameof(default(List<int>)); // default returns an instance, not a member nameof(int); // int is a keyword, not a member- you could do nameof(Int32) nameof(x[2]); // returns an instance using an indexer, so not a member nameof("hello"); // a string isn't a member nameof(1 + 2); // an int isn't a member
همانطور که مشاهده میکنید، همهی حالتهای فوق با موفقیت پاس شدهاند.
راه حل دیگر: استفاده از روش Containment بجای Inheritance
public class UserModel { [MaxLength(200)] [Display(Name = "Full name")] [Required] public string Name { get; set; } } public class UserModalViewModel { public UserModel Model { get; set; } public bool IsAdmin { get; set; } public IReadonlyList<lookupitem> Roles { get; set; } }
اکشن متد متناظر با درخواست GET
[HttpGet] public async Task<IActionResult> Edit(int id) { var user = await _service.FindAsync(id); //return Maybe<UserModel> if (!user.HasValue) { return NotFound(); } // prepare model var model = new UserModalViewModel { Model = user.Value, IsAdmin = true, Roles = await _lookupService.ReadRolesAsync() }; return View(model); }
اکشن متد متناظر با درخواست POST
[HttPost] public async Task<IActionResult> Edit([Bind(Prefix = "Model")] UserModel model) { //todo: check ModelState and save model await _service.EditAsync(model); }
مطالب دورهها
جلوگیری از deadlock در برنامههای async
توضیح مطلب جاری نیاز به یک مثال دارد. به همین جهت یک برنامهی WinForms یا WPF را آغاز کنید (تفاوتی نمیکند). سپس یک دکمه و یک برچسب را در صفحه قرار دهید. در ادامه کدهای فرم را به نحو ذیل تغییر دهید.
این کدها برای کامپایل نیاز به نصب بستهی
و همچنین افزودن ارجاعی به اسمبلی استاندارد System.Net.Http نیز دارند.
در اینجا قصد داریم اطلاعات JSON دریافتی را در یک TextBox نمایش دهیم. کاری که انجام شده، فراخوانی متد async ایی است به نام GetJsonAsync و سپس استفاده از خاصیت Result این Task برای صبر کردن تا پایان عملیات.
اگر برنامه را اجرا کنید و بر روی دکمهی دریافت اطلاعات کلیک نمائید، برنامه قفل خواهد کرد. چرا؟
البته تفاوتی هم نمیکند که این یک برنامهی دسکتاپ است یا یک برنامهی وب. در هر دو حالت یک deadlock کامل را مشاهده خواهید کرد.
علت بروز deadlock در کدهای async چیست؟
همواره نتیجهی await، در context فراخوان آن بازگشت داده میشود. اگر برنامهی دسکتاپ است، این context همان ترد اصلی UI برنامه میباشد و اگر برنامهی وب است، این context، زمینهی درخواست در حال پردازش میباشد.
خاصیت Result و یا استفاده از متد Wait یک Task، به صورت همزمان عمل میکنند و نه غیرهمزمان. متد GetJsonAsync یک Task ناتمام را که فراخوان آن باید جهت پایاناش صبر کند، بازگشت میدهد. سپس در همینجا کد فراخوان، تردجاری را توسط فراخوانی خاصیت Result قفل میکند. متد GetJsonAsync منتظر خواهد ایستاد تا این ترد آزاد شده و بتواند به کارش که بازگردان نتیجهی عملیات به context جاری است، ادامه دهد.
به عبارتی، کدهای async منتظر پایان کار Result هستند تا نتیجه را بازگردانند. در همین لحظه کدهای همزمان برنامه نیز منتظر کدهای async هستند تا خاتمه یابند. نتیجهی کار یک deadlock است.
روشهای جلوگیری از deadlock در کدهای async؟
الف) در مورد متد ConfigureAwait در قسمتهای قبل بحث شد و به عنوان یک best practice مطرح است:
با استفاده از ConfigureAwait false سبب خواهیم شد تا نتیجهی عملیات به context جاری بازگشت داده نشود و نتیجه بر روی thread pool thread ادامه یابد. با اعمال این تغییر، کدهای متد btnGo_Click بدون مشکل اجرا خواهند شد.
ب) راه حل دوم، عدم استفاده از خواص و متدهای همزمان با متدهای غیر همزمان است:
ابتدا امضای متد رویدادگردان را اندکی تغییر داده و واژهی کلیدی async را به آن اضافه میکنیم. سپس از await برای صبر کردن تا پایان عملیات متد GetJsonAsync استفاده خواهیم کرد. صبر کردنی که در اینجا انجام شده، یک asynchronous waits است؛ برخلاف روش همزمان استفاده از خاصیت Result یا متد Wait.
خلاصهی بحث
Await را با متدهای همزمان Wait یا خاصیت Result بلاک نکنید. در غیراینصورت در ترد اجرا کنندهی دستورات، یک deadlock رخخواهد داد؛ زیرا نتیجهی await باید به context جاری بازگشت داده شود اما این context توسط خواص یا متدهای همزمان فراخوانی شده بعدی، قفل شدهاست.
using System; using System.Net.Http; using System.Threading.Tasks; using System.Windows.Forms; using Newtonsoft.Json.Linq; namespace Async13 { public static class JsonExt { public static async Task<JObject> GetJsonAsync(this Uri uri) { using (var client = new HttpClient()) { var jsonString = await client.GetStringAsync(uri); return JObject.Parse(jsonString); } } } public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void btnGo_Click(object sender, EventArgs e) { var url = "http://api.geonames.org/citiesJSON?north=44.1&south=-9.9&east=-22.4&west=55.2&lang=de&username=demo"; txtResult.Text = new Uri(url).GetJsonAsync().Result.ToString(); } } }
PM> Install-Package Newtonsoft.Json
در اینجا قصد داریم اطلاعات JSON دریافتی را در یک TextBox نمایش دهیم. کاری که انجام شده، فراخوانی متد async ایی است به نام GetJsonAsync و سپس استفاده از خاصیت Result این Task برای صبر کردن تا پایان عملیات.
اگر برنامه را اجرا کنید و بر روی دکمهی دریافت اطلاعات کلیک نمائید، برنامه قفل خواهد کرد. چرا؟
البته تفاوتی هم نمیکند که این یک برنامهی دسکتاپ است یا یک برنامهی وب. در هر دو حالت یک deadlock کامل را مشاهده خواهید کرد.
علت بروز deadlock در کدهای async چیست؟
همواره نتیجهی await، در context فراخوان آن بازگشت داده میشود. اگر برنامهی دسکتاپ است، این context همان ترد اصلی UI برنامه میباشد و اگر برنامهی وب است، این context، زمینهی درخواست در حال پردازش میباشد.
خاصیت Result و یا استفاده از متد Wait یک Task، به صورت همزمان عمل میکنند و نه غیرهمزمان. متد GetJsonAsync یک Task ناتمام را که فراخوان آن باید جهت پایاناش صبر کند، بازگشت میدهد. سپس در همینجا کد فراخوان، تردجاری را توسط فراخوانی خاصیت Result قفل میکند. متد GetJsonAsync منتظر خواهد ایستاد تا این ترد آزاد شده و بتواند به کارش که بازگردان نتیجهی عملیات به context جاری است، ادامه دهد.
به عبارتی، کدهای async منتظر پایان کار Result هستند تا نتیجه را بازگردانند. در همین لحظه کدهای همزمان برنامه نیز منتظر کدهای async هستند تا خاتمه یابند. نتیجهی کار یک deadlock است.
روشهای جلوگیری از deadlock در کدهای async؟
الف) در مورد متد ConfigureAwait در قسمتهای قبل بحث شد و به عنوان یک best practice مطرح است:
public static class JsonExt { public static async Task<JObject> GetJsonAsync(this Uri uri) { using (var client = new HttpClient()) { var jsonString = await client.GetStringAsync(uri).ConfigureAwait(continueOnCapturedContext: false); return JObject.Parse(jsonString); } } }
ب) راه حل دوم، عدم استفاده از خواص و متدهای همزمان با متدهای غیر همزمان است:
private async void btnGo_Click(object sender, EventArgs e) { var url = "http://api.geonames.org/citiesJSON?north=44.1&south=-9.9&east=-22.4&west=55.2&lang=de&username=demo"; var data = await new Uri(url).GetJsonAsync(); txtResult.Text = data.ToString(); }
خلاصهی بحث
Await را با متدهای همزمان Wait یا خاصیت Result بلاک نکنید. در غیراینصورت در ترد اجرا کنندهی دستورات، یک deadlock رخخواهد داد؛ زیرا نتیجهی await باید به context جاری بازگشت داده شود اما این context توسط خواص یا متدهای همزمان فراخوانی شده بعدی، قفل شدهاست.
اشتراکها
درک عمیق JavaScript به صورت بصری
سری مقالات آموزشی درک عمیق و راحتتر جاوااسکریپت به صورت بصری
مقدمه
همانطور که میدانید، Blazor دارای یک سیستم لاگ گیری توکار است که میتوان از آن توسط تزریق ILogger در کامپوننتها بهره برد. این سیستم لاگ گیری در زمان توسعهی نرم افزار، در قالب یک کنسول، لاگها را به توسعه دهنده نشان میدهد. اما پس از تولید و پابلیش اپلیکیشن، دیگر این کنسول وجود ندارد. برای ذخیرهی لاگها در یک فایل متنی بر روی سرور هاست، میتوان از Serilog بهره برد که روش آن در اینجا توضیح داده شده است. حال اگر بخواهیم این لاگها را در یک پایگاه داده ذخیره کنیم چطور؟
ایجاد کلاس لاگ
برای این منظور ابتدا کلاسی را برای ذخیرهی لاگها در پایگاه داده به شکل زیر ایجاد مینماییم:
public class DBLog { public int DBLogId { get; set; } public string? LogLevel { get; set; } public string? EventName { get; set; } public string? Message { get; set; } public string? StackTrace { get; set; } public DateTime CreatedDate { get; set; }=DateTime.Now; }
ایجاد دیتابیس لاگر
کلاس DBLogger از اینترفیس ILogger ارث بری میکند و دارای سه متد است که مهمترین آنها متد Log میباشد که درحقیقت با هر بار فراخوانی Logger در برنامه فراخوانی میشود. برای مطالعهی بیشتر در رابطه با دو متد دیگر میتوانید به اینجا مراجعه نمایید.
public class DBLogger:ILogger { private bool _isDisposed; private readonly ApplicationDbContext _dbContext; public DBLogger(ApplicationDbContext dbContext) { _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); } public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter) { var dblLogItem = new DBLog() { EventName = eventId.Name, LogLevel = logLevel.ToString(), Message = exception?.Message, StackTrace=exception?.StackTrace }; _dbContext.DBLogs.Add(dblLogItem); _dbContext.SaveChanges(); } public bool IsEnabled(LogLevel logLevel) { return true; } public IDisposable BeginScope<TState>(TState state) { return null; } }
ایجاد یک لاگ پروایدر سفارشی
حال باید یک لاگ پروایدر سفارشی را ایجاد کنیم تا بتوان یک نمونه از دیتابیس لاگر سفارشی بالا (DBLogger) را ایجاد کرد.
public class DbLoggerProvider:ILoggerProvider { private bool _isDisposed; private readonly ApplicationDbContext _dbContext; public DbLoggerProvider(ApplicationDbContext dbContext) { _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); } public ILogger CreateLogger(string categoryName) { return new DBLogger(_dbContext); } public void Dispose() { } }
همانطور که ملاحظه مینمایید، این لاگ پروایدر، از اینترفیس ILoggerProvider ارث بری کردهاست که دارای متد CreateLogger میباشد ئ این متد با شروع برنامه، یک نمونه از دیتابیس لاگر سفارشی ما را ایجاد میکند. در سازندهی این کلاس، DatabaseContext را مقدار دهی نمودهایم تا آنرا به کلاس DBLogger ارسال نماییم.
در انتها کافیست در کلاس Startup.cs این لاگ پروایدر سفارشی (DbLoggerProvider ) را صدا بزنیم.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) { . . . #region CustomLogProvider var serviceProvider = app.ApplicationServices.CreateScope().ServiceProvider; var appDbContext = serviceProvider.GetRequiredService<ApplicationDbContext>(); loggerFactory.AddProvider(new DbLoggerProvider(appDbContext)); #endregion . . .
مشکل!
منطق کدهای بالا کاملا صحیح میباشد، اما با اجرای یک اپلیکیشن واقعی، در ابتدای کار اینقدر تعداد فراخوانی ثبت لاگها در پایگاه داده بالا میرود که اپلیکیشن هنگ میکند. برای حل این مشکل باید یک صف همزمانی برای ثبت لاگها تشکیل شود. برای این منظور من از این مطلب پروژهی DNTIdentity بهره بردم. بنابراین باید پروایدر را به شکل زیر تصحیح کنیم:
public class DbLoggerProvider:ILoggerProvider { private readonly CancellationTokenSource _cancellationTokenSource = new(); private readonly IList<DBLog> _currentBatch = new List<DBLog>(); private readonly TimeSpan _interval = TimeSpan.FromSeconds(2); private readonly BlockingCollection<DBLog> _messageQueue = new(new ConcurrentQueue<DBLog>()); private readonly Task _outputTask; private readonly ApplicationDbContext _dbContext; private bool _isDisposed; public DbLoggerProvider(ApplicationDbContext dbContext) { _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); _outputTask = Task.Run(ProcessLogQueue); } public ILogger CreateLogger(string categoryName) { return new DBLogger(this,categoryName); } private async Task ProcessLogQueue() { while (!_cancellationTokenSource.IsCancellationRequested) { while (_messageQueue.TryTake(out var message)) { try { _currentBatch.Add(message); } catch { //cancellation token canceled or CompleteAdding called } } await SaveLogItemsAsync(_currentBatch, _cancellationTokenSource.Token); _currentBatch.Clear(); await Task.Delay(_interval, _cancellationTokenSource.Token); } } internal void AddLogItem(DBLog appLogItem) { if (!_messageQueue.IsAddingCompleted) { _messageQueue.Add(appLogItem, _cancellationTokenSource.Token); } } private async Task SaveLogItemsAsync(IList<DBLog> items, CancellationToken cancellationToken) { try { if (!items.Any()) { return; } // We need a separate context for the logger to call its SaveChanges several times, // without using the current request's context and changing its internal state. foreach (var item in items) { var addedEntry = _dbContext.DbLogs.Add(item); } await _dbContext.SaveChangesAsync(cancellationToken); } catch { // don't throw exceptions from logger } } [SuppressMessage("Microsoft.Usage", "CA1031:catch a more specific allowed exception type, or rethrow the exception", Justification = "don't throw exceptions from logger")] private void Stop() { _cancellationTokenSource.Cancel(); _messageQueue.CompleteAdding(); try { _outputTask.Wait(_interval); } catch { // don't throw exceptions from logger } } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (!_isDisposed) { try { if (disposing) { Stop(); _messageQueue.Dispose(); _cancellationTokenSource.Dispose(); _dbContext.Dispose(); } } finally { _isDisposed = true; } } } }
public class DBLogger:ILogger { private readonly LogLevel _minLevel; private readonly DbLoggerProvider _loggerProvider; private readonly string _categoryName; public DBLogger( DbLoggerProvider loggerProvider, string categoryName ) { _loggerProvider= loggerProvider ?? throw new ArgumentNullException(nameof(loggerProvider)); _categoryName= categoryName; } public IDisposable BeginScope<TState>(TState state) { return new NoopDisposable(); } public bool IsEnabled(LogLevel logLevel) { return logLevel >= _minLevel; } public void Log<TState>( LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) { if (!IsEnabled(logLevel)) { return; } if (formatter == null) { throw new ArgumentNullException(nameof(formatter)); } var message = formatter(state, exception); if (exception != null) { message = $"{message}{Environment.NewLine}{exception}"; } if (string.IsNullOrEmpty(message)) { return; } var dblLogItem = new DBLog() { EventName = eventId.Name, LogLevel = logLevel.ToString(), Message = $"{_categoryName}{Environment.NewLine}{message}", StackTrace=exception?.StackTrace }; _loggerProvider.AddLogItem(dblLogItem); } private class NoopDisposable : IDisposable { public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { } } }
توسط ترکیب روش توضیح داده شده در این مقاله با مدیریت استثناءها در Blazor Server - قسمت دوم، علاوه برلاگهای معمولی، میتوان تمامی استثناءهای یک اپلیکیشن را نیز به راحتی در پایگاه داده ذخیره نمود.