- Configuration
- Routing
- MVC
- Application
- و ...
- کاربر یک درخواست Http را توسط مرورگر ارسال میکند.
- یکی از اولین میان افزارها یعنی میان افزار Routing، آدرس درخواست را میخواند، کنترلر و اکشن مورد نظر را مییابد و بهوسیلهی Activator Utility، سعی در فعال سازی آن کنترلر میکند.
- DI Container لیست پارامترهای سازندهی کنترلر را مشاهده میکند و سرویسهای مورد نیاز را از درون خود واکشی کرده، از آنها نمونه سازی میکند و نمونههای ساخته شده را به درون شیء کنترلر تزریق میکند.
- Routing درخواست HttpRequest را تجزیه کرده و اکشن متد مورد نظر را برای اجرای آن فراخوانی کرده
- و نتیجهی اجرای اکشن را به درخواست دهنده بر میگرداند.
هر چند که کنترلرها درون DI Container ثبت نشدهاند، ولی توسط کلاسهایی درون فریم ورک، از آنها نمونه سازی میشود و در حین نمونه سازی، DI Container سرویسهای مورد نظر آنها را در صورت وجود، فراهم میکند.
ثبت تنظیمات وبسایت و فراخوانی آنها در برنامه
در تمام برنامههای ASP.NET Core شما نیاز به تنظیماتی برای پیکربندی کار برنامهی خود دارید. این تنظیمات میتوانند شامل Connection String اتصال به پایگاه داده، تنظیمات اتصال به سرویسهای خارجی مثل درگاههای پرداخت آنلاین بانکها و ... باشند. در اینجا ما تنظیمات اختصاصی را درون فایل AppSetting اضافه میکنیم. بعد برای هر بخش از تنظیمات، در پوشهی Configs یک کلاس سادهی سی شارپ را میسازیم و سپس با گرفتن و تزریق کردن این فایلهای Config درون DI Container، هر زمانی خواستیم، از آنها استفاده میکنیم.
ابتدا به سراغ تنظیمات کلی میرویم و دو تنظیم نام برنامه و پیغام خوش آمد گویی را به برنامه اضافه میکنیم (فایل appSettings را به صورت زیر تغییر میدهیم) :
"ApplicationName": "Dependency Injection Demo", "GreetingMessage": "Welcome to Dependency Injection Demo", "AllowedHosts": "*", "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } },
برای سادگی کار، با بخش Logging
کاری نداریم . اکنون فایل AppConfig.cs را به برنامه اضافه میکنیم:
namespace AspNetCoreDependencyInjection.Configs { public class AppConfig { public string ApplicationName { get; set; } public string GreetingMessage { get; set; } public string AllowedHosts { get; set; } } }
برای دسترسی بهتر میتوانیم سازندهی کلاس Startup را تغییر دهیم:
public IWebHostEnvironment Environment { get; } public IConfiguration Configuration { get; } public IServiceCollection Services { get; set; } public Startup(IWebHostEnvironment environment) { var builder = new ConfigurationBuilder() .SetBasePath(environment.ContentRootPath) .AddJsonFile("appsettings.json", optional: true) .AddEnvironmentVariables(); this.Environment = environment; this.Configuration = builder.Build(); }
var appSettingsFile = environment.IsProduction() ? "appsettings.json" : "appsettings_dev.json"; var builder = new ConfigurationBuilder() .SetBasePath(environment.ContentRootPath) .AddJsonFile( appSettingsFile , optional: true) .AddEnvironmentVariables();
services.AddSingleton(services => new AppConfig { ApplicationName = this.Configuration["ApplicationName"], GreetingMessage = this.Configuration["GreetingMessage"], AllowedHosts = this.Configuration["AllowedHosts"] });
در کد بالا در هنگام اجرای برنامه، یک نمونه از کلاس AppConfig را با طول حیات Singleton ثبت کردیم و Property های این شیء را به وسیلهی ایندکس Configuration[“FieldName”]، تک تک پر کردیم.
حالا میتوانیم سرویس AppConfig را
در هر کلاسی از برنامهی خودمان تزریق و از آن استفاده کنیم. برای مثل در اینجا یک
کنترلر به نام AppSettingsController ساختم و کلاس فوق را به آن تزریق کردم:
public class AppSettingsController : Controller { private readonly AppConfig _appConfig; public AppSettingsController(AppConfig appConfig) { _appConfig = appConfig; } // codes here … }
می توانیم از همین الگو برای تعریف، ثبت و استفاده از سایر تنظیمات نیز استفاده کنیم:
"UserOptionConfig": { "UsersAvatarsFolder": "avatars", "UserDefaultPhoto": "icon-user-default.png", "UserAvatarImageOptions": { "MaxWidth": 150, "MaxHeight": 150 } }, "LiteDbConfig": { "ConnectionString": "Filename=\\Data\\DependencyInjectionDemo.db;Connection=direct;Password=@123456;" }
برای LiteDbConfig
مانند AppConfig عمل میکنیم، ولی در هنگام ثبت آن، به روش زیر عمل میکنیم. تنها تفاوتی
که وجود دارد، نحوهی دستیابی به فیلدهای درونی فایل JSON به وسیلهی شیء Configuration
است:
services.AddSingleton(services => new LiteDbConfig { ConnectionString = this.Configuration["LiteDbConfig:ConnectionString"], });
اکنون برای استفادهی از مدخل UserOptionConfig،
کلاسهای زیر را میسازیم:
namespace AspNetCoreDependencyInjection.Configs { public class UserOptionConfig { public string UsersAvatarsFolder { get; set; } public string UserDefaultPhoto { get; set; } public UserAvatarImageOptions UserAvatarImageOptions { get; set; } } public class UserAvatarImageOptions { public int MaxHeight { get; set; } public int MaxWidth { get; set; } } }
جداسازی بخشهای مختلف تنظیمات پیکربندی باعث میشود تا بتوانیم دو اصل اساسی از طراحی نرم افزار را رعایت کنیم :
- Interface Segregation Principle (ISP) or Encapsulation : کلاسهایی که به تنظیمات نیاز دارند، فقط به آن بخشی از تنظیمات دسترسی خواهند داشتند که واقعا مورد نیازشان باشد.
- Separation Of Concerns : تنظیمات بخشهای مختلف برنامه، به یکدیگر وابسته و جفت شده نیستند.
در اینجا نیاز به استفاده از پکیج Microsoft.Extensions.Options.ConfigurationExtensions را داریم که به صورت درونی در ASP.NET Core تعبیه شده است.
برای ثبت این تنظیمات درون DI Container، از نمونهی جنریک متد Configure در IServiceCollection به صورت زیر استفاده میکنیم:
services.Configure<UserOptionConfig>(this.Configuration.GetSection("UserOptionConfig"));
متد GetSection بر اساس نام بخش تنظیمات، خود آن تنظیم و تمامی تنظیمات درونی آن را به صورت یک IConfigurationSection بر میگرداند و متد Configure<TOption> یک IConfiguration را گرفته و به صورت خودکار به TOption اتصال میدهد و سپس این شیء را درون DI Container به عنوان یک IConfigurationOptions<TOption> و با طول حیات Singleton ثبت میکند.
برای دسترس به UserOptionConfig درون کلاس مورد نظر ما، اینترفیس <IOptionMonitor<TOption را به سازندهی کلاس مورد نظر تزریق میکنیم. کد زیر را که نسخهی تغییر یافتهی کلاس AppSettingsController است را مشاهده کنید:private readonly LiteDbConfig _liteDbConfig; private readonly AppConfig _appConfig; private readonly UserOptionConfig _userOptionConfig; public AppSettingsController(AppConfig appConfig , LiteDbConfig liteDbConfig , IOptionsMonitor<UserOptionConfig> userOptionConfig) { _appConfig = appConfig; _liteDbConfig = liteDbConfig; _userOptionConfig = userOptionConfig.CurrentValue; }
نکته ای که وجود دارد، کلاسهای تعریف شده برای استفادهی از این الگو باید شرایط زیر را داشته باشند ( مثل کلاس UserOptionConfig ) :
- باید سطح دسترسی public داشته باشند.
- باید دارای سازندهی پیش فرض باشند.
- باید نام Property های آنها دقیقا همنام فیلدهای تنظیمات باشد تا فرایند mapping خودکار به درستی انجام شود.
- باید Property ها و Setter آنها ، سطح دسترسی public داشته باشند.
هر دو روش بالا که یکی به
صورت عادی تنظیمات را ثبت میکند و دیگری با استفاده از Option Pattern بخشهای مختلف را ثبت میکند،
مناسب هستند. البته گاهی اوقات فایلهای تنظیمات پروژهی شما در لایههای زیرین (یا درونیتر اگر از onion architecture استفاده میکنید) قرار دارند و شما نمیخواهید
در آن لایهها و لایههای درونیتر، وابستگی به پکیجهای ASP.NET Core ایجاد کنید. در این حالت با در
نظر گرفتن دو اصل ISP و Separation of Concerns ،
به ازای هر بخش مختلف از تنظیمات، فایلهای تنظیمات را در لایههای زیرین/درونی
تعریف کرده، بعد در لایههای بالاتر/بیرونیتر آنها را به درون سرویسها یا کلاسهای مورد نیاز، تزریق کنید. البته مثل همین مثال، ثبت این سرویسها درون برنامهی ASP.NET Core که
معمولا بالاترین/بیرونیترین لایه از پروژهی ما هست، انجام میشود.