اشتراکها
قلم +M
برای هماهنگی این کتابخانه با ASP.NET MVC نیاز به نصب FluentValidation.Mvc3 یا FluentValidation.Mvc4 از طریق Nuget یا دانلود کتابخانه از سایت CodePlex میباشد. بعد از نصب کتابخانه، نیاز به تنظیم FluentValidationModelValidatorProvider داخل متد Application_Start (فایل Global.asax) داریم:
protected void Application_Start() { AreaRegistration.RegisterAllAreas(); RegisterGlobalFilters(GlobalFilters.Filters); RegisterRoutes(RouteTable.Routes); FluentValidationModelValidatorProvider.Configure(); }
تصور کنید دو کلاس Person و PersonValidator را به صورت زیر داریم:
[Validator(typeof(PersonValidator))] public class Person { public int Id { get; set; } public string Name { get; set; } public string Email { get; set; } public int Age { get; set; } } public class PersonValidator : AbstractValidator<Person> { public PersonValidator() { RuleFor(x => x.Id).NotNull(); RuleFor(x => x.Name).Length(0, 10); RuleFor(x => x.Email).EmailAddress(); RuleFor(x => x.Age).InclusiveBetween(18, 60); } }
همان طور که ملاحظه میکنید، در بالای تعریف کلاس Person با استفاده از ValidatorAttribute مشخص کرده ایم که از PersonValidator جهت اعتبارسنجی استفاده کند.
در آخر میتوانیم Controller و View ی برنامه مان را درست کنیم:
public class PeopleController : Controller { public ActionResult Create() { return View(); } [HttpPost] public ActionResult Create(Person person) { if(! ModelState.IsValid) { // re-render the view when validation failed. return View("Create", person); } TempData["notice"] = "Person successfully created"; return RedirectToAction("Index"); } }
@Html.ValidationSummary() @using (Html.BeginForm()) { Id: @Html.TextBoxFor(x => x.Id) @Html.ValidationMessageFor(x => x.Id) <br /> Name: @Html.TextBoxFor(x => x.Name) @Html.ValidationMessageFor(x => x.Name) <br /> Email: @Html.TextBoxFor(x => x.Email) @Html.ValidationMessageFor(x => x.Email) <br /> Age: @Html.TextBoxFor(x => x.Age) @Html.ValidationMessageFor(x => x.Age) <input type="submit" value="submit" /> }
اکنون DefaultModelBinder موجود در MVC برای اعتبارسنجی شیء Person از FluentValidationModelValidatorProvider استفاده خواهد کرد.
توجه داشته باشید که FluentValidation با اعتبارسنجی سمت کاربر ASP.NET MVC به خوبی کار خواهد کرد منتها نه برای تمامی اعتبارسنجی ها. به عنوان مثال تمام قوانینی که از شرطهای When/Unless استفاده کرده اند، Validatorهای سفارشی، و قوانینی که در آنها از Must استفاده شده باشد، اعتبارسنجی سمت کاربر نخواهند داشت. در زیر لیست Validator هایی که با اعتبارسنجی سمت کاربر به خوبی کار خواهند کرد آمده است:
- NotNull/NotEmpty
- Matches
- InclusiveBetween
- CreditCard
- Email
- EqualTo
- Length
صفت CustomizeValidator
با استفاده از CustomizeValidatorAttribute میتوان نحوه اجرای Validator را تنظیم کرد. به عنوان مثال اگر میخواهید که Validator تنها برای یک RuleSet مخصوص انجام شود میتوانید مانند زیر عمل کنید:
public ActionResult Save([CustomizeValidator(RuleSet="MyRuleset")] Customer cust) { // ... }
مواردی که تا اینجا گفته شد برای استفاده در یک برنامهی ساده MVC کافی به نظر میرسد، اما از اینجا به بعد مربوط به مواقعی است که نخواهیم از Attributeها استفاده کنیم و کار را به IoC بسپاریم.
استفاده از Validator Factory با استفاده از یک IoC Container
Validator Factory چیست؟ Validator Factory یک کلاس میباشد که وظیفه ساخت نمونه از Validatorها را بر عهده دارد. اینترفیس IValidatorFactory به صورت زیر میباشد:
public interface IValidatorFactory { IValidator<T> GetValidator<T>(); IValidator GetValidator(Type type); }
ساخت Validator Factory سفارشی:
برای ساخت یک Validator Factory شما میتوانید به طور مستقیم IValidatorFactory را پیاده سازی نمایید یا از کلاس ValidatorFactoryBase به عنوان کلاس پایه استفاده کنید (که مقداری از کارها را برای شما انجام داده است). در این مثال نحوه ایجاد یک Validator Factory که از StructureMap استفاده میکند را بررسی خواهیم کرد.
ابتدا نیاز به ثبت Validatorها در StructureMap داریم:
ObjectFactory.Configure(cfg => cfg.AddRegistry(new MyRegistry())); public class MyRegistry : Registry { public MyRegistry() { For<IValidator<Person>>() .Singleton() .Use<PersonValidator>(); } }
در اینجا StructureMap را طوری تنظیم کرده ایم که از یک Registry سفارشی استفاده کند. در داخل این Registry به StructureMap میگوییم که زمانی که خواسته شد تا یک نمونه از IValidator<Person> ایجاد کند، PersonValidator را بر گرداند. متد CreateInstance نوع مناسب را نمونه سازی میکند (CustomerValidator) و آن را بازمی گرداند ( یا Null بر میگرداند اگر نوع مناسبی وجود نداشته باشد)
استفاده از AssemblyScanner
FluentValidation دارای یک AssemblyScanner میباشد که کار ثبت Validatorها داخل یک اسمبلی را راحتتر میسازد. با استفاده از AssemblyScanner کلاس MyRegistery ما شبیه قطعه کد زیر خواهد شد:
public class MyRegistry : Registry { public MyRegistry() { AssemblyScanner.FindValidatorsInAssemblyContaining<MyValidator>() .ForEach(result => { For(result.InterfaceType) .Singleton() .Use(result.ValidatorType); }); } }
حالا زمان استفاده از factory ساخته شده در متد Application_Start برنامه میباشد:
اکنون FluentValidation از StructureMap برای نمونه سازی Validatorها استفاده خواهد کرد و کار اعتبارسنجی مدلها به FluentValidaion سپرده شده است.
protected void Application_Start() { RegisterRoutes(RouteTable.Routes); //Configure structuremap ObjectFactory.Configure(cfg => cfg.AddRegistry(new MyRegistry())); ControllerBuilder.Current.SetControllerFactory(new StructureMapControllerFactory()); //Configure FV to use StructureMap var factory = new StructureMapValidatorFactory(); //Tell MVC to use FV for validation ModelValidatorProviders.Providers.Add(new FluentValidationModelValidatorProvider(factory)); DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false; }
اس کیوال سرور، از سال 2005 به بعد، به صورت توکار امکان تعریف و ذخیره سازی اطلاعات schema less و یا schema free را به کمک فیلدهایی از نوع XML ارائه داده است؛ به همراه یکپارچگی آن با زبان XQuery برای تهیه کوئریهای سریع سمت سرور. در فیلدهای XML میتوان اطلاعات انواع و اقسام اشیاء را بدون اینکه نیازی به تعریف تک تک فیلدهای مورد نیاز، در بانک اطلاعاتی وجود داشته باشد، ذخیره کرد. یک نمونه از کاربرد چنین امکانی، نوشتن برنامههای «فرم ساز» است. برنامههایی که کاربران آن میتوانند فیلد اضافه و کم کرده و نهایتا اطلاعات را ذخیره و از آنها کوئری بگیرند.
خوب، این فیلد کمتر بحث شده XML، فقط در اس کیوال سرور و نگارشهای اخیر آن وجود دارد. اگر نیاز به کار با بانکهای اطلاعاتی سبکتری وجود داشت چطور؟ یک راه حل عمومی برای این مساله مراجعه به روشهای NoSQL است. یعنی بطور کلی بانکهای اطلاعاتی رابطهای کنار گذاشته شده و به یک سکوی کاری دیگر سوئیچ کرد. در این بین، SisoDb راه حل میانهای را ارائه داده است. با کمک SisoDb میتوان اطلاعات را به صورت schema less و بدون نیاز به تعریف فیلدهای متناظر آنها، در انواع و اقسام بانکهای اطلاعاتی SQL Server با فرمت JSON ذخیره و بازیابی کرد. این انواع و اقسام، شامل SQL Server CE نیز میشود.
دریافت و نصب SisoDb
دریافت و نصب SisoDb بسیار ساده است. به کمک package manager و امکانات NuGet، کلمه Sisodb را جستجو کنید. در بین مداخل ظاهر شده، پروایدر مورد علاقه خود را انتخاب و نصب نمائید. برای مثال اگر قصد دارید با SQL Server CE کار کنید، SisoDb.SqlCe4 را انتخاب و یا اگر SQL Server 2008 مدنظر شما است، SisoDb.Sql2008 را انتخاب و نصب نمائید.
ثبت و بازیابی اطلاعات به کمک SisoDb
کار با SisoDb بسیار روان است. نیازی به تعاریف نگاشتها و ORM خاصی نیست. یک مثال مقدماتی آنرا در ادامه ملاحظه میکنید:
در این مثال، ابتدا اتصال به بانک اطلاعاتی برقرار شده و سپس بانک اطلاعاتی جدید تهیه میشود. سپس یک وهله از شیء مشتری ایجاد و ذخیره میگردد. در ادامه دو کوئری بر روی بانک اطلاعاتی انجام شده است.
ساختار داخلی SisoDb
SisoDb به ازای هر کلاس، حداقل 9 جدول را ایجاد میکند. در ادامه نحوه ذخیره سازی شیء مشتری ایجاد شده و مقادیر خواص آنرا نیز مشاهده مینمائید:
همانطور که ملاحظه میکنید، یک جدول کلی SisoDbIdentities ایجاد شده است که اطلاعات نام اشیاء را در خود نگهداری میکند. سپس اطلاعات خواص اشیاء یکبار به صورت JSON ذخیره میشوند؛ با تمام اطلاعات تو در توی ذخیره شده در آنها و همچنین یکبار هم هر خاصیت را به صورت یک رکورد جداگانه، بر اساس نوع کلی آنها، در جداول رشتهای، عددی و امثال آن ذخیره میکند.
شاید بپرسید که چرا به همان فیلد رشتهای JSON اکتفاء نشده است؟ از این جهت که پردازشگر سمت بانک اطلاعاتی آن همانند فیلدهای XML در SQL Server و نگارشهای مختلف آن وجود ندارد (برای مثال به کمک زبان T-SQL میتوان از زبان XQuery در خود بانک اطلاعاتی، بدون نیاز به واکشی کل اطلاعات در سمت کلاینت، به صورت یکپارچه استفاده کرد). به همین جهت برای کوئری گرفتن و یا تهیه ایندکس، نیاز است این موارد جداگانه ذخیره شوند.
به این ترتیب زمانیکه کوئری تهیه میشود، برای مثال:
به کوئری زیر ترجمه میگردد:
و یا کوئری ذیل:
معادل زیر را خواهد داشت:
در هر دو حالت از جداول کمکی تعریف شده برای تهیه کوئری استفاده کرده و نهایتا فیلد JSON اصلی را برای نگاشت نهایی به اشیاء تعریف شده در برنامه بازگشت میدهد.
در کل این هم یک روش تفکر و طراحی Schema less است که با بسیاری از بانکهای اطلاعاتی موجود سازگاری دارد.
برای مشاهده اطلاعات بیشتری در مورد جزئیات این روش میتوان به Wiki آن مراجعه کرد.
خوب، این فیلد کمتر بحث شده XML، فقط در اس کیوال سرور و نگارشهای اخیر آن وجود دارد. اگر نیاز به کار با بانکهای اطلاعاتی سبکتری وجود داشت چطور؟ یک راه حل عمومی برای این مساله مراجعه به روشهای NoSQL است. یعنی بطور کلی بانکهای اطلاعاتی رابطهای کنار گذاشته شده و به یک سکوی کاری دیگر سوئیچ کرد. در این بین، SisoDb راه حل میانهای را ارائه داده است. با کمک SisoDb میتوان اطلاعات را به صورت schema less و بدون نیاز به تعریف فیلدهای متناظر آنها، در انواع و اقسام بانکهای اطلاعاتی SQL Server با فرمت JSON ذخیره و بازیابی کرد. این انواع و اقسام، شامل SQL Server CE نیز میشود.
دریافت و نصب SisoDb
دریافت و نصب SisoDb بسیار ساده است. به کمک package manager و امکانات NuGet، کلمه Sisodb را جستجو کنید. در بین مداخل ظاهر شده، پروایدر مورد علاقه خود را انتخاب و نصب نمائید. برای مثال اگر قصد دارید با SQL Server CE کار کنید، SisoDb.SqlCe4 را انتخاب و یا اگر SQL Server 2008 مدنظر شما است، SisoDb.Sql2008 را انتخاب و نصب نمائید.
ثبت و بازیابی اطلاعات به کمک SisoDb
کار با SisoDb بسیار روان است. نیازی به تعاریف نگاشتها و ORM خاصی نیست. یک مثال مقدماتی آنرا در ادامه ملاحظه میکنید:
using SisoDb.Sql2008; namespace SisoDbTests { public class Customer { public int Id { get; set; } public int CustomerNo { get; set; } public string Name { get; set; } } class Program { static void Main(string[] args) { /*var cnInfo = new SqlCe4ConnectionInfo(@"Data source=sisodb2013.sdf;"); var db = new SqlCe4DbFactory().CreateDatabase(cnInfo); db.EnsureNewDatabase();*/ var cnInfo = new Sql2008ConnectionInfo(@"Data Source=(local);Initial Catalog=sisodb2013;Integrated Security = true"); var db = new Sql2008DbFactory().CreateDatabase(cnInfo); db.EnsureNewDatabase(); var customer = new Customer { CustomerNo = 20, Name = "Vahid" }; db.UseOnceTo().Insert(customer); using (var session = db.BeginSession()) { var info = session.Query<Customer>().Where(c => c.CustomerNo == 20).FirstOrDefault(); var info2 = session.Query<Customer>().Where(c => c.CustomerNo == 20 && c.Name=="Vahid").FirstOrDefault(); } } } }
ساختار داخلی SisoDb
SisoDb به ازای هر کلاس، حداقل 9 جدول را ایجاد میکند. در ادامه نحوه ذخیره سازی شیء مشتری ایجاد شده و مقادیر خواص آنرا نیز مشاهده مینمائید:
ذخیره سازی جداگانه خواص عددی
ذخیره سازی جداگانه خواص رشتهای
ذخیره سازی کلی شیء مشتری با فرمت JSON به صورت یک رشته
همانطور که ملاحظه میکنید، یک جدول کلی SisoDbIdentities ایجاد شده است که اطلاعات نام اشیاء را در خود نگهداری میکند. سپس اطلاعات خواص اشیاء یکبار به صورت JSON ذخیره میشوند؛ با تمام اطلاعات تو در توی ذخیره شده در آنها و همچنین یکبار هم هر خاصیت را به صورت یک رکورد جداگانه، بر اساس نوع کلی آنها، در جداول رشتهای، عددی و امثال آن ذخیره میکند.
شاید بپرسید که چرا به همان فیلد رشتهای JSON اکتفاء نشده است؟ از این جهت که پردازشگر سمت بانک اطلاعاتی آن همانند فیلدهای XML در SQL Server و نگارشهای مختلف آن وجود ندارد (برای مثال به کمک زبان T-SQL میتوان از زبان XQuery در خود بانک اطلاعاتی، بدون نیاز به واکشی کل اطلاعات در سمت کلاینت، به صورت یکپارچه استفاده کرد). به همین جهت برای کوئری گرفتن و یا تهیه ایندکس، نیاز است این موارد جداگانه ذخیره شوند.
به این ترتیب زمانیکه کوئری تهیه میشود، برای مثال:
var info = session.Query<Customer>().Where(c => c.CustomerNo == 20).FirstOrDefault();
SELECT DISTINCT TOP(1) (s.[StructureId]), s.[Json] FROM [CustomerStructure] s LEFT JOIN [CustomerIntegers] mem0 ON mem0.[StructureId] = s.[StructureId] AND mem0.[MemberPath] = 'CustomerNo' WHERE (mem0.[Value] = 20);
var info2 = session.Query<Customer>().Where(c => c.CustomerNo == 20 && c.Name=="Vahid").FirstOrDefault();
SELECT DISTINCT TOP(1) (s.[StructureId]), s.[Json] FROM [CustomerStructure] s LEFT JOIN [CustomerIntegers] mem0 ON mem0.[StructureId] = s.[StructureId] AND mem0.[MemberPath] = 'CustomerNo' LEFT JOIN [CustomerStrings] mem1 ON mem1.[StructureId] = s.[StructureId] AND mem1.[MemberPath] = 'Name' WHERE ((mem0.[Value] = 20) AND (mem1.[Value] = 'Vahid'));
در کل این هم یک روش تفکر و طراحی Schema less است که با بسیاری از بانکهای اطلاعاتی موجود سازگاری دارد.
برای مشاهده اطلاعات بیشتری در مورد جزئیات این روش میتوان به Wiki آن مراجعه کرد.
در قسمت قبل، نحوهی پیاده سازی الگوی Decorator را با استفاده از امکانات تزریق وابستگیهای NET Core. بررسی کردیم؛ اما ... این روزها کسی Decoratorها را دستی ایجاد نمیکند. یعنی اگر قرار باشد به ازای هر کلاسی و هر سرویسی، یکبار کلاس Decorator آنرا با پیاده سازی همان اینترفیس سرویس اصلی و فراخوانی دستی تک تک متدهای سرویس اصلی تزریق شدهی در سازندهی آن انجام دهیم، آنچنان کاربردی به نظر نمیرسد. به همین منظور کتابخانههایی تحت عنوان Dynamic Proxy تهیه شدهاند تا کار ساخت و پیاده سازی پویای Decoratorها را انجام دهند. در این بین ما فقط منطق برای مثال مدیریت استثناءها، لاگ کردن ورودیها و خروجیهای متدها، کش کردن خروجی متدها، سعی مجدد اجرای متدهای با شکست مواجه شده و ... تک تک متدهای یک سرویس را به آنها معرفی میکنیم و سپس پروکسیهای پویا، کار محصور سازی خودکار اشیاء ساخته شدهی از سرویس اصلی را برای ما انجام میدهند. این روش نه فقط کار نوشتن دستی Decorator کلاسها را حذف میکند، بلکه عمومیتر نیز بوده و به تمام سرویسها قابل اعمال است.
Interceptors پایهی پروکسیهای پویا هستند
برای پیاده سازی پروکسیهای پویا نیاز است با مفهوم Interceptors آشنا شویم. به کمک Interceptors فرآیند فراخوانی متدها و خواص یک کلاس، تحت کنترل و نظارت قرار خواهند گرفت. زمانیکه یک IOC Container کار وهله سازی کلاس سرویس خاصی را انجام میدهد، در همین حین میتوان مراحل شروع، پایان و خطاهای متدها یا فراخوانیهای خواص را نیز تحت نظر قرار داد و به این ترتیب مصرف کننده، امکان تزریق کدهایی را در این مکانها خواهد یافت. مزیت مهم استفاده از Interceptors، عدم نیاز به کامپایل ثانویه اسمبلیهای موجود، برای تغییری در کدهای آنها است (برای تزریق نواحی تحت کنترل قرار دادن اعمال) و تمام کارها به صورت خودکار در زمان اجرای برنامه مدیریت میگردند.
با اضافه کردن Interception به پروسه وهله سازی سرویسها توسط یک IoC Container، مراحل کار اینبار به صورت زیر تغییر میکنند:
الف) در اینجا نیز در ابتدا فراخوان، درخواست وهلهای را بر اساس اینترفیسی خاص، به IOC Container ارائه میدهد.
ب) IOC Container نیز سعی در وهله سازی درخواست رسیده را بر اساس تنظیمات اولیهی خود میکند.
ج) اما در این حالت IOC Container تشخیص میدهد نوعی که باید بازگشت دهد، علاوه بر وهله سازی، نیاز به مزین سازی و پیاده سازی Interceptors را نیز دارد. بنابراین نوع مورد انتظار را در صورت وجود، به یک Dynamic Proxy، بجای بازگشت مستقیم به فراخوان ارائه میدهد.
د) در ادامه Dynamic Proxy، نوع مورد انتظار را توسط Interceptors محصور کرده و به فراخوان بازگشت میدهد.
ه) اکنون فراخوان، در حین استفاده از امکانات شیء وهله سازی شده، به صورت خودکار مراحل مختلف اجرای یک Decorator را سبب خواهد شد.
یعنی به صورت خلاصه، فراخوان سرویسی را درخواست میدهد، اما وهلهای را که دریافت میکند، یک لایهی اضافهتر تزئین کننده را نیز به همراه دارد که کاملا از دید فراخوان مخفی است و نحوهی کار کردن با آن سرویس، با و بدون این تزئین کننده، دقیقا یکی است. وجود این لایهی تزئین کننده سبب میشود تا فراخوانی هر متد این سرویس، از این لایه گذشته و سبب اجرای یک سری کد سفارشی، پیش و پس از اجرای این متد نیز گردد.
پیاده سازی پروکسیهای پویا توسط کتابخانهی Castle.Core در برنامههای NET Core.
در ادامه از کتابخانهی بسیار معروف Castle.Core برای پیاده سازی پروکسیهای پویا استفاده خواهیم کرد. از این کتابخانه در پروژهی EF Core، برای پیاده سازی Lazy loading نیز استفاده شدهاست.
برای دریافت آن یکی از دستورات زیر را اجرا نمائید:
و یا به صورت خلاصه برای افزودن آن، فایل csproj برنامه به صورت زیر تغییر میکند:
تبدیل ExceptionHandlingDecorator مثال قسمت قبل، به یک Interceptor مخصوص Castle.Core
در قسمت قبل، کلاس MyTaskServiceDecorator را جهت اعمال یک try/catch به همراه logging، به متد Run سرویس MyTaskService، تهیه کردیم. در اینجا قصد داریم نگارش عمومیتر این تزئین کننده را با طراحی یک Interceptor مخصوص Castle.Core انجام دهیم:
برای تهیهی یک کلاس Interceptor، کار با پیاده سازی اینترفیس IInterceptor شروع میشود. در اینجا فراخوانی متد ()invocation.Proceed، دقیقا معادل فراخوانی متد اصلی سرویس است؛ شبیه به کاری که در قسمت قبل انجام دادیم:
فراخوان، یک نمونهی تزئین شدهی از سرویس درخواستی را دریافت میکند. زمانیکه متد Run این نمونهی ویژه را اجرا میکند، در حقیقت وارد متد Run این Decorator شدهاست که اینبار در پشت صحنه، توسط Dynamic proxy ما به صورت پویا ایجاد میشود. اکنون جائیکه ()invocation.Proceed فراخوانی میشود، دقیقا معادل همان ()decorated.Run_ قسمت قبل است؛ یا همان اجرای متد اصلی سرویس مدنظر. اینجا است که میتوان منطقهای سفارشی را مانند لاگ کردن، کش کردن، سعی مجدد در اجرا و بسیاری از حالات دیگر، پیاده سازی کرد.
اتصال ExceptionHandlingInterceptor تهیه شده به سیستم تزریق وابستگیها
در ادامه روش معرفی ExceptionHandlingInterceptor تهیه شده را به سیستم تزریق وابستگیها، توسط متد Decorate کتابخانهی Scrutor که آنرا در قسمت قبل بررسی کردیم، ملاحظه میکنید:
- ProxyGenerator به همین نحو static readonly باید تعریف شود و در کل برنامه یک وهله از آن مورد نیاز است.
- سپس نیاز است خود سرویس اصلی غیر تزئین شده، به نحو متداولی به سیستم معرفی شود.
- در ادامه توسط متد الحاقی Decorate، کار تزئین ITaskService را با یک dynamicProxy که ExceptionHandlingInterceptor را به صورت پویا تبدیل به یک Decorator کرده و بر روی تک تک متدهای سرویس ITaskService اجرا میکند، انجام میدهیم.
- کاری که Scrutor در اینجا انجام میدهد، یافتن سرویس ITaskService معرفی شدهی پیشین و تعویض آن با dynamicProxy میباشد. بنابراین نیاز است تعریف services.AddTransient، پیش از تعریف services.Decorate انجام شده باشد.
یک نکته: چون ExceptionHandlingInterceptor دارای پارامتر تزریق شدهای در سازندهی آن است، بهتر است خود آنرا نیز به صورت یک سرویس ثبت کنیم و سپس وهلهای از آنرا از طریق serviceProvider.GetRequiredService در قسمت interceptors متد CreateInterfaceProxyWithTargetInterface معرفی کنیم تا نیازی به مقدار دهی دستی تک تک پارامترهای سازندهی آن نباشد.
اکنون اگر برنامه را اجرا کنیم و برای مثال ITaskService را در سازندهی یک کنترلر تزریق کنیم، بجای دریافت وهلهای از کلاس MyTaskService، اینبار وهلهای از Castle.Proxies.ITaskServiceProxy را دریافت میکنیم.
به این معنا که Castle.Core به صورت پویا وهلهی سرویس MyTaskService را داخل یک Castle.Proxies پیچیدهاست و از این پس ما از طریق این واسط، با سرویس اصلی MyTaskService ارتباط برقرار خواهیم کرد. برای درک بهتر این مراحل، بر روی سازندهی کلاس کنترلر و همچنین متد Intercept، تعدادی break-point را قرار دهید.
Interceptors پایهی پروکسیهای پویا هستند
برای پیاده سازی پروکسیهای پویا نیاز است با مفهوم Interceptors آشنا شویم. به کمک Interceptors فرآیند فراخوانی متدها و خواص یک کلاس، تحت کنترل و نظارت قرار خواهند گرفت. زمانیکه یک IOC Container کار وهله سازی کلاس سرویس خاصی را انجام میدهد، در همین حین میتوان مراحل شروع، پایان و خطاهای متدها یا فراخوانیهای خواص را نیز تحت نظر قرار داد و به این ترتیب مصرف کننده، امکان تزریق کدهایی را در این مکانها خواهد یافت. مزیت مهم استفاده از Interceptors، عدم نیاز به کامپایل ثانویه اسمبلیهای موجود، برای تغییری در کدهای آنها است (برای تزریق نواحی تحت کنترل قرار دادن اعمال) و تمام کارها به صورت خودکار در زمان اجرای برنامه مدیریت میگردند.
با اضافه کردن Interception به پروسه وهله سازی سرویسها توسط یک IoC Container، مراحل کار اینبار به صورت زیر تغییر میکنند:
الف) در اینجا نیز در ابتدا فراخوان، درخواست وهلهای را بر اساس اینترفیسی خاص، به IOC Container ارائه میدهد.
ب) IOC Container نیز سعی در وهله سازی درخواست رسیده را بر اساس تنظیمات اولیهی خود میکند.
ج) اما در این حالت IOC Container تشخیص میدهد نوعی که باید بازگشت دهد، علاوه بر وهله سازی، نیاز به مزین سازی و پیاده سازی Interceptors را نیز دارد. بنابراین نوع مورد انتظار را در صورت وجود، به یک Dynamic Proxy، بجای بازگشت مستقیم به فراخوان ارائه میدهد.
د) در ادامه Dynamic Proxy، نوع مورد انتظار را توسط Interceptors محصور کرده و به فراخوان بازگشت میدهد.
ه) اکنون فراخوان، در حین استفاده از امکانات شیء وهله سازی شده، به صورت خودکار مراحل مختلف اجرای یک Decorator را سبب خواهد شد.
یعنی به صورت خلاصه، فراخوان سرویسی را درخواست میدهد، اما وهلهای را که دریافت میکند، یک لایهی اضافهتر تزئین کننده را نیز به همراه دارد که کاملا از دید فراخوان مخفی است و نحوهی کار کردن با آن سرویس، با و بدون این تزئین کننده، دقیقا یکی است. وجود این لایهی تزئین کننده سبب میشود تا فراخوانی هر متد این سرویس، از این لایه گذشته و سبب اجرای یک سری کد سفارشی، پیش و پس از اجرای این متد نیز گردد.
پیاده سازی پروکسیهای پویا توسط کتابخانهی Castle.Core در برنامههای NET Core.
در ادامه از کتابخانهی بسیار معروف Castle.Core برای پیاده سازی پروکسیهای پویا استفاده خواهیم کرد. از این کتابخانه در پروژهی EF Core، برای پیاده سازی Lazy loading نیز استفاده شدهاست.
برای دریافت آن یکی از دستورات زیر را اجرا نمائید:
> Install-Package Castle.Core > dotnet add package Castle.Core
<Project Sdk="Microsoft.NET.Sdk.Web"> <ItemGroup> <PackageReference Include="castle.core" Version="4.3.1" /> </ItemGroup> </Project>
تبدیل ExceptionHandlingDecorator مثال قسمت قبل، به یک Interceptor مخصوص Castle.Core
در قسمت قبل، کلاس MyTaskServiceDecorator را جهت اعمال یک try/catch به همراه logging، به متد Run سرویس MyTaskService، تهیه کردیم. در اینجا قصد داریم نگارش عمومیتر این تزئین کننده را با طراحی یک Interceptor مخصوص Castle.Core انجام دهیم:
using System; using Castle.DynamicProxy; using Microsoft.Extensions.Logging; namespace CoreIocSample02.Utils { public class ExceptionHandlingInterceptor : IInterceptor { private readonly ILogger<ExceptionHandlingInterceptor> _logger; public ExceptionHandlingInterceptor(ILogger<ExceptionHandlingInterceptor> logger) { _logger = logger; } public void Intercept(IInvocation invocation) { try { invocation.Proceed(); //فراخوانی متد اصلی در اینجا صورت میگیرد } catch (Exception ex) { _logger.LogCritical(ex, "An unhandled exception has been occurred."); } } } }
public void Run() { try { _decorated.Run(); } catch (Exception ex) { _logger.LogCritical(ex, "An unhandled exception has been occurred."); } }
اتصال ExceptionHandlingInterceptor تهیه شده به سیستم تزریق وابستگیها
در ادامه روش معرفی ExceptionHandlingInterceptor تهیه شده را به سیستم تزریق وابستگیها، توسط متد Decorate کتابخانهی Scrutor که آنرا در قسمت قبل بررسی کردیم، ملاحظه میکنید:
namespace CoreIocSample02 { public class Startup { private static readonly ProxyGenerator _dynamicProxy = new ProxyGenerator(); public void ConfigureServices(IServiceCollection services) { services.AddTransient<ITaskService, MyTaskService>(); services.AddTransient<ExceptionHandlingInterceptor>(); services.Decorate(typeof(ITaskService), (target, serviceProvider) => _dynamicProxy.CreateInterfaceProxyWithTargetInterface( interfaceToProxy: typeof(ITaskService), target: target, interceptors: serviceProvider.GetRequiredService<ExceptionHandlingInterceptor>()) );
- سپس نیاز است خود سرویس اصلی غیر تزئین شده، به نحو متداولی به سیستم معرفی شود.
- در ادامه توسط متد الحاقی Decorate، کار تزئین ITaskService را با یک dynamicProxy که ExceptionHandlingInterceptor را به صورت پویا تبدیل به یک Decorator کرده و بر روی تک تک متدهای سرویس ITaskService اجرا میکند، انجام میدهیم.
- کاری که Scrutor در اینجا انجام میدهد، یافتن سرویس ITaskService معرفی شدهی پیشین و تعویض آن با dynamicProxy میباشد. بنابراین نیاز است تعریف services.AddTransient، پیش از تعریف services.Decorate انجام شده باشد.
یک نکته: چون ExceptionHandlingInterceptor دارای پارامتر تزریق شدهای در سازندهی آن است، بهتر است خود آنرا نیز به صورت یک سرویس ثبت کنیم و سپس وهلهای از آنرا از طریق serviceProvider.GetRequiredService در قسمت interceptors متد CreateInterfaceProxyWithTargetInterface معرفی کنیم تا نیازی به مقدار دهی دستی تک تک پارامترهای سازندهی آن نباشد.
اکنون اگر برنامه را اجرا کنیم و برای مثال ITaskService را در سازندهی یک کنترلر تزریق کنیم، بجای دریافت وهلهای از کلاس MyTaskService، اینبار وهلهای از Castle.Proxies.ITaskServiceProxy را دریافت میکنیم.
به این معنا که Castle.Core به صورت پویا وهلهی سرویس MyTaskService را داخل یک Castle.Proxies پیچیدهاست و از این پس ما از طریق این واسط، با سرویس اصلی MyTaskService ارتباط برقرار خواهیم کرد. برای درک بهتر این مراحل، بر روی سازندهی کلاس کنترلر و همچنین متد Intercept، تعدادی break-point را قرار دهید.
نظرات مطالب
PHP سریعتر از ASP.NET! افسانه یا واقعیت؟
به دلیل وسعت استفاده بیشتر از php و نیز استفاده سایتها و شرکتهای بزرگ از php خیلیها فکر میکنن php بهتر و سریعتر از asp.net هست در حالیکه این وسعت استفاده بخاطر اوپن سورس و رایگان بودنه php هست و چون وب سرور apache هم معمولا رو لینوکس نصب میشه و خود لینوکس هم اپن سورسه، تمام این دلایل دست به دست هم داده تا php بهتر به نظر بیاد.
جدا از بحث سرعت اگر از لحاظ ساختاری بررسی کنیم php بیشتر یک زبان اسکریپتی است تا برنامه نویسی و ویژیگیهای زیانهای خوب و شی گرا رو نداره.
لیست خلاصه ای نکات بهرهوری گفته شده از اسکات هانسلمن که در بلاگ آقای دنی شریبر آورده شده و دوباره توسط هانسلمن در بلاگ خودشان ارائه گردیده. در ادامه نیز جملاتی که از ایشان در متن بوده که در زیر آورده شده
- تفاوت بین "اثربخشی": انجام کارهای درست و "بهروری": انجام درست کارها
- امید نقشه نیست، امید چیزی نیست جز انتظار کشیدن و اجازه دادن به زندگی که برای شما اتفاق بیفتد
- تفاوت بین "اثربخشی": انجام کارهای درست و "بهروری": انجام درست کارها
- امید نقشه نیست، امید چیزی نیست جز انتظار کشیدن و اجازه دادن به زندگی که برای شما اتفاق بیفتد
مطالب دورهها
آشنایی با نحوه ایجاد یک IoC Container
قبل از اینکه وارد بحث استفاده از کتابخانههای بسیار غنی IoC Container موجود شویم، بهتر است یک نمونه ساده آنها را طراحی کنیم تا بهتر بتوان با عملکرد و ساختار درونی آنها آشنا شد.
IoC Container چیست؟
IoC Container، فریم ورکی است برای انجام تزریق وابستگیها. در این فریم ورک امکان تنظیم اولیه وابستگیهای سیستم وجود دارد. برای مثال زمانیکه برنامه از یک IoC Container، نوع اینترفیس خاصی را درخواست میکند، این فریم ورک با توجه به تنظیمات اولیهاش، کلاسی مشخص را بازگشت خواهد داد.
IoC Containerهای قدیمیتر، برای انجام تنظیمات اولیه خود از فایلهای کانفیگ استفاده میکردند. نمونههای جدیدتر آنها از روشهای Fluent interfaces برای مشخص سازی تنظیمات خود بهره میبرند.
زمانیکه از یک IOC Container در کدهای خود استفاده میکنید، مراحلی چند رخ خواهند داد:
الف) کد فراخوان، از IOC Container، یک شیء مشخص را درخواست میکند. عموما اینکار با درخواست یک اینترفیس صورت میگیرد؛ هرچند محدودیتی نیز نداشته و امکان درخواست یک کلاس از نوعی مشخص نیز وجود دارد.
ب) در ادامه IOC Container به لیست اشیاء قابل ارائه توسط خود نگاه کرده و در صورت وجود، وهله سازی شیء درخواست شده را انجام و نهایتا شیء مطلوب را بازگشت خواهد داد.
در این بین زنجیرهی وابستگیهای مورد نیاز نیز وهله سازی خواهند شد. برای مثال اگر وابستگی اول به وابستگی دوم برای وهله سازی نیاز دارد، کار وهله سازی وابستگیهای وابستگی دوم نیز به صورت خودکار انجام خواهند شد. (این موردی است که بسیاری از تازه واردان به این بحث تا یکبار آنرا امتحان نکنند باور نخواهند کرد!)
ج) سپس کد فراخوان وهله دریافتی را مورد پردازش قرار داده و سپس شروع به استفاده از متدها و خواص آن خواهد نمود.
در تصویر فوق محل قرارگیری یک IOC Container را مشاهده میکنید. یک IOC Container در مورد تمام وابستگیهای مورد نیاز، اطلاعات لازم را دارد. همچنین این فریم ورک در مورد کلاسی که قرار است از وابستگیهای سیستم استفاده نماید نیز مطلع است؛ به این ترتیب میتواند به صورت خودکار در زمان وهله سازی آن، نوعهای وابستگیهای مورد نیاز آنرا در اختیارش قرار دهد.
برای مثال در اینجا MyClass، وابستگی مشخص شده در سازنده خود را به نام IDependency از IOC Container درخواست میکند. سپس این IOC Container بر اساس تنظیمات اولیه خود، یکی از وابستگیهای A یا B را بازگشت خواهد داد.
آغاز به کار ساخت یک IOC Container نمونه
در ابتدا کدهای آغازین مثال بحث جاری را در نظر بگیرید:
در اینجا وابستگیهای کلاس خریدار از طریق سازنده آن که متداولترین روش تزریق وابستگیها است، در اختیار آن قرار خواهد گرفت. یک اینترفیس کردیت کارت تعریف شدهاست به همراه دو پیاده سازی نمونه آن مانند مسترکارت و ویزا کارت. سادهترین نوع فراخوانی آن نیز میتواند مانند کدهای ذیل باشد (تزریق وابستگیهای دستی):
در ادامه قصد داریم این فراخوانیها را اندکی هوشمندتر کنیم تا بتوان بر اساس تنظیمات برنامه، کار تزریق وابستگیها صورت گیرد و به سادگی بتوان اینترفیسهای متفاوتی را در اینجا درخواست و مورد استفاده قرار داد. اینجا است که به اولین IoC Container خود خواهیم رسید:
در اینجا کدهای کلاس Resolver یا همان IoC Container ابتدایی بحث را مشاهده میکنید. توضیحات قسمتهای مختلف آن به صورت کامنت ارائه شدهاند.
در ادامه نحوه استفاده از IoC Container ایجاد شده را مشاهده میکنید.
ابتدا کار تعاریف نگاشتهای اولیه انجام میشود. در این صورت زمانیکه متد Resolve فراخوانی میگردد، نوع درخواستی آن به همراه سازنده دارای آرگومانی از نوع ICreditCard وهله سازی شده و بازگشت داده خواهد شد. سپس با در دست داشتن یک وهله آماده، متد Charge آنرا فراخوانی خواهیم کرد.
بررسی نحوه استفاده از Microsoft Unity به عنوان یک IoC Container
Unity چیست؟
Unity یک فریم ورک IoC Container تهیه شده توسط مایکروسافت میباشد که آنرا به عنوان جزئی از Enterprise Library خود قرار داده است. بنابراین برای دریافت آن یا میتوان کل مجموعه Enterprise Library را دریافت کرد و یا به صورت مجزا به عنوان یک بسته نیوگت نیز قابل تهیه است.
برای این منظور در خط فرمان پاورشل نیوگت در VS.NET دستور ذیل را اجرا کنید:
پیاده سازی مثال خریدار توسط Unity
همان مثال قسمت قبل را درنظر بگیرید. قصد داریم اینبار بجای IoC Container دست سازی که تهیه شد، پیاده سازی آنرا به کمک MS Unity انجام دهیم.
همانطور که ملاحظه میکنید، API آن بسیار شبیه به کلاس دست سازی است که در قسمت قبل تهیه کردیم.
مطابق کدهای فوق، ابتدا تنظیمات IoC Container انجام شده است. به آن اعلام کردهایم که در صورت نیاز به ICreditCard، نوع MasterCard را یافته و وهله سازی کن. با این تفاوت که Unity هوشمندتر بوده و سطر مربوط به ثبت کلاس Shoper ایی را که در قسمت قبل انجام دادیم، در اینجا حذف شده است.
سپس به این IoC Container اعلام کردهایم که نیاز به یک وهله از کلاس خریدار داریم. در اینجا Unity کار وهله سازیهای خودکار وابستگیها و تزریق آنها را در سازنده کلاس خریدار انجام داده و نهایتا یک وهله قابل استفاده را در اختیار ادامه برنامه قرار خواهد داد.
یک نکته:
به صورت پیش فرض کار تزریق وابستگیها در سازنده کلاسها به صورت خودکار انجام میشود. اگر نیاز به Setter injection و مقدار دهی خواص کلاس وجود داشت میتوان به نحو ذیل عمل کرد:
نام خاصیت و مقدار مورد نظر به عنوان پارامتر متد RegisterType باید تعریف شوند.
مدیریت طول عمر اشیاء در Unity
توسط یک IoC Container میتوان یک وهله معمولی از شیءایی را درخواست کرد و یا حتی طول عمر این وهله را به صورت Singleton معرفی نمود (یک وهله در طول عمر کل برنامه). در Unity اگر تنظیم خاصی اعمال نشود، هربار که متد Resolve فراخوانی میگردد، یک وهله جدید را در اختیار ما قرار خواهد داد. اما اگر پارامتر متد RegisterType را با وهلهای از ContainerControlledLifetimeManager مقدار دهی کنیم:
از این پس با هربار فراخوانی متد Resolve، در صورت نیاز به وابستگی از نوع ICreditCard، تنها یک وهله مشترک از MasterCard ارائه خواهد شد.
حالت پیش فرض مورد استفاده، بدون ذکر پارامتر متد RegisterType، مقدار TransientLifetimeManager میباشد.
IoC Container چیست؟
IoC Container، فریم ورکی است برای انجام تزریق وابستگیها. در این فریم ورک امکان تنظیم اولیه وابستگیهای سیستم وجود دارد. برای مثال زمانیکه برنامه از یک IoC Container، نوع اینترفیس خاصی را درخواست میکند، این فریم ورک با توجه به تنظیمات اولیهاش، کلاسی مشخص را بازگشت خواهد داد.
IoC Containerهای قدیمیتر، برای انجام تنظیمات اولیه خود از فایلهای کانفیگ استفاده میکردند. نمونههای جدیدتر آنها از روشهای Fluent interfaces برای مشخص سازی تنظیمات خود بهره میبرند.
زمانیکه از یک IOC Container در کدهای خود استفاده میکنید، مراحلی چند رخ خواهند داد:
الف) کد فراخوان، از IOC Container، یک شیء مشخص را درخواست میکند. عموما اینکار با درخواست یک اینترفیس صورت میگیرد؛ هرچند محدودیتی نیز نداشته و امکان درخواست یک کلاس از نوعی مشخص نیز وجود دارد.
ب) در ادامه IOC Container به لیست اشیاء قابل ارائه توسط خود نگاه کرده و در صورت وجود، وهله سازی شیء درخواست شده را انجام و نهایتا شیء مطلوب را بازگشت خواهد داد.
در این بین زنجیرهی وابستگیهای مورد نیاز نیز وهله سازی خواهند شد. برای مثال اگر وابستگی اول به وابستگی دوم برای وهله سازی نیاز دارد، کار وهله سازی وابستگیهای وابستگی دوم نیز به صورت خودکار انجام خواهند شد. (این موردی است که بسیاری از تازه واردان به این بحث تا یکبار آنرا امتحان نکنند باور نخواهند کرد!)
ج) سپس کد فراخوان وهله دریافتی را مورد پردازش قرار داده و سپس شروع به استفاده از متدها و خواص آن خواهد نمود.
در تصویر فوق محل قرارگیری یک IOC Container را مشاهده میکنید. یک IOC Container در مورد تمام وابستگیهای مورد نیاز، اطلاعات لازم را دارد. همچنین این فریم ورک در مورد کلاسی که قرار است از وابستگیهای سیستم استفاده نماید نیز مطلع است؛ به این ترتیب میتواند به صورت خودکار در زمان وهله سازی آن، نوعهای وابستگیهای مورد نیاز آنرا در اختیارش قرار دهد.
برای مثال در اینجا MyClass، وابستگی مشخص شده در سازنده خود را به نام IDependency از IOC Container درخواست میکند. سپس این IOC Container بر اساس تنظیمات اولیه خود، یکی از وابستگیهای A یا B را بازگشت خواهد داد.
آغاز به کار ساخت یک IOC Container نمونه
در ابتدا کدهای آغازین مثال بحث جاری را در نظر بگیرید:
using System; namespace DI01 { public interface ICreditCard { string Charge(); } public class Visa : ICreditCard { public string Charge() { return "Charging with the Visa!"; } } public class MasterCard : ICreditCard { public string Charge() { return "Swiping the MasterCard!"; } } public class Shopper { private readonly ICreditCard creditCard; public Shopper(ICreditCard creditCard) { this.creditCard = creditCard; } public void Charge() { var chargeMessage = creditCard.Charge(); Console.WriteLine(chargeMessage); } } }
var shopper = new Shopper(new Visa()); shopper.Charge();
using System; using System.Collections.Generic; using System.Linq; namespace DI01 { public class Resolver { //کار ذخیره سازی و نگاشت از یک نوع به نوعی دیگر در اینجا توسط این دیکشنری انجام خواهد شد private Dictionary<Type, Type> dependencyMap = new Dictionary<Type, Type>(); /// <summary> /// یک نوع خاص از آن درخواست شده و سپس بر اساس تنظیمات برنامه، کار وهله سازی /// نمونه معادل آن صورت خواهد گرفت /// </summary> public T Resolve<T>() { return (T)Resolve(typeof(T)); } private object Resolve(Type typeToResolve) { Type resolvedType; // ابتدا بررسی میشود که آیا در تنظیمات برنامه نگاشت متناظری برای نوع درخواستی وجود دارد؟ if (!dependencyMap.TryGetValue(typeToResolve, out resolvedType)) { //اگر خیر، کار متوقف خواهد شد throw new Exception(string.Format("Could not resolve type {0}", typeToResolve.FullName)); } var firstConstructor = resolvedType.GetConstructors().First(); var constructorParameters = firstConstructor.GetParameters(); // در ادامه اگر این نوع، دارای سازندهی بدون پارامتری است // بلافاصله وهله سازی خواهد شد if (!constructorParameters.Any()) return Activator.CreateInstance(resolvedType); var parameters = new List<object>(); foreach (var parameterToResolve in constructorParameters) { // در اینجا یک فراخوانی بازگشتی صورت گرفته است برای وهله سازی // خودکار پارامترهای مختلف سازنده یک کلاس parameters.Add(Resolve(parameterToResolve.ParameterType)); } return firstConstructor.Invoke(parameters.ToArray()); } public void Register<TFrom, TTo>() { dependencyMap.Add(typeof(TFrom), typeof(TTo)); } } }
var resolver = new Resolver(); //تنظیمات اولیه resolver.Register<Shopper, Shopper>(); resolver.Register<ICreditCard, Visa>(); //تزریق وابستگیها و وهله سازی var shopper = resolver.Resolve<Shopper>(); shopper.Charge();
ابتدا کار تعاریف نگاشتهای اولیه انجام میشود. در این صورت زمانیکه متد Resolve فراخوانی میگردد، نوع درخواستی آن به همراه سازنده دارای آرگومانی از نوع ICreditCard وهله سازی شده و بازگشت داده خواهد شد. سپس با در دست داشتن یک وهله آماده، متد Charge آنرا فراخوانی خواهیم کرد.
بررسی نحوه استفاده از Microsoft Unity به عنوان یک IoC Container
Unity چیست؟
Unity یک فریم ورک IoC Container تهیه شده توسط مایکروسافت میباشد که آنرا به عنوان جزئی از Enterprise Library خود قرار داده است. بنابراین برای دریافت آن یا میتوان کل مجموعه Enterprise Library را دریافت کرد و یا به صورت مجزا به عنوان یک بسته نیوگت نیز قابل تهیه است.
برای این منظور در خط فرمان پاورشل نیوگت در VS.NET دستور ذیل را اجرا کنید:
PM> Install-Package Unity
پیاده سازی مثال خریدار توسط Unity
همان مثال قسمت قبل را درنظر بگیرید. قصد داریم اینبار بجای IoC Container دست سازی که تهیه شد، پیاده سازی آنرا به کمک MS Unity انجام دهیم.
using Microsoft.Practices.Unity; namespace DI02 { class Program { static void Main(string[] args) { var container = new UnityContainer(); container.RegisterType<ICreditCard, MasterCard>(); var shopper = container.Resolve<Shopper>(); shopper.Charge(); } } }
مطابق کدهای فوق، ابتدا تنظیمات IoC Container انجام شده است. به آن اعلام کردهایم که در صورت نیاز به ICreditCard، نوع MasterCard را یافته و وهله سازی کن. با این تفاوت که Unity هوشمندتر بوده و سطر مربوط به ثبت کلاس Shoper ایی را که در قسمت قبل انجام دادیم، در اینجا حذف شده است.
سپس به این IoC Container اعلام کردهایم که نیاز به یک وهله از کلاس خریدار داریم. در اینجا Unity کار وهله سازیهای خودکار وابستگیها و تزریق آنها را در سازنده کلاس خریدار انجام داده و نهایتا یک وهله قابل استفاده را در اختیار ادامه برنامه قرار خواهد داد.
یک نکته:
به صورت پیش فرض کار تزریق وابستگیها در سازنده کلاسها به صورت خودکار انجام میشود. اگر نیاز به Setter injection و مقدار دهی خواص کلاس وجود داشت میتوان به نحو ذیل عمل کرد:
container.RegisterType<ICreditCard, MasterCard>(new InjectionProperty("propertyName", 5));
مدیریت طول عمر اشیاء در Unity
توسط یک IoC Container میتوان یک وهله معمولی از شیءایی را درخواست کرد و یا حتی طول عمر این وهله را به صورت Singleton معرفی نمود (یک وهله در طول عمر کل برنامه). در Unity اگر تنظیم خاصی اعمال نشود، هربار که متد Resolve فراخوانی میگردد، یک وهله جدید را در اختیار ما قرار خواهد داد. اما اگر پارامتر متد RegisterType را با وهلهای از ContainerControlledLifetimeManager مقدار دهی کنیم:
container.RegisterType<ICreditCard, MasterCard>(new ContainerControlledLifetimeManager());
حالت پیش فرض مورد استفاده، بدون ذکر پارامتر متد RegisterType، مقدار TransientLifetimeManager میباشد.
Source Generators که به همراه C# 9.0 ارائه شدند، یک فناوری نوین meta-programming است و به عنوان جزئی از پروسهی استاندارد کامپایل برنامه، ظاهر میشود. هدف اصلی از ارائهی Source Generators، تولید کدهای تکراری مورد استفادهی در برنامهها است. برای مثال بجای انجام کارهای تکراری مانند پیاده سازی متدهای GetHashCode، ToString و یا حتی یک AutoMapper و یا Serializer، برای تمام کلاسهای برنامه، Source Generators میتوانند آنها را به صورت خودکار پیاده سازی کنند و همچنین با هر تغییری در کدهای کلاسها، این پیاده سازیها به صورت خودکار به روز خواهند شد. مزیت این روش نه فقط تولید پویای کدها است، بلکه سبب بهبود کارآیی برنامه هم خواهند شد؛ از این جهت که برای مثال میتوان اعمالی مانند Serialization را بدون انجام Reflection در زمان اجرا، توسط آنها پیاده سازی کرد.
زمانیکه پروسهی کامپایل برنامه شروع میشود، در این بین، به مرحلهی جدیدی به نام «تولید کدها» میرسد. در این حالت، کامپایلر تمام اطلاعاتی را که در مورد پروژهی جاری در اختیار دارد، به تولید کنندهی کد معرفی شدهی به آن ارائه میدهد. بر اساس این اطلاعات غنی ارائه شدهی توسط کامپایلر، تولید کنندهی کد، شروع به تولید کدهای جدیدی کرده و آنها را در اختیار ادامهی پروسهی کامپایل، قرار میدهد. پس از آن، کامپایلر با این کدهای جدید، همانند سایر کدهای موجود در پروژه رفتار کرده و عملکرد عادی خودش را ادامه میدهد.
یک برنامه میتواند از چندین Source Generators نیز استفاده کند که روش قرار گرفتن آنها را در پروسهی کامپایل، در شکل زیر مشاهده میکنید:
Source Generators از یکدیگر کاملا مستقل هستند و اطلاعات آنها Immutable است. یعنی نمیتوان اطلاعات تولیدی توسط یک Source Generator را در دیگری تغییر داد و تمام فایلهای تولیدی توسط انواع Source Generators موجود، به پروسهی کامپایل نهایی اضافه میشوند. هرچند زمانیکه فایلی توسط یک تولید کنندهی کد، به کامپایلر اضافه میشود، بلافاصله اطلاعات آن در کل برنامه و IDE و تمام Source Generators موجود دیگر، قابل مشاهده و استفاده است.
مقایسهای بین تولید کنندههای کد و فناوری IL Weaving
Source Generators، تنها راه و روش تولید کد، نیستند و پیش از آن روشهایی مانند استفاده از T4 templates ، Fody ، PostSharp و امثال آن نیز ارائه شدهاست. در ادامه مقایسهای را بین تولید کنندههای کد و فناوری IL Weaving را که پیشتر در سری AOP در این سایت مطالعه کردهاید، مشاهده میکنید:
تولید کنندههای کد:
- تنها میتوانند فایلهای جدید را اضافه کنند. یعنی «در حین» پروسهی کامپایل ظاهر میشوند و به عنوان یک مکمل، تاثیر گذارند. برای مثال نمیتوانند محتوای یک خاصیت یا متد از پیش موجود را تغییر دهند. اما میتوانند هر نوع کد partial ای را «تکمیل» کنند.
- محتوای اضافه شدهی توسط یک تولید کنندهی کد، بلافاصله توسط Compiler شناسایی شده و بررسی میشود و همچنین در Intellisense ظاهر شده و به سادگی قابل دسترسی است. همچنین، قابلیت دیباگ نیز دارد.
IL Weaving:
- میتوانند bytecode برنامه را تغییر دهند. یعنی «پس از» پروسهی کامپایل ظاهر شده و کدهایی را به اسمبلی نهایی تولید شده اضافه میکنند. در این حالت محدودیتی از لحاظ محل تغییر کدها وجود ندارد. برای مثال میتوان بدنهی یک متد یا خاصیت را بطور کامل بازنویسی کرد و کارکردهایی مانند تزریق کدهای caching و logging را دارند.
- کدهایی که توسط این پروسه اضافه میشوند، در حین کدنویسی متداول، قابلیت دسترسی ندارند؛ چون پس از پروسهی کامپایل، به فایل باینری نهایی تولیدی، اضافه میشوند. بنابراین قابلیت دیباگ به همراه سایر کدهای برنامه را نیز ندارند. به علاوه چون توسط کامپایلر در حین پروسهی کامپایل، بررسی نمیشوند، ممکن است به همراه قطعه کدهای غیرقابل اجرایی نیز باشند و دیباگ آنها بسیار مشکل است.
آیندهی Reflection به چه صورتی خواهد شد؟
هرچند Reflection کار تولید کدی را انجام نمیدهد، اما یکی از کارهای متداول با آن، یافتن و محاسبهی اطلاعات خواص و فیلدهای اشیاء، در زمان اجرا است و مزیت کار کردن با آن نیز این است که اگر خاصیتی یا فیلدی تغییر کند، نیازی به بازنویسی قسمتهای پیاده سازی شدهی با Reflection نیست. به همین جهت برای مثال تقریبا تمام کتابخانههای Serialization، از Reflection برای پیاده سازی اعمال خود استفاده میکنند.
امروز، تمام اینگونه عملیات را توسط Source Generators نیز میتوان انجام داد و این فناوری جدید، قابلیت به روز رسانی خودکار کدهای تولیدی را با کم و زیاد شدن خواص و فیلدهای کلاسها دارد و نمونهای از آن، Source Generator توکار مرتبط با کار با JSON در دات نت 6 است که به شدت سبب بهبود کارآیی برنامه، در مقایسه با استفادهی از Reflection میشود؛ چون اینبار تمام محاسبات دقیق مرتبط با Serialization به صورت خودکار در زمان کامپایل برنامه انجام میشود و جزئی از خروجی برنامهی نهایی خواهد شد و دیگر نیازی به محاسبهی هربارهی اطلاعات مورد نیاز، در زمان اجرای برنامه نیست.
نمونهای از روش دسترسی به اطلاعات کلاسها و خواص و فیلدهای آنها را در زمان کامپایل برنامه توسط Source Generators، در مثال قسمت بعد، مشاهده خواهید کرد.
وضعیت T4 templates چگونه خواهد شد؟
در سالهای آغازین ارائهی دات نت، استفاده از T4 templates جهت تولید کدها بسیار مرسوم بود؛ اما با ارائهی Source Generators، این ابزار نیز منسوخ شده در نظر گرفته میشود.
T4 Templates همانند Source Generators تنها کدها و فایلهای جدیدی را تولید میکنند و توانایی تغییر کدهای موجود را ندارند. اما مشکل مهم آن، داشتن Syntax ای خاص است که توسط اکثر IDEها پشتیبانی نمیشود. همچنین عموما اجرای آنها نیز دستی است و برخلاف Source Generators، با تغییرات کدها، به صورت خودکار به روز نمیشوند.
تغییرات زبان #C در جهت پشتیبانی از تولید کنندههای کد
از سالهای اول ارائهی زبان #C، واژهی کلیدی partial، جهت فراهم آوردن امکان تقسیم کدهای یک کلاس، به چندین فایل، میسر شد که از این قابلیت در فناوری T4 Templates زیاد استفاده میشد. اکنون با ارائهی تولید کنندههای کد، واژهی کلیدی partial را میتوان به متدها نیز افزود تا پیاده سازی اصلی آنها، در فایلی دیگر، توسط تولید کنندههای کد انجام شود. تا C# 8.0 امکان افزودن واژهی کلیدی partial به متدهای خصوصی یک کلاس و آن هم از نوع void وجود داشت و در C# 9.0 به متدهای عمومی کلاسها نیز اضافه شدهاست و اکنون این متدها میتوانند void هم نباشند:
زمانیکه پروسهی کامپایل برنامه شروع میشود، در این بین، به مرحلهی جدیدی به نام «تولید کدها» میرسد. در این حالت، کامپایلر تمام اطلاعاتی را که در مورد پروژهی جاری در اختیار دارد، به تولید کنندهی کد معرفی شدهی به آن ارائه میدهد. بر اساس این اطلاعات غنی ارائه شدهی توسط کامپایلر، تولید کنندهی کد، شروع به تولید کدهای جدیدی کرده و آنها را در اختیار ادامهی پروسهی کامپایل، قرار میدهد. پس از آن، کامپایلر با این کدهای جدید، همانند سایر کدهای موجود در پروژه رفتار کرده و عملکرد عادی خودش را ادامه میدهد.
یک برنامه میتواند از چندین Source Generators نیز استفاده کند که روش قرار گرفتن آنها را در پروسهی کامپایل، در شکل زیر مشاهده میکنید:
Source Generators از یکدیگر کاملا مستقل هستند و اطلاعات آنها Immutable است. یعنی نمیتوان اطلاعات تولیدی توسط یک Source Generator را در دیگری تغییر داد و تمام فایلهای تولیدی توسط انواع Source Generators موجود، به پروسهی کامپایل نهایی اضافه میشوند. هرچند زمانیکه فایلی توسط یک تولید کنندهی کد، به کامپایلر اضافه میشود، بلافاصله اطلاعات آن در کل برنامه و IDE و تمام Source Generators موجود دیگر، قابل مشاهده و استفاده است.
مقایسهای بین تولید کنندههای کد و فناوری IL Weaving
Source Generators، تنها راه و روش تولید کد، نیستند و پیش از آن روشهایی مانند استفاده از T4 templates ، Fody ، PostSharp و امثال آن نیز ارائه شدهاست. در ادامه مقایسهای را بین تولید کنندههای کد و فناوری IL Weaving را که پیشتر در سری AOP در این سایت مطالعه کردهاید، مشاهده میکنید:
تولید کنندههای کد:
- تنها میتوانند فایلهای جدید را اضافه کنند. یعنی «در حین» پروسهی کامپایل ظاهر میشوند و به عنوان یک مکمل، تاثیر گذارند. برای مثال نمیتوانند محتوای یک خاصیت یا متد از پیش موجود را تغییر دهند. اما میتوانند هر نوع کد partial ای را «تکمیل» کنند.
- محتوای اضافه شدهی توسط یک تولید کنندهی کد، بلافاصله توسط Compiler شناسایی شده و بررسی میشود و همچنین در Intellisense ظاهر شده و به سادگی قابل دسترسی است. همچنین، قابلیت دیباگ نیز دارد.
IL Weaving:
- میتوانند bytecode برنامه را تغییر دهند. یعنی «پس از» پروسهی کامپایل ظاهر شده و کدهایی را به اسمبلی نهایی تولید شده اضافه میکنند. در این حالت محدودیتی از لحاظ محل تغییر کدها وجود ندارد. برای مثال میتوان بدنهی یک متد یا خاصیت را بطور کامل بازنویسی کرد و کارکردهایی مانند تزریق کدهای caching و logging را دارند.
- کدهایی که توسط این پروسه اضافه میشوند، در حین کدنویسی متداول، قابلیت دسترسی ندارند؛ چون پس از پروسهی کامپایل، به فایل باینری نهایی تولیدی، اضافه میشوند. بنابراین قابلیت دیباگ به همراه سایر کدهای برنامه را نیز ندارند. به علاوه چون توسط کامپایلر در حین پروسهی کامپایل، بررسی نمیشوند، ممکن است به همراه قطعه کدهای غیرقابل اجرایی نیز باشند و دیباگ آنها بسیار مشکل است.
آیندهی Reflection به چه صورتی خواهد شد؟
هرچند Reflection کار تولید کدی را انجام نمیدهد، اما یکی از کارهای متداول با آن، یافتن و محاسبهی اطلاعات خواص و فیلدهای اشیاء، در زمان اجرا است و مزیت کار کردن با آن نیز این است که اگر خاصیتی یا فیلدی تغییر کند، نیازی به بازنویسی قسمتهای پیاده سازی شدهی با Reflection نیست. به همین جهت برای مثال تقریبا تمام کتابخانههای Serialization، از Reflection برای پیاده سازی اعمال خود استفاده میکنند.
امروز، تمام اینگونه عملیات را توسط Source Generators نیز میتوان انجام داد و این فناوری جدید، قابلیت به روز رسانی خودکار کدهای تولیدی را با کم و زیاد شدن خواص و فیلدهای کلاسها دارد و نمونهای از آن، Source Generator توکار مرتبط با کار با JSON در دات نت 6 است که به شدت سبب بهبود کارآیی برنامه، در مقایسه با استفادهی از Reflection میشود؛ چون اینبار تمام محاسبات دقیق مرتبط با Serialization به صورت خودکار در زمان کامپایل برنامه انجام میشود و جزئی از خروجی برنامهی نهایی خواهد شد و دیگر نیازی به محاسبهی هربارهی اطلاعات مورد نیاز، در زمان اجرای برنامه نیست.
نمونهای از روش دسترسی به اطلاعات کلاسها و خواص و فیلدهای آنها را در زمان کامپایل برنامه توسط Source Generators، در مثال قسمت بعد، مشاهده خواهید کرد.
وضعیت T4 templates چگونه خواهد شد؟
در سالهای آغازین ارائهی دات نت، استفاده از T4 templates جهت تولید کدها بسیار مرسوم بود؛ اما با ارائهی Source Generators، این ابزار نیز منسوخ شده در نظر گرفته میشود.
T4 Templates همانند Source Generators تنها کدها و فایلهای جدیدی را تولید میکنند و توانایی تغییر کدهای موجود را ندارند. اما مشکل مهم آن، داشتن Syntax ای خاص است که توسط اکثر IDEها پشتیبانی نمیشود. همچنین عموما اجرای آنها نیز دستی است و برخلاف Source Generators، با تغییرات کدها، به صورت خودکار به روز نمیشوند.
تغییرات زبان #C در جهت پشتیبانی از تولید کنندههای کد
از سالهای اول ارائهی زبان #C، واژهی کلیدی partial، جهت فراهم آوردن امکان تقسیم کدهای یک کلاس، به چندین فایل، میسر شد که از این قابلیت در فناوری T4 Templates زیاد استفاده میشد. اکنون با ارائهی تولید کنندههای کد، واژهی کلیدی partial را میتوان به متدها نیز افزود تا پیاده سازی اصلی آنها، در فایلی دیگر، توسط تولید کنندههای کد انجام شود. تا C# 8.0 امکان افزودن واژهی کلیدی partial به متدهای خصوصی یک کلاس و آن هم از نوع void وجود داشت و در C# 9.0 به متدهای عمومی کلاسها نیز اضافه شدهاست و اکنون این متدها میتوانند void هم نباشند:
partial class MyType { partial void OnModelCreating(string input); // C# 8.0 public partial bool IsPet(string input); // C# 9.0 } partial class MyType { public partial bool IsPet(string input) => input is "dog" or "cat" or "fish"; }