پیکربندی Options Pattern - ASP.NET Core
شناسه پست: 2636
بازدید: 630

در این مقاله، میخواهیم شیوه دیگری از خواندن داده های پیکربندی در NET Core. به نام options pattern را یاد بگیریم. options pattern به ما کمک میکند تا تنظیمات مرتبط پیکربندی را در یک گروه قرار دهیم و همچنین دسترسی به تنظیمات را به صورت strongly typed در اختیار ما قرار میدهد. ما میخواهیم یاد بگیریم که options pattern چطور به ما کمک میکند و اینکه چطور میتوانیم نحوه دسترسی موجود به configuration را بهبود دهیم و یا حتی configuration را در زمان اجرا، reload کنیم.

اگر مفاهیم پایه مربوط به پیکربندی را فراموش کرده اید، مفاهیم پایه در ASP.NET Core را بررسی کنید.

اگر میخواهید در طول مسیر این مقاله، ما را دنبال کنید، از نقطه شروع پروژه استفاده کنید. برای بررسی پروژه تکمیل شده این مقاله، از پروژه نهایی این مقاله استفاده کنید.

در این مقاله، در مورد موارد زیر صحبت میکنیم:

بریم که شروع کنیم.

چرا Options Pattern؟

در مقاله قبلی، دیدیم که چطور میتوانیم داده های configuration را به آبجکتهای strongly typed، بایند کنیم. options pattern، امکانات مشابهی را به ما میدهد، با این تفاوت که، یک رویکرد ساختاریافته تر و ویژگی های بیشتری مانند اعتبار سنجی، بارگیری مجدد مستقیم (live reloading) و آزمایش آسان تر را در اختیار ما قرار میدهد.

زمانیکه کلاس شامل پیکربندیمان را تنظیم کردیم، میتوانیم آن را با IOptions<T> از طریق تزریق وابستگی، inject کنیم و بنابراین، فقط قسمتی از configuration مان یا بهتر است بگوییم که فقط آن قسمتی که نیاز داریم را inject کنیم.

اگر نیاز داریم که بدون متوقف کردن اپلیکیشن، پیکربندی را reload کنیم، بسته به موقعیت، میتوانیم از اینترفیس IOptionsSnapshot<T> یا اینترفیس IOptionsMonitor<T> استفاده کنیم. ما خواهیم دید که این اینترفیسها چه زمانی و چرا باید مورد استفاده قرار گیرند.

بنابراین options pattern، یک مکانیسم اعتبارسنجی خوبی را فراهم میکند که از ویژگیهای پرکاربرد DataAnotations استفاده میکند تا بررسی کند که آیا پیکربندی، به قوانین منطقی اپلیکیشن ما، پایبند است یا خیر.

تست آپشنها و آزمایش کلاسهای options نیز به دلیل وجود helper method ها، آسان است.

بنابراین، خلاصه اینکه،  options pattern کمک میکند به ما تا:

  • داده های configuration را به آبجکتهای strongly typed بایند کنیم
  • داده های configuration را در قسمتهای منطقی، در یک گروه قرار دهیم
  • در حالیکه اپلیکیشن در حال اجراست، configuration را reload نماییم
  • configuration را اعتبارسنجی کنیم
  • فقط قسمتهای مورد نیاز configuration را به قسمتهای مختلف اپلیکیشن تزریق کنیم
  • configuration را راحت تر تست کنیم

تعدادی مثالهای واقعی از کاربرد options pattern را ببینیم.

چطور از Options Pattern با استفاده از اینترفیس IOptions برای خواندن Configuration استفاده کنیم؟

خب، نگاهی به متد Index در کنترلر home بیندازیم که هنوز هیچ options pattern در آن پیاده سازی نشده است:

اینجا، configuration از طریق اینترفیس IConfiguration در سازنده HomeController تزریق شده است، بنابراین، ما در واقع میتوانیم به کل configuration چه به آن نیاز داشته باشیم چه نداشته باشیم، دسترسی داشته باشیم.

بعد از آن، ما LogLevel subsection از configuration را برای ارسال به Index view، به کلاس LoggingLevelConfiguration بایند کردیم.

خب، حالا ببینیم که آن چطور با اینترفیس IOptions<T> کار میکند.

اول، تعدادی مقادیر به فایل appsettings.json خود اضافه میکنیم:

ما میخواهیم از HomePage subsection برای پیکربندی Index view از HomeController استفاده کنیم.

سپس به یک class نیاز داریم که شامل این خصوصیات باشد، بنابراین آن را در پوشه Models پروژه ایجاد میکنیم:

ما باید مطمئن شویم که نام این خصوصیات با section مورد نظر در فایل appsettings.json مطابقت داشته باشد.

حالا میتوانیم HomeController را برای پشتیبانی از options pattern تغییر دهیم. اول، IOptions<TitleConfiguration> را به جای IConfiguration همانطور که قبلا تزریق کرده بودیم تزریق میکنیم:

همانطور که میبینید، از طریق ویژگی Value اینترفیس IOptions، داده های configuration را مورد دسترسی قرار میدهیم.

همچنین کلاس HomeModel را برای انعکاس این تغییرات، نیز تغییر میدهیم:

باید متد Index را نیز تغییر دهیم:

و باید مقداری logic به view اضافه کنیم، بنابراین از خصوصیاتی که تعریف کرده ایم میتوانیم اینجا استفاده کنیم:

این باید یک عنوان درشت به رنگ قرمز، درست در وسط Home Page ما نمایش دهد.

تنها کاری که باقی مانده است این است که TitleConfiguration را در کلاس Startup خود، ثبت و پیکربندی کنیم:

این خط باید HomePage configuration ما را ثبت کند.

حالا اگر اپلیکیشن را اجرا کنیم، نتیجه کاملا واضح میباشد:

پیکربندی home page

عالی! ما خصوصیات مورد نظر را در view توانستیم دریافت کنیم.

اما به عنوان مثال، اگر بخواهیم رنگ عنوان را به سبز تغییر دهیم، باید اپلیکیشن را restart کنیم.

اما یک شیوه بهتر برای این منظور وجود دارد که به آن IOptionsSnapshot گفته میشود.

استفاده از IOptionsSnapshot برای خواندن پیکربندی update شده

IOptionsSnapshot حاوی مقادیری است که فقط برای طول عمر (lifetime) یک request در نظر گرفته شده است. بنابراین این بدان معنی است که به عنوان یک سرویس دامنه دار (scoped service) در برنامه ما ثبت شده است و ما می توانیم از آن فقط به همراه وابستگیهای دامنه دار و ناپایدار استفاده کنیم. ما نمیتوانیم آن را به داخل سرویسهای singleton تزریق کنیم.

اگر نیاز داریم که configuration را بدون restart کردن اپلیکیشن تغییر دهیم باید IOptionsSnapshot<T> را پیاده سازی کنیم، زیرا IOptions<T> از آن پشتیبانی نمیکند.

کدمان را برای استفاده از IOptionsSnapshot<T> به جای IOptions<T> تغییر میدهیم.

در این مورد، تغییر کدمان راحت است، فقط باید سازنده HomeController را تغییر دهیم:

اگر برنامه را اجرا کنیم، همان نتیجه ای که قبلا گرفته بودیم (عنوان درشت قرمز رنگ) را دریافت میکنیم.

اما برای تست IOptionsSnapshot، به سراغ فایل appsettings.json خود میرویم و color متن را به  “blue” تغییر میدهیم:

حالا فقط باید HomePage را refresh کنیم و اگر همه چیز را درست انجام داده باشیم، رنگ عنوان باید به آبی تغییر پیدا کند:

تغییر پیکربندی با IOptionsSnapshot

عالی! ما اپلیکیشنمان را برای بارگذاری مجدد داده های پیکربندی به طور داینامیک، با موفقیت تغییر دادیم.

استفاده از IOptionsMonitor برای سرویسهای Singleton

یک مشکلی در solution کنونی ما وجود دارد. که ما در بالا آن را ذکر کردیم. IOptionsSnapshot برای تزریق به داخل service های ثبت شده به عنوان service های singleton در اپلیکیشن ما مناسب نیست.

برای به اثبات رساندن این مشکل، یک service ساده ایجاد میکنیم و سعی میکنیم IOptionsSnapshot را به داخل آن تزریق کنیم.

اول، title configuration مربوط به Home Page خود را کمی توسعه میدهیم. یک ویژگی جدید پیکربندی به نام UseRandomTitleColor به کلاس TitleConfiguration خود اضافه میکنیم:

و appsettings.json را برای انعکاس آن تغییر میدهیم:

بعد از آن، به یک service نیاز داریم که بتوانیم به عنوان یک singleton آن را ثبت کنیم. بنابراین یک service جدید میسازیم و نام آن را ITitleColorService میگذاریم و آن را در یک پوشه جداگانه به نام Services ایجاد میکنیم:

ITitleColorService فقط یک متد تعریف میکند و آن GetTitleColor میباشد که باید از بین لیست رنگهای تعریف شده، یک رنگ به طور تصادفی را برگرداند. سپس TitleColorService را در همین پوشه Services ایجاد میکنیم و این اینترفیس ITitleColorService را در آن پیاده سازی میکنیم:

این یک پیاده سازی ساده است که در آن، اگر UseRandomTitleColor به true ست شده باشد، یک رنگ را از بین آرایه colors برمیگرداند و اگر به false ست شده باشد، آنگاه مقدار ویژگی Color ثبت شده در فایل appsettings.json را به عنوان نتیجه برمیگرداند. توجه کنید که ما اینجا، برای inject کردن configuration خود، داریم از IOptionsSnapshot استفاده میکنیم.

حالا باید  این service را در اپلیکیشنمان ثبت کنیم و ما این کار را در کلاس Startup در متد ConfigureServices انجام میدهیم:

و در آخر، HomeController را برای override کردن رنگ title تغییر میدهیم:

تمام.

حال اگر برنامه را اجرا کنیم، برنامه به مشکل میخورد و خطای زیر را نشان میدهد:

Some services are not able to be constructed (Error while validating the service descriptor ‘ServiceType: ProjectConfigurationDemo.Services.ITitleColorService Lifetime: Singleton ImplementationType: ProjectConfigurationDemo.Services.TitleColorService’: Cannot consume scoped service ‘Microsoft.Extensions.Options.IOptionsSnapshot`1[ProjectConfigurationDemo.Models.TitleConfiguration]’ from singleton ‘ProjectConfigurationDemo.Services.ITitleColorService’.)

این خطا اتفاق می افتد به این دلیل که ASP.NET Core سعی میکند که مانع اشتباه ما از ارجاع دادن یک scoped service (سرویس دامنه دار) از یک singleton شود. این یک اشتباه کلاسیک است و در غیر این صورت می تواند منجر به یک رفتار غیر منتظره شود. به بیان ساده اگر parent یک singleton است، ما نمی توانیم در هر بارگذاری صفحه، یک child service را ایجاد کنیم.

و اینجا دقیقا جایی است که IOptionsMonitor وارد عمل میشود.

هدف IOptionsMonitor

به service خود برگردیم و IOptionsSnapshot را با IOptionsMonitor جایگزین کنیم:

تنها چیزی را که باید تغییر دهیم، سازنده میباشد. IOptionsMonitor برای دریافت مقادیر پیکربندی، به جای Value از CurrentValue استفاده میکند.

حالا اگر مججدا برنامه را اجرا کنیم، این خطا را دیگر دریافت نمیکنیم. اگر صفحه را هربار refresh کنیم، صفحه title ما با رنگ های مختلفی که قبلاً تعریف کرده ایم نمایش داده می شود.

عالی!

یک مفهوم دیگر وجود دارد که باید آن را پوشش دهیم.

Named Options

Named options ویژگی نیست که بخواهیم اغلب از آن استفاده کنیم، اما یک مورد خاص در مورد آن وجود دارد که میتواند مفید باشد.

تصور میکنیم که ما یک configuration به مانند این داریم:

ما اینجا یک ساختار یکسان پیکربندی برای subsection های مختلف تنظیمات مربوط به سکشن “Pages” را داریم. هردوی “HomePage” و “ProductPage” دقیقا خصوصیات یکسانی برای پیکربندی دارند.

به جای اضافه کردن کلاس پیکربندی دیگر مشابه کلاس TitleConfiguration که قبلاً پیاده سازی کرده ایم، می توانیم از همان کلاس برای map کردن subsection های مختلف مربوط به Pages section استفاده کنیم و فقط آنها را به صورت متفاوت نامگذاری کنیم تا بتوانیم بین آنها تمایز قائل شویم.

این ممکن است کمی سردرگم کننده باشد، پس اجازه دهید تا آن را پیاده سازی کنیم. در کلاس Startup خود، باید این تنظیمات را انجام دهیم:

حالا هردو subsection ها به یک کلاس configuration مپ شده اند که منطقی به نظر میرسد. ما نمیخواهیم چند کلاس با خصوصیات یکسان ایجاد کنیم و فقط نام متفاوتی به آنها بدهیم. این شیوه ی خیلی بهتری برای انجام این کار است.

فراخوانی option مورد نظر اکنون با استفاده از متد ()Get انجام میشود، بنابراین باید کلاس TitleColorService خود را مجدداً refactor کنیم:

همچنین اینترفیس ITitleColorService را نیز تغییر میدهیم:

و متعاقبا، HomeController را نیز تغییر میدهیم:

تمام. حالا اگر برنامه را اجرا کنیم، همان نتیجه مشابه قبل را دریافت میکنیم. تنها چیزی که در اینجا می خواستیم تغییر دهیم استفاده از ثابت های رشته ای به جای استفاده از حروف رشته ای برای دریافت subsection ها است تا از خطاهای زمان اجرا جلوگیری شود. اما میتوانید کمی با آن کلنجار بروید و آن را امتحان کنید.

خلاصه کنیم.

انتخاب بین IOptions, IOptionsSnapshot و IOptionsMonitor

بنابراین به طور خلاصه  IOptions<T>:

  • اینترفیس اصلی Options است و بهتر از bind کردن کل پیکربندی است
  • از بارگذاری مجدد configuration پشتیبانی نمیکند
  • به عنوان یک سرویس  singleton ثبت شده است و میتواند هرجایی inject شود
  • مقادیر configuration را فقط یکبار در حین ثبت، bind میکند و همان مقادیر را همیشه برمیگرداند
  • از named options پشتیبانی نمیکند

IOptionsSnapshot<T>:

  • به عنوان یک scoped service (سرویس دامنه دار) ثبت شده است
  • از بارگذاری مجدد configuration پشتیبانی میکند
  • به داخل service های singleton نمیتواند inject شود
  • در هر request، مقادیر مجددا بارگذاری میشود
  • از named options پشتیبانی میکند

IOptionsMonitor<T>:

  • به عنوان یک singleton service ثبت شده است
  • از بارگذاری مجدد configuration پشتیبانی میکند
  • به داخل چرخه عمر (lifetime) هر سرویسی میتواند تزریق شود
  • مقادیر cashe میشوند و دومرتبه فورا بارگذاری میشوند
  • از named options پشتیبانی میکند

با این تفاسیر، میتوانیم ببینیم که اگر نمیخواهیم بارگذاری مجددا مستقیم (live reloading) را فعال کنیم یا به named options نیاز نداریم، میتوانیم در ساده ترین حالت از IOptions<T> استفاده کنیم. اما اگر میخواهیم آنها را داشته باشیم، میتوانیم یا از IOptionsSnapshot<T> و یا از IOptionsMonitor<T> استفاده کنیم، اما IOptionsMonitor<T> میتواند به داخل  service های singleton تزریق شود، در حالیکه IOptionsSnapshot<T> نمیتواند.

گاهی اوقات ماهیت پروژه حکم می کند که نباید تنظیمات خود را فی البداهه تغییر دهیم. برخی اوقات، نیاز است که زمانیکه “گزینه صحیح” را انتخاب میکنید حواستان را جمع کنید.

نتیجه گیری

در این مقاله، یاد گرفتیم که  option ها چیست و چرا برای ما مفید هستند. ما شیوه های مختلف را برای پیاده سازی option ها و مزایا و معایب هریک را نیز پوشش دادیم. همانطور که اشاره کردیم، این یک مکانیسم قدرتمند است و باید به دقت تصمیم بگیرید که کدام یک برای پروژه واقعی شما بهتر است.

در مقاله بعدی، ما  options validation را پوشش میدهیم و میتوانید دیگر قسمتهای این سری از آموزش سریالی را در صفحه ASP.NET Core Web API پیدا کنید.

نویسنده

امید عباسی
من امید عباسی هستم. سالهاست که در زمینه برنامه نویسی با تکنولوژی دات نت فعالیت میکنم و عاشق این هستم که تجربیات و دانش خودم را در این زمینه با دیگران به اشتراک بزارم. خیلی دوست دارم که نظر و انتقاد خودتون رو در مورد این نوشته برای من بنویسید تا بتونم در آینده، مطالب بهتر و ارزشمندتری را برای شما فراهم کنم. در صورت داشتن هرگونه سوال هم در قسمت دیدگاه ها میتونید با بنده در ارتباط باشید