یکی دیگر از تغییرات ASP.NET Core با نگارشهای قبلی آن، تغییرات اساسی در مورد نحوهی کار با تنظیمات برنامه و فایلهای مرتبط با آنها است. در ASP.NET Core میتوانید:
- تنظیمات برنامه را از چندین منبع مختلف خوانده و آنها را یکی کنید.
- تنظیمات را بر اساس تنظیمات مختلف محیطی برنامه، بارگذاری کنید.
- امکان نگاشت اطلاعات خوانده شدهی از فایلهای کانفیگ به کلاسها پیش بینی شدهاست.
- امکان بارگذاری مجدد فایلهای کانفیگ درصورت تغییر، بدون ریاستارت کل برنامه وجود دارد.
- امکان تزریق وابستگیهای تنظیمات برنامه، به قسمتهای مختلف آن پیش بینی شدهاست.
نصب پیشنیاز خواندن تنظیمات برنامه از یک فایل JSON
برای شروع به کار با خواندن تنظیمات برنامه در ASP.NET Core، نیاز است ابتدا بستهی نیوگت Microsoft.Extensions.Configuration.Json را نصب کنیم.
برای این منظور بر روی گره references کلیک راست کرده و گزینهی manage nuget packages را انتخاب کنید. سپس در برگهی browse آن Microsoft.Extensions.Configuration.Json را جستجو کرده و نصب نمائید:
البته همانطور که در تصویر مشاهده میکنید، اگر صرفا Microsoft.Extensions.Configuration را جستجو کنید (بدون ذکر JSON)، بستههای مرتبط با خواندن فایلهای کانفیگ از نوع XML و یا حتی INI را هم خواهید یافت.
انجام این مراحل معادل هستند با افزودن یک سطر ذیل به فایل project.json برنامه:
{
"dependencies": {
//same as before
"Microsoft.Extensions.Configuration.Json": "1.0.0"
},
افزودن یک فایل کانفیگ JSON دلخواه
بر روی پروژه کلیک راست کرده و از طریق منوی add->new item یک فایل خالی جدید را به نام appsettings.json ایجاد کنید (نام این فایل دلخواه است)؛ با این محتوا:
{
"Key1": "Value1",
"Auth": {
"Users": [ "Test1", "Test2", "Test3" ]
},
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}
در نگارشهای پیشین ASP.NET که از web.config برای تعریف تنظیمات برنامه استفاده میشد، حالت پیش فرض ذکر تنظیمات برنامه میتوانست تنها یک سطحی و با ساختار ذیل باشد (البته امکان کدنویسی و نوشتن مداخل سفارشی هم وجود داشت؛ ولی حالت پیش فرض appSettings، تنها key/valueهای یک سطحی هستند):
<appSettings>
<add key="Logging-IncludeScopes" value="false" />
<add key="Logging-Level-Default" value="verbose" />
<add key="Logging-Level-System" value="Information" />
<add key="Logging-Level-Microsoft" value="Information" />
</appSettings>
اما اکنون یک فایل JSON را با هر تعداد سطح مورد نیاز میتوان تعریف و استفاده کرد و برای اینکار نیازی به نوشتن کدهای سفارشی تعریف مداخل خاص، وجود ندارد.
در فایل JSON فوق، نمونهای از key/valueها، آرایهها و اطلاعات چندین سطحی را مشاهده میکنید.
خواندن فایل تنظیمات appsettings.json در برنامه
پس از نصب پیشنیاز خواندن فایلهای کانفیگ از نوع JSON، به فایل آغازین برنامه مراجعه کرده و سازندهی جدیدی را به آن اضافه کنید:
public class Startup
{
public IConfigurationRoot Configuration { set; get; }
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json");
Configuration = builder.Build();
}
در اینجا نحوهی خواندن فایل کانفیگ جدید appsettings.json را مشاهده میکنید. چند نکته در اینجا حائز اهمیت هستند:
الف) این خواندن، در سازندهی کلاس آغازین برنامه و پیش از تمام تنظیمات دیگر باید انجام شود.
ب) جهت در معرض دید قرار دادن اطلاعات خوانده شده، آنرا به یک خاصیت عمومی انتساب دادهایم.
ج) متد SetBasePath جهت مشخص کردن محل یافتن فایل appsettings.json ذکر شدهاست. این اطلاعات را میتوان از سرویس توکار IHostingEnvironment و خاصیت ContentRootPath آن دریافت کرد. همانطور که ملاحظه میکنید، این تزریق وابستگی نیز به صورت خودکار توسط ASP.NET Core مدیریت میشود.
دسترسی به تنظیمات خوانده شده توسط اینترفیس IConfigurationRoot
تا اینجا موفق شدیم تا تنظیمات خوانده شده را به خاصیت عمومی Configuration از نوع IConfigurationRoot انتساب دهیم. اما ساختار ذخیره شدهی در این اینترفیس به چه صورتی است؟
همانطور که مشاهده میکنید، هر سطح از سطح قبلی آن با : جدا شدهاست. همچنین اعضای آرایه، دارای ایندکسهای 0: و 1: و 2: هستند. بنابراین برای خواندن این اطلاعات میتوان نوشت:
var key1 = Configuration["Key1"];
var user1 = Configuration["Auth:Users:0"];
var authUsers = Configuration.GetSection("Auth:Users").GetChildren().Select(x => x.Value).ToArray();
var loggingIncludeScopes = Configuration["Logging:IncludeScopes"];
var loggingLoggingLogLevelDefault = Configuration["Logging:LogLevel:Default"];
خاصیت Configuration نیز در نهایت بر اساس key/valueها کار میکند و این keyها اگر چند سطحی بودند، با : از هم جدا میشوند و اگر نیاز به دسترسی اعضای خاصی از آرایهها وجود داشت میتوان آن ایندکس خاص را در انتهای زنجیره ذکر کرد. همچنین در اینجا نحوهی استخراج تمام اعضای یک آرایه را نیز مشاهده میکنید.
یک نکته: خاصیت Configuration، دارای متد GetValue نیز هست که توسط آن میتوان نوع مقدار دریافتی و یا حتی مقدار پیش فرضی را در صورت عدم وجود این key، مشخص کرد:
var val = Configuration.GetValue<int>("key-name", defaultValue: 10);
در متد GetValue، آرگومان جنریک آن، یک کلاس را نیز میپذیرد. یعنی میتوان خواص تو در توی مشخص شدهی با : را به یک کلاس نیز نگاشت کرد. در اینجا مقدار کلید معرفی شده، اولین سطحی خواهد بود که باید این اطلاعات از آن استخراج و نگاشت شوند.
سرویس IConfigurationRoot قابل تزریق است در قسمت قبل، سرویسها و تزریق وابستگیها را بررسی کردیم. نکتهی جالبی را که میتوان به آن اضافه کرد، قابلیت تزریق خاصیت عمومی
public class Startup
{
public IConfigurationRoot Configuration { set; get; }
به تمام قسمتهای برنامه است. برای نمونه در همان مثال
قسمت قبل، قصد داریم تنظیمات برنامه را در لایه سرویس آن خوانده و مورد استفاده قرار دهیم. برای اینکار باید مراحل ذیل طی شوند:
الف) اعلام موجودیت IConfigurationRoot به IoC Container
اگر از استراکچرمپ استفاده میکنید، باید مشخص کنید، زمانیکه IConfigurationRoot درخواست شد، آنرا چگونه باید از خاصیت مرتبط با آن دریافت کند:
var container = new Container();
container.Configure(config =>
{
config.For<IConfigurationRoot>().Singleton().Use(() => Configuration);
و یا اگر از همان IoC Container توکار ASP.NET Core استفاده میکنید، روش انجام اینکار در متد ConfigureServices به صورت زیر است:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IConfigurationRoot>(provider => { return Configuration; });
طول عمر آن هم singleton مشخص شدهاست تا تنها یکبار وهله سازی و سپس کش شود (مناسب برای کار با تنظیمات سراسری برنامه).
ب) فایل project.json کتابخانهی Core1RtmEmptyTest.Services را گشوده و وابستگی Microsoft.Extensions.Configuration.Abstractions را به آن اضافه کنید:
{
"dependencies": {
//same as before
"Microsoft.Extensions.Configuration.Abstractions": "1.0.0"
}
این وابستگی امکان دسترسی به اینترفیس IConfigurationRoot را در اسمبلیهای دیگر میسر میکند.
ج) سپس فایل MessagesService.cs را گشوده و این اینترفیس را به سازندهی سرویس MessagesService تزریق میکنیم:
public interface IMessagesService
{
string GetSiteName();
}
public class MessagesService : IMessagesService
{
private readonly IConfigurationRoot _configurationRoot;
public MessagesService(IConfigurationRoot configurationRoot)
{
_configurationRoot = configurationRoot;
}
public string GetSiteName()
{
var key1 = _configurationRoot["Key1"];
return $"DNT {key1}";
}
}
در ادامه، نحوهی استفادهی از آن، همانند نکاتی است که در قسمت «دسترسی به تنظیمات خوانده شده توسط اینترفیس IConfigurationRoot» عنوان شد.
اکنون اگر برنامه را اجرا کنید، با توجه به اینکه میان افزار Run از این سرویس سفارشی استفاده میکند:
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
IMessagesService messagesService)
{
app.Run(async context =>
{
var siteName = messagesService.GetSiteName();
await context.Response.WriteAsync($"Hello {siteName}");
});
}
چنین خروجی را خواهیم داشت:
خواندن تنظیمات از حافظه
الزاما نیازی به استفاده از فایلهای JSON و یا XML در اینجا وجود ندارد. ابتداییترین حالت کار با بستهی Microsoft.Extensions.Configuration، متد AddInMemoryCollection آن است که در اینجا میتوان لیستی از key/valueها را ذکر کرد:
var builder = new ConfigurationBuilder()
.AddInMemoryCollection(new[]
{
new KeyValuePair<string,string>("the-key", "the-value"),
});
و نحوهی کار با آن نیز همانند قبل است:
var theValue = Configuration["the-key"];
امکان بازنویسی تنظیمات انجام شده، بسته به شرایط محیطی
در اینجا محدود به یک فایل JSON و یک فایل تنظیمات برنامه، نیستیم. برای کار با ConfigurationBuilder میتوان از Fluent interface آن استفاده کرد و به هر تعدادی که نیاز بود، متدهای خواندن از فایلهای کانفیگ دیگر را اضافه کرد:
public class Startup
{
public IConfigurationRoot Configuration { set; get; }
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddInMemoryCollection(new[]
{
new KeyValuePair<string,string>("the-key", "the-value"),
})
.AddJsonFile("appsettings.json", reloadOnChange: true, optional: false)
.AddJsonFile($"appsettings.{env}.json", optional: true);
Configuration = builder.Build();
}
و نکتهی مهم اینجا است که تنظیمات فایل دوم، تنظیمات مشابه فایل اول را بازنویسی میکند.
برای مثال در اینجا آخرین AddJsonFile تعریف شده، بنابر متغیر محیطی فعلی به appsettings.development.json تفسیر شده و در صورت وجود این فایل (با توجه به optional بودن آن) اطلاعات آن دریافت گردیده و اطلاعات مشابه فایل appsettings.json قبلی را بازنویسی میکند.
امکان دسترسی به متغیرهای محیطی سیستم عامل
در انتهای زنجیرهی ConfigurationBuilder میتوان متد AddEnvironmentVariables را نیز ذکر کرد:
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
این متد سبب میشود تا تمام اطلاعات قسمت Environment سیستم عامل، به مجموعهی تنظیمات جاری اضافه شوند (در صورت نیاز) که نمونهای از آنرا در تصویر ذیل مشاهده میکنید:
امکان نگاشت تنظیمات برنامه به کلاسهای متناظر
کار کردن با key/valueهای رشتهای، هرچند روش پایهای استفادهی از تنظیمات برنامه است، اما آنچنان refactoring friendly نیست. در ASP.NET Core امکان تعریف تنظیمات strongly typed نیز پیش بینی شدهاست. برای این منظور باید مراحل زیر طی شوند:
به عنوان نمونه تنظیمات فرضی smtp ذیل را به انتهای فایل appsettings.json اضافه کنید:
{
"Key1": "Value1",
"Auth": {
"Users": [ "Test1", "Test2", "Test3" ]
},
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
},
"Smtp": {
"Server": "0.0.0.1",
"User": "user@company.com",
"Pass": "123456789",
"Port": "25"
}
}
مثال جاری که بر اساس ASP.NET Core Web Application و با قالب خالی آن ایجاد شدهاست، دارای نام فرضی Core1RtmEmptyTest است. در همین پروژه بر روی پوشهی src کلیک راست کرده و گزینهی Add new project را انتخاب کنید و سپس یک پروژهی جدید از نوع NET Core -> Class library. را به آن با نام Core1RtmEmptyTest.ViewModels اضافه کنید (تصویر ذیل).
در این کتابخانهی جدید که محل نگهداری ViewModelهای برنامه خواهد بود، کلاس معادل قسمت smtp فایل config فوق را اضافه کنید:
namespace Core1RtmEmptyTest.ViewModels
{
public class SmtpConfig
{
public string Server { get; set; }
public string User { get; set; }
public string Pass { get; set; }
public int Port { get; set; }
}
}
از این جهت این کلاس را در یک library جداگانه قرار دادهایم تا بتوان از آن در لایهی سرویس و همچنین خود برنامه استفاده کرد. اگر این کلاس را در برنامهی اصلی قرار میدادیم، امکان دسترسی به آن در لایهی سرویس میسر نمیشد.
سپس به پروژهی Core1RtmEmptyTest مراجعه کرده و بر روی گره references آن کلیک راست کنید. در اینجا گزینهی add reference را انتخاب کرده و سپس Core1RtmEmptyTest.ViewModels را انتخاب کنید، تا اسمبلی آنرا بتوان در پروژهی جاری استفاده کرد.
انجام اینکار معادل است با افزودن یک سطر ذیل به فایل project.json پروژه:
{
"dependencies": {
// same as before
"Core1RtmEmptyTest.ViewModels": "1.0.0-*"
},
اکنون با فرض وجود تنظیمات خواندن فایل appsettings.json در سازندهی کلاس آغازین برنامه، نیاز است بستهی نیوگت Microsoft.Extensions.Configuration.Binder را نصب کنید:
و سپس در کلاس آغازین برنامه و متد ConfigureServices آن، نحوهی نگاشت قسمت Smtp فایل کانفیگ را مشخص کنید:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.Configure<SmtpConfig>(options => Configuration.GetSection("Smtp").Bind(options));
در اینجا مشخص شدهاست که کار وهله سازی کلاس SmtpConfig بر اساس اطلاعات قسمت smtp فایل کانفیگ تامین میشود. متغیر Configuration ایی که در اینجا استفاده شدهاست همان خاصیت عمومی public IConfigurationRoot Configuration کلاس آغازین برنامه است.
سپس برای استفاده از این تنظیمات strongly typed (برای نمونه در لایه سرویس برنامه)، ابتدا ارجاعی را به پروژهی Core1RtmEmptyTest.ViewModels به لایهی سرویس برنامه اضافه میکنیم (بر روی گره references آن کلیک راست کنید. در اینجا گزینهی add reference را انتخاب کرده و سپس Core1RtmEmptyTest.ViewModels را انتخاب کنید).
در ادامه نیاز است بستهی نیوگت جدیدی را به نام Microsoft.Extensions.Options به لایهی سرویس برنامه اضافه کنیم. به این ترتیب قسمت وابستگیهای فایل project.json این لایه چنین شکلی را پیدا میکند:
"dependencies": {
"Core1RtmEmptyTest.ViewModels": "1.0.0-*",
"Microsoft.Extensions.Configuration.Abstractions": "1.0.0",
"Microsoft.Extensions.Options": "1.0.0",
"NETStandard.Library": "1.6.0"
}
پس از ذخیره سازی این کلاس و بازیابی خودکار وابستگیهای آن، اکنون برای دسترسی به این تنظیم باید از اینترفیس ویژهی IOptions استفاده کرد (به همین جهت بستهی جدید نیوگت Microsoft.Extensions.Options را نصب کردیم):
public interface IMessagesService
{
string GetSiteName();
}
public class MessagesService : IMessagesService
{
private readonly IConfigurationRoot _configurationRoot;
private readonly IOptions<SmtpConfig> _settings;
public MessagesService(IConfigurationRoot configurationRoot, IOptions<SmtpConfig> settings)
{
_configurationRoot = configurationRoot;
_settings = settings;
}
public string GetSiteName()
{
var key1 = _configurationRoot["Key1"];
var server = _settings.Value.Server;
return $"DNT {key1} - {server}";
}
}
همانطور که ملاحظه میکنید <IOptions<SmtpConfig به سازندهی کلاس تزریق شدهاست و سپس از طریق خاصیت Value آن میتوان به تمام اطلاعات کلاس SmtpConfig به شکل strongly typed دسترسی یافت.
اکنون اگر برنامه را جرا کنید، این خروجی را میتوان مشاهده کرد (که در آن آدرس Server دریافت شدهی از فایل کانفیگ نیز مشخص است):
البته همانطور که در قسمت قبل نیز عنوان شد، این تزریق وابستگیها در تمام قسمتهای برنامه کار میکند. برای مثال در کنترلرها هم میتوان <IOptions<SmtpConfig را به همین نحو تزریق کرد.
نحوهی واکنش به تغییرات فایلهای کانفیگ
در نگارشهای قبلی ASP.NET، هر تغییری در فایل web.config، سبب ریاستارت شدن کل برنامه میشد که این مساله نیز خود سبب بروز مشکلات زیادی مانند از دست رفتن سشن تمام کاربران میشد.
در ASP.NET Core، برنامهی وب ما دیگر متکی به فایل web.config نبوده و همچنین میتوان چندین و چند نوع فایل config داشت. به علاوه در اینجا متدهای مرتبط معرفی فایلهای کانفیگ دارای پارامتر مخصوص reloadOnChange نیز هستند:
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
این پارامتر در صورت true بودن، به صورت خودکار سبب بارگذاری مجدد اطلاعات فایل کانفیگ میشود (بدون ریاستارت کل برنامه).