مدیریت یکپارچه اکسپشن های بیزینسی در دات نت کور و بلیزر - How to global StatusCode handling in asp.net core web api & blazor and show MudBlazor Snackbar

اگر مثل من از نوشتن کدهای تکراری فراری هستی اینو بخون. در دات نت کور میشه کاری کرد که وقتی به دلیل یه شرط بیزینسی قراره یه کدی اجرا نشه و پیامی رو برگردونه به کلاینت هی try , catch و if , else ننوشت و به اصطلاح Global Exception Handling انجام داد.مثلا وقتی یه api داریم به نام ConfirmEmail که کارش اینه که یه UserId بگیره و فیلد IsConfirm جدول User دیتابیس رو به مقدار 1 آپدیت کنه. دو تا چک قبل آپدیت انجام میدیم : 1. آیا رکوردی برای این ایمیل موجود هست؟ 2. آیا قبلا این ایمیل تایید شده یا نه؟ اگر هر بار هندلینگ این چک ها رو بخوایم بنویسیم که اینجوری میشه : در لایه سرویس که appDbContext_ رو داره باید اون شرط ها رو چک کنیم ، بعد یه return انجام بدیم به کنترلر ، بعد در کنترلر روی نتیجه دریافتی از سرویس هم یه if دیگه بنویسیم که اگر فلان چیز رو از سرویس گرفتی یه استتوس کد http رو ریترن کن مثلا Conflict که میشه کد 409. این if و else ها برای بقیه شرط های بیزینسی هم تکرار میشه و اون دستور عملیاتی (Statement) اصلی متد سرویس گم میشه لای اون همه ایف و الس و نوشتن و نگهداری کدها رو خیلی سخت میکنه و تعداد خط های برنامه رو بی جهت زیاد میکنه ، میتونید با یه حساب سرانگشتی تعداد خطوط ایف و الس ها رو در تعداد متدهای سرویس و اکشن متدهای کنترلر ضرب کنید و ببینید چند خط کد میشه.با توجه به هندل کردن استتوس کد 500 توسط میدل ویر ExceptionHandler خود asp.net core نیاز به if نیست و چک های بیزینسی باید throw new AppException("MyMsg") انجام بدن و هندل شدنشون باید یکجا صورت بگیره.راه حل اصولی اینه که در کلاس سرویس throw new Exception کنیم و با این کار جلوی اجرای باقی خطوط رو بگیریم و در کلاس گلوبال هندلری که مینویسیم (ExceptionHandler) یه مدل json با ویژگی های StatusCode و Message رو برگردونیم و از شر نوشتن if و else ها یا try catch ها راحت بشیم.یا اینکه ساده تر کار کنیم و از استتوس کدهای http استفاده کنیم به همراه یه استرینگ ساده که در Response بنویسیم و من این روش رو ترجیح میدم.به جای throw new Exception میشه یه کلاس AppException رو throw کرده که از Exception دات نت ارث بری کرده و به خاطر این ارث بری چند ریختی شده و علاوه بر اکسپشن ، AppException هم هست و میشه با کلمه کلیدی is تشخیص داد که این اکسپشن ، اپ اکسپشن هست و از اکسپشن های تولید شده خود دات نت نیست پس در کلاس گلوبال اکسپشن هندلر رفتاری که ما میخوایم رو داشته باش یعنی 409 با Message اکسپشن رو به کلاینت برگردون.public class AppException : Exception{ public AppException(string message) : base(message) { }}کلاس ExceptionHandler :public static class ExceptionHandler{ public static void AddExceptionHandler(this IApplicationBuilder app) { app.UseExceptionHandler(appError => { appError.Run(async context => { context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; context.Response.ContentType = "application/json"; var contextFeature = context.Features.Get<IExceptionHandlerFeature>(); if (contextFeature?.Error is AppException) { context.Response.StatusCode = StatusCodes.Status409Conflict; await context.Response.WriteAsync(CheckException(contextFeature.Error)); } }); }); } private static string CheckException(Exception ex) { if (ex.InnerException is not null) return ex.Message + " - InnerException : " + ex.InnerException.Message; return ex.Message; }}حالا این میدل ویر رو در فایل program.cs پروژه web api قبل از app.UseHttpsRedirection اضافه کنید:app.AddExceptionHandler();حالا دیگه نیاز نیست ایف و الس های اضافی بنویسید و فقط کافیه در متد سرویس throw new AppException کنید و تمام. public async Task<int> ConfirmEmail(Guid userId) { var user = await _appDbContext.Users.FirstOrDefaultAsync(u => u.Id == userId); if (user is null) throw new AppException("This activate code is not valid"); if (user.IsConfirm == true) throw new AppException("This account is activated before"); user.IsConfirm = true; return await _appDbContext.SaveChangesAsync(); }حالا همین داستان رو در فرانت اند هم داریم ، وقتی یه Api رو کال میکنیم و استتوس کد 409=Conflict رو میگیریم باید بعد از کال کردن api شرط های مختلف بزاریم که مثلا اگه 409 بود یه توستر یا اسنک بار رو در ui نمایش بده. همه این ایف و الس های فرانتی هم قابل حذف شدنه.در انگولار برای مدیریت یکجای همه استتوس کدها به یه http interceptor نیازه. در Blazor این کار به راحتی امکان پذیره توسط ایجاد یک کلاس و ارث بری از کلاس DelegatingHandler که در نیم اسپیس System.Net.Http قرار داره. در واقع این کلاس یک middleware هست که امکان اجرای کدهایی رو قبل از ارسال ریکوئست http و بعد از ارسال ریکوئست http به ما میده. در کدهای بعد از ارسال ریکوئست http چند if میزاریم و هر کدوم از StatusCode های دریافتی از بک اند رو در همین یکجا هندل میکنیم و تمام ، و دیگر در کامپوننت های ui و صفحات سایتمون مدام ایف و الس نمی نویسیم. برای استتوس کد 409 که خودمون throw میکردیم یه اسنک بار از نوع Warning نمایش میدیم با نمایش جمله ای که از بک اند اومده. برای استتوس کد 500 یه اسنک بار از نوع Error نمایش میدیم با نمایش یک جمله ثابت مثلا : سیستم به مشکل برخورد لطفا با پشتیبانی تماس بگیرید. بهتره متن انگلیسی اکسپشنی که دات نت کور در بک اند تولید کرده رو نمایش ندیم چون توی اون متن میتونه اسم دیتابیس یا جداول دیتابیس باشه و از نظر امنیتی نمایش اون کار درستی نیست.public class HttpStatusCodeService : DelegatingHandler{ private readonly ISnackbar _snackbar; private readonly NavigationManager _navigationManager; public HttpStatusCodeService(ISnackbar snackbar, NavigationManager navigationManager) { _snackbar = snackbar; _navigationManager = navigationManager; } protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { // before sending the request var response = await base.SendAsync(request, cancellationToken); // after sending the request if (!response.IsSuccessStatusCode) { if (response.StatusCode == HttpStatusCode.Conflict) // here _snackbar.Add(await response.Content.ReadAsStringAsync(), Severity.Warning); if (response.StatusCode == HttpStatusCode.InternalServerError) _snackbar.Add("Server error , tell us please", Severity.Error); if (response.StatusCode == HttpStatusCode.Unauthorized) _navigationManager.NavigateTo("/"); } return response; }}اگه کدها قابل خوندن و مشاهده به صورت مناسب نیست در اینجا ، کدها رو توی stackoverflow هم گذاشتم: c# - How to global StatusCode handling in blazor and show MudBlazor Snackbar - Stack Overflow