namespace Microsoft.Extensions.DependencyInjection { public interface IServiceCollection : ICollection<ServiceDescriptor>, IEnumerable<ServiceDescriptor>, IEnumerable, IList<ServiceDescriptor> { } }
ServiceProviderو مؤلفههای درونی آن، از یک مجموعه از ServiceDescriptorها برای برنامهی شما بر اساس سرویسهای ثبت شدهی توسط IServiceCollectionاستفاده میکنند.ServiceDescriptorحاوی اطلاعاتی در مورد سرویسهای ثبت شدهاست. اگر به کد منبع این کلاس برویم، میبینیم پنج Propertyاصلی دارد که با استفاده از آنها اطلاعات یک سرویس ثبت و نگهداری میشوند.با استفاده از این اطلاعات در هنگام اجرا ، DI Containerبه واکشی و ساخت نمونههایی از سرویس درخواستی اقدام میکند:
public Type ImplementationType { get; } public object ImplementationInstance { get; } public Func<IServiceProvider, object> ImplementationFactory { get; } public ServiceLifetime Lifetime { get; } public Type ServiceType { get; }
هر کدام از این Propertyها کاربرد خاص خود را دارند:
- ·ServiceType: نوع سرویسی را که میخواهیم ثبت شود، مشخص میکنیم ( مثلا اینترفیس IMessageService ) .
- ·ImplementionType: نوع پیاده سازی سرویس مورد نظرمان را مشخص میکند ( مثلا کلاس MessageService ).
- ·LifeTime: طول حیات سرویس را مشخص میکند. DI Containerبر اساس این ویژگی، اقدام به ساخت و از بین بردن نمونههایی از سرویس میکند.
- ·ImplementionInstance: نمونهی ساخته شدهی از سرویس است.
- ·ImplementionFactory: یک Delegateاست که چگونگی ساخته شدن یک نمونه از پیاده سازی سرویس را در خود نگه میدارد. این Delegateیک IServiceProviderرا به عنوان ورودی دریافت میکند و یک objectرا بازگشت میدهد.
به صورت عادی، در سناریوهای معمول ثبت سرویسها درون IServiceCollection، نیازی به استفاده از ServiceDescriptorنیست؛ ولی اگر بخواهیم سرویسها را به روشهای پیشرفتهتری ثبت کنیم، مجبوریم که به صورت مستقیم با این کلاس کار کنیم.
می توانیم یک ServiceDesciriptorرا به روشهای زیر تعریف کنیم:
var serviceDescriptor1 = new ServiceDescriptor( typeof(IMessageServiceB), typeof(MessageServiceBB), ServiceLifetime.Scoped); var serviceDescriptor2 = ServiceDescriptor.Describe( typeof(IMessageServiceB), typeof(MessageServiceBB), ServiceLifetime.Scoped); var serviceDescriptor3 = ServiceDescriptor.Singleton(typeof(IMessageServiceB), typeof(MessageServiceBB)); var serviceDescriptor4 = ServiceDescriptor.Singleton<IMessageServiceB, MessageServiceBB>();
همانطور که دیدیم، IServiceCollectionدر
واقع لیست و مجموعهای از اشیاء است که از نمونههای جنریک IServiceCollection، IList، IEnumerableو Ienumberablارث بری میکند؛ بنابراین میتوان از متدهای تعریف شدهی در این
اینترفیسها برای IServiceCollectionنیز استفاده کرد. حالا ما برای اضافه کردن این سرویسهای جدید،
بدین طریق عمل میکنیم:
Services.Add(serviceDescriptor1);
استفاده از متدهای TryAdd()
به کد زیر نگاه کنید :
services.AddScoped<IMessageServiceB, MessageServiceBA>(); services.AddScoped<IMessageServiceB, MessageServiceBB>();
برای جلوگیری از این خطا میتوانیم از متدهای TryAddSingleton()، TryAddScoped()و TryAddTransient()استفاده کنیم. این متدها درون فضای نام Microsoft.Extionsion.DependencyInjection.Extensionقرار دارند.
عملکرد کلی این
متدها درست مثل متدهای Add()است؛ با این تفاوت که این متد ابتدا IServiceCollectionرا جستجو میکند واگر برای typeمورد نظر سرویسی ثبت نشده بود،
آن را ثبت میکند:
services.TryAddScoped<IMessageServiceB, MessageServiceBA>(); services.TryAddScoped<IMessageServiceB, MessageServiceBB>();
جایگذاری یک سرویس با نمونهای دیگر
گاهی اوقات میخواهیم یک پیاده سازی دیگر را بجای پیاده سازی فعلی، در DI Containerثبت کنیم. در این حالت از متد Replace()بر روی IServiceCollectionبرای این کار استفاده میکنیم. این متد فقط یک ServiceDescriptorرا به عنوان پارامتر ورودی میگیرد:
services.Replace(serviceDescriptor3);
services.RemoveAll<IMessageServiceB>();
معمولا در پروژههای معمول خودمان نیازی به استفاده از Replace()و RemoveAll()نداریم؛ مگر اینکه بخواهیم پیاده سازی اختصاصی خودمان را برای سرویسهای درونی فریم ورک یا کتابخانههای شخص ثالث، بجای پیاده سازی پیش فرض، ثبت و استفاده کنیم.
AddEnumerable()
فرض کنید دارید برنامهی نوبت دهی یک کلینیک را مینویسید و به صورت پیش فرض از شما خواستهاند که هنگام صدور نوبت، این قوانین را بررسی کنید:
- هر شخص در هفته نتواند بیش از 2 نوبت برای یک تخصص بگیرد.
- اگر شخص در ماه بیش از 3 نوبت رزرو شده داشته باشد ولی مراجعه نکرده باشد، تا پایان ماه، امکان رزرو نوبت را نداشته باشد.
- تعداد نوبتهای ثبت شدهی برای پزشک در آن روز نباید بیش از تعدادی باشد که پزشک پذیرش میکند.
- و ...
یک روش معمول برای پیاده سازی این قابلیت، ساخت سرویسی برای ثبت نوبت است که درون آن متدی برای بررسی کردن قوانین ثبت نام وجود دارد. خب، ما این کار را انجام میدهیم. تستهای واحد و تستهای جامع را هم مینویسیم و بعد برنامه را انتشار میدهیم و همه چیز خوب است؛ تا اینکه مالک محصول یک نیازمندی جدید را میخواهد که در آن ما باید قانون زیر را در هنگام ثبت نوبت بررسی کنیم:
- نوبتهای ثبت شده برای یک شخص نباید دارای تداخل باشند.
در این حالت ما باید دوباره سرویس Registerرا باز کنیم و به متد بررسی کردن قوانین برویم و دوباره کدهایی را برای بررسی کردن قانون جدید بنویسیم و احتمالا کد ما به این صورت خواهد شد:
public class RegisterAppointmentService : RegisterAppointmentService { public Task<Result> RegisterAsync( PatientInfoDTO patientIfno , DateTimeOffset requestedDateTime , PhysicianId phusicianId ) { CheckRegisterantionRule(patientInfo); // code here } private Task CheckRegisterationRule(PatientInfoDTO patientInfo) { CheckRule1(patientInfo); CheckRule2(patientInfo); CheckRule3(patientInfo); } }
در این حالت باید به ازای هر قانون جدید، به متد CheckRegisterationRuleبرویم و به ازای هر قانون، یک متد private جدید را بسازیم. مشکل این روش این است که در این حالت ما مجبوریم با هر کم و زیاد شدن قانون، این کلاس را باز کنیم و آن را تغییر دهیم و با هر تغییر دوباره، تستهای واحد آن را دوباره نویسی کنیم. در یک کلام در کد بالا اصول Separation of Concern و Open/Closed Principleرا رعایت نمیشود.
یک راهکار این است که یک
سرویس جداگانه را برای بررسی کردن قوانین بنویسیم و آن را به سرویس ثبت نوبت تزریق کنیم:
public class ICheckRegisterationRuleForAppointmentService : ICheckRegisterationRuleForAppointmentService { public Task CheckRegisterantionRule(PatientInfoDTO patientInfo) { CheckRule1(patientInfo); CheckRule2(patientInfo); CheckRule3(patientInfo); } } public class RegisterAppointmentService : IRegisterAppointmentService { private ICheckRegisterationRuleForAppointmentService _ruleChecker; public RegisterAppointmentService (RegisterAppointmentService ruleChecker) { _ruleChecker = ruleChecker; } public Task<Result> RegisterAsync( PatientInfoDTO patientIfno , DateTimeOffset requestedDateTime , PhysicianId phusicianId ) { _ruleChecker.CheckRegisterantionRule(patientInfo); // code here } }
با این کار وظیفهی چک کردن قوانین و وظیفهی ثبت و ذخیره سازی قوانین را از یکدیگر جدا کردیم؛ ولی همچنان در سرویس بررسی کردن قوانین، اصل Open/Closedرعایت نشدهاست.خب راه حل چیست !؟
یکی از راه حلهای موجود، استفاده از الگوی قوانین یا Rule Patternاست. برای اجرای این الگو، میتوانیم با تعریف یک اینترفیس کلی برای بررسی کردن قانون، به ازای هر قانون یک پیاده سازی اختصاصی را داشته باشیم:
interface IAppointmentRegisterationRule { Task CheckRule(PatientInfo patientIfno); } public class AppointmentRegisterationRule1 : IAppointmentRegisterationRule { public Task CheckRule(PatientInfo patientIfno) { Console.WriteLine("Rule 1 is checked"); return Task.CompletedTask; } } public class AppointmentRegisterationRule2 : IAppointmentRegisterationRule { public Task CheckRule(PatientInfo patientIfno) {
Console.WriteLine("Rule 2 is checked"); return Task.CompletedTask; } } public class AppointmentRegisterationRule3 : IAppointmentRegisterationRule { public Task CheckRule(PatientInfo patientIfno) { Console.WriteLine("Rule 3 is checked"); return Task.CompletedTask; } } public class AppointmentRegisterationRule4 : IAppointmentRegisterationRule { public Task CheckRule(PatientInfo patientIfno) { Console.WriteLine("Rule 4 is checked"); return Task.CompletedTask; } }
services.AddScoped<IAppointmentRegisterationRule, AppointmentRegisterationRule1>(); services.AddScoped<IAppointmentRegisterationRule, AppointmentRegisterationRule2>(); services.AddScoped<IAppointmentRegisterationRule, AppointmentRegisterationRule3>(); services.AddScoped<IAppointmentRegisterationRule, AppointmentRegisterationRule4>();
public class CheckRegisterationRuleForAppointmentService : ICheckRegisterationRuleForAppointmentService { private IEnumerable<IAppointmentRegisterationRule> _rules ; public CheckRegisterationRuleForAppointmentService(IEnumerable<IAppointmentRegisterationRule> rules) { _rules = rules; } public Task CheckRegisterantionRule(PatientInfoDTO patientInfo) { foreach(var rule in rules) { rule.CheckRule(patientInfo); } } }
کد بالا به
نظر کامل میآید ولی مشکلی دارد! اگر در DI Containerبرای IAppointmentRegisterationRuleیک قانون را دو یا چند بار ثبت کنیم، در هر بار بررسی کردن قوانین، آن را به همان تعداد بررسی میکند و اگر این فرآیند منابع زیادی را به
کار میگیرد، میتواند عملکرد برنامهی ما را به هم بریزد. برای جلوگیری از این مشکل، از متد TryAddEnumerabl()
استفاده میکنیم که لیستی از ServiceDescriptorها را میگیرد و هر serviceDescriptorرا فقط یکبار ثبت میکند:
services.TryAddEnumerable(new[] { ServiceDescriptor.Scoped(typeof(IAppointmentRegisterationRule), typeof(AppointmentRegisterationRule1)), ServiceDescriptor.Scoped(typeof(IAppointmentRegisterationRule), typeof(AppointmentRegisterationRule2)), ServiceDescriptor.Scoped(typeof(IAppointmentRegisterationRule), typeof(AppointmentRegisterationRule3)), ServiceDescriptor.Scoped(typeof(IAppointmentRegisterationRule), typeof(AppointmentRegisterationRule4)), });