قسمت اول - الگوهای سازنده (Best C# article of July 2012 - Best overall article of July 2012)
قسمت دوم - الگوهای ساختاری
سایت Patterns.dev
معرفی الگوی استراتژی
چیزی که ما تا الان شنیدم در مورد شی گرایی اینه که خیلی خوبه و همیشه کار راه انداز هستش, ولی واقعیت اینه که همیشه این طوری نیست که بخواد کار راه انداز باشه و باعث کاهش حجم کد بشه اتفاقا بعضی جاها ممکنه استفاده از ویژگیهای شی گرایی باعث افزایش حجم کد و سختتر شدن نگه داری کد بشه
فرض رو میزاریم که به ما گفتن قراره یه سیستم نرم افزاری جدید طراحی کنیم( اینجا سعی میکنم اول با یه بازی شروع کنیم تا بعدا اگه فرصت شد توی دنیای واقعی این مسائل رو مطرح کنیم) وقتی میریم و در مورد این سیستم صحبت میکنیم متوجه میشیم که قراره یه بازی خیلی ساده طراحی کنیم
آشنایی با الگوی MVVM
حدود یک سال قبل الگوی MVVM زیاد معروف نبود (Model-View-ViewModel pattern). اما در 6 ماه اخیر، این الگو به یک متدولوژی جدی توسعه برنامههای WPF و سیلورلایت تبدیل شده. نمیشود به یک وبلاگ خوب WPF سر زد و خبری از این روش نباشد. حتی فریم ورکهایی هم برای آن طراحی شده که لیست آنها را در این مقاله میتوانید مشاهده نمائید.
مزایای این الگو چیست؟
- جدا سازی Model و View
- تولید کدهایی با قابلیت تست بالا
- فایلهای code-behind ایی با حداقل کد
اگر علاقمند به آشنایی با این الگوی طراحی باشید ویدیوی آموزشی زیر در طی یک ساعت و نیم به توضیح این مطلب پرداخته است.
ماخذ
همانطور که قول داده بودم، به اصول GRASP میپردازیم.
اصول GRASP-General Responsibility Assignment
Software Principles
این اصول به بررسی نحوه تقسیم وظایف بین کلاسها و مشارکت اشیاء برای به انجام رساندن یک مسئولیت میپردازند. اینکه هر کلاس در ساختار نرم افزار چه وظیفهای دارد و چگونه با کلاسهای دیگر مشارکت میکند تا یک عملکرد به سیستم اضافه گردد. این اصول به چند بخش تقسیم میشوند:
- کنترلر ( Controller )
- ایجاد کننده ( Creator )
- انسجام قوی ( High Cohesion )
- واسطه گری ( Indirection )
- دانای اطلاعات ( Information Expert )
- اتصال ضعیف ( Low Coupling )
- چند ریختی ( Polymorphism )
- حفاظت از تاثیر تغییرات ( Protected Variations )
- مصنوع خالص ( Pure Fabrication )
Controller
این الگو بیان میکند که مسئولیت پاسخ به رویدادهای (Events ) یک سناریوی محدود مانند یک مورد کاربردی ( Use Case ) باید به عهده یک کلاس غیر UI باشد. کنترلر باید کارهایی را که نیاز است در پاسخ رویداد انجام شود، به دیگران بسپرد و نتایج را طبق درخواست رویداد بازگرداند. در اصل، کنترلر دریافت کننده رویداد، راهنمای مسیر پردازش برای پاسخ به رویداد و در نهایت برگرداننده پاسخ به سمت مبداء رویداد است. در زیر مثالی را میبینیم که رویداد اتفاق افتاده توسط واسط گرافیکی به سمت یک handler (که متدی است با ورودیِ فرستنده و آرگمانهای مورد نیاز) در کنترلر فرستاده میشود. این روش event handling، در نمونههای وب فرم و ویندوز فرم دیده میشود. به صورتی خود کلاسهای .Net وظیفه Event Raising از سمت UI با کلیک روی دکمه را انجام میدهد:
public class UserController { protected void OnClickCreate(object sender, EventArgs e) { // call validation services // call create user services } }
در مثال بعد عملیات مربوط به User در یک WebApiController پاسخ داده میشود. در اینجا به جای استفاده از Event Raising برای کنترل کردن رویداد، از فراخوانی یک متد در کنترلر توسط درخواست HttpPost انجام میگیرد. در اینجا نیاز است که در سمت کلاینت درخواستی را ارسال کنیم:
public class UserWebApiController { [HttpPost] public HttpResponseMessage Create(UserViewModel user) { // call validation services // call create user services } }
Creator :
این اصل میگوید شیء ای میتواند یک شیء دیگر را بسازد ( instantiate ) که: (اگر کلاس B بخواهد کلاس A را instantiate کند)
- کلاس B شیء از کلاس A را در خود داشته باشد؛
- یا اطلاعات کافی برای instantiate کردن از A را داشته باشد؛
- یا به صورت نزدیک با A در ارتباط باشد؛
- یا بخواهد شیء A را ذخیره کند.
از آنجایی که این اصل بدیهی به نظر میرسد، با مثال نقض، درک بهتری را نسبت به آن میتوان پیدا کرد:
// سازنده public class B { public static A CreateA(string name, string lastName, string job) { return new A() { Name =name, LastName = lastName, Job = job }; } } // ایجاد شونده public class A { public string Name { get; set; } public string LastName { get; set; } public string Job { get; set; } } public class Context { public void Main() { var name = "Rasoul"; var lastName = "Abbasi"; var job = "Developer"; var obj = B.CreateA(name, lastName, job); } }
و اما چرا این مثال، اصل Creator را نقض میکند. در مثال میبینید که کلاس B، یک شیء از نوع A را در متد Main کلاس Context ایجاد میکند. کلاس B فقط یک متد برای تولید A دارد و در عملیات تولید A هیچ منطق خاصی را پیاده سازی نمیکند.کلاس B شیء ای را از کلاس A ، در خود ندارد، با آن ارتباط نزدیک ندارد و آنرا ذخیره نمیکند. با اینکه کلاس B اطلاعات کافی را برای تولید A از ورودی میگیرد، ولی این کلاس Context است که اطلاعات کافی را ارسال مینماید. اگر در کلاس B منطقی اضافه بر instance گیریِ ساده وجود داشت (مانند بررسی صحت و اعتبار سنجی)، میتوانستیم بگوییم کلاس B از یک مجموعه عملیات instance گیری با خبر است که کلاس Context نباید از آن خبر داشته باشد. لذا اکنون هیچ دلیلی وجود ندارد که وظیفه تولید A را در Context انجام ندهیم و این مسئولیت را به کلاس B منتقل کنیم. این مورد ممکن است در ذهن شما با الگوی Factory تناقض داشته باشد. ولی نکته اصلی در الگو Factory انجام عملیات instance گیری با توجه به منطق برنامه است؛ یعنی وظیفهای که کلاس Context نباید از آن خبر داشته باشد را به کلاس Factory منتقل میکنیم. در غیر اینصورت ایجاد کلاس Factory بی معنا خواهد بود (مگر به عنوان افزایش انعطاف پذیری معماری که بتوان به راحتی نوع پیاده سازی یک واسط را تغییر داد).
High Cohesion :
این اصل اشاره به یکی از اصول اساسی طراحی نرم افزار دارد. انسجام واحدهای نرم افزاری باعث افزایش خوانایی، سهولت اشکال زدایی، قابلیت نگهداری و کاهش تاثیر زنجیرهای تغییرات میشود. طبق این اصل، مسئولیتهای هر واحد باید مرتبط باشد. لذا اجزایی کوچک با مسئولیتهای منسجم و متمرکز بهتر از اجزایی بزرگ با مسئولیتهای پراکنده است. اگر واحدهای سازنده نرم افزار انسجام ضعیفی داشته باشند، درک همکاریها، استفاده مجدد آنها، نگه داری نرم افزار و پاسخ به تغییرات سختتر خواهد شد.
در مثال زیر نقض این اصل را مشاهده میکنیم:
class Controller { public void CreateProduct(string name, int categoryId) { } public void EditProduct(int id, string name) { } public void DeleteProduct(int id) { } public void CreateCategory(string name) { } public void EditCategory(int id, string name) { } public void DeleteCategory(int id) { } }
همانطور که میبینید، کلاس
کنترلر ما، مسئولیت مدیریت Product و Category را بر عهده دارد. بزرگ شدن این کلاس، باعث سختتر شدن
خواندن کد و رفع اشکال میگردد. با جداسازی کنترلر مربوط به Product از Category میتوان انسجام را بالا برد.
Indirection :
این اصل بیان میکند که با تعریف یک واسط بین دو مولفه نرم افزاری میتوان میزان اتصال نرم افزار را کاهش داد. بدین ترتیب وظیفه هماهنگی ارتباط دو مؤلفه، به عهده این واسط خواهد بود و نیازی نیست دادههای ورودی و خروجی دو مؤلفه، هماهنگ باشند. در اینجا واسط، از وابستگی بین دو مؤلفه با پنهان کردن ضوابط هر مؤلفه از دیگری و ایجاد وابستگی ضعیف خود با دو مؤلفه، باعث کاهش اتصال کلی طراحی میگردد.
الگوهای Adapter و Delegate و همچنین نقش کنترلر در الگوی معماری MVC از این اصل پیروی میکنند.
class SenderA { public Mediator mediator { get; } public SenderA() { mediator = new Mediator(); } public void Send(string message, string reciever) { mediator.Send(message, reciever); } } class SenderB { public Mediator mediator { get; } public SenderB() { mediator = new Mediator(); } public void Send(string message) { } } public class RecieverA { public void DoAction(string message) { // انجام عملیات بر اساس پیغام دریافت شده switch (message) { case "create": break; case "delete": break; default: break; } } } public class RecieverB { public void DoAction(string message) { // انجام عملیات بر اساس پیغام دریافت شده switch (message) { case "edit": break; case "rollback": break; default: break; } } } class Mediator { internal void Send(string message, string reciever) { switch (reciever) { case "A": var recieverObjA = new RecieverA(); recieverObjA.DoAction(message); break; case "B": var recieverObjB = new RecieverB(); recieverObjB.DoAction(message); break; default: break; } } } class IndirectionContext { public void Main() { var senderA = new SenderA(); senderA.Send("rollback", "B"); var senderB = new SenderA(); senderB.Send("create", "A"); } }
در این مثال کلاس Mediator به عنوان واسط ارتباطی بین کلاسهای Sender و Receiver قرار گرفته و نقش تحویل پیغام را دارد.
در مقاله بعدی، به بررسی سایر اصول GRASP خواهم پرداخت.
کمپین ضد IF !
بکارگیری بیش از حد If و خصوصا Switch برخلاف اصول طراحی شیءگرا است؛ تا این حد که یک کمپین ضد IF هم وجود دارد!
البته سایت فوق بیشتر جنبه تبلیغی برای سمینارهای گروه مذکور را دارد تا اینکه جنبهی آموزشی/خود آموزی داشته باشد.
یک مثال کاربردی:
فرض کنید دارید یک سیستم گزارشگیری را طراحی میکنید. به جایی میرسید که نیاز است با Aggregate functions سروکار داشته باشید؛ مثلا جمع مقادیر یک ستون را نمایش دهید یا معدل امتیازهای نمایش داده شده را محاسبه کنید و امثال آن. طراحی متداول آن به صورت زیر خواهد بود:
using System.Collections.Generic;
using System.Linq;
namespace CircularDependencies
{
public enum AggregateFunc
{
Sum,
Avg
}
public class AggregateFuncCalculator
{
public decimal Calculate(IList<decimal> list, AggregateFunc func)
{
switch (func)
{
case AggregateFunc.Sum:
return getSum(list);
case AggregateFunc.Avg:
return getAvg(list);
default:
return 0m;
}
}
private decimal getAvg(IList<decimal> list)
{
if (list == null || !list.Any()) return 0;
return list.Sum() / list.Count;
}
private decimal getSum(IList<decimal> list)
{
if (list == null || !list.Any()) return 0;
return list.Sum();
}
}
}
در کلاس AggregateFuncCalculator یک متد Calculate داریم که توسط آن قرار است روی list دریافتی یک سری عملیات انجام شود. عملیات پشتیبانی شده هم توسط یک enum معرفی شده؛ برای مثال اینجا فقط جمع و میانگین پشتیبانی میشوند.
و مشکل طراحی این کلاس، همان switch است که برخلاف اصول طراحی شیءگرا میباشد. یکی از اصول طراحی شیءگرا بر این مبنا است که:
یک کلاس باید جهت تغییر، بسته اما جهت توسعه، باز باشد.
یعنی چی؟
داستان طراحی Aggregate functions که فقط به جمع و میانگین خلاصه نمیشود. امروز میگویند واریانس چطور؟ فردا خواهند گفت حداقل و حداکثر چطور؟ پس فردا ...
به عبارتی این کلاس جهت تغییر بسته نیست و هر روز باید بر اساس نیازهای جدید دستکاری شود.
چکار باید کرد؟
آیا میتوانید در کلاس AggregateFuncCalculator یک الگوی تکراری را تشخیص دهید؟ الگوی تکراری موجود، محاسبات بر روی یک لیست است. پس میشود بر اساس آن یک اینترفیس عمومی را تعریف کرد:
public interface IAggregateFunc
{
decimal Calculate(IList<decimal> list);
}
اکنون هر کدام از پیاده سازیهای موجود در کلاس AggregateFuncCalculator را به یک کلاس جدا منتقل خواهیم کرد تا یک اصل دیگر طراحی شیءگرا نیز محقق شود:
هر کلاس باید تنها یک کار را انجام دهد.
public class Sum : IAggregateFunc
{
public decimal Calculate(IList<decimal> list)
{
if (list == null || !list.Any()) return 0;
return list.Sum();
}
}
public class Avg : IAggregateFunc
{
public decimal Calculate(IList<decimal> list)
{
if (list == null || !list.Any()) return 0;
return list.Sum() / list.Count;
}
}
تا اینجا 2 هدف مهم حاصل شده است:
- کم کم کلاس AggregateFuncCalculator دارد خلوت میشود. قرار است هر کلاس یک کار را بیشتر انجام ندهد.
- برنامه از بسته بودن جهت توسعه هم خارج شده است (یکی دیگر از اصول طراحی شیءگرا). اگر تعاریف توابع محاسباتی را تماما در یک کلاس قرار دهیم صاحب اول و آخر آن کتابخانه خودمان خواهیم بود. این کلاس بسته است جهت تغییر. اما با معرفی IAggregateFunc، من امروز 2 تابع را تعریف کردهام، شما فردا توابع خاص خودتان را تعریف کنید. باز هم برنامه کار خواهد کرد. نیازی نیست تا من هر روز یک نگارش جدید از کتابخانه را ارائه دهم که در آن فقط یک تابع دیگر اضافه شده است.
اکنون یکی از چندین و چند روش بازنویسی کلاس AggregateFuncCalculator به صورت زیر میتواند باشد
public class AggregateFuncCalculator
{
public decimal Calculate(IList<decimal> list, IAggregateFunc func)
{
return func.Calculate(list);
}
}
بله! دیگر سوئیچی در کار نیست. این کلاس تنها یک کار را انجام میدهد. همچنین دیگر نیازی به تغییر هم ندارد (محاسبات از آن خارج شده) و باز است جهت توسعه (شما نگارشهای دلخواه IAggregateFunc دیگر خود را توسعه داده و استفاده کنید).
در این مطلب قصد داریم علاوه بر طراحی زیرساختی برای راه اندازی هرچه سریعتر ServiceLayer، طراحی ای برای مکانیزم Validation به عنوان یک Cross Cutting Concern، نیز ارائه داده و آن را پیاده سازی کنیم.
پیش نیازها:
- قبلا در سایت در مورد لایه بندی نرم افزار و ServiceLayer مطلب منتشر شده است؛ لذا مطالعه این سری مقالات برگرفته از کتاب Professional ASP.NET Design Patterns جزء پیش نیازهای این مطلب میباشد.
- دوره Aspect oriented programming
- مطالب مربوط به کتابخانه FluentValidation
- دوره بررسی مفاهیم معکوس سازی وابستگیها و ابزارهای مرتبط با آن
- دوره AutoMapper
ServiceLayer در معماری لایهای، در برگیرنده ApplicationService هایی میباشد که به عنوان مدخل ورودی (Entry Point) برنامه، در معرض دید لایه Presentation قرار گرفته و داده را به فرمت مورد نیاز Presentation در اختیارش قرار خواهند داد.
این سرویسها DTOها را به عنوان پارامتر دریافت کرده و DTO هایی را به عنوان خروجی برگشت خواهند داد. مباحثی مانند Logging، Caching، Business Validation Authorization و مدیریت تراکنشها را میتوان در این لایه در نظر گرفت.
در ادامه اگر واژه «سرویس» به کار گرفته میشود منظور ما ApplicationServiceها میباشند.
کار را با ارائه یکسری واسط و کلاس پایه برای عملیات CRUD در سرویسها به صورت زیر پیش میبریم.
قرار است به صورت قراردادی، تمام سرویسهای ما واسط زیر را پیاده سازی کرده باشند. این مورد در مباحث تعریف Policyهای مربوط به StructureMap مفید خواهد بود.
namespace MvcFramework.Framework.Application.Services { public interface IApplicationService : ITransientDependency { } }
دو واسط دیگر برای اعمال طول عمر اشیاء به صورت قراردادی در StructureMap به شکل زیر در نظر گرفته شدهاند.
namespace MvcFramework.Framework.Dependency { public interface ISingletonDependency { } } namespace MvcFramework.Framework.Dependency { public interface ITransientDependency { } }
و با پیاده سازی یک LifeCyclePolicy از دو واسط بالا به شکل زیر استفاده خواهیم کرد.
namespace MvcFramework.Framework.Dependency { public class LifeCyclePolicy : IInstancePolicy { public void Apply(Type pluginType, Instance instance) { if (typeof(ISingletonDependency).IsAssignableFrom(instance.ReturnedType)) instance.SetLifecycleTo<SingletonLifecycle>(); else if (typeof(ITransientDependency).IsAssignableFrom(instance.ReturnedType)) instance.SetLifecycleTo<TransientLifecycle>(); } } }
به این صورت تنظیم طول عمر اشیاء ساخته شده توسط StructureMap این بار به صورت قرادادی بوده و لازم به ذکر تک تک این موارد در تنظیمات اولیه مربوط به Container آن نیست.
کلاس پایهای را که پیاده ساز واسط IApplicationService میباشد، برای مقابله با عدم نگارش پذیری واسطها، به شکل زیر در نظر میگیریم.
namespace MvcFramework.Framework.Application.Services { public abstract class ApplicationService : IApplicationService { } }
بسته به نیاز پروژه خودتان میتوانید اعضای مشترک بین سرویسها را در دل این کلاس قرار دهید.
در ادامه واسط ICrudApplicationSevie را به شکل زیر طراحی خواهیم کرد.
namespace MvcFramework.Framework.Application.Services { public interface ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel> : ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, PagedListRequest, PagedListResponse<TModel, PagedListRequest>, DynamicListRequest> where TEditModel : class, IEditModel where TModel : class, IModel where TDeleteModel : class, IDeleteModel { } public interface ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, in TDynamicListRequest> : ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, PagedListRequest, PagedListResponse<TModel, PagedListRequest>, TDynamicListRequest> where TEditModel : class, IEditModel where TModel : class, IModel where TDeleteModel : class, IDeleteModel where TDynamicListRequest : DynamicListRequest { } public interface ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, in TPagedListRequest, TPagedListResponse> : ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, TPagedListRequest, TPagedListResponse, DynamicListRequest> where TEditModel : class, IEditModel where TModel : class, IModel where TDeleteModel : class, IDeleteModel where TPagedListRequest : PagedListRequest, new() where TPagedListResponse : PagedListResponse<TModel, TPagedListRequest> { } public interface ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, in TPagedListRequest, TPagedListResponse, in TDynamicListRequest> : IApplicationService where TEditModel : class, IEditModel where TModel : class, IModel where TDeleteModel : class, IDeleteModel where TPagedListRequest : PagedListRequest, new() where TPagedListResponse : PagedListResponse<TModel, TPagedListRequest> where TDynamicListRequest : DynamicListRequest { void Create(TCreateModel model); void Create(IList<TCreateModel> models); Task CreateAsync(TCreateModel model); Task CreateAsync(IList<TCreateModel> models); IList<TModel> GetList(); DynamicListResponse GetDynamicList(TDynamicListRequest request); TPagedListResponse GetPagedList(TPagedListRequest request); IList<LookupItem> GetLookup(); TModel GetById(long id); TEditModel GetForEdit(long id); bool Exists(long id); Task<IList<TModel>> GetListAsync(); Task<DynamicListResponse> GetDynamicListAsync(TDynamicListRequest request); Task<TPagedListResponse> GetPagedListAsync(TPagedListRequest request); Task<IList<LookupItem>> GetLookupAsync(); Task<TModel> GetByIdAsync(long id); Task<TEditModel> GetForEditAsync(long id); Task<bool> ExistsAsync(long id); void Edit(TEditModel model); void Edit(IList<TEditModel> models); Task EditAsync(TEditModel model); Task EditAsync(IList<TEditModel> models); void Delete(TDeleteModel model); void Delete(IList<TDeleteModel> models); Task DeleteAsync(TDeleteModel model); Task DeleteAsync(IList<TDeleteModel> models); } }
سرویسی که نیاز دارد از عملیات CRUD نیز پشتیبانی داشته باشد، بهتر است واسط آن از یک چنین واسطی که در بالا معرفی شد، ارث بری کند.
مدلها و واسطهای پیش فرضی را که در واسط بالا از آنها استفاده شده است، در زیر مشاهده میکنید:
واسط IModel
namespace MvcFramework.Framework.Application.Models { public interface IModel { long Id { get; set; } } }
واسط IEditModel
namespace MvcFramework.Framework.Application.Models { public interface IEditModel : IModel { byte[] RowVersion { get; set; } } }
واسط IDeleteModel
namespace MvcFramework.Framework.Application.Models { public interface IDeleteModel : IModel { byte[] RowVersion { get; set; } } }
کلاس LookupItem
namespace MvcFramework.Framework.Application.Models { public class LookupItem { public string Value { get; set; } public string Text { get; set; } public bool Selected { get; set; } } }
کلاس PagedListRequest
namespace MvcFramework.Framework.Application.Models { public class PagedListRequest : IShouldNormalize { public long TotalCount { get; set; } public int PageNumber { get; set; } public int PageSize { get; set; } /// <summary> /// Sorting information. /// Should include sorting field and optionally a direction (ASC or DESC) /// Can contain more than one field separated by comma (,). /// </summary> /// <example> /// Examples: /// "Name" /// "Name DESC" /// "Name ASC, Age DESC" /// </example> public string SortBy { get; set; } public void Normalize() { if (PageNumber < 1) PageNumber = 1; if (PageSize < 0) PageSize = 10; if (SortBy.IsEmpty()) SortBy = "Id DESC"; } } }
در این طراحی دو شکل از GetPagedList در نظر گرفته شده است؛ یکی با ورودی و خروجی داینامیک مثلا جهت استفاده برای نمایش اطلاعات در کندو گرید که در ادامه با آن بیشتر آشنا خواهید شد و دیگری هم برای زمانیکه نیاز دارید اطلاعات صفحه بندی شدهای را در اختیار داشته باشید. کلاس بالا برای پیاده سازی شکل دومی که صحبت شد، استفاده میشود. پیاده سازی واسط IShouldNormalize باعث خواهد شد که قبل از اجرای خود متد، این نوع پارامترها با استفاده از یک Interceptor شناسایی شده و متد Normalize آنها اجرا شود.
کلاس PagedListResponse
namespace MvcFramework.Framework.Application.Models { public class PagedListResponse<TModel, TPagedListRequest> where TPagedListRequest : PagedListRequest, new() where TModel : IModel { public PagedListResponse() { Result = new List<TModel>(); Request = new TPagedListRequest(); } public IList<TModel> Result { get; set; } public TPagedListRequest Request { get; set; } } }
کلاس بالا به عنوان نوع خروجی متد GetPagedList مورد استفاده قرار میگرد. وجود خصوصیتی از نوع PagedListRequest هم برای مواردی مانند صفحه بندی نیز میتواند مفید باشد.
کلاسهای DynamicListRequest و DynamicListResponse برگرفته از کتابخانه Kendo.DynamicLinq می باشند.
کلاس Entity
namespace MvcFramework.Framework.Domain.Entities { public abstract class Entity { #region Properties public long Id { get; set; } public byte[] RowVersion { get; set; } public EntityChangeState State { get; set; } #endregion #region Public Methods [SuppressMessage("ReSharper", "BaseObjectGetHashCodeCallInGetHashCode")] [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")] public override int GetHashCode() { if (IsTransient()) return base.GetHashCode(); unchecked { var hash = this.GetRealType().GetHashCode(); return (hash * 31) ^ Id.GetHashCode(); } } public virtual bool IsTransient() { return Id == 0; } public override bool Equals(object obj) { var other = obj as Entity; if (ReferenceEquals(other, null)) return false; if (ReferenceEquals(this, other)) return true; var typeOfThis = this.GetRealType(); var typeOfOther = other.GetRealType(); if (typeOfThis != typeOfOther) return false; if (IsTransient() || other.IsTransient()) return false; return Id.Equals(other.Id); } public override string ToString() { return $"[{this.GetRealType().Name} : {Id}]"; } #endregion #region Operators public static bool operator ==(Entity left, Entity right) { return Equals(left, right); } public static bool operator !=(Entity left, Entity right) { return !(left == right); } #endregion } }
در این کلاس یکسری خصوصیات پایه ای مانند Id و متدهای مشترک بین Entityها قرار گرفته شده است. این کلاس پایه تمام Entityهای سیستم میباشد.
پیاده سازی پیش فرض از واسط ICrudApplicationService به شکل زیر میباشد.
namespace MvcFramework.Framework.Application.Services { public abstract class CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel> : CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel, PagedListRequest, PagedListResponse<TModel, PagedListRequest>, DynamicListRequest> where TEntity : Entity where TCreateModel : class where TEditModel : class, IEditModel where TModel : class, IModel where TDeleteModel : class, IDeleteModel { protected CrudApplicationService(IUnitOfWork unitOfWork, IMapper mapper) : base(unitOfWork, mapper) { } } public abstract class CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel, TDynamicListRequest> : CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel, PagedListRequest, PagedListResponse<TModel, PagedListRequest>, TDynamicListRequest> where TEntity : Entity where TCreateModel : class where TEditModel : class, IEditModel where TModel : class, IModel where TDeleteModel : class, IDeleteModel where TDynamicListRequest : DynamicListRequest { protected CrudApplicationService(IUnitOfWork unitOfWork, IMapper mapper) : base(unitOfWork, mapper) { } } public abstract class CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel, TPagedListRequest, TPagedListResponse> : CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel, TPagedListRequest, TPagedListResponse, DynamicListRequest> where TEntity : Entity where TCreateModel : class where TEditModel : class, IEditModel where TModel : class, IModel where TDeleteModel : class, IDeleteModel where TPagedListRequest : PagedListRequest, new() where TPagedListResponse : PagedListResponse<TModel, TPagedListRequest>, new() { protected CrudApplicationService(IUnitOfWork unitOfWork, IMapper mapper) : base(unitOfWork, mapper) { } } public abstract class CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel, TPagedListRequest, TPagedListResponse, TDynamicListRequest> : ApplicationService, ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, TPagedListRequest, TPagedListResponse, TDynamicListRequest> where TEntity : Entity where TCreateModel : class where TEditModel : class, IEditModel where TModel : class, IModel where TDeleteModel : class, IDeleteModel where TPagedListRequest : PagedListRequest, new() where TPagedListResponse : PagedListResponse<TModel, TPagedListRequest>, new() where TDynamicListRequest : DynamicListRequest { #region Constructor protected CrudApplicationService(IUnitOfWork unitOfWork, IMapper mapper) { Guard.ArgumentNotNull(unitOfWork, nameof(unitOfWork)); Guard.ArgumentNotNull(mapper, nameof(mapper)); UnitOfWork = unitOfWork; Mapper = mapper; EntitySet = UnitOfWork.Set<TEntity>(); } #endregion #region Properties protected IQueryable<TEntity> UnTrackedEntitySet => EntitySet.AsNoTracking(); protected IUnitOfWork UnitOfWork { get; } protected IMapper Mapper { get; } protected IDbSet<TEntity> EntitySet { get; } #endregion #region ICrudApplicationService Members #region Methods [Transactional] public virtual void Create(TCreateModel model) { Guard.ArgumentNotNull(model, nameof(model)); var entity = Mapper.Map<TEntity>(model); EntitySet.Add(entity); UnitOfWork.SaveChanges(); } [Transactional] public virtual void Create(IList<TCreateModel> models) { Guard.ArgumentNotEmpty(models, nameof(models)); var entities = Mapper.Map<IList<TEntity>>(models); UnitOfWork.AddRange(entities); UnitOfWork.SaveChanges(); } [Transactional] public virtual Task CreateAsync(TCreateModel model) { Guard.ArgumentNotNull(model, nameof(model)); var entity = Mapper.Map<TEntity>(model); EntitySet.Add(entity); return UnitOfWork.SaveChangesAsync(); } [Transactional] public virtual Task CreateAsync(IList<TCreateModel> models) { Guard.ArgumentNotEmpty(models, nameof(models)); var entities = Mapper.Map<IList<TEntity>>(models); UnitOfWork.AddRange(entities); return UnitOfWork.SaveChangesAsync(); } [Transactional] public virtual void Edit(TEditModel model) { Guard.ArgumentNotNull(model, nameof(model)); var entity = Mapper.Map<TEntity>(model); UnitOfWork.MarkAsChanged(entity); UnitOfWork.SaveChanges(); } [Transactional] public virtual void Edit(IList<TEditModel> models) { Guard.ArgumentNotNull(models, nameof(models)); Guard.ArgumentNotEmpty(models, nameof(models)); var entities = Mapper.Map<IList<TEntity>>(models); UnitOfWork.UpdateRange(entities); UnitOfWork.SaveChanges(); } [Transactional] public virtual Task EditAsync(TEditModel model) { Guard.ArgumentNotNull(model, nameof(model)); var entity = Mapper.Map<TEntity>(model); UnitOfWork.MarkAsChanged(entity); return UnitOfWork.SaveChangesAsync(); } [Transactional] public virtual Task EditAsync(IList<TEditModel> models) { Guard.ArgumentNotNull(models, nameof(models)); Guard.ArgumentNotEmpty(models, nameof(models)); var entities = Mapper.Map<IList<TEntity>>(models); UnitOfWork.UpdateRange(entities); return UnitOfWork.SaveChangesAsync(); } public virtual IList<TModel> GetList() { return EntitySet.ProjectToList<TModel>(Mapper.ConfigurationProvider); } public virtual DynamicListResponse GetDynamicList(TDynamicListRequest request) { Guard.ArgumentNotNull(request, nameof(request)); var query = ApplyFiltering(request); return query.ProjectTo<TModel>().ToListResponse(request); } public virtual TPagedListResponse GetPagedList(TPagedListRequest request) { Guard.ArgumentNotNull(request, nameof(request)); var query = ApplyFiltering(request); request.TotalCount = query.LongCount(); query = ApplySorting(query, request); query = ApplyPaging(query, request); var result = query.ProjectToList<TModel>(Mapper.ConfigurationProvider); return new TPagedListResponse { Result = result, Request = request }; } public virtual IList<LookupItem> GetLookup() { return EntitySet.ProjectToList<LookupItem>(Mapper.ConfigurationProvider); } public virtual TModel GetById(long id) { Guard.ArgumentInRange(id, 1, long.MaxValue, nameof(id)); var entity = EntitySet.Where(a => a.Id == id).ProjectToFirstOrDefault<TModel>(Mapper.ConfigurationProvider); if (entity == null) throw new EntityNotFoundException($"Couldn't Find Entity {id} When GetById"); return entity; } public virtual TEditModel GetForEdit(long id) { Guard.ArgumentInRange(id, 1, long.MaxValue, nameof(id)); var entity = EntitySet.Where(a => a.Id == id).ProjectToFirstOrDefault<TEditModel>(Mapper.ConfigurationProvider); if (entity == null) throw new EntityNotFoundException($"Couldn't Find Entity {id} When GetForEdit"); return entity; } public virtual bool Exists(long id) { Guard.ArgumentInRange(id, 1, long.MaxValue, nameof(id)); return EntitySet.Any(a => a.Id == id); } public virtual async Task<IList<TModel>> GetListAsync() { return await EntitySet.ProjectToListAsync<TModel>(Mapper.ConfigurationProvider); } public virtual Task<DynamicListResponse> GetDynamicListAsync(TDynamicListRequest request) { Guard.ArgumentNotNull(request, nameof(request)); var query = ApplyFiltering(request); return query.ProjectTo<TModel>().ToListResponseAsync(request); } public virtual async Task<TPagedListResponse> GetPagedListAsync(TPagedListRequest request) { Guard.ArgumentNotNull(request, nameof(request)); var query = ApplyFiltering(request); request.TotalCount = await query.LongCountAsync().ConfigureAwait(false); query = ApplySorting(query, request); query = ApplyPaging(query, request); var result = await query.ProjectToListAsync<TModel>(Mapper.ConfigurationProvider).ConfigureAwait(false); return new TPagedListResponse { Result = result, Request = request }; } public virtual async Task<IList<LookupItem>> GetLookupAsync() { return await EntitySet.ProjectToListAsync<LookupItem>(Mapper.ConfigurationProvider); } public virtual async Task<TModel> GetByIdAsync(long id) { Guard.ArgumentInRange(id, 1, long.MaxValue, nameof(id)); var entity = await UnTrackedEntitySet.Where(a => a.Id == id) .ProjectToFirstOrDefaultAsync<TModel>(Mapper.ConfigurationProvider); if (entity == null) throw new EntityNotFoundException($"Couldn't Find Entity {id} When GetByIdAsync"); return entity; } public virtual async Task<TEditModel> GetForEditAsync(long id) { Guard.ArgumentInRange(id, 1, long.MaxValue, nameof(id)); var entity = await UnTrackedEntitySet.Where(a => a.Id == id) .ProjectToFirstOrDefaultAsync<TEditModel>(Mapper.ConfigurationProvider); if (entity == null) throw new EntityNotFoundException($"Couldn't Find Entity {id} When GetForEditAsync"); return entity; } public virtual Task<bool> ExistsAsync(long id) { Guard.ArgumentInRange(id, 1, long.MaxValue, nameof(id)); return EntitySet.AnyAsync(a => a.Id == id); } [Transactional] public virtual void Delete(TDeleteModel model) { Guard.ArgumentNotNull(model, nameof(model)); var entity = Mapper.Map<TEntity>(model); UnitOfWork.MarkAsDeleted(entity); UnitOfWork.SaveChanges(); } [Transactional] public virtual void Delete(IList<TDeleteModel> models) { Guard.ArgumentNotEmpty(models, nameof(models)); Guard.ArgumentNotEmpty(models, nameof(models)); var entities = Mapper.Map<IList<TEntity>>(models); UnitOfWork.RemoveRange(entities); UnitOfWork.SaveChanges(); } [Transactional] public virtual Task DeleteAsync(TDeleteModel model) { Guard.ArgumentNotNull(model, nameof(model)); var entity = Mapper.Map<TEntity>(model); UnitOfWork.MarkAsDeleted(entity); return UnitOfWork.SaveChangesAsync(); } [Transactional] public virtual Task DeleteAsync(IList<TDeleteModel> models) { Guard.ArgumentNotEmpty(models, nameof(models)); Guard.ArgumentNotEmpty(models, nameof(models)); var entities = Mapper.Map<IList<TEntity>>(models); UnitOfWork.RemoveRange(entities); return UnitOfWork.SaveChangesAsync(); } #endregion #endregion #region Protected Methods /// <summary> /// Apply Filtering To GetDynamicList /// </summary> /// <param name="request"></param> /// <returns></returns> protected virtual IQueryable<TEntity> ApplyFiltering(TDynamicListRequest request) { Guard.ArgumentNotNull(request, nameof(request)); return UnTrackedEntitySet; } /// <summary> /// Apply Filtering To GetPagedList and GetPagedListAsync /// </summary> /// <param name="request"></param> /// <returns></returns> protected virtual IQueryable<TEntity> ApplyFiltering(TPagedListRequest request) { Guard.ArgumentNotNull(request, nameof(request)); return UnTrackedEntitySet; } /// <summary> /// Apply Sorting To GetPagedList and GetPagedListAsync /// </summary> /// <param name="query">query</param> /// <param name="request">PagedListRequest</param> /// <returns></returns> protected virtual IQueryable<TEntity> ApplySorting(IQueryable<TEntity> query, TPagedListRequest request) { Guard.ArgumentNotNull(request, nameof(request)); Guard.ArgumentNotNull(query, nameof(query)); return !request.SortBy.IsEmpty() ? query.OrderBy(request.SortBy) : query.OrderByDescending(e => e.Id); } /// <summary> /// Apply Paging To GetPagedList and GetPagedListAsync /// </summary> /// <param name="request">PagedListRequest</param> /// <param name="query">query</param> /// <returns></returns> protected virtual IQueryable<TEntity> ApplyPaging(IQueryable<TEntity> query, TPagedListRequest request) { Guard.ArgumentNotNull(request, nameof(request)); Guard.ArgumentNotNull(query, nameof(query)); return request != null ? query.Page((request.PageNumber - 1) * request.PageSize, request.PageSize) : query; } #endregion } }
همه متدهای این کلاس پایه، قابلیت override شدن را دارند. به عنوان مثال یکسری متد با دسترسی protected مثلا ApplyFiltering هم برای بازنویسی نحوه فیلتر کردن خروجی GetPagedList میتوانند در SubClassها مورد استفاده قرار گیرند. برای مباحث مرتب سازی هم از کتابخانه System.Linq.Dynamic استفاده شده است.
برای مکانیزم Validation خودکار هم از کتابخانه FluentValidatoin کمک گرفته شده است و با استفاده از Interceptor زیر در صورت یافتن Validator مربوط به Model ورودی، عملیات اعتبارسنجی انجام میگرد و در صورت معتبر نبودن، استثنایی صادر خواهد شد که حاوی اطلاعات مربوط به جزئیات خطاها نیز میباشد.
ValidatorInterceptor
namespace MvcFramework.Framework.Aspects.Validation { public class ValidatorInterceptor : ISyncInterceptionBehavior { private readonly IValidatorFactory _validatorFactory; public ValidatorInterceptor(IValidatorFactory validatorFactory) { _validatorFactory = validatorFactory; } public IMethodInvocationResult Intercept(ISyncMethodInvocation methodInvocation) { var argumentValues = methodInvocation.Arguments.Select(a => a.Value).ToArray(); var validator = new MethodInvocationValidator(_validatorFactory, methodInvocation.MethodInfo, argumentValues); validator.Validate(); return methodInvocation.InvokeNext(); } } }
کتابخانه جانبی دیگری برای AOP توسط تیم StructureMap به نام StructureMap.DynamicInterception ارائه شده است. نمونهی استفاده از آن، در بالا مشخص میباشد. در اینجا انتقال مسئولیت اعتبارسنجی پارامترهای متدی که قرار است Intercept شود، به کلاسی به نام MethodInvocationValidator سپرده شدهاست.
کلاس MethodInvocationValidator
namespace MvcFramework.Framework.Aspects.Validation { internal class MethodInvocationValidator { #region Constructor public MethodInvocationValidator(IValidatorFactory validatorFactory, MethodInfo method, object[] parameterValues) { Guard.ArgumentNotNull(method, nameof(method)); Guard.ArgumentNotNull(parameterValues, nameof(parameterValues)); Guard.ArgumentNotNull(validatorFactory, nameof(validatorFactory)); _method = method; _parameterValues = parameterValues; _validatorFactory = validatorFactory; _parameters = method.GetParameters(); _parametersToBeNormalized = new List<IShouldNormalize>(); } #endregion #region Public Methods public void Validate() { if (!CheckShouldBeValidate()) return; foreach (var parameterValue in _parameterValues) ValidateMethodParameter(parameterValue); foreach (var parameterToBeNormalized in _parametersToBeNormalized) parameterToBeNormalized.Normalize(); } #endregion #region Fields private readonly MethodInfo _method; private readonly object[] _parameterValues; private readonly ParameterInfo[] _parameters; private readonly IValidatorFactory _validatorFactory; private readonly List<IShouldNormalize> _parametersToBeNormalized; #endregion #region Private Methods private bool CheckShouldBeValidate() { if (!_method.IsPublic) return false; if (IsValidationDisabled()) return false; if (_parameters.IsNullOrEmpty()) return false; if (_parameters.Length != _parameterValues.Length) throw new Exception("Method parameter count does not match with argument count!"); return true; } private bool IsValidationDisabled() { if (_method.IsDefined(typeof(EnableValidationAttribute), true)) return false; return ReflectionHelper .GetSingleAttributeOfMemberOrDeclaringTypeOrDefault<DisableValidationAttribute>(_method) != null; } private void ValidateMethodParameter(object parameterValue) { if (parameterValue == null) return; var parameterValueList = parameterValue as IEnumerable<object>; if (parameterValueList != null) { var valueList = parameterValueList.ToList(); ValidateMethodParameterValues(valueList); } else { ValidateMethodParameterValues(new List<object> { parameterValue }); } if (parameterValue is IShouldNormalize) _parametersToBeNormalized.Add(parameterValue as IShouldNormalize); } private void ValidateMethodParameterValues(List<object> valueList) { var ruleSet = GetRuleSet(_method); var validator = _validatorFactory.GetValidator(valueList.First().GetType()); if (validator == null) return; foreach (var item in valueList) ValidateWithReflection(validator, item, ruleSet); } private static string GetRuleSet(MemberInfo method) { const string @default = "default"; var attribute = method.GetCustomAttribute<ValidateWithRuleAttribute>(); if (attribute == null) return @default; var rules = new List<string> { @default }; rules.AddRange(attribute.RuleSetNames); return string.Join(",", rules).TrimEnd(','); } private static void ValidateAndThrow<T>(IValidator<T> validator, T argument, string ruleSet) { validator.ValidateAndThrow(argument, ruleSet); } private void ValidateWithReflection(IValidator validator, object argument, string ruleSet) { GetType().GetMethod(nameof(ValidateAndThrow), BindingFlags.Static | BindingFlags.NonPublic) .MakeGenericMethod(argument.GetType()) .Invoke(null, new[] { validator, argument, ruleSet }); } #endregion } }
در متد Validate آن ابتدا چک میشود که آیا اعتبارسنجی میبایستی انجام شود یا خیر. سپس تک تک آرگومانهای ارسالی را با استفاده از متد ValidateMethodParameter وارد مکانیزم اعتبارسنجی میکند. در داخل این متد ابتدا نوع آرگومان تشخیص داده شده و این مقادیر به متد ValidateMethodParameterValues ارسال شده و داخل آن ابتدا Validator مرتبط را یافته و آن را به متد ValidateWithReflection ارسال میکند. در این بین متد GetRuleSets وظیفه واکشی اسامی RuleSet هایی که بر روی متد مورد نظر تنظیم شده اند را دارد؛ برای مواقعی که از یک ویومدل برای ویرایش، درج و حذف استفاده کنید، در این صورت با توجه به اینکه برای یک ویومدل یک Validator خواهید داشت، امکانات RuleSet مربوط به FluentValidation کارساز خواهند بود. به این صورت که برای هر کدام از عملیات حذف، ویرایش و درج، RuleSet مناسب را تعریف کرده و با استفاده از ValidateWithRuleAttribute برروی متدهای مورد نظر، این ruleها در سیستم اعتبارسنجی ارائه شده اعمال خواهند شد.
با توجه به اینکه متد ValidateAndThrow در واسط IValidator<T> تعریف شدهاست و از آنجاییکه ما نوع داده مدل مورد نظر را هم نداریم لازم است با استفاده از MakeGenericMethod به صورت داینامیک نوع داده T را مشخص کنیم و فراخوانی متد استاتیک ValidatorWithThrow<T> را با Reflection انجام دهیم.
در ادامه لازم است ValidatorInterceptor معرفی شده را به StructureMap نیز معرفی کنیم. برای این منظور به شکل زیر عمل خواهیم کرد.
namespace MvcFramework.Framework { public class FrameworkRegistry : Registry { public FrameworkRegistry() { For<IValidatorFactory>().Singleton().Use<StructureMapValidatorFactory>(); Scan(scan => { scan.TheCallingAssembly(); scan.WithDefaultConventions(); scan.LookForRegistries(); }); Policies.Interceptors(new DynamicProxyInterceptorPolicy(f => typeof(IApplicationService).IsAssignableFrom(f), typeof(ValidatorInterceptor),typeof(TransactionInterceptor))); } } }
در کد بالا با استفاده از DynamicProxyInterceptorPolicy، یک Policy را برای Intercept کردن متدهای مربوط به کلاس هایی که پیاده ساز IApplicationService میباشند، معرفی کردهایم.
کار اعتبارسنجی هم به پایان رسید؛ در زیر استفاده از سرویس پایه معرفی شده را میتوانید مشاهده کنید.
namespace MyApp.ServiceLayer.Roles { public interface IRoleApplicationService : ICrudApplicationService<RoleViewModel, RoleCreateViewModel, RoleEditViewModel, RoleDeleteViewModel, RolePagedListRequest, RoleListViewModel> { } } namespace MyApp.ServiceLayer.Roles { public class RoleApplicationService : CrudApplicationService<Role, RoleViewModel, RoleCreateViewModel, RoleEditViewModel, RoleDeleteViewModel, RolePagedListRequest, RoleListViewModel>, IRoleApplicationService { #region Constructor public RoleApplicationService(IUnitOfWork unitOfWork, IMapper mapper) : base(unitOfWork, mapper) { } #endregion } }
نکته: در این لایه بندی نکات مربوط به مطلب «پیاده سازی ماژولار Autofac» نیز با استفاده از StructureMap اعمال شده است. بدین ترتیب در هر لایه یک Registry مربوط به StructureMap ایجاد شده است. به شکل زیر:
FrameworkRegistry
namespace MyApp.Framework { public class FrameworkRegistry : Registry { public FrameworkRegistry() { For<IValidatorFactory>().Singleton().Use<StructureMapValidatorFactory>(); Scan(scan => { scan.TheCallingAssembly(); scan.WithDefaultConventions(); scan.AssembliesFromApplicationBaseDirectory(); scan.AddAllTypesOf<IRunOnEndTask>(); scan.AddAllTypesOf<IRunOnOwinStartupTask>(); scan.AddAllTypesOf<IRunOnStartTask>(); scan.AddAllTypesOf<IRunOnBeginRequestTask>(); scan.AddAllTypesOf<IRunOnErrorTask>(); scan.AddAllTypesOf<IRunOnEndRequestTask>(); scan.LookForRegistries(); }); Policies.Interceptors(new DynamicProxyInterceptorPolicy(f => typeof(IApplicationService).IsAssignableFrom(f), typeof(ValidatorInterceptor)/*, typeof(TransactionInterceptor)*/)); } } }
DataLayerRegistry
namespace MyApp.DataLayer { public class DataLayerRegistry : Registry { public DataLayerRegistry() { Scan(scan => { scan.TheCallingAssembly(); scan.WithDefaultConventions(); scan.AssembliesFromApplicationBaseDirectory(); scan.AddAllTypesOf<IRunOnStartTask>(); }); //todo:use container per request (Nested Containers) instead of HttpContextLifeCycle For<IUnitOfWork>().Use<ApplicationDbContext>(); } } }
ServiceLayerRegistry
namespace MyApp.ServiceLayer { public class ServiceLayerRegistry : Registry { #region Constructor public ServiceLayerRegistry() { Scan(scan => { scan.TheCallingAssembly(); scan.WithDefaultConventions(); scan.AssembliesFromApplicationBaseDirectory(); scan.AddAllTypesOf<IRunOnEndTask>(); scan.AddAllTypesOf<IRunOnOwinStartupTask>(); scan.AddAllTypesOf<IRunOnStartTask>(); scan.AddAllTypesOf<IRunOnBeginRequestTask>(); scan.AddAllTypesOf<IRunOnErrorTask>(); scan.AddAllTypesOf<IRunOnEndRequestTask>(); scan.Assembly(typeof(DataLayerRegistry).Assembly); scan.LookForRegistries(); scan.AddAllTypesOf<Profile>().NameBy(item => item.FullName); scan.AddAllTypesOf<IHaveCustomMappings>().NameBy(item => item.FullName); }); FluentValidationConfig(); AutoMapperConfig(); } #endregion #region Private Methods private void AutoMapperConfig() { For<MapperConfiguration>().Singleton().Use("MapperConfig", ctx => { var config = new MapperConfiguration(cfg => { cfg.CreateMissingTypeMaps = true; AddProfiles(ctx, cfg); AddIHaveCustomMappings(ctx, cfg); AddMapFrom(cfg); }); config.AssertConfigurationIsValid(); return config; }); For<IMapper>().Singleton().Use(ctx => ctx.GetInstance<MapperConfiguration>().CreateMapper(ctx.GetInstance)); } private void FluentValidationConfig() { AssemblyScanner.FindValidatorsInAssembly(Assembly.GetExecutingAssembly()) .ForEach(result => { For(result.InterfaceType) .Singleton() .Use(result.ValidatorType); }); } private static void AddMapFrom(IProfileExpression cfg) { var types = typeof(RoleViewModel).Assembly.GetExportedTypes(); var maps = (from t in types from i in t.GetInterfaces() where i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IMapFrom<>) && !t.IsAbstract && !t.IsInterface select new { Source = i.GetGenericArguments()[0], Destination = t }).ToArray(); foreach (var map in maps) cfg.CreateMap(map.Source, map.Destination); } private static void AddProfiles(IContext ctx, IMapperConfigurationExpression cfg) { var profiles = ctx.GetAllInstances<Profile>().ToList(); foreach (var profile in profiles) cfg.AddProfile(profile); } private static void AddIHaveCustomMappings(IContext ctx, IMapperConfigurationExpression cfg) { var mappings = ctx.GetAllInstances<IHaveCustomMappings>().ToList(); foreach (var mapping in mappings) mapping.CreateMappings(cfg); } #endregion } }
WebRegistry
namespace MyApp.Web { public class WebRegistry : Registry { public WebRegistry() { Scan(scan => { scan.TheCallingAssembly(); scan.WithDefaultConventions(); scan.AssembliesFromApplicationBaseDirectory(); scan.AddAllTypesOf<IRunOnEndTask>(); scan.AddAllTypesOf<IRunOnOwinStartupTask>(); scan.AddAllTypesOf<IRunOnStartTask>(); scan.AddAllTypesOf<IRunOnBeginRequestTask>(); scan.AddAllTypesOf<IRunOnErrorTask>(); scan.AddAllTypesOf<IRunOnEndRequestTask>(); scan.Assembly(typeof(ServiceLayerRegistry).Assembly); scan.LookForRegistries(); }); } } }
در این طراحی، لایه Web یا همان Presentation به DataLayer و DomainClasses هیچ ارجاعی ندارد.
در قسمت بعد استفاده از این سرویس را در یک برنامه ASP.NET MVC با هم بررسی خواهیم کرد.
کدهای کامل این قسمت را میتوانید از اینجا دریافت کنید.