Commento is the best way to foster discussion on your website without compromising your readers' privacy. It's fast, modern, featureful, secure, and easy to embed
اشتراکها
مقدمه ای بر Resharper Build
قبلاً در سایت جاری در رابطه با پیادهسازی الگوی Context Per Request مطالبی منتشر شده است. در ادامه میخواهیم تمامی درخواستهای خود را اتمیک کنیم. همانطور که قبلاً در این مطلب مطالعه کردید یکی از مزایای الگوی Context Per Request، استفادهی صحیح از تراکنشها میباشد. به عنوان مثال اگر در حین فراخوانی متد SaveChanges، خطایی رخ دهد، کلیهی عملیات RollBack خواهد شد. اما حالت زیر را در نظر بگیرید:
همانطور که در کدهای فوق مشاهده میکنید، قبل از ریدایرکت شدن صفحه، یک استثناء را صادر کردهایم. در این حالت، تغییرات درون دیتابیس ذخیره میشوند! یعنی حتی اگر یک استثناء نیز در طول درخواست رخ دهد، قسمتی از درخواست که در اینجا ذخیرهسازی گروه محصولات است، درون دیتایس ذخیره خواهد شد؛ در نتیجه درخواست ما اتمیک نیست.
خوب، این اینترفیسها همانطور که از نامشان پیداست، همان اعمال را پیاده سازی خواهند کرد:
_categoryService.AddNewCategory(category); _uow.SaveAllChanges(); throw new InvalidOperationException(); return RedirectToAction("Index");
برای رفع این مشکل میتوانیم یکسری وظایف (Tasks) را تعریف کنیم که در نقاط مختلف چرخهی حیات برنامه اجرا شوند. هر کدام از این وظایف تنها کاری که انجام میدهند فراخوانی متد Execute خودشان است. در ادامه میخواهیم از این وظایف جهت پیادهسازی الگوی Transaction Per Request استفاده کنیم. در نتیجه اینترفیسهای زیر را ایجاد خواهیم کرد:
public interface IRunAtInit { void Execute(); } public interface IRunAfterEachRequest { void Execute(); } public interface IRunAtStartUp { void Execute(); } public interface IRunOnEachRequest { void Execute(); } public interface IRunOnError { void Execute(); }
IRunAtInit: اجرای وظایف در زمان بارگذاری اولیهی برنامه.
IRunAfterEachRequest: اجرای وظایف بعد از اینکه درخواستی فراخوانی (ارسال) شد.
IRunAtStartUp: اجرای وظایف در زمان StartUp برنامه.
IRunOnEachRequest: اجرای وظایف در ابتدای هر درخواست.
با این کار استراکچرمپ اسمبلی معرفی شده را بررسی کرده و هر کلاسی که اینترفیسهای ذکر شده را پیادهسازی کرده باشد، رجیستر میکند. قدم بعدی افزودن رجیستری فوق و بارگذاری آن درون کانتینرمان است:
اکنون وظایف درون کانتینرمان بارگذاری شدهاند. سپس نوبت به استفادهی از این وظایف است.
همانطور که مشاهده میکنید، هر task در قسمت خاص خود فراخوانی خواهد شد. مثلاً IRunOnError درون رویداد Application_Error و دیگر وظایف نیز به همین ترتیب.
توضیحات کلاس فوق:
خواهید دید که عملیات roll back شده و تغییرات در دیتابیس (در اینجا ذخیره سازی گروه محصولات) اعمال نخواهد شد.
IRunOnError: اجرای وظایف در زمان بروز خطا یا استثناءهای مدیریت نشدهی برنامه.
خوب، یک کلاس میتواند با پیادهسازی هر کدام از اینترفیسهای فوق تبدیل به یک task شود. همچنین از این جهت که اینترفیسهای ما ساده هستند و هر اینترفیس یک متد Execute دارد، عملکرد آنها تنها اجرای یکسری دستورات در حالات مختلف میباشد.
قدم بعدی افزودن قابلیت پشتیبانی از این وظایف در برنامهمان است. اینکار را با پیادهسازی ریجستری زیر انجام خواهیم داد:
public class TaskRegistry : StructureMap.Configuration.DSL.Registry { public TaskRegistry() { Scan(scan => { scan.Assembliy("yourAssemblyName"); scan.AddAllTypesOf<IRunAtInit>(); scan.AddAllTypesOf<IRunAtStartUp>(); scan.AddAllTypesOf<IRunOnEachRequest>(); scan.AddAllTypesOf<IRunOnError>(); scan.AddAllTypesOf<IRunAfterEachRequest>(); }); } }
ioc.AddRegistry(new TaskRegistry());
خوب، باید درون فایل Global.asax کدهای زیر را قرار دهیم. چون همانطور که عنوان شد وظایف ایجاد شده میبایستی در نقاط مختلف برنامه اجرا شوند:
protected void Application_Start() { // other code foreach (var task in SmObjectFactory.Container.GetAllInstances<IRunAtInit>()) { task.Execute(); } } protected void Application_BeginRequest() { foreach (var task in SmObjectFactory.Container.GetAllInstances<IRunOnEachRequest>()) { task.Execute(); } } protected void Application_EndRequest(object sender, EventArgs e) { try { foreach (var task in SmObjectFactory.Container.GetAllInstances<IRunAfterEachRequest>()) { task.Execute(); } } finally { HttpContextLifecycle.DisposeAndClearAll(); MiniProfiler.Stop(); } } protected void Application_Error() { foreach (var task in SmObjectFactory.Container.GetAllInstances<IRunOnError>()) { task.Execute(); } }
اکنون برنامه به صورت کامل از وظایف پشتیبانی میکند. در ادامه، کلاس زیر را ایجاد خواهیم کرد. این کلاس چندین اینترفیس را از اینترفیسهای ذکر شده، پیادهسازی میکند:
public class TransactionPerRequest : IRunOnEachRequest, IRunOnError, IRunAfterEachRequest { private readonly IUnitOfWork _uow; private readonly HttpContextBase _httpContext; public TransactionPerRequest(IUnitOfWork uow, HttpContextBase httpContext) { _uow = uow; _httpContext = httpContext; } void IRunOnEachRequest.Execute() { _httpContext.Items["_Transaction"] = _uow.Database.BeginTransaction(System.Data.IsolationLevel.ReadCommitted); } void IRunOnError.Execute() { _httpContext.Items["_Error"] = true; } void IRunAfterEachRequest.Execute() { var transaction = (DbContextTransaction) _httpContext.Items["_Transaction"]; if (_httpContext.Items["_Error"] != null) { transaction.Rollback(); } else { transaction.Commit(); } } }
در کلاس TransactionPerRequest به دو وابستگی نیاز خواهیم داشت: IUnitOfWork برای کار با تراکنشها و HttpContextBase برای دریافت درخواست جاری. همانطور که مشاهده میکنید در متد IRunOnEachRequest.Execute یک تراکنش را آغاز کردهایم و در IRunAfterEachRequest.Execute یعنی در پایان یک درخواست، تراکنش را commit کردهایم. این مورد را با چک کردن یک فلگ در صورت عدم بروز خطا انجام دادهایم. اگر خطایی نیز وجود داشته باشد، کل عملیات roll back خواهد شد. لازم به ذکر است که فلگ خطا نیز درون متد IRunOnError.Execute به true مقداردهی شده است.
خوب، پیادهسازی الگوی Transaction Per Request به صورت کامل انجام گرفته است. اکنون اگر برنامه را در حالت زیر اجرا کنید:
_categoryService.AddNewCategory(category); _uow.SaveAllChanges(); throw new InvalidOperationException(); return RedirectToAction("Index");
پروژهی ASP.NET Identity که نسل جدید سیستم Authentication و Authorization مخصوص ASP.NET است، دارای دو سری مثال رسمی است:
الف) مثالهای کدپلکس
ب) مثال نیوگت
در ادامه قصد داریم مثال نیوگت آنرا که مثال کاملی است از نحوهی استفاده از ASP.NET Identity در ASP.NET MVC، جهت اعمال الگوی واحد کار و تزریق وابستگیها، بازنویسی کنیم.
پیشنیازها
- برای درک مطلب جاری نیاز است ابتدا دورهی مرتبطی را در سایت مطالعه کنید و همچنین با نحوهی پیاده سازی الگوی واحد کار در EF Code First آشنا باشید.
- به علاوه فرض بر این است که یک پروژهی خالی ASP.NET MVC 5 را نیز آغاز کردهاید و توسط کنسول پاور شل نیوگت، فایلهای مثال Microsoft.AspNet.Identity.Samples را به آن افزودهاید:
ساختار پروژهی تکمیلی
همانند مطلب پیاده سازی الگوی واحد کار در EF Code First، این پروژهی جدید را با چهار اسمبلی class library دیگر به نامهای
AspNetIdentityDependencyInjectionSample.DataLayer
AspNetIdentityDependencyInjectionSample.DomainClasses
AspNetIdentityDependencyInjectionSample.IocConfig
AspNetIdentityDependencyInjectionSample.ServiceLayer
تکمیل میکنیم.
ساختار پروژهی AspNetIdentityDependencyInjectionSample.DomainClasses
مثال Microsoft.AspNet.Identity.Samples بر مبنای primary key از نوع string است. برای نمونه کلاس کاربران آنرا به نام ApplicationUser در فایل Models\IdentityModels.cs میتوانید مشاهده کنید. در مطلب جاری، این نوع پیش فرض، به نوع متداول int تغییر خواهد یافت. به همین جهت نیاز است کلاسهای ذیل را به پروژهی DomainClasses اضافه کرد:
در اینجا نحوهی تغییر primary key از نوع string را به نوع int، مشاهده میکنید. این تغییر نیاز به اعمال به کلاسهای کاربران و همچنین نقشهای آنها نیز دارد. به همین جهت صرفا تغییر کلاس ابتدایی ApplicationUser کافی نیست و باید کلاسهای فوق را نیز اضافه کرد و تغییر داد.
بدیهی است در اینجا کلاس پایه کاربران را میتوان سفارشی سازی کرد و خواص دیگری را نیز به آن افزود. برای مثال در اینجا یک کلاس جدید آدرس تعریف شدهاست که ارجاعی از آن در کلاس کاربران نیز قابل مشاهده است.
سایر کلاسهای مدلهای اصلی برنامه که جداول بانک اطلاعاتی را تشکیل خواهند داد نیز در آینده به همین اسمبلی DomainClasses اضافه میشوند.
ساختار پروژهی AspNetIdentityDependencyInjectionSample.DataLayer جهت اعمال الگوی واحد کار
اگر به همان فایل Models\IdentityModels.cs ابتدایی پروژه که اکنون کلاس ApplicationUser آنرا به پروژهی DomainClasses منتقل کردهایم، مجددا مراجعه کنید، کلاس DbContext مخصوص ASP.NET Identity نیز در آن تعریف شدهاست:
این کلاس را به پروژهی DataLayer منتقل میکنیم و از آن به عنوان DbContext اصلی برنامه استفاده خواهیم کرد. بنابراین دیگر نیازی نیست چندین DbContext در برنامه داشته باشیم. IdentityDbContext، در اصل از DbContext مشتق شدهاست.
اینترفیس IUnitOfWork برنامه، در پروژهی DataLayer چنین شکلی را دارد که نمونهای از آنرا در مطلب آشنایی با نحوهی پیاده سازی الگوی واحد کار در EF Code First، پیشتر ملاحظه کردهاید.
اکنون کلاس ApplicationDbContext منتقل شده به DataLayer یک چنین امضایی را خواهد یافت:
تعریف آن باید جهت اعمال کلاسهای سفارشی سازی شدهی کاربران و نقشهای آنها برای استفاده از primary key از نوع int به شکل فوق، تغییر یابد. همچنین در انتهای آن مانند قبل، IUnitOfWork نیز ذکر شدهاست. پیاده سازی کامل این کلاس را از پروژهی پیوست انتهای بحث میتوانید دریافت کنید.
کار کردن با این کلاس، هیچ تفاوتی با DbContextهای متداول EF Code First ندارد و تمام اصول آنها یکی است.
در ادامه اگر به فایل App_Start\IdentityConfig.cs مراجعه کنید، کلاس ذیل در آن قابل مشاهدهاست:
نیازی به این کلاس به این شکل نیست. آنرا حذف کنید و در پروژهی DataLayer، کلاس جدید ذیل را اضافه نمائید:
در این مثال، بحث migrations به حالت خودکار تنظیم شدهاست و تمام تغییرات در پروژهی DomainClasses را به صورت خودکار به بانک اطلاعاتی اعمال میکند. تا همینجا کار تنظیم DataLayer به پایان میرسد.
ساختار پروژهی AspNetIdentityDependencyInjectionSample.ServiceLayer
در ادامه مابقی کلاسهای موجود در فایل App_Start\IdentityConfig.cs را به لایه سرویس برنامه منتقل خواهیم کرد. همچنین برای آنها یک سری اینترفیس جدید نیز تعریف میکنیم، تا تزریق وابستگیها به نحو صحیحی صورت گیرد. اگر به فایلهای کنترلر این مثال پیش فرض مراجعه کنید (پیش از تغییرات بحث جاری)، هرچند به نظر در کنترلرها، کلاسهای موجود در فایل App_Start\IdentityConfig.cs تزریق شدهاند، اما به دلیل عدم استفاده از اینترفیسها، وابستگی کاملی بین جزئیات پیاده سازی این کلاسها و نمونههای تزریق شده به کنترلرها وجود دارد و عملا معکوس سازی واقعی وابستگیها رخ ندادهاست. بنابراین نیاز است این مسایل را اصلاح کنیم.
الف) انتقال کلاس ApplicationUserManager به لایه سرویس برنامه
کلاس ApplicationUserManager فایل App_Start\IdentityConfig.c را به لایه سرویس منتقل میکنیم:
تغییراتی که در اینجا اعمال شدهاند، به شرح زیر میباشند:
- متد استاتیک Create این کلاس حذف و تعاریف آن به سازندهی کلاس منتقل شدهاند. به این ترتیب با هربار وهله سازی این کلاس توسط IoC Container به صورت خودکار این تنظیمات نیز به کلاس پایه UserManager اعمال میشوند.
- اگر به کلاس پایه UserManager دقت کنید، به آرگومانهای جنریک آن یک int هم اضافه شدهاست. این مورد جهت استفاده از primary key از نوع int ضروری است.
- در کلاس پایه UserManager تعدادی متد وجود دارند. تعاریف آنها را به اینترفیس IApplicationUserManager منتقل خواهیم کرد. نیازی هم به پیاده سازی این متدها در کلاس جدید ApplicationUserManager نیست؛ زیرا کلاس پایه UserManager پیشتر آنها را پیاده سازی کردهاست. به این ترتیب میتوان به یک تزریق وابستگی واقعی و بدون وابستگی به پیاده سازی خاص UserManager رسید. کنترلری که با IApplicationUserManager بجای ApplicationUserManager کار میکند، قابلیت تعویض پیاده سازی آنرا جهت آزمونهای واحد خواهد یافت.
- در کلاس اصلی ApplicationDbInitializer پیش فرض این مثال، متد Seed هم قابل مشاهدهاست. این متد را از کلاس جدید Configuration اضافه شده به DataLayer حذف کردهایم. از این جهت که در آن از متدهای کلاس ApplicationUserManager مستقیما استفاده شدهاست. متد Seed اکنون به کلاس جدید اضافه شده به لایه سرویس منتقل شده و در آغاز برنامه فراخوانی خواهد شد. DataLayer نباید وابستگی به لایه سرویس داشته باشد. لایه سرویس است که از امکانات DataLayer استفاده میکند.
- اگر به سازندهی کلاس جدید ApplicationUserManager دقت کنید، چند اینترفیس دیگر نیز به آن تزریق شدهاند. اینترفیس IApplicationRoleManager را ادامه تعریف خواهیم کرد. سایر اینترفیسهای تزریق شده مانند IUserStore، IDataProtectionProvider و IIdentityMessageService جزو تعاریف اصلی ASP.NET Identity بوده و نیازی به تعریف مجدد آنها نیست. فقط کلاسهای EmailService و SmsService فایل App_Start\IdentityConfig.c را نیز به لایه سرویس منتقل کردهایم. این کلاسها بر اساس تنظیمات IoC Container مورد استفاده، در اینجا به صورت خودکار ترزیق خواهند شد. حالت پیش فرض آن، وهله سازی مستقیم است که مطابق کدهای فوق به حالت تزریق وابستگیها بهبود یافتهاست.
ب) انتقال کلاس ApplicationSignInManager به لایه سرویس برنامه
کلاس ApplicationSignInManager فایل App_Start\IdentityConfig.c را نیز به لایه سرویس منتقل میکنیم.
در اینجا نیز اینترفیس جدید IApplicationSignInManager را برای مخفی سازی پیاده سازی کلاس پایه توکار SignInManager، اضافه کردهایم. این اینترفیس دقیقا حاوی تعاریف متدهای کلاس پایه SignInManager است و نیازی به پیاده سازی مجدد در کلاس ApplicationSignInManager نخواهد داشت.
ج) انتقال کلاس ApplicationRoleManager به لایه سرویس برنامه
کلاس ApplicationRoleManager فایل App_Start\IdentityConfig.c را نیز به لایه سرویس منتقل خواهیم کرد:
روش کار نیز در اینجا همانند دو کلاس قبل است. اینترفیس جدید IApplicationRoleManager را که حاوی تعاریف متدهای کلاس پایه توکار RoleManager است، به لایه سرویس اضافه میکنیم. کنترلرهای برنامه با این اینترفیس بجای استفاده مستقیم از کلاس ApplicationRoleManager کار خواهند کرد.
تا اینجا کار تنظیمات لایه سرویس برنامه به پایان میرسد.
ساختار پروژهی AspNetIdentityDependencyInjectionSample.IocConfig
پروژهی IocConfig جایی است که تنظیمات StructureMap را به آن منتقل کردهایم:
در اینجا نحوهی اتصال اینترفیسهای برنامه را به کلاسها و یا نمونههایی که آنها را میتوانند پیاده سازی کنند، مشاهده میکنید. برای مثال IUnitOfWork به ApplicationDbContext مرتبط شدهاست و یا دوبار تعاریف متناظر با DbContext را مشاهده میکنید. از این تعاریف به صورت توکار توسط ASP.NET Identity زمانیکه قرار است UserStore و RoleStore را وهله سازی کند، استفاده میشوند و ذکر آنها الزامی است.
در تعاریف فوق یک مورد را به فایل Startup.cs موکول کردهایم. برای مشخص سازی نمونهی پیاده سازی کنندهی IDataProtectionProvider نیاز است به IAppBuilder کلاس Startup برنامه دسترسی داشت. این کلاس آغازین Owin اکنون به نحو ذیل بازنویسی شدهاست و در آن، تنظیمات IDataProtectionProvider را به همراه وهله سازی CreatePerOwinContext مشاهده میکنید:
این تعاریف از فایل پیش فرض Startup.Auth.cs پوشهی App_Start دریافت و جهت کار با IoC Container برنامه، بازنویسی شدهاند.
تنظیمات برنامهی اصلی ASP.NET MVC، جهت اعمال تزریق وابستگیها
الف) ابتدا نیاز است فایل Global.asax.cs را به نحو ذیل بازنویسی کنیم:
در اینجا در متد setDbInitializer، نحوهی استفاده و تعریف فایل Configuration لایه Data را ملاحظه میکنید؛ به همراه متد آغاز بانک اطلاعاتی و اعمال تغییرات لازم به آن در ابتدای کار برنامه. همچنین ControllerFactory برنامه نیز به StructureMapControllerFactory تنظیم شدهاست تا کار تزریق وابستگیها به کنترلرهای برنامه به صورت خودکار میسر شود. در پایان کار هر درخواست نیز منابع Disposable رها میشوند.
ب) به پوشهی Models برنامه مراجعه کنید. در اینجا در هر کلاسی که Id از نوع string وجود داشت، باید تبدیل به نوع int شوند. چون primary key برنامه را به نوع int تغییر دادهایم. برای مثال کلاسهای EditUserViewModel و RoleViewModel باید تغییر کنند.
ج) اصلاح کنترلرهای برنامه جهت اعمال تزریق وابستگیها
اکنون اصلاح کنترلرها جهت اعمال تزریق وابستگیها سادهاست. در ادامه نحوهی تغییر امضای سازندههای این کنترلرها را جهت استفاده از اینترفیسهای جدید مشاهده میکنید:
پس از این تغییرات، فقط کافی است بجای خواص برای مثال RoleManager سابق از فیلدهای تزریق شده در کلاس، مثلا roleManager_ جدید استفاده کرد. امضای متدهای یکی است و تنها به یک search و replace نیاز دارد.
البته تعدادی اکشن متد نیز در اینجا وجود دارند که از string id استفاده میکنند. اینها را باید به int? Id تغییر داد تا با نوع primary key جدید مورد استفاده تطابق پیدا کنند.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید:
AspNetIdentityDependencyInjectionSample
معادل این پروژه جهت ASP.NET Core Identity : «سفارشی سازی ASP.NET Core Identity - قسمت اول - موجودیتهای پایه و DbContext برنامه »
الف) مثالهای کدپلکس
ب) مثال نیوگت
در ادامه قصد داریم مثال نیوگت آنرا که مثال کاملی است از نحوهی استفاده از ASP.NET Identity در ASP.NET MVC، جهت اعمال الگوی واحد کار و تزریق وابستگیها، بازنویسی کنیم.
پیشنیازها
- برای درک مطلب جاری نیاز است ابتدا دورهی مرتبطی را در سایت مطالعه کنید و همچنین با نحوهی پیاده سازی الگوی واحد کار در EF Code First آشنا باشید.
- به علاوه فرض بر این است که یک پروژهی خالی ASP.NET MVC 5 را نیز آغاز کردهاید و توسط کنسول پاور شل نیوگت، فایلهای مثال Microsoft.AspNet.Identity.Samples را به آن افزودهاید:
PM> Install-Package Microsoft.AspNet.Identity.Samples -Pre
ساختار پروژهی تکمیلی
همانند مطلب پیاده سازی الگوی واحد کار در EF Code First، این پروژهی جدید را با چهار اسمبلی class library دیگر به نامهای
AspNetIdentityDependencyInjectionSample.DataLayer
AspNetIdentityDependencyInjectionSample.DomainClasses
AspNetIdentityDependencyInjectionSample.IocConfig
AspNetIdentityDependencyInjectionSample.ServiceLayer
تکمیل میکنیم.
ساختار پروژهی AspNetIdentityDependencyInjectionSample.DomainClasses
مثال Microsoft.AspNet.Identity.Samples بر مبنای primary key از نوع string است. برای نمونه کلاس کاربران آنرا به نام ApplicationUser در فایل Models\IdentityModels.cs میتوانید مشاهده کنید. در مطلب جاری، این نوع پیش فرض، به نوع متداول int تغییر خواهد یافت. به همین جهت نیاز است کلاسهای ذیل را به پروژهی DomainClasses اضافه کرد:
using System.ComponentModel.DataAnnotations.Schema; using Microsoft.AspNet.Identity.EntityFramework; namespace AspNetIdentityDependencyInjectionSample.DomainClasses { public class ApplicationUser : IdentityUser<int, CustomUserLogin, CustomUserRole, CustomUserClaim> { // سایر خواص اضافی در اینجا [ForeignKey("AddressId")] public virtual Address Address { get; set; } public int? AddressId { get; set; } } } using System.Collections.Generic; namespace AspNetIdentityDependencyInjectionSample.DomainClasses { public class Address { public int Id { get; set; } public string City { get; set; } public string State { get; set; } public virtual ICollection<ApplicationUser> ApplicationUsers { set; get; } } } using Microsoft.AspNet.Identity.EntityFramework; namespace AspNetIdentityDependencyInjectionSample.DomainClasses { public class CustomRole : IdentityRole<int, CustomUserRole> { public CustomRole() { } public CustomRole(string name) { Name = name; } } } using Microsoft.AspNet.Identity.EntityFramework; namespace AspNetIdentityDependencyInjectionSample.DomainClasses { public class CustomUserClaim : IdentityUserClaim<int> { } } using Microsoft.AspNet.Identity.EntityFramework; namespace AspNetIdentityDependencyInjectionSample.DomainClasses { public class CustomUserLogin : IdentityUserLogin<int> { } } using Microsoft.AspNet.Identity.EntityFramework; namespace AspNetIdentityDependencyInjectionSample.DomainClasses { public class CustomUserRole : IdentityUserRole<int> { } }
بدیهی است در اینجا کلاس پایه کاربران را میتوان سفارشی سازی کرد و خواص دیگری را نیز به آن افزود. برای مثال در اینجا یک کلاس جدید آدرس تعریف شدهاست که ارجاعی از آن در کلاس کاربران نیز قابل مشاهده است.
سایر کلاسهای مدلهای اصلی برنامه که جداول بانک اطلاعاتی را تشکیل خواهند داد نیز در آینده به همین اسمبلی DomainClasses اضافه میشوند.
ساختار پروژهی AspNetIdentityDependencyInjectionSample.DataLayer جهت اعمال الگوی واحد کار
اگر به همان فایل Models\IdentityModels.cs ابتدایی پروژه که اکنون کلاس ApplicationUser آنرا به پروژهی DomainClasses منتقل کردهایم، مجددا مراجعه کنید، کلاس DbContext مخصوص ASP.NET Identity نیز در آن تعریف شدهاست:
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
اینترفیس IUnitOfWork برنامه، در پروژهی DataLayer چنین شکلی را دارد که نمونهای از آنرا در مطلب آشنایی با نحوهی پیاده سازی الگوی واحد کار در EF Code First، پیشتر ملاحظه کردهاید.
using System.Collections.Generic; using System.Data.Entity; namespace AspNetIdentityDependencyInjectionSample.DataLayer.Context { public interface IUnitOfWork { IDbSet<TEntity> Set<TEntity>() where TEntity : class; int SaveAllChanges(); void MarkAsChanged<TEntity>(TEntity entity) where TEntity : class; IList<T> GetRows<T>(string sql, params object[] parameters) where T : class; IEnumerable<TEntity> AddThisRange<TEntity>(IEnumerable<TEntity> entities) where TEntity : class; void ForceDatabaseInitialize(); } }
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, CustomRole, int, CustomUserLogin, CustomUserRole, CustomUserClaim>, IUnitOfWork { public DbSet<Category> Categories { set; get; } public DbSet<Product> Products { set; get; } public DbSet<Address> Addresses { set; get; }
کار کردن با این کلاس، هیچ تفاوتی با DbContextهای متداول EF Code First ندارد و تمام اصول آنها یکی است.
در ادامه اگر به فایل App_Start\IdentityConfig.cs مراجعه کنید، کلاس ذیل در آن قابل مشاهدهاست:
public class ApplicationDbInitializer : DropCreateDatabaseIfModelChanges<ApplicationDbContext>
using System.Data.Entity.Migrations; namespace AspNetIdentityDependencyInjectionSample.DataLayer.Context { public class Configuration : DbMigrationsConfiguration<ApplicationDbContext> { public Configuration() { AutomaticMigrationsEnabled = true; AutomaticMigrationDataLossAllowed = true; } } }
ساختار پروژهی AspNetIdentityDependencyInjectionSample.ServiceLayer
در ادامه مابقی کلاسهای موجود در فایل App_Start\IdentityConfig.cs را به لایه سرویس برنامه منتقل خواهیم کرد. همچنین برای آنها یک سری اینترفیس جدید نیز تعریف میکنیم، تا تزریق وابستگیها به نحو صحیحی صورت گیرد. اگر به فایلهای کنترلر این مثال پیش فرض مراجعه کنید (پیش از تغییرات بحث جاری)، هرچند به نظر در کنترلرها، کلاسهای موجود در فایل App_Start\IdentityConfig.cs تزریق شدهاند، اما به دلیل عدم استفاده از اینترفیسها، وابستگی کاملی بین جزئیات پیاده سازی این کلاسها و نمونههای تزریق شده به کنترلرها وجود دارد و عملا معکوس سازی واقعی وابستگیها رخ ندادهاست. بنابراین نیاز است این مسایل را اصلاح کنیم.
الف) انتقال کلاس ApplicationUserManager به لایه سرویس برنامه
کلاس ApplicationUserManager فایل App_Start\IdentityConfig.c را به لایه سرویس منتقل میکنیم:
using System; using System.Security.Claims; using System.Threading.Tasks; using AspNetIdentityDependencyInjectionSample.DomainClasses; using AspNetIdentityDependencyInjectionSample.ServiceLayer.Contracts; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.Owin; using Microsoft.Owin.Security.Cookies; using Microsoft.Owin.Security.DataProtection; namespace AspNetIdentityDependencyInjectionSample.ServiceLayer { public class ApplicationUserManager : UserManager<ApplicationUser, int>, IApplicationUserManager { private readonly IDataProtectionProvider _dataProtectionProvider; private readonly IIdentityMessageService _emailService; private readonly IApplicationRoleManager _roleManager; private readonly IIdentityMessageService _smsService; private readonly IUserStore<ApplicationUser, int> _store; public ApplicationUserManager(IUserStore<ApplicationUser, int> store, IApplicationRoleManager roleManager, IDataProtectionProvider dataProtectionProvider, IIdentityMessageService smsService, IIdentityMessageService emailService) : base(store) { _store = store; _roleManager = roleManager; _dataProtectionProvider = dataProtectionProvider; _smsService = smsService; _emailService = emailService; createApplicationUserManager(); } public void SeedDatabase() { } private void createApplicationUserManager() { // Configure validation logic for usernames this.UserValidator = new UserValidator<ApplicationUser, int>(this) { AllowOnlyAlphanumericUserNames = false, RequireUniqueEmail = true }; // Configure validation logic for passwords this.PasswordValidator = new PasswordValidator { RequiredLength = 6, RequireNonLetterOrDigit = true, RequireDigit = true, RequireLowercase = true, RequireUppercase = true, }; // Configure user lockout defaults this.UserLockoutEnabledByDefault = true; this.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5); this.MaxFailedAccessAttemptsBeforeLockout = 5; // Register two factor authentication providers. This application uses Phone and Emails as a step of receiving a code for verifying the user // You can write your own provider and plug in here. this.RegisterTwoFactorProvider("PhoneCode", new PhoneNumberTokenProvider<ApplicationUser, int> { MessageFormat = "Your security code is: {0}" }); this.RegisterTwoFactorProvider("EmailCode", new EmailTokenProvider<ApplicationUser, int> { Subject = "SecurityCode", BodyFormat = "Your security code is {0}" }); this.EmailService = _emailService; this.SmsService = _smsService; if (_dataProtectionProvider != null) { var dataProtector = _dataProtectionProvider.Create("ASP.NET Identity"); this.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser, int>(dataProtector); } } } }
- متد استاتیک Create این کلاس حذف و تعاریف آن به سازندهی کلاس منتقل شدهاند. به این ترتیب با هربار وهله سازی این کلاس توسط IoC Container به صورت خودکار این تنظیمات نیز به کلاس پایه UserManager اعمال میشوند.
- اگر به کلاس پایه UserManager دقت کنید، به آرگومانهای جنریک آن یک int هم اضافه شدهاست. این مورد جهت استفاده از primary key از نوع int ضروری است.
- در کلاس پایه UserManager تعدادی متد وجود دارند. تعاریف آنها را به اینترفیس IApplicationUserManager منتقل خواهیم کرد. نیازی هم به پیاده سازی این متدها در کلاس جدید ApplicationUserManager نیست؛ زیرا کلاس پایه UserManager پیشتر آنها را پیاده سازی کردهاست. به این ترتیب میتوان به یک تزریق وابستگی واقعی و بدون وابستگی به پیاده سازی خاص UserManager رسید. کنترلری که با IApplicationUserManager بجای ApplicationUserManager کار میکند، قابلیت تعویض پیاده سازی آنرا جهت آزمونهای واحد خواهد یافت.
- در کلاس اصلی ApplicationDbInitializer پیش فرض این مثال، متد Seed هم قابل مشاهدهاست. این متد را از کلاس جدید Configuration اضافه شده به DataLayer حذف کردهایم. از این جهت که در آن از متدهای کلاس ApplicationUserManager مستقیما استفاده شدهاست. متد Seed اکنون به کلاس جدید اضافه شده به لایه سرویس منتقل شده و در آغاز برنامه فراخوانی خواهد شد. DataLayer نباید وابستگی به لایه سرویس داشته باشد. لایه سرویس است که از امکانات DataLayer استفاده میکند.
- اگر به سازندهی کلاس جدید ApplicationUserManager دقت کنید، چند اینترفیس دیگر نیز به آن تزریق شدهاند. اینترفیس IApplicationRoleManager را ادامه تعریف خواهیم کرد. سایر اینترفیسهای تزریق شده مانند IUserStore، IDataProtectionProvider و IIdentityMessageService جزو تعاریف اصلی ASP.NET Identity بوده و نیازی به تعریف مجدد آنها نیست. فقط کلاسهای EmailService و SmsService فایل App_Start\IdentityConfig.c را نیز به لایه سرویس منتقل کردهایم. این کلاسها بر اساس تنظیمات IoC Container مورد استفاده، در اینجا به صورت خودکار ترزیق خواهند شد. حالت پیش فرض آن، وهله سازی مستقیم است که مطابق کدهای فوق به حالت تزریق وابستگیها بهبود یافتهاست.
ب) انتقال کلاس ApplicationSignInManager به لایه سرویس برنامه
کلاس ApplicationSignInManager فایل App_Start\IdentityConfig.c را نیز به لایه سرویس منتقل میکنیم.
using AspNetIdentityDependencyInjectionSample.DomainClasses; using AspNetIdentityDependencyInjectionSample.ServiceLayer.Contracts; using Microsoft.AspNet.Identity.Owin; using Microsoft.Owin.Security; namespace AspNetIdentityDependencyInjectionSample.ServiceLayer { public class ApplicationSignInManager : SignInManager<ApplicationUser, int>, IApplicationSignInManager { private readonly ApplicationUserManager _userManager; private readonly IAuthenticationManager _authenticationManager; public ApplicationSignInManager(ApplicationUserManager userManager, IAuthenticationManager authenticationManager) : base(userManager, authenticationManager) { _userManager = userManager; _authenticationManager = authenticationManager; } } }
ج) انتقال کلاس ApplicationRoleManager به لایه سرویس برنامه
کلاس ApplicationRoleManager فایل App_Start\IdentityConfig.c را نیز به لایه سرویس منتقل خواهیم کرد:
using AspNetIdentityDependencyInjectionSample.DomainClasses; using AspNetIdentityDependencyInjectionSample.ServiceLayer.Contracts; using Microsoft.AspNet.Identity; namespace AspNetIdentityDependencyInjectionSample.ServiceLayer { public class ApplicationRoleManager : RoleManager<CustomRole, int>, IApplicationRoleManager { private readonly IRoleStore<CustomRole, int> _roleStore; public ApplicationRoleManager(IRoleStore<CustomRole, int> roleStore) : base(roleStore) { _roleStore = roleStore; } public CustomRole FindRoleByName(string roleName) { return this.FindByName(roleName); // RoleManagerExtensions } public IdentityResult CreateRole(CustomRole role) { return this.Create(role); // RoleManagerExtensions } } }
تا اینجا کار تنظیمات لایه سرویس برنامه به پایان میرسد.
ساختار پروژهی AspNetIdentityDependencyInjectionSample.IocConfig
پروژهی IocConfig جایی است که تنظیمات StructureMap را به آن منتقل کردهایم:
using System; using System.Data.Entity; using System.Threading; using System.Web; using AspNetIdentityDependencyInjectionSample.DataLayer.Context; using AspNetIdentityDependencyInjectionSample.DomainClasses; using AspNetIdentityDependencyInjectionSample.ServiceLayer; using AspNetIdentityDependencyInjectionSample.ServiceLayer.Contracts; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.EntityFramework; using Microsoft.Owin.Security; using StructureMap; using StructureMap.Web; namespace AspNetIdentityDependencyInjectionSample.IocConfig { public static class SmObjectFactory { private static readonly Lazy<Container> _containerBuilder = new Lazy<Container>(defaultContainer, LazyThreadSafetyMode.ExecutionAndPublication); public static IContainer Container { get { return _containerBuilder.Value; } } private static Container defaultContainer() { return new Container(ioc => { ioc.For<IUnitOfWork>() .HybridHttpOrThreadLocalScoped() .Use<ApplicationDbContext>(); ioc.For<ApplicationDbContext>().HybridHttpOrThreadLocalScoped().Use<ApplicationDbContext>(); ioc.For<DbContext>().HybridHttpOrThreadLocalScoped().Use<ApplicationDbContext>(); ioc.For<IUserStore<ApplicationUser, int>>() .HybridHttpOrThreadLocalScoped() .Use<UserStore<ApplicationUser, CustomRole, int, CustomUserLogin, CustomUserRole, CustomUserClaim>>(); ioc.For<IRoleStore<CustomRole, int>>() .HybridHttpOrThreadLocalScoped() .Use<RoleStore<CustomRole, int, CustomUserRole>>(); ioc.For<IAuthenticationManager>() .Use(() => HttpContext.Current.GetOwinContext().Authentication); ioc.For<IApplicationSignInManager>() .HybridHttpOrThreadLocalScoped() .Use<ApplicationSignInManager>(); ioc.For<IApplicationUserManager>() .HybridHttpOrThreadLocalScoped() .Use<ApplicationUserManager>(); ioc.For<IApplicationRoleManager>() .HybridHttpOrThreadLocalScoped() .Use<ApplicationRoleManager>(); ioc.For<IIdentityMessageService>().Use<SmsService>(); ioc.For<IIdentityMessageService>().Use<EmailService>(); ioc.For<ICustomRoleStore>() .HybridHttpOrThreadLocalScoped() .Use<CustomRoleStore>(); ioc.For<ICustomUserStore>() .HybridHttpOrThreadLocalScoped() .Use<CustomUserStore>(); //config.For<IDataProtectionProvider>().Use(()=> app.GetDataProtectionProvider()); // In Startup class ioc.For<ICategoryService>().Use<EfCategoryService>(); ioc.For<IProductService>().Use<EfProductService>(); }); } } }
در تعاریف فوق یک مورد را به فایل Startup.cs موکول کردهایم. برای مشخص سازی نمونهی پیاده سازی کنندهی IDataProtectionProvider نیاز است به IAppBuilder کلاس Startup برنامه دسترسی داشت. این کلاس آغازین Owin اکنون به نحو ذیل بازنویسی شدهاست و در آن، تنظیمات IDataProtectionProvider را به همراه وهله سازی CreatePerOwinContext مشاهده میکنید:
using System; using AspNetIdentityDependencyInjectionSample.IocConfig; using AspNetIdentityDependencyInjectionSample.ServiceLayer.Contracts; using Microsoft.AspNet.Identity; using Microsoft.Owin; using Microsoft.Owin.Security.Cookies; using Microsoft.Owin.Security.DataProtection; using Owin; using StructureMap.Web; namespace AspNetIdentityDependencyInjectionSample { public class Startup { public void Configuration(IAppBuilder app) { configureAuth(app); } private static void configureAuth(IAppBuilder app) { SmObjectFactory.Container.Configure(config => { config.For<IDataProtectionProvider>() .HybridHttpOrThreadLocalScoped() .Use(()=> app.GetDataProtectionProvider()); }); SmObjectFactory.Container.GetInstance<IApplicationUserManager>().SeedDatabase(); // Configure the db context, user manager and role manager to use a single instance per request app.CreatePerOwinContext(() => SmObjectFactory.Container.GetInstance<IApplicationUserManager>()); // Enable the application to use a cookie to store information for the signed in user // and to use a cookie to temporarily store information about a user logging in with a third party login provider // Configure the sign in cookie app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, LoginPath = new PathString("/Account/Login"), Provider = new CookieAuthenticationProvider { // Enables the application to validate the security stamp when the user logs in. // This is a security feature which is used when you change a password or add an external login to your account. OnValidateIdentity = SmObjectFactory.Container.GetInstance<IApplicationUserManager>().OnValidateIdentity() } }); app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie); // Enables the application to temporarily store user information when they are verifying the second factor in the two-factor authentication process. app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5)); // Enables the application to remember the second login verification factor such as phone or email. // Once you check this option, your second step of verification during the login process will be remembered on the device where you logged in from. // This is similar to the RememberMe option when you log in. app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie); } } }
تنظیمات برنامهی اصلی ASP.NET MVC، جهت اعمال تزریق وابستگیها
الف) ابتدا نیاز است فایل Global.asax.cs را به نحو ذیل بازنویسی کنیم:
using System; using System.Data.Entity; using System.Web; using System.Web.Mvc; using System.Web.Optimization; using System.Web.Routing; using AspNetIdentityDependencyInjectionSample.DataLayer.Context; using AspNetIdentityDependencyInjectionSample.IocConfig; using StructureMap.Web.Pipeline; namespace AspNetIdentityDependencyInjectionSample { public class MvcApplication : HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); setDbInitializer(); //Set current Controller factory as StructureMapControllerFactory ControllerBuilder.Current.SetControllerFactory(new StructureMapControllerFactory()); } protected void Application_EndRequest(object sender, EventArgs e) { HttpContextLifecycle.DisposeAndClearAll(); } public class StructureMapControllerFactory : DefaultControllerFactory { protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType) { if (controllerType == null) throw new InvalidOperationException(string.Format("Page not found: {0}", requestContext.HttpContext.Request.RawUrl)); return SmObjectFactory.Container.GetInstance(controllerType) as Controller; } } private static void setDbInitializer() { Database.SetInitializer(new MigrateDatabaseToLatestVersion<ApplicationDbContext, Configuration>()); SmObjectFactory.Container.GetInstance<IUnitOfWork>().ForceDatabaseInitialize(); } } }
ب) به پوشهی Models برنامه مراجعه کنید. در اینجا در هر کلاسی که Id از نوع string وجود داشت، باید تبدیل به نوع int شوند. چون primary key برنامه را به نوع int تغییر دادهایم. برای مثال کلاسهای EditUserViewModel و RoleViewModel باید تغییر کنند.
ج) اصلاح کنترلرهای برنامه جهت اعمال تزریق وابستگیها
اکنون اصلاح کنترلرها جهت اعمال تزریق وابستگیها سادهاست. در ادامه نحوهی تغییر امضای سازندههای این کنترلرها را جهت استفاده از اینترفیسهای جدید مشاهده میکنید:
[Authorize] public class AccountController : Controller { private readonly IAuthenticationManager _authenticationManager; private readonly IApplicationSignInManager _signInManager; private readonly IApplicationUserManager _userManager; public AccountController(IApplicationUserManager userManager, IApplicationSignInManager signInManager, IAuthenticationManager authenticationManager) { _userManager = userManager; _signInManager = signInManager; _authenticationManager = authenticationManager; } [Authorize] public class ManageController : Controller { // Used for XSRF protection when adding external logins private const string XsrfKey = "XsrfId"; private readonly IAuthenticationManager _authenticationManager; private readonly IApplicationUserManager _userManager; public ManageController(IApplicationUserManager userManager, IAuthenticationManager authenticationManager) { _userManager = userManager; _authenticationManager = authenticationManager; } [Authorize(Roles = "Admin")] public class RolesAdminController : Controller { private readonly IApplicationRoleManager _roleManager; private readonly IApplicationUserManager _userManager; public RolesAdminController(IApplicationUserManager userManager, IApplicationRoleManager roleManager) { _userManager = userManager; _roleManager = roleManager; } [Authorize(Roles = "Admin")] public class UsersAdminController : Controller { private readonly IApplicationRoleManager _roleManager; private readonly IApplicationUserManager _userManager; public UsersAdminController(IApplicationUserManager userManager, IApplicationRoleManager roleManager) { _userManager = userManager; _roleManager = roleManager; }
البته تعدادی اکشن متد نیز در اینجا وجود دارند که از string id استفاده میکنند. اینها را باید به int? Id تغییر داد تا با نوع primary key جدید مورد استفاده تطابق پیدا کنند.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید:
AspNetIdentityDependencyInjectionSample
معادل این پروژه جهت ASP.NET Core Identity : «سفارشی سازی ASP.NET Core Identity - قسمت اول - موجودیتهای پایه و DbContext برنامه »
تا قسمت قبل موفق شدیم فایل Program.cs برنامهی Minimal API's را خلوت کنیم و همچنین زیرساختی را برای توسعهی مبتنی بر ویژگیها، ارائه دهیم. اما ... هنوز endpoints ما چنین شکلی را دارند:
و یک چنین رویهای جهت کار مستقیم با DbContext در اکشن متدهای MVC هیچگاه توصیه نمیشود. برای مثال به طور معمول، عملیاتی که در بدنهی Lambda expressions فوق انجام شده، عموما به Repositories و Services محول شده و در نهایت از سرویسها، در اکشن متدها استفاده میشود. در معماری جاری که در پیش گرفتهایم، دو لایهی Repositories و Services حذف شدهاند و دیگر خبری از آنها نیست. در اینجا کار سرویسها و مخازن، به هندلرهای معماری CQRS واگذار خواهند شد. هر هندلر نیز متکی به خود است و مستقل از سایر هندلرها طراحی میشود و اینها صرفا بر اساس نیازهای ویژگی جاری توسعه خواهند یافت و دقیقا در همان پوشهی ویژگی مورد بررسی نیز قرار میگیرند؛ و نه پراکنده در لایهای و یا پروژهای دیگر. به این ترتیب درک یک ویژگی متکی به خود برنامه، سادهتر شده و در طول زمان، نگهداری و توسعهی آن نیز سادهتر خواهد شد. مشکل داشتن سرویسهایی بزرگ که در معماریهای متداول وجود دارند، استفادهی از متدهای آنها در چندین اکشن متد و چندین کنترلر مختلف است و اگر یکی از متدهای این سرویس بزرگ ما تغییر کند، بر روی چندین کنترلر تاثیر میگذارد که ممکن است سبب از کار افتادگی بعضی از آنها شود؛ اما در اینجا هرکاری که انجام میشود و هر هندلری که توسعه مییابد، فقط مختص به یک کار و یک ویژگی مشخص است.
ایجاد Command و هندلر مخصوص ایجاد یک نویسندهی جدید
در الگوی CQRS، یک دستور، کاری را بر روی بانک اطلاعاتی انجام میدهد. برای مثال در اینجا قرار است نویسندهای را ثبت کند. در ادامه میخواهیم بدنهی endpoints.MapPost فوق را با الگوی CQRS انطباق دهیم. به همین جهت به یک Command نیاز داریم:
اینترفیس IRequest کتابخانهی MediatR که در انتهای قسمت قبل به پروژه اضافه شد، چنین امضایی را دارد:
یعنی <IRequest<Author به این معنا است که قرار است «خروجی» این عملیات، یک Author باشد و CreateAuthorCommand میتواند شامل تمام خواصی باشد که در جهت برآورده کردن این دستور مورد نیاز هستند؛ برای مثال کل اطلاعات شیء AuthorDto در اینجا.
سپس نیاز به یک هندلر است تا دستور رسیده را پردازش کند:
اینترفیس IRequestHandler چنین امضایی را دارد:
که اولین آرگومان جنریک آن، همان Command ای است که قرار است پردازش کند و خروجی آن، اطلاعاتی است که قرار است بازگشت دهد. یعنی متد Handle فوق، قرار است عملیات endpoints.MapPost را پیاده سازی کند و در اینجا با استفاده از AutoMapper، انتسابهای آن حذف و ساده شدهاند و مابقی آن، با بدنهی lambda expression مربوط به endpoints.MapPost، یکی است. این هندلر، معادل یک یا چند متد از متدهای یک سرویس بزرگ است که در اینجا به صورت اختصاصی جهت پردازش فرمانی در کنار هم قرار میگیرند و متکی به خود هستند.
پس از این تغییرات، بدنهی lambda expression مربوط به endpoints.MapPost به صورت زیر تغییر کرده و ساده میشود:
در اینجا تزریق وابستگی IMediator را مشاهده میکنید. با فراخوانی متد Send آن، شیءای به هندلر متناظری ارسال شده، پردازش میشود و در نهایت شیءای را بازگشت خواهد داد. برای مثال در اینجا شیء Dto یک نویسنده به هندلر CreateAuthorCommandHandler ارسال و تبدیل به شیءای از نوع Author مربوط به دومین برنامه شده، سپس در بانک اطلاعاتی ذخیره میشود و در نهایت این نویسنده که اکنون به همراه یک Id نیز هست، بازگشت داده میشود. بنابراین هر هندلر یک object in و یک object out دارد که به عنوان آرگومانهای جنریک IRequestHandler تعریف میشوند.
نکته 1: await داخل بدنهی lambda expression مربوط به endpoints را فراموش نکنید. تمام متدهای IMediator از نوع aysnc هستند؛ هرچند روش نامگذاری SendAsync را رعایت نکردهاند و اگر این await فراموش شود، مشاهده خواهید کرد که برنامه در حین فراخوانی endpoints در مرورگر، در حالت هنگ و صبر کردن نامحدود قرار میگیرد، بدون اینکه کاری را انجام دهد و یا حتی استثنایی را صادر کند.
نکته 2: در پیاده سازی هندلر، استفاده از cancellationToken را نیز مشاهده میکنید. تقریبا تمام متدهای async مربوط به EF-Core به همراه پارامتری جهت دریافت cancellationToken هم هستند. اگر کاربری قصد لغو یک درخواست طولانی را داشته باشد و بر روی دکمهی stop مرورگر کلیک کند و یا حتی صفحه را چندین بار ریفرش کند، این به معنای abort درخواست(های) رسیدهاست. وجود این cancellationTokenها، بار سرور را کاهش داده و عملیات در حال اجرای سمت سرور را در یک چنین حالتهایی متوقف میکند.
البته هندلری که در اینجا تعریف شده، این cancellationToken را باید از mediator دریافت کند که در کدهای endpoint فوق، چنین نیست. برای رفع این مشکل باید به صورت زیر عمل کرد:
این مورد را میتوان به صورت یک best practice، به تمام endpoints اضافه کرد.
نکته 3: هندلرها عموما چیزی را بازگشت نمیدهند؛ صرف نظر از هندلر فوق که نیاز بوده تا Id شیء ذخیره شده را بازگشت دهد، عموما به همراه هیچ خروجی نیستند. به همین جهت در حین تعریف آنها فقط کافی است در آرگومانهای جنریک آنها، نوع خروجی را ذکر نکنیم:
در یک چنین حالتی، امضای IRequestHandler به صورت خودکار به همراه خروجی از نوع Unit خواهد بود:
که این Unit معادل Void در کتابخانهی mediator است و به نحو زیر در هندلرها مدیریت میشود:
در یک چنین حالتی، تعریف یک Command نیز بر اساس اینترفیس IRequest انجام میشود:
ایجاد Query و هندلر مخصوص بازگشت لیست نویسندهها
در الگوی CQRS، یک کوئری قرار است اطلاعاتی را بازگشت دهد و ... وضعیت بانک اطلاعاتی را تغییر نمیدهد. بنابراین در اینجا یک IRequest که قرار است لیستی از نویسندگان را بازگشت دهد، تعریف میکنیم. بدنهی آن هم میتواند خالی باشد و یا به همراه خواصی مانند اطلاعات صفحه بندی و یا مرتب سازی گزارشگیری رسیدهی از درخواست:
سپس نیاز به یک هندلر است تا درخواست رسیده را پردازش کند. این هندلر، کوئری فوق را دریافت کرده و لیست کاربران را بازگشت میدهد:
پس از این تغییرات، بدنهی lambda expression مربوط به endpoints.MapGet به صورت زیر تغییر کرده و ساده میشود:
مزیت استفادهی از الگوی CQRS، تنها به حذف لایهی سرویس و رسیدن به ویژگیهایی مستقل و متکی به خود، منحصر نیست. با استفاده از این الگو میتوان مقیاس پذیری برنامه را نیز افزایش داد. برای مثال یک بانک اطلاعاتی بهینه سازی شده را صرفا برای کوئریها، درنظر گرفت و بانک اطلاعاتی دیگری را تنها برای اعمال Write که Commands بر روی آن اجرا میشوند و در اینجا تنها نیاز به همگام سازی اطلاعات بانک اطلاعاتی Write، با بانک اطلاعاتی Read است که در بسیاری از اوقات پرکارتر از بانکهای اطلاعاتی دیگر است:
و یا حتی معماری CQRS با معماری Event store نیز قابل ترکیب است:
در اینجا بجای استفاده از بانک اطلاعاتی Write، از یک Event store استفاده میشود. کار event store، دریافت رویدادهای write است و سپس باز پخش آنها به بانک اطلاعاتی Read؛ تا کار همگام سازی به این نحو صورت گیرد.
روشی برای نظم دادن به نحوهی تعریف کلاسهای الگوی CQRS
تا اینجا برای مثال کلاسCreateAuthorCommand را در یک فایل مجزا و سپس هندلر آنرا به نام CreateAuthorCommandHandler در یک فایل دیگر تعریف کردیم. میتوان جهت بالابردن خوانایی برنامه، کاهش رفت و برگشتها برای یافتن کلاسهای مرتبط و همچنین سهولت یافتن هندلرهای مرتبط با هر متد mediator.Send، از روش زیر نیز استفاده کرد:
در اینجا از nested classes استفاده شدهاست. ابتدا نام اصلی Command و یا کوئری ذکر میشود؛ که نام کلاس دربرگیرندهی اصلی را تشکیل میدهد. سپس دو کلاس بعدی فقط Command و Handler نام میگیرند و نه هیچ نام دیگری. به این ترتیب به یکسری نام یک دست در کل پروژه خواهیم رسید. زمانیکه قرار است mediator.Send فراخوانی شود، اینبار چنین شکلی را پیدا میکند که مزیت آن، سهولت یافتن هندلر مرتبط، فقط با پیگیری کلاس اصلی CreateAuthor است:
در مورد کوئریها هم میتوان به قالب مشابهی رسید که در اینجا هم کوئری و هندلر آن، ذیل نام اصلی مدنظر قرار میگیرند:
و اگر کدهای نهایی این سری را که از قسمت اول قابل دریافت است بررسی کنید، از همین ساختار یکدست، برای تعاریف دستورات و کوئریها استفاده شدهاست.
endpoints.MapGet("/api/authors", async (MinimalBlogDbContext ctx) => { var authors = await ctx.Authors.ToListAsync(); return authors; }); endpoints.MapPost("/api/authors", async (MinimalBlogDbContext ctx, AuthorDto authorDto) => { var author = new Author(); author.FirstName = authorDto.FirstName; author.LastName = authorDto.LastName; author.Bio = authorDto.Bio; author.DateOfBirth = authorDto.DateOfBirth; ctx.Authors.Add(author); await ctx.SaveChangesAsync(); return author; });
ایجاد Command و هندلر مخصوص ایجاد یک نویسندهی جدید
در الگوی CQRS، یک دستور، کاری را بر روی بانک اطلاعاتی انجام میدهد. برای مثال در اینجا قرار است نویسندهای را ثبت کند. در ادامه میخواهیم بدنهی endpoints.MapPost فوق را با الگوی CQRS انطباق دهیم. به همین جهت به یک Command نیاز داریم:
using MediatR; using MinimalBlog.Domain.Model; namespace MinimalBlog.Api.Features.Authors; public class CreateAuthorCommand : IRequest<Author> { public AuthorDto AuthorDto { get; set; } = default!; }
public interface IRequest<out TResponse> : IBaseRequest
سپس نیاز به یک هندلر است تا دستور رسیده را پردازش کند:
namespace MinimalBlog.Api.Features.Authors; public class CreateAuthorCommandHandler : IRequestHandler<CreateAuthorCommand, Author> { private readonly MinimalBlogDbContext _context; private readonly IMapper _mapper; public CreateAuthorCommandHandler(MinimalBlogDbContext context, IMapper mapper) { _context = context ?? throw new ArgumentNullException(nameof(context)); _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); } public async Task<Author> Handle(CreateAuthorCommand request, CancellationToken cancellationToken) { if (request == null) { throw new ArgumentNullException(nameof(request)); } var toAdd = _mapper.Map<Author>(request.AuthorDto); _context.Authors.Add(toAdd); await _context.SaveChangesAsync(cancellationToken); return toAdd; } }
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse>
پس از این تغییرات، بدنهی lambda expression مربوط به endpoints.MapPost به صورت زیر تغییر کرده و ساده میشود:
endpoints.MapPost("/api/authors", async (IMediator mediator, AuthorDto authorDto) => { var command = new CreateAuthorCommand { AuthorDto = authorDto }; var author = await mediator.Send(command); return author; });
نکته 1: await داخل بدنهی lambda expression مربوط به endpoints را فراموش نکنید. تمام متدهای IMediator از نوع aysnc هستند؛ هرچند روش نامگذاری SendAsync را رعایت نکردهاند و اگر این await فراموش شود، مشاهده خواهید کرد که برنامه در حین فراخوانی endpoints در مرورگر، در حالت هنگ و صبر کردن نامحدود قرار میگیرد، بدون اینکه کاری را انجام دهد و یا حتی استثنایی را صادر کند.
نکته 2: در پیاده سازی هندلر، استفاده از cancellationToken را نیز مشاهده میکنید. تقریبا تمام متدهای async مربوط به EF-Core به همراه پارامتری جهت دریافت cancellationToken هم هستند. اگر کاربری قصد لغو یک درخواست طولانی را داشته باشد و بر روی دکمهی stop مرورگر کلیک کند و یا حتی صفحه را چندین بار ریفرش کند، این به معنای abort درخواست(های) رسیدهاست. وجود این cancellationTokenها، بار سرور را کاهش داده و عملیات در حال اجرای سمت سرور را در یک چنین حالتهایی متوقف میکند.
البته هندلری که در اینجا تعریف شده، این cancellationToken را باید از mediator دریافت کند که در کدهای endpoint فوق، چنین نیست. برای رفع این مشکل باید به صورت زیر عمل کرد:
endpoints.MapGet("/api/authors", async (IMediator mediator, CancellationToken ct) => { var request = new GetAllAuthorsQuery(); var authors = await mediator.Send(request, ct); return authors; });
نکته 3: هندلرها عموما چیزی را بازگشت نمیدهند؛ صرف نظر از هندلر فوق که نیاز بوده تا Id شیء ذخیره شده را بازگشت دهد، عموما به همراه هیچ خروجی نیستند. به همین جهت در حین تعریف آنها فقط کافی است در آرگومانهای جنریک آنها، نوع خروجی را ذکر نکنیم:
public class Handler : IRequestHandler<Command>
public interface IRequestHandler<in TRequest> : IRequestHandler<TRequest, Unit> where TRequest : IRequest<Unit>
public async Task<Unit> Handle(Command request, CancellationToken cancellationToken) { // ... return Unit.Value; }
public class Command : IRequest
ایجاد Query و هندلر مخصوص بازگشت لیست نویسندهها
در الگوی CQRS، یک کوئری قرار است اطلاعاتی را بازگشت دهد و ... وضعیت بانک اطلاعاتی را تغییر نمیدهد. بنابراین در اینجا یک IRequest که قرار است لیستی از نویسندگان را بازگشت دهد، تعریف میکنیم. بدنهی آن هم میتواند خالی باشد و یا به همراه خواصی مانند اطلاعات صفحه بندی و یا مرتب سازی گزارشگیری رسیدهی از درخواست:
using MediatR; using MinimalBlog.Domain.Model; namespace MinimalBlog.Api.Features.Authors; public class GetAllAuthorsQuery : IRequest<List<Author>> { }
namespace MinimalBlog.Api.Features.Authors; public class GetAllAuthorsHandler : IRequestHandler<GetAllAuthorsQuery, List<Author>> { private readonly MinimalBlogDbContext _context; public GetAllAuthorsHandler(MinimalBlogDbContext context) { _context = context ?? throw new ArgumentNullException(nameof(context)); } public Task<List<Author>> Handle(GetAllAuthorsQuery request, CancellationToken cancellationToken) { return _context.Authors.ToListAsync(cancellationToken); } }
endpoints.MapGet("/api/authors", async (IMediator mediator) => { var request = new GetAllAuthorsQuery(); var authors = await mediator.Send(request); return authors; });
و یا حتی معماری CQRS با معماری Event store نیز قابل ترکیب است:
در اینجا بجای استفاده از بانک اطلاعاتی Write، از یک Event store استفاده میشود. کار event store، دریافت رویدادهای write است و سپس باز پخش آنها به بانک اطلاعاتی Read؛ تا کار همگام سازی به این نحو صورت گیرد.
روشی برای نظم دادن به نحوهی تعریف کلاسهای الگوی CQRS
تا اینجا برای مثال کلاسCreateAuthorCommand را در یک فایل مجزا و سپس هندلر آنرا به نام CreateAuthorCommandHandler در یک فایل دیگر تعریف کردیم. میتوان جهت بالابردن خوانایی برنامه، کاهش رفت و برگشتها برای یافتن کلاسهای مرتبط و همچنین سهولت یافتن هندلرهای مرتبط با هر متد mediator.Send، از روش زیر نیز استفاده کرد:
public static class CreateAuthor { public class Command : IRequest<AuthorGetDto> { // ... } public class Handler : IRequestHandler<Command, AuthorGetDto> { // ... } }
var command = new CreateAuthor.Command { AuthorDto = authorDto }; var author = await mediator.Send(command, ct);
در مورد کوئریها هم میتوان به قالب مشابهی رسید که در اینجا هم کوئری و هندلر آن، ذیل نام اصلی مدنظر قرار میگیرند:
public static class GetAllAuthors { public class Query : IRequest<List<AuthorGetDto>> { //... } public class Handler : IRequestHandler<Query, List<AuthorGetDto>> { //... } }
اشتراکها
دوره مبانی Azure
اشتراکها
Immutable Collections در دات نت
اشتراکها