در بعضی از شرایط پیش رفته، ممکن است نمونه سازی از یک Implementation Type، نیاز به دخالت مستقیم ما را داشته باشد. Implementation Factory کنترل بیشتری بر چگونگی استفادهی از Implementation Typeها را به ما ارائه میدهد. در هنگام ثبت سرویسی که Implementation Factory را در اختیار ما قرار میدهد، ما یک Delegate را برای فراخوانی سرویس استفاده میکنیم. این Delegate مسئول ساخت یک نمونه از Service Type است. برای مثال وقتی از الگوهای builder یا factory برای ساخت یک شیء استفاده میکنید، شاید نیاز باشد که Implementation Factory را به صورت دستی پیاده سازی کنید. اولین قدم این است که کدتان را در صورت امکان چنان refactor کنید تا DI Container بتواند آن را به صورت خودکار بسازد؛ ولی اینکار همیشه ممکن نیست. برای مثال بعضی از برنامه نویسان ترجیح میدهند یک Config را مستقیما از IOptionMonitor بگیرند و بعد در هر جائیکه خواستند، بجای تزریق IOptionMonitor به سرویس، مستقیما از همان سرویس ثبت شده استفاده کنند:
services.AddSingleton<ILiteDbConfig>(sp =>sp.GetRequiredService<IOptionsMonitor<LiteDbConfig>>().CurrentValue);
یک کاربرد بالقوهی دیگر برای Implementation Factory ، استفاده از Composite Pattern است. هر چند Microsoft DI Container به صورت پیش فرض از Composite Pattern پشتیبانی نمیکند، ولی ما میتوانیم آنرا پیاده سازی کنیم. فرض کنید که قبلا به ازای انجام کاری، به کاربر یک ایمیل را میفرستادیم؛ ولی حالا مالک محصول میآید و میگوید که علاوه بر ایمیل، باید پیامک هم بفرستید و ما یا این سرویس پیامک را از قبل داریم و یا باید آن را بسازیم که فرض میکنیم از قبل آن را داریم. برای این کار ما یک اینترفیس کلیتر به نام INotificationService میسازیم که دو سرویس IEmailNotificationService و ISmsNotificaitonService از آن ارث بری میکنند:
public interface INotificationService { void SendMessage(string msg, int userId); }
public class CompositeNotificationService : INotificationService { private readonly IEnumerable<INotificationService> _notificationServices; public CompositeNotificationService(IEnumerable<INotificationService> notificationServices) { _notificationServices = notificationServices; } public void SendMessage(string msg, int userId) { foreach (var notificationServicei in _notificationServices) { notificationServicei.SendMessage(msg, userId); } } }
services.AddScoped<IEmailNotificationService, EmailNotificationService>(); services.AddScoped<ISMSNotificationService, SMSNotificationService>(); services.AddSingleton<INotificationService>(sp => new CompositeNotificationService( new INotificationService[] { sp.GetRequiredService<IEmailNotificationService>() , sp.GetRequiredService<ISMSNotificationService>() } ));
وهله سازی سفارشی
در مثال بعدی نشان میدهیم که چطور میتوانیم از Implementation Factory برای برگرداندن پیادهسازی سرویسهایی که Service Provider امکان ساخت
خودکار آنها را ندارد، استفاده کنیم. فرض کنید یک کلاس Account داریم که از IAccount ارث بری میکند
و برای ساخت آن باید از متدهای IAccountBuilder که فرآیند ساخت را انجام میدهند، استفاده کنیم. بنابراین امکان ساخت مستقیم یک
شیء از IAccount وجود ندارد. در این حالت بدین صورت عمل میکنیم:
services.AddTransient<IAccountBuilder, AccountBuilder>(); services.AddScoped<IAccount>(sp => { var builder = sp.GetService<IAccountBuilder>(); builder.WithAccountType(AccountType.Guest); return builder.Build(); });
ثبت یک نمونه برای چندین اینترفیس
ممکن است بنا به دلایلی مجبور باشیم یک implementation Type را برای چند سرویس (اینترفیس) به ثبت برسانیم. در این حالت
نمونهی شیء ساخته شده، توسط هر کدام از اینترفیسها قابل استفاده است. فرض کنید یک سرویس Greeting
داریم که پیش از این فقط اینترفیس IHomeGreetingService را پیاده سازی میکرد؛ ولی بنا به دلایلی تصمیم گرفتیم که سرویسی
جامعتر را با نیازمندیهای دیگری نیز تعریف کنیم و GreetingService آن را پیاده سازی کند:
public class GreetingService : IHomeGreetingService , IGreetingService
{ // code here }
احتمالا اولین چیزی که به ذهنمان میرسد این است:
services.TryAddSingleton<IHomeGreetingService, GreetingService>(); services.TryAddSingleton<IGreetingService, GreetingService>();
مشکل روش بالا این است که دو نمونه از GreetingService ساخته میشود و درون حافظه باقی میماند و در حقیقت برای هر اینترفیس، یک نوع جداگانه از GreetingService ثبت میشود؛ در حالیکه ما میخواهیم هر دو اینترفیس فقط از یک نمونه از شیء GreetingService استفاده کنند. دو راه حل برای این مشکل وجود دارد:
var greetingService = new GreetingService(Environment); services.TryAddSingleton<IHomeGreetingService>(greetingService); services.TryAddSingleton<IGreetingService>(greetingService);
در اینجا سازندهی کلاس GreetingService فقط به environment نیاز داشت که یکی از سرویسهای پایهی فریم ورک هست و در این مرحله در دسترس است. به صورت کلی مشکل روش بالا این است که ما مسئول نمونه سازی از سرویس GreetingService هستیم! اگر GreetingService برای ساخته شدن به سرویسها یا ورودی هایی نیاز داشته باشد که فقط در زمان اجرا در دسترس باشند، ما امکان نمونه سازی آنها را نداریم؛ علاوه بر این نمیتوان از روشهای بالای برای حالتهای Scoped یا Transient استفاده کرد.
روش بعدی همان روش استفاده
از Implementation Factory است که در ادامه آن را میبینید:
services.TryAddSingleton<GreetingService>(); services.TryAddSingleton<IHomeGreetingService>(sp => sp.GetRequiredService<GreetingService>()); services.TryAddSingleton<IGreetingService>(sp => sp.GetRequiredService<GreetingService>());
در این روش خود DI Container مسئول نمونه سازی از GreetingService است. علاوه بر این میتوان با استفاده از روش فوق از طول حیاتهای Scoped و Transient هم استفاده کرد؛ در حالیکه در روش قبلی این کار امکان پذیر نبود.
Open Generics Service
گاهی از اوقات میخواهید
سرویسهایی را ثبت کنید که از اینترفیسی جنریک ارث بری میکنند. هر نوع جنریک در
زمان اجرا، نوع مخصوص به خود را واکشی میکند. ثبت کردن دستی این سرویسها میتواند خسته کننده باشد. برای همین مایکروسافت در DI Container خود قابلیت ثبت و واکشی سرویسهای جنریک را نیز در
اختیار ما گذاشتهاست. بیایید نگاهی به سرویس ILogger<T> بیندازیم. این یک سرویس درونی فریمورک است و میتواند به ازای هر نوع، کارهای
مربوط به ثبت log را
انجام بدهد و در پروژهها معمولا از این اینترفیس برای ثبت لاگها در سطح
کنترلر و سرویسها استفاده میشود:
public interface ILogger<out TCategoryName> : ILogger { }
در حالت عادی اگر سرویسی
مشابه سرویس فوق را داشته باشیم، برای ثبت کردن هر سرویس با نوع جنریک اختصاصی آن،
مجبوریم به صورت دستی آن را درون DI Container ثبت کنیم؛ مثلا باید به این صورت عمل کنیم:
services.TryAddScoped<ILogger<HomeController>,Logger<HomeController>>();
services.TryAddScoped(typeof(ILogger<>) , typeof(Logger<>));
دسته بندی سرویسها درون متدهای مختلف و پاکسازی متد ConfigurationService
تا اینجای کار ما سرویسهای مختلفی را به روشهای مختلفی ثبت کردهایم. حتی در همین آموزش ساده، تعداد زیاد سرویسهای ثبت شده، باعث شلوغی و در هم ریختگی کدهای ما میشوند که خوانایی و در ادامه اشکال زدایی و توسعهی کدها را برای ما سختتر میکنند. سادهترین کار برای دسته بندی کدها، استفاده از متدهای private محلی یا استفاده از متدهای توسعهای(الحاقی) است که در اینجا مثالی از استفاده از متدهای توسعهای را آوردهام:namespace AspNetCoreDependencyInjection.Extensions { public static class DICRegisterationExetnsion { /// <summary> /// مثال ثبت برای اپن جنریت /// </summary> /// <param name="services"></param> public static void OpenGenericRegisterationExample(this IServiceCollection services) { services.TryAddScoped<ILogger<HomeController>, Logger<HomeController>>(); services.TryAddScoped(typeof(ILogger<>), typeof(Logger<>)); } /// <summary> /// ثبت تنظیمات به روشهای مختلف /// </summary> public static void RegisterConfiguration(this IServiceCollection services, IConfiguration configuration) { services.AddSingleton(services => new AppConfig { ApplicationName = configuration["ApplicationName"], GreetingMessage = configuration["GreetingMessage"], AllowedHosts = configuration["AllowedHosts"] }); services.AddSingleton(services => new AccountTypeBalanceConfig( new List<(AccountType, long)> { (AccountType.Guest , Convert.ToInt64 (configuration["AccountInitialBalance.Guest"]) ) , (AccountType.Silver , Convert.ToInt64 (configuration["AccountInitialBalance.Silver"]) ) , (AccountType.Gold , Convert.ToInt64 (configuration["AccountInitialBalance.Gold"]) ) , (AccountType.Platinum , Convert.ToInt64 (configuration["AccountInitialBalance.Platinum"]) ) , (AccountType.Titanium , Convert.ToInt64 (configuration["AccountInitialBalance.Titanium"]) ) , }) ); services.AddSingleton(services => new LiteDbConfig { ConnectionString = configuration["LiteDbConfig:ConnectionString"], }); services.Configure<UserOptionConfig>(configuration.GetSection("UserOptionConfig")); } } }
حالا در کلاس ConfigureServices ، درون متدStartup ، به این صورت از این متدهای توسعهای استفاده میکنیم:
services.RegisterConfiguration(this.Configuration); services.OpenGenericRegisterationExample();
میتوانید کد منبع این آموزش را در اینجا ببینید.