قابلیت چند زبانه و Localization در AngularJs- بخش چهارم و نهایی: Best Practiceهای angular-translate
ex7_load_static_files
<script src="Scripts/angular.js"></script> <script src="Scripts/angular-cookies.js"></script> <script src="Scripts/angular-translate.js"></script> <script src="Scripts/angular-translate-storage-cookie.js"></script> <!-- for override loader methods in angular translate --> <script src="/src/service/loader-static-files.js"></script>
// Register a loader for the static files // So, the module will search missing translation tables under the specified urls. // Those urls are [prefix][langKey][suffix]. $translateProvider.useStaticFilesLoader({ prefix: '/l10n/', suffix: '.json' });
حال ببینیم که این فرآیند در loader-static-files چگونه پیاده سازی شده است. در این فایل یک متد load نوشته شده است که فایلهای static را طبق یک الگوی مشتمل بر prefix و suffix از سرور میخواند. لزومی ندارد که شما فایلها را حتما به صورت JSON و با این پسوند ذخیره کنید. اما چیزی که قطعی است این است که فایلها حتما باید به صورت key value ذخیره شده باشند.
تکه کد زیر اطلاعات فایل loader-static-files را نمایش میدهد.
angular.module('pascalprecht.translate') .factory('$translateStaticFilesLoader', $translateStaticFilesLoader); function $translateStaticFilesLoader($q, $http) { 'use strict'; return function (options) { if (!options || (!angular.isArray(options.files) && (!angular.isString(options.prefix) || !angular.isString(options.suffix)))) { throw new Error('Couldn\'t load static files, no files and prefix or suffix specified!'); } if (!options.files) { options.files = [{ prefix: options.prefix, suffix: options.suffix }]; } var load = function (file) { if (!file || (!angular.isString(file.prefix) || !angular.isString(file.suffix))) { throw new Error('Couldn\'t load static file, no prefix or suffix specified!'); } var deferred = $q.defer(); $http(angular.extend({ url: [ file.prefix, options.key, file.suffix ].join(''), method: 'GET', params: '' }, options.$http)).success(function (data) { deferred.resolve(data); }).error(function () { deferred.reject(options.key); }); return deferred.promise; }; var deferred = $q.defer(), promises = [], length = options.files.length; for (var i = 0; i < length; i++) { promises.push(load({ prefix: options.files[i].prefix, key: options.key, suffix: options.files[i].suffix })); } $q.all(promises).then(function (data) { var length = data.length, mergedData = {}; for (var i = 0; i < length; i++) { for (var key in data[i]) { mergedData[key] = data[i][key]; } } deferred.resolve(mergedData); }, function (data) { deferred.reject(data); }); return deferred.promise; }; } $translateStaticFilesLoader.displayName = '$translateStaticFilesLoader';
همانطور که ملاحظه میکنید، کد فوق یک سرویس با نام $translateStaticFilesLoader را تعریف نموده است. در صورتیکه ما در کنترلر فایل ex7، اصلا نامی از آن نبردیم و تنها از $translateProvider.useStaticFilesLoader استفاده نمودیم! جواب در نحوهی نگارش کد angular-translate نهفته است. در خط 866 فایل angular-translate تکه کد زیر مربوط به تعریف translateStaticFileLoader میباشد. همانطور که ملاحظه میکنید سرویس translateStaticFilesLoader درون فضای نام سرویس translateTable قرار گرفته است. بنابراین ما تنها با تعریف سرویس translateStaticFilesLoader، در حقیقت آن را override نمودهایم. در کد نمونهای که در بخشهای قبلی قرار دادهام یک فایل translate.js نیز قرار دارد که در فولدر src/services قرار گرفته است. این فایل نیز برخی از امکانات و سرویسهای built-in درون angular-translate را سفارشی نموده است.
/** * @ngdoc function * @name pascalprecht.translate.$translateProvider#useStaticFilesLoader * @methodOf pascalprecht.translate.$translateProvider * * @description * Tells angular-translate to use `$translateStaticFilesLoader` extension service as loader. * * @param {Object=} options Optional configuration object */ this.useStaticFilesLoader = function (options) { return this.useLoader('$translateStaticFilesLoader', options); };
در این 4 مجموعه سعی کردم تمامی آنچه را که برای ایجاد قابلیت چند زبانه و localization نیاز است و حیاتی بود، تشریح کنم. بنابراین تا کنون دانش خوبی دربارهی این کتابخانه کسب نمودهاید. باقی تمرینها را میتوانید بر حسب نیاز با استفاده از مستندات موجود در angular-translate مطالعه و استفاده نمایید.
PdfReport
به کمک کتابخانه PdfReport دسترسی گستردهای به منابع دادهای مختلف خواهید یافت. منابعی که لزوما بانک اطلاعاتی نیستند؛ مانند یک لیست جنریک و یا حتی یک anonymously typed list حاصل از یک کوئری LINQ.
این کتابخانه علاوه بر تبدیل اطلاعات شما به گزارشات مبتنی بر PDF، امکان تهیه خروجی خودکار اکسل (2007 به بعد) را نیز دارد. فایل خروجی آن، به صورت پیوست درون فایل PDF تهیه شده قرار میگیرد و جزئی از آن میشود.
مسایل امنیتی مانند رمزنگاری فایل PDF حاصل و یا حتی افزودن امضای دیجیتال به فایل نهایی تولیدی نیز در آن لحاظ شده است.
کتابخانه PdfReport بر پایه کتابخانههای معروف سورس باز iTextSharp و EPPlus تهیه شده است. حداقل مزیت استفاده از آن، صرفه جویی در وقت شما جهت آموختن ریزه کاریهای مرتبط با هر کدام از کتابخانههای یاده شده است. برای نمونه جهت فراگیری کار با iTextSharp نیاز است یک کتاب 600 صفحهای به نام iText in action را مطالعه و تمرین کنید. این مورد منهای مسایل و نکات متعدد مرتبط با زبان فارسی است که در این کتاب به آنها اشارهای نشده است.
مقدمات راهبری (Navigation) در سیلورلایت را در اینجا میتوانید مطالعه نمائید : +
مطلبی را که در فصل فوق نخواهید یافت در مورد نحوهی بکارگیری الگوی MVVM جهت پیاده سازی Navigation در یک برنامهی سیلورلایت است؛ علت آن هم به این بر میگردد که این فصل پیش از مباحث Binding مطرح شد.
صورت مساله:
یکی از اصول MVVM این است که در ViewModel نباید ارجاعی از View وجود داشته باشد (ViewModel باید در بیخبری کامل از وجود اشیاء UI و ارجاع مستقیم به آنها طراحی شود)، اما برای پیاده سازی مباحث Navigation نیاز است به نحوی به شیء Frame قرار داده شده در صفحهی اصلی یا قالب اصلی برنامه دسترسی یافت تا بتوان درخواست رهنمون شدن به صفحات مختلف را صادر کرد. اکنون چکار باید کرد؟
راه حل:
یکی از راه حلهای جالبی که برای این منظور وجود دارد استفاده از امکانات کلاس Messenger مجموعهی MVVM Light toolkit است. از طریق ViewModel برنامه، آدرس صفحهی مورد نظر را به صورت یک پیغام به View مورد نظر ارسال میکنیم و سپس View برنامه که به این پیغامها گوش فرا میدهد، پس از دریافت آدرس مورد نظر، نسبت به فراخوانی تابع Navigate شیء Frame رابط کاربری برنامه اقدام خواهد کرد. به این صورت ViewModel برنامه به View خود جهت اعمال راهبری برنامه، گره نخواهد خورد.
روش پیاده سازی:
ابتدا ساختار پروژه را در نظر بگیرید (این شکل دگرگون شدهی Solution explorer مرتبط است با productivity tools نصب شده):
در پوشهی Views ، دو صفحه اضافه شدهاند که توسط user control ایی به نام menu لیست شده و راهبری خواهند شد. مونتاژ نهایی هم در MainPage.xaml صورت میگیرد.
کدهای XAML مرتبط با منوی ساده برنامه به شرح زیر هستند (Menu.xaml) :
<UserControl x:Class="MvvmLight6.Views.Menu"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:MvvmLight6.ViewModels" mc:Ignorable="d"
FlowDirection="RightToLeft" d:DesignHeight="300" d:DesignWidth="400">
<UserControl.Resources>
<vm:MenuViewModel x:Key="vmMenuViewModel" />
</UserControl.Resources>
<StackPanel DataContext="{Binding Source={StaticResource vmMenuViewModel}}">
<HyperlinkButton Content="صفحه یک" Margin="5"
Command="{Binding DoNavigate}"
CommandParameter="/Views/Page1.xaml"
/>
<HyperlinkButton Content="صفحه دو" Margin="5"
Command="{Binding DoNavigate}"
CommandParameter="/Views/Page2.xaml"
/>
</StackPanel>
</UserControl>
کدهای ViewModel مرتبط با این View که کار Command گردانی را انجام خواهد داد به شرح زیر است:
using GalaSoft.MvvmLight.Command;
using GalaSoft.MvvmLight.Messaging;
namespace MvvmLight6.ViewModels
{
public class MenuViewModel
{
public RelayCommand<string> DoNavigate { set; get; }
public MenuViewModel()
{
DoNavigate = new RelayCommand<string>(doNavigate);
}
private static void doNavigate(string url)
{
Messenger.Default.Send(url, "MyNavigationService");
}
}
}
تمام آیتمهای منوی فوق یک روال را صدا خواهند زد : DoNavigate . تنها تفاوت آنها در CommandParameter ارسالی به RelayCommand ما است که حاوی آدرس قرارگیری فایلهای صفحات تعریف شده است. این آدرسها با کمک امکانات کلاس Messenger مجموعهی MVVM light toolkit به View اصلی برنامه ارسال میگردند.
کدهای XAML مرتبط با MainPage.xaml به شرح زیر هستند:
<UserControl x:Class="MvvmLight6.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:sdk="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation"
xmlns:usr="clr-namespace:MvvmLight6.Views"
mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400">
<Grid x:Name="LayoutRoot" Background="White">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="268" />
</Grid.ColumnDefinitions>
<usr:Menu Grid.Column="1" />
<sdk:Frame Margin="5"
Name="frame1"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
Grid.Column="0" />
</Grid>
</UserControl>
و کار دریافت پیغامها (یا همان آدرس صفحات جهت انجام راهبری) و عکس العمل نشان دادن به آنها توسط کدهای ذیل صورت خواهد گرفت:
using System;
using GalaSoft.MvvmLight.Messaging;
namespace MvvmLight6
{
public partial class MainPage
{
public MainPage()
{
registerMessenger();
InitializeComponent();
}
private void registerMessenger()
{
Messenger.Default.Register<string>(this, "MyNavigationService", doNavigate);
}
private void doNavigate(string uri)
{
frame1.Navigate(new Uri(uri, UriKind.Relative));
}
}
}
ابتدا یک Messenger در اینجا رجیستر میشود و سپس به ازای هر بار دریافت پیغامی با token مساوی MyNavigationService ، متد doNavigate فراخوانی خواهد گردید.
کدهای این مثال را از اینجا میتوانید دریافت کنید.
Blazor 5x - قسمت 19 - کار با فرمها - بخش 7 - نکات ویژهی کار با EF-Core در برنامههای Blazor Server
طول عمر سرویسها، در برنامههای Blazor Server متفاوت هستند
هنگامیکه با یک ASP.NET Core Web API متداول کار میکنیم، درخواستهای HTTP رسیده، از میانافزارهای موجود رد شده و پردازش میشوند. اما هنگامیکه با Blazor Server کار میکنیم، به علت وجود یک اتصال دائم SignalR که عموما از نوع Web socket است، دیگر درخواست HTTP وجود ندارد. تمام رفت و برگشتهای برنامه به سرور و پاسخهای دریافتی، از طریق Web socket منتقل میشوند و نه درخواستها و پاسخهای متداول HTTP.
این روش پردازشی، اولین تاثیری را که بر روی رفتار یک برنامه میگذارد، تغییر طول عمر سرویسهای آن است. برای مثال در برنامههای Web API، طول عمر درخواستها، از نوع Scoped هستند و با شروع پردازش یک درخواست، سرویسهای مورد نیاز وهله سازی شده و در پایان درخواست، رها میشوند.
این مساله در حین کار با EF-Core نیز بسیار مهم است؛ از این جهت که در برنامههای Web API نیز EF-Core و DbContext آن، به صورت سرویسهایی با طول عمر Scoped تعریف میشوند. برای مثال زمانیکه یک چنین تعریفی را در برنامه داریم:
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString));
public static IServiceCollection AddDbContext<TContext>( [NotNullAttribute] this IServiceCollection serviceCollection, [CanBeNullAttribute] Action<DbContextOptionsBuilder> optionsAction = null, ServiceLifetime contextLifetime = ServiceLifetime.Scoped, ServiceLifetime optionsLifetime = ServiceLifetime.Scoped) where TContext : DbContext;
اما زمانیکه مانند یک برنامهی مبتنی بر Blazor Server، دیگر HTTP Requests متداولی را نداریم، چطور؟ در این حالت زمانیکه یک اتصال SignalR برقرار شد، وهلهای از DbContext که در اختیار برنامهی Blazor Server قرار میگیرد، تا زمانیکه کاربر این اتصال را به نحوی قطع نکرده (مانند بستن کامل مرورگر و یا ریفرش صفحه)، ثابت باقی خواهد ماند. یعنی به ازای هر اتصال SignalR، طول عمر ServiceLifetime.Scoped پیشفرض تعریف شده، همانند یک وهلهی با طول عمر Singleton عمل میکند. در این حالت تمام صفحات و کامپوننتهای یک برنامهی Blazor Server، از یک تک وهلهی مشخص DbContext که در ابتدای کار دریافت کردهاند، کار میکنند و از آنجائیکه DbContext به صورت thread-safe کار نمیکند، این تک وهله مشکلات زیادی را ایجاد خواهد کرد که یک نمونه از آنرا در عمل، در پایان قسمت قبل مشاهده کردید:
«اگر برنامه را اجرا کرده و سعی در حذف یک ردیف کنیم، به خطای زیر میرسیم و یا حتی اگر کاربر شروع کند به کلیک کردن سریع در قسمتهای مختلف برنامه، باز هم این خطا مشاهده میشود:
An exception occurred while iterating over the results of a query for context type 'BlazorServer.DataAccess.ApplicationDbContext'. System.InvalidOperationException: A second operation was started on this context before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
هر درخواست Web API نیز بر روی یک ترد جداگانه اجرا میشود؛ اما چون ابتدا و انتهای درخواستها مشخص است، طول عمر Scoped، در ابتدای درخواست شروع شده و در پایان آن رها سازی میشود. به همین جهت استثنائی را که در اینجا مشاهده میکنید، در برنامههای Web API شاید هیچگاه مشاهده نشود.
معرفی DbContextFactory در EF Core 5x
همواره باید طول عمر DbContext را تا جای ممکن، کوتاه نگه داشت. مشکل فعلی ما، Singleton رفتار کردن DbContextها (داشتن طول عمر طولانی) در برنامههای Blazor Server هستند. یک چنین رفتاری را شاید در برنامههای دسکتاپ هم پیشتر مشاهده کرده باشید. برای مثال در برنامههای دسکتاپ WPF، تا زمانیکه یک فرم باز است، Context ایجاد شدهی در آن هم برقرار است و Dispose نمیشود. در یک چنین حالتهایی، عموما Context را در زمان نیاز، ایجاد کرده و پس از پایان آن کار کوتاه، Context را رها میکنند. به همین جهت نیاز به DbContext Factory ای وجود دارد که بتواند یک چنین پیاده سازیهایی را میسر کند و خوشبختانه از زمان EF Core 5x، یک چنین امکانی خصوصا برای برنامههای Blazor Server تحت عنوان DbContextFactory ارائه شدهاست که به عنوان راه حل استاندارد دسترسی به DbContext در اینگونه برنامهها مورد استفاده قرار میگیرد.
برای کار با DbContextFactory، اینبار در فایل BlazorServer.App\Startup.cs، بجای استفاده از services.AddDbContext، از متد AddDbContextFactory استفاده میشود:
public void ConfigureServices(IServiceCollection services) { var connectionString = Configuration.GetConnectionString("DefaultConnection"); //services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString)); services.AddDbContextFactory<ApplicationDbContext>(options => options.UseSqlServer(connectionString));
روش اول کار با DbContextFactory در کامپوننتهای Blazor Server : وهله سازی از نو، به ازای هر متد
در این روش پس از ثبت AddDbContextFactory در فایل Startup برنامه مانند مثال فوق، ابتدا سرویس IDbContextFactory که به ApplicationDbContext اشاره میکند به ابتدای کامپوننت تزریق میشود:
@inject IDbContextFactory<ApplicationDbContext> DbFactory
private async Task DeleteImageAsync() { using var context = DbFactory.CreateDbContext(); var image = await context.HotelRoomImages.FindAsync(1); // ... }
روش دوم کار با DbContextFactory در کامپوننتهای Blazor Server : یکبار وهله سازی Context به ازای هر کامپوننت
در این روش میتوان طول عمر Context را معادل طول عمر کامپوننت تعریف کرد که مزیت استفادهی از Change tracking موجود در EF-Core را به همراه خواهد داشت. در این حالت کامپوننتهای Blazor Server، شبیه به فرمهای برنامههای دسکتاپ عمل میکنند:
@implements IDisposable @inject IDbContextFactory<ApplicationDbContext> DbFactory @code { private ApplicationDbContext Context; protected override async Task OnInitializedAsync() { Context = DbFactory.CreateDbContext(); await base.OnInitializedAsync(); } private async Task DeleteImageAsync() { var image = await Context.HotelRoomImages.FindAsync(1); // ... } public void Dispose() { Context.Dispose(); } }
- اما بجای اینکه به ازای هر متد، کار فراخوانی DbFactory.CreateDbContext صورت گیرد، یکبار در آغاز کار کامپوننت و در روال رویدادگردان OnInitializedAsync، کار وهله سازی Context کامپوننت انجام شده و از این تک Context در تمام متدهای کامپوننت استفاده خواهد شد.
- در این حالت کار Dispose خودکار این Context به متد Dispose نهایی کل کامپوننت واگذار شدهاست. برای اینکه این متد فراخوانی شود، نیاز است در ابتدای تعاریف کامپوننت، از دایرکتیو implements IDisposable@ استفاده کرد.
سؤال: اگر سرویسی از ApplicationDbContext تزریق شدهی در سازندهی خود استفاده میکند، چکار باید کرد؟
برای نمونه سرویسهای از پیش تعریف شدهی ASP.NET Core Identity، در سازندهی خود از ApplicationDbContext استفاده میکنند و نه از IDbContextFactory. در این حالت برای تامین ApplicationDbContextهای تزریق شده، فقط کافی است از روش زیر استفاده کنیم:
services.AddScoped<ApplicationDbContext>(serviceProvider => serviceProvider.GetRequiredService<IDbContextFactory<ApplicationDbContext>>().CreateDbContext());
سؤال: روش پیاده سازی سرویسهای یک برنامه Blazor Server به چه صورتی باید تغییر کند؟
تا اینجا روشهایی که برای استفاده از IDbContextFactory معرفی شدند (که روشهای رسمی و توصیه شدهی اینکار نیز هستند)، فرض را بر این گذاشتهاند که ما قرار است تمام منطق تجاری کار با بانک اطلاعاتی را داخل همان متدهای کامپوننتها انجام دهیم (این روش برنامه نویسی، بسیار مورد علاقهی مایکروسافت است و در تمام مثالهای رسمی آن به صورت ضمنی توصیه میشود!). اما اگر همانند مثالی که تاکنون در این سری بررسی کردیم، نخواهیم اینکار را انجام دهیم و علاقمند باشیم تا این منطق تجاری را به سرویسهای مجزایی، با مسئولیتهای مشخصی انتقال دهیم، روش استفادهی از IDbContextFactory چگونه خواهد بود؟
در این حالت از ترکیب روش دوم مطرح شدهی استفاده از IDbContextFactory که به همراه مزیت دسترسی کامل به Change Tracking توکار EF-Core و پیاده سازی الگوی واحد کار است و وهله سازی خودکار ApplicationDbContext که معرفی شد، استفاده خواهیم کرد؛ به این صورت:
الف) تمام سرویسهای EF-Core یک برنامهی Blazor Server باید اینترفیس IDisposable را پیاده سازی کنند.
این مورد برای سرویسهای پروژههای Web API، ضروری نیست؛ چون طول عمر Context آنها توسط خود IoC Container مدیریت میشود؛ اما در برنامههای Blazor Server، مطابق توضیحاتی که ارائه شد، خودمان باید این طول عمر را مدیریت کنیم.
بنابراین به پروژهی سرویسهای برنامه مراجعه کرده و هر سرویسی که ApplicationDbContext تزریق شدهای را در سازندهی خود میپذیرد، یافته و تعریف اینترفیس آنرا به صورت زیر تغییر میدهیم:
public interface IHotelRoomService : IDisposable { // ... } public interface IHotelRoomImageService : IDisposable { // ... }
public class HotelRoomService : IHotelRoomService { private bool _isDisposed; // ... public void Dispose() { Dispose(disposing: true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (!_isDisposed) { try { if (disposing) { _dbContext.Dispose(); } } finally { _isDisposed = true; } } } }
ب) Dispose دستی تمام سرویسها، در کامپوننتهای مرتبط
در ادامه تمام کامپوننتهایی را که از سرویسهای فوق استفاده میکنند یافته و ابتدا دایرکتیو implements IDisposable@ را به ابتدای آنها اضافه میکنیم. سپس متد Dispose آنها را جهت فراخوانی متد Dispose سرویسهای فوق، تکمیل خواهیم کرد:
بنابراین ابتدا به فایل BlazorServer\BlazorServer.App\Pages\HotelRoom\HotelRoomUpsert.razor مراجعه کرده و تغییرات زیر را اعمال میکنیم:
@page "/hotel-room/create" @page "/hotel-room/edit/{Id:int}" @implements IDisposable // ... @code { // ... public void Dispose() { HotelRoomImageService.Dispose(); HotelRoomService.Dispose(); } }
@page "/hotel-room" @implements IDisposable // ... @code { // ... public void Dispose() { HotelRoomService.Dispose(); } }
مشکل! اینبار خطای dispose شدن context را دریافت میکنیم!
System.ObjectDisposedException: Cannot access a disposed context instance. A common cause of this error is disposing a context instance that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling 'Dispose' on the context instance, or wrapping it in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances. Object name: 'ApplicationDbContext'.
مشکلی که در اینجا رخ داده این است که سرویسهایی را داریم با طول عمر به ظاهر Scoped که یکی از وابستگیهای آنها را به صورت دستی Dispose کردهایم. چون طول عمر Scoped در اینجا وجود ندارد و طول عمرها در اصل Singleton هستند، هربار که سرویس مدنظر مجددا درخواست شود، همان وهلهی ابتدایی که اکنون یکی از وابستگیهای آن Dispose شده، در اختیار برنامه قرار میگیرد.
پس از این تغییرات، اولین باری که برنامه را اجرا میکنیم، لیست اتاقها به خوبی نمایش داده میشوند و مشکلی نیست. بعد در همین حال و در همین صفحه، اگر بر روی دکمهی افزودن یک اتاق جدید کلیک کنیم، اتفاقی که رخ میدهد، فراخوانی متد Dispose کامپوننت لیست اتاقها است (بر روی آن یک break-point قرار دهید). بنابراین متد Dispose یک کامپوننت، با هدایت به یک مسیر دیگر، به صورت خودکار فراخوانی میشود. در این حالت Context برنامه Dispose شده و در کامپوننت ثبت یک اتاق جدید دیگر، در دسترس نخواهد بود؛ چون IHotelRoomService مورد استفاده مجددا وهله سازی نمیشود و از همان وهلهای که بار اول ایجاد شده، استفاده خواهد شد.
بنابراین سؤال اینجا است که چگونه میتوان سیستم تزریق وابستگیها را وادار کرد تا تمام سرویسهای تزریق شدهی به سازندههای سرویسهای HotelRoomService و HotelRoomImageService را مجددا وهله سازی کند و سعی نکند از همان وهلههای قبلی استفاده کند؟
پاسخ: یک روش این است که IHotelRoomImageService را خودمان به ازای هر کامپوننت به صورت دستی در روال رویدادگردان OnInitializedAsync وهله سازی کرده و DbFactory.CreateDbContext جدیدی را مستقیما به سازندهی آن ارسال کنیم. در این حالت مطمئن خواهیم شد که این وهله، جای دیگری به اشتراک گذاشته نمیشود:
@code { private IHotelRoomImageService HotelRoomImageService; protected override async Task OnInitializedAsync() { HotelRoomImageService = new HotelRoomImageService(DbFactory.CreateDbContext(), mapper); await base.OnInitializedAsync(); } private async Task DeleteImageAsync() { await HotelRoomImageService.DeleteAsync(1); // ... } public void Dispose() { HotelRoomImageService.Dispose(); } }
وادار کردن Blazor Server به وهله سازی مجدد سرویسهای کامپوننتها
بنابراین مشکل ما Singleton رفتار کردن سرویسها، در برنامههای Blazor است. برای مثال در برنامههای Blazor Server، تا زمانیکه اتصال SignalR برنامه برقرار است (مرورگر بسته نشده، برگهی جاری بسته نشده و یا کاربر صفحه را ریفرش نکرده)، هیچ سرویسی دوباره وهله سازی نمیشود.
برای رفع این مشکل، امکان Scoped رفتار کردن سرویسهای یک کامپوننت نیز در نظر گرفته شدهاند. برای نمونه کدهای کامپوننت HotelRoomList.razor را به صورت زیر تغییر میدهیم:
@page "/hotel-room" @*@implements IDisposable*@ @*@inject IHotelRoomService HotelRoomService*@ @inherits OwningComponentBase<IHotelRoomService>
چند نکته:
- فقط یکبار به ازای هر کامپوننت میتوان از دایرکتیو inherits استفاده کرد.
- زمانیکه طول عمر سرویسی را توسط OwningComponentBase مدیریت میکنیم، در حقیقت یک کلاس پایه را برای آن کامپوننت درنظر گرفتهایم که به همراه یک خاصیت عمومی ویژه، به نام Service و از نوع سرویس مدنظر ما است. در این حالت یا میتوان از خاصیت Service به صورت مستقیم استفاده کرد و یا میتوان به صورت زیر، همان کدهای قبلی را داشت و هربار که نیازی به HotelRoomService بود، آنرا به خاصیت عمومی Service هدایت کرد:
@code { private IHotelRoomService HotelRoomService => Service;
@page "/preferences" @using Microsoft.Extensions.DependencyInjection @inherits OwningComponentBase @code { private IHotelRoomService HotelRoomService { get; set; } private IHotelRoomImageService HotelRoomImageService { get; set; } protected override void OnInitialized() { HotelRoomService = ScopedServices.GetRequiredService<IHotelRoomService>(); HotelRoomImageService = ScopedServices.GetRequiredService<IHotelRoomImageService>(); } }
خلاصهی بحث جاری در مورد روش مدیریت DbContext برنامههای Blazor Server:
- بجای services.AddDbContext متداول، باید از AddDbContextFactory استفاده کرد:
services.AddDbContextFactory<ApplicationDbContext>(options => options.UseSqlServer(connectionString)); services.AddScoped<ApplicationDbContext>(serviceProvider => serviceProvider.GetRequiredService<IDbContextFactory<ApplicationDbContext>>().CreateDbContext());
- کامپوننتهای برنامه، سرویسهایی را که باید Scoped عمل کنند، دیگر نباید از طریق تزریق مستقیم آنها دریافت کنند؛ چون در این حالت همواره به همان وهلهای که در ابتدای کار ایجاد شده، میرسیم:
@inject IHotelRoomService HotelRoomService
@inherits OwningComponentBase<IHotelRoomService>
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-19.zip
چند روز قبل مشکلی رخ داده بود به این شرح!
سروری کهSQL server بر روی آن نصب بود بخاطر SQL server ، بیش از 50 درصد CPU usage مداوم پیدا کرده بود. عموما مصرف CPU اس کیوال سرور روی سرورهای قوی بالا نیست و تداوم این حالت به این شدت یعنی بروز مشکل.
اینجا است که این سؤال پیش میاد، SQL Server الان داره چکار میکنه که تا این حد به صورت مداوم مصرف CPU آن بالا رفته؟ (حدودا 2 ساعت تمام به صورت مداوم مصرف CPU بالای 50 درصد بود)
با استفاده از کوئری زیر میشود، عبارات SQL ایی را که هم اکنون در حال اجرا هستند را به صورت زنده مشاهده کرد: (در اس کیوال سرور 2005 به بعد)
USE master;
SELECT st.text, r.session_id, r.status, r.command, r.cpu_time, r.total_elapsed_time
FROM sys.dm_exec_requests r
CROSS APPLY sys.dm_exec_sql_text(sql_handle) AS st
همچنین با استفاده از کوئری زیر میتوان آخرین 50 کوئری اجرا شده در SQL server را به همراه زمان اجرای آنها گزارشگیری کرد:
SELECT TOP 50
deqs.last_execution_time AS [Time],
dest.text AS [Query]
FROM sys.dm_exec_query_stats AS deqs
CROSS apply
sys.dm_exec_sql_text(deqs.sql_handle) AS dest
ORDER BY
deqs.last_execution_time DESC
http://www.sqlteam.com/article/dynamic-management-views
برای شروع به کار در زمینه رایانش ابری از کجا شروع کنیم؟
مطالعه منابع مفید بیشتر در جامعه آزاد رایانش ابری ایران
Docker for Windows چگونه از هر دوی کانتینرهای ویندوزی و لینوکسی پشتیبانی میکند؟
زمانیکه docker for windows را اجرا میکنیم، سرویسی را ایجاد میکند که سبب اجرای پروسهی ویژهای به نام com.docker.proxy.exe میشود:
هنگامیکه برای مثال فرمان docker run nginx را توسط Docker CLI اجرا میکنیم، Docker CLI از طریق واسط یاد شده، دستورات را به MobyLinuxVM منتقل میکند. به این صورت است که امکان اجرای Linux Containers، بر روی ویندوز میسر میشوند:
اکنون اگر به Windows Containers سوئیچ کنیم (از طریق کلیک راست بر روی آیکن Docker در قسمت Tray Icons ویندوز)، پروسهی dockerd.exe یا docker daemon شروع به کار خواهد کرد:
اینبار اگر مجددا از Docker CLI برای اجرای مثلا IIS Container استفاده کنیم، دستور ما از طریق واسطهای com.docker.proxy و dockerd، به کانتینر ویندوزی منتقل و اجرا میشود:
نگاهی به معماری Docker بر روی ویندوز سرور
داکر بر روی ویندوز سرور، تنها به همراه موتور مدیریت کنندهی Windows Containers است:
در اینجا با صدور فرمانهای Docker CLI، پیامها مستقیما به dockerd یا موتور داکر بر روی ویندوز سرور ارسال شده و سپس کار اجرا و مدیریت یک Windows Container انجام میشود.
نصب Docker بر روی ویندوز سرور
جزئیات مفصل و به روز Windows Containers را همواره میتوانید در این آدرس در سایت مستندات مجازی سازی مایکروسافت مطالعه کنید (قسمت Container Host Deployment - Windows Server آن). پیشنیاز کار با آن نیز نصب حداقل ویندوز سرور 2016 میباشد و بهتر است تمام به روز رسانیهای آنرا نیز نصب کرده باشید؛ چون تعدادی از بهبودهای کار با کانتینرهای آن، به همراه به روز رسانیها آن ارائه شدهاند.
برای شروع به نصب، نیاز است کنسول PowerShell ویندوز را با دسترسی Admin اجرا کنید.
سپس اولین دستوراتی را که نیاز است اجرا کنید، کار نصب موتور Docker و CLI آنرا به صورت خودکار بر روی ویندوز سرور انجام میدهند:
Install-Module -Name DockerMsftProvider -Repository PSGallery -Force Install-Package -Name docker -ProviderName DockerMsftProvider Restart-Computer -Force
- به علاوه اگر دستور *get-service *docker را در کنسول PowerShell صادر کنید، مشاهده خواهید کرد که سرویس جدیدی را به نام Docker نیز نصب و راه اندازی کردهاست که به dockerd.exe اشاره میکند.
- و یا اگر در کنسول PowerShell دستور docker را صادر کنید، ملاحظه خواهید کرد که CLI آن، فعال و قابل استفادهاست. برای مثال، دستور docker version را صادر کنید تا بتوانید نگارش docker نصب شده را ملاحظه نمائید.
اجرای Image مخصوص NET Core. بر روی ویندوز سرور
تگهای مختلف Image مخصوص NET Core. را در اینجا ملاحظه میکنید. در ادامه قصد داریم tag مرتبط با nanoserver آنرا نصب کنیم (با حجم 802MB):
docker run microsoft/dotnet:nanoserver
docker run -it microsoft/dotnet:nanoserver
چرا حجم Image مخصوص NET Core. نگارش nanoserver آن حدود 800 مگابایت است؟
در مثال قبلی، دسترسی به command prompt مجزایی نسبت به command prompt اصلی سیستم، در داخل یک container، شاید اندکی غیر منتظره بود و اکنون این سؤال مطرح میشود که یک image، شامل چه چیزهایی است؟
یک image شاید در ابتدای کار صرفا شامل فایلهای اجرایی یک برنامهی خاص به نظر برسد؛ اما زمانیکه قرار است تبدیل به یک container قابل اجرا شود، شامل بسیاری از فایلهای دیگر نیز خواهد شد. برای درک این موضوع نیاز است لایههای نرم افزاری که یک سیستم را تشکیل میدهند، بررسی کنیم:
در این تصویر از پایینترین لایهای را که با سخت افزار ارتباط برقرار میکند تا بالاترین لایهی موجود نرم افزاری را مشاهده میکنید. دراینجا هر چیزی را که در ناحیهی کرنل قرار نمیگیرد، User Space مینامند. برنامههای قرار گرفتهی در User Space برای کار با سخت افزار نیاز است با کرنل ارتباط برقرار کنند و برای این منظور از System Calls استفاده میکنند که عموما کتابخانههایی هستند که جزئی از سیستم عامل میباشند؛ مانند API ویندوز. برای مثال MongoDB توسط Win32 API و System Calls، فرامینی را به کرنل منتقل میکند.
در روش متداول توزیع و نصب نرم افزار، ما عموما همان بالاترین سطح را توزیع و نصب میکنیم؛ برای مثال خود MongoDB را. در اینجا نصاب MongoDB فرض میکند که در سیستم جاری، تمام لایههای دیگر، موجود و آمادهی استفاده هستند و اگر اینگونه نباشد، به مشکل برخواهد خورد و اجرا نمیشود. برای اجتناب از یک چنین مشکلاتی مانند عدم حضور وابستگیهایی که یک برنامه برای اجرا نیاز دارد، imageهای docker، نحوهی توزیع نرم افزارها را تغییر دادهاند. اینبار یک image بجای توزیع فقط MongoDB، شامل تمام قسمتهای مورد نیاز User Space نیز هست:
به این ترتیب دیگر مشکلاتی مانند عدم وجود یک وابستگی یا حتی وجود یک وابستگی غیرسازگار با نرم افزار مدنظر، وجود نخواهند داشت. حتی میتوان تصویر فوق را به صورت زیر نیز خلاصه کرد:
به همین جهت بود که برای مثال در قسمت قبل موفق شدیم IIS مخصوص ویندوز سرور با تگ nanoserver را بر روی ویندوز 10 که بسیاری از وابستگیهای مرتبط را به همراه ندارد، با موفقیت اجرا کنیم.
به علاوه چون یک container صرفا به معنای یک running process از یک image است، هر فایل اجرایی داخل آن image را نیز میتوان به صورت یک container اجرا کرد؛ مانند cmd.exe داخل image مرتبط با NET Core. که آنرا بررسی کردیم.
کارآیی Docker Containers نسبت به ماشینهای مجازی بسیار بیشتر است
مزیت دیگر یک چنین توزیعی این است که اگر چندین container در حال اجرا را داشته باشیم:
در نهایت تمام آنها فقط با یک لایهی کرنل کار میکنند و آن هم کرنل اصلی سیستم جاری است. به همین جهت کارآیی docker containers نسبت به ماشینهای مجازی بیشتر است؛ چون هر ماشین مجازی، کرنل مجازی خاص خودش را نسبت به یک ماشین مجازی در حال اجرای دیگر دارد. در اینجا برای ایجاد یک لایه ایزولهی اجرای برنامهها، تنها کافی است یک container جدید را اجرا کنیم و در این حالت وارد فاز بوت شدن یک سیستم عامل کامل، مانند ماشینهای مجازی نمیشویم.
شاید مطابق تصویر فوق اینطور به نظر برسد که هرچند تمام این containers از یک کرنل استفاده میکنند، اما اگر قرار باشد هر کدام OS Apps & Libs خاص خودشان را در حافظه بارگذاری کنند، با کمبود شدید منابع روبرو شویم. دقیقا مانند حالتیکه چند ماشین مجازی را اجرا کردهایم و دیگر سیستم اصلی قادر به پاسخگویی به درخواستهای رسیده به علت کمبود منابع نیست. اما در واقعیت، یک image داکر، از لایههای مختلفی تشکیل میشود که فقط خواندنی هستند و غیرقابل تغییر و زمانیکه docker یک لایهی فقط خواندنی را در حافظه بارگذاری کرد، اگر container دیگری، از همان لایهی تعریف شده، در image خود نیز استفاده میکند، لایهی بارگذاری شدهی فقط خواندنی در حال اجرای موجود را با آن به اشتراک میگذارد (مانند تصویر زیر). به این ترتیب میزان مصرف منابع docker containers نسبت به ماشینهای مجازی بسیار کمتر است:
روش کنترل پروسهای که درون یک کانتینر اجرا میشود
با اجرای دستور docker run -it microsoft/dotnet:nanoserver ابتدا به command prompt داخلی و مخصوص این container منتقل میشویم و سپس میتوان برای مثال با NET Core CLI. کار کرد. اما امکان اجرای این CLI به صورت زیر نیز وجود دارد:
docker run -it microsoft/dotnet:nanoserver dotnet --info
بدیهی است در این حالت تمام فایلهای اجرایی داخل این container را نیز میتوان اجرا کرد. برای مثال میتوان کنسول پاورشل داخل این container را اجرا کرد:
docker run -it microsoft/dotnet:nanoserver powershell
هر کانتینر دارای یک File System ایزولهی خاص خود است
تا اینجا دریافتیم که هر image، به همراه فایلهای user space مورد نیاز خود نیز میباشد. به عبارتی هر image یک file system را نیز ارائه میدهد که تنها درون همان container قابل دسترسی میباشد و از مابقی سیستم جاری ایزوله شدهاست.
برای آزمایش آن، کنسول پاورشل را در سیستم میزبان (سیستم عامل اصلی که docker را اجرا میکند)، باز کرده و دستور \:ls c را صادر کنید. به این ترتیب میتوانید لیست پوشهها و فایلهای موجود در درایو C میزبان را مشاهده نمائید. سپس دستور docker run -it microsoft/dotnet:nanoserver powershell را اجرا کنید تا به powershell داخل کانتینر NET Core. دسترسی پیدا کنیم. اکنون دستور \:ls c را مجددا اجرا کنید. خروجی آن کاملا متفاوت است نسبت به گزارشی که پیشتر بر روی سیستم میزبان تهیه کردیم؛ دقیقا مانند اینکه هارد درایو یک container متفاوت است با هارد درایو سیستم میزبان.
این تصویر زمانی تهیه شدهاست که دستور docker run یاد شده را صادر کردهایم و درون powershell آن قرار داریم. همانطور که مشاهده میکنید یک Disk جدید، به ازای این Container در حال اجرا، به سیستم میزبان اضافه شدهاست. این Disk زمانیکه در powershell داخل container، دستور exit را صادر کنیم، بلافاصله محو میشود. چون پروسهی container، به این ترتیب خاتمه یافتهاست.
اگر دستور docker run یاد شده را دو بار اجرا کنیم، دو Disk جدید ظاهر خواهند شد:
یک نکته: اگر بر روی این درایوهای مجازی کلیک راست کرده، گزینهی change drive letter or path را انتخاب نموده و یک drive letter را به آنها نسبت دهید، میتوانید محتویات داخل آنها را توسط Windows Explorer ویندوز میزبان نیز به صورت یک درایو جدید، مشاهده کنید.
خلاصهای از ایزوله سازیهای کانتینرها تا به اینجا
تا اینجا یک چنین ایزوله سازیهایی را بررسی کردیم:
- ایزوله سازی File System و وجود یک disk مجازی مجزا به ازای هر کانتینر در حال اجرا.
- پروسههای کانتینرها از پروسههای میزبان ایزوله هستند. برای مثال اگر دستور get-process را داخل یک container اجرا کنید، خروجی آن با خروجی اجرای این دستور بر روی سیستم میزبان یکی نیست. یعنی نمیتوان از داخل کانتینرها، به پروسههای میزبان دسترسی داشت و دخل و تصرفی را در آنها انجام داد که از لحاظ امنیتی بسیار مفید است. هر چند اگر به task manager ویندوز میزبان مراجعه کنید، میتوان پروسههای داخل یک container را توسط Job Object ID یکسان آنها تشخیص دهید (مثال آخر قسمت قبل)، اما یک container، قابلیت شمارش پروسههای خارج از مرز خود را ندارد.
- ایزوله سازی شبکه مانند کارت شبکهی مجازی کانتینر IIS که در قسمت قبل بررسی کردیم. برای آزمایش آن دستور ipconfig را در داخل container و سپس در سیستم میزبان اجرا کنید. نتیجهای را که مشاهده خواهید کرد، کاملا متفاوت است. یعنی network stack این دو کاملا از هم مجزا است. شبیه به اینکه به یک سیستم، چندین کارت شبکه را متصل کرده باشید. اینکار در اینجا با تعریف virtual network adaptors انجام میشود و لیست آنها را در قسمت «All Control Panel Items\Network Connections» سیستم میزبان میتوانید مشاهده کنید. یکی از مهمترین مزایای آن این است که اگر در یک container، وب سروری را بر روی پورت 80 آن اجرا کنید، مهم نیست که در سیستم میزبان، یک IIS در حال سرویس دهی بر روی پورت 80 هم اکنون موجود است. این دو پورت با هم تداخل نمیکنند.
- در حالت کار با Windows Containers، رجیستری کانتینر نیز از میزبان آن مجزا است و یا متغیرهای محیطی اینها یکی نیست. برای مثال دستور \:ls env را در کانتینر و سیستم میزبان اجرا کنید تا environment variables را گزارش گیری کنید. خروجی این دو کاملا متفاوت است. برای مثال حداقل computer name، user nameهای قابل مشاهدهی در این گزارشها، متفاوت است و یا دستور \:ls hkcu را در هر دو اجرا کنید تا خروجی رجیستری متعلق به کاربر جاری هر کدام را مشاهده کنید که در هر دو متفاوت است.
- در حالت کار با Linux Containers هر چیزی که ذیل عنوان namespace مطرح میشود مانند شبکه، PID، User، UTS، Mount و غیره شامل ایزوله سازی میشوند.
دو نوع Windows Containers وجود دارند
در ویندوز، Windows Server Containers و Hyper-V Containers وجود دارند. در این قسمت تمام کارهایی را که بر روی ویندوز سرور انجام دادیم، در حقیقت بر روی Windows Server Containers انجام شدند و تمام Containerهای ویندوزی را که در قسمت قبل بر روی ویندوز 10 ایجاد کردیم، از نوع Hyper-V Containers بودند.
تفاوت مهم اینها در مورد نحوهی پیاده سازی ایزوله سازی آنها است. در حالت Windows Server Containers، کار ایزوله سازی پروسهها توسط کرنل اشتراکی بین کانتینرها صورت میگیرد اما در Hyper-V Containers، این ایزوله سازی توسط hypervisor آن انجام میشود؛ هرچند نسبت به ماشینهای مجازی متداول بسیار سریعتر است، اما بحث به اشتراک گذاری کرنل هاست را که پیشتر در این قسمت بررسی کردیم، در این حالت شاهد نخواهیم بود. ویندوز سرور 2016 میتواند هر دوی این ایزوله سازیها را پشتیبانی کند، اما ویندوز 10، فقط نوع Hyper-V را پشتیبانی میکند.
روش اجرای Hyper-V Containers بر روی ویندوز سرور
در صورت نیاز برای کار با Hyper-V Containers، نیاز است مانند قسمت قبل، ابتدا Hyper-V را بر روی ویندوز سرور، فعالسازی کرد:
Install-WindowsFeature hyper-v Restart-Computer -Force
docker run -it --isolation=hyperv microsoft/dotnet:nanoserver powershell
EF Code First #6
- این رو به نظر در قسمتهای قبل ذکر کردم که فقط پیغامهای خطا رو نمیتونید اینجا ذکر کنید و گرنه حداکثر طول و فیلداجباری و غیره همان اثر را دارد.
روش افزودن میانافزار RateLimiter به برنامههای ASP.NET Core
شبیه به سایر میانافزارها، جهت فعالسازی میانافزار RateLimiter، ابتدا باید سرویسهای متناظر با آنرا به برنامه معرفی کرد و پس از فعالسازی میانافزار مسیریابی، آنرا به زنجیرهی مدیریت یک درخواست معرفی نمود. برای نمونه در مثال زیر، امکان دسترسی به تمام درخواستها، به 10 درخواست در دقیقه، محدود میشود که پارتیشن بندی آن (در مورد پارتیشن بندی در قسمت قبل بیشتر بحث شد)، بر اساس username کاربر اعتبارسنجی شده و یا hostname یک کاربر غیراعتبارسنجی شدهاست:
var builder = WebApplication.CreateBuilder(args); builder.Services.AddRateLimiter(options => { options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext => RateLimitPartition.GetFixedWindowLimiter( partitionKey: httpContext.User.Identity?.Name ?? httpContext.Request.Headers.Host.ToString(), factory: partition => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = 10, QueueLimit = 0, Window = TimeSpan.FromMinutes(1) })); }); // ... var app = builder.Build(); // ... app.UseRouting(); app.UseRateLimiter(); app.MapGet("/", () => "Hello World!"); app.Run();
- فراخوانی builder.Services.AddRateLimiter، سبب معرفی سرویسهای میانافزار rate limiter به سیستم تزریق وابستگیهای ASP.NET Core میشود.
- در اینجا میتوان برای مثال خاصیت options.GlobalLimiter تنظیمات آنرا نیز مقدار دهی کرد. GlobalLimiter، سبب تنظیم یک محدود کنندهی سراسری نرخ، برای تمام درخواستهای رسیدهی به برنامه میشود.
- GlobalLimiter را میتوان با هر نوع PartitionedRateLimiter مقدار دهی کرد که در اینجا از نوع FixedWindowLimiter انتخاب شدهاست تا بتوان «الگوریتمهای بازهی زمانی مشخص» را به برنامه اعمال نمود تا برای مثال فقط امکان پردازش 10 درخواست در هر دقیقه برای هر کاربر، وجود داشته باشد.
- در پایان کار، فراخوانی app.UseRateLimiter را نیز مشاهده میکنید که سبب فعالسازی میانافزار، بر اساس تنظیمات صورت گرفته میشود.
برای آزمایش برنامه، آنرا اجرا کرده و سپس به سرعت شروع به refresh کردن صفحهی اصلی آن کنید. پس از 10 بار ریفرش، پیام 503 Service Unavailable را مشاهده خواهید کرد که به معنای مسدود شدن دسترسی به برنامه توسط میانافزار rate limiter است.
بررسی تنظیمات رد درخواستها توسط میانافزار rate limiter
اگر پس از محدود شدن دسترسی به برنامه توسط میان افزار rate limiter از status code = 503 دریافتی راضی نیستید، میتوان آنرا هم تغییر داد:
builder.Services.AddRateLimiter(options => { options.RejectionStatusCode = 429; // ... });
علاوه بر آن در اینجا گزینهی OnRejected نیز پیش بینی شدهاست تا بتوان response ارائه شده را در حالت رد درخواست، سفارشی سازی کرد تا بتوان پیام بهتری را به کاربری که هم اکنون دسترسی او محدود شدهاست، ارائه داد:
builder.Services.AddRateLimiter(options => { options.OnRejected = async (context, token) => { context.HttpContext.Response.StatusCode = 429; if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter)) { await context.HttpContext.Response.WriteAsync( $"Too many requests. Please try again after {retryAfter.TotalMinutes} minute(s). " + $"Read more about our rate limits at https://example.org/docs/ratelimiting.", cancellationToken: token); } else { await context.HttpContext.Response.WriteAsync( "Too many requests. Please try again later. " + "Read more about our rate limits at https://example.org/docs/ratelimiting.", cancellationToken: token); } }; // ... });
یک نکته: باتوجه به اینکه در اینجا به HttpContext دسترسی داریم، یعنی به context.HttpContext.RequestServices نیز دسترسی خواهیم داشت که توسط آن میتوان برای مثال سرویس ILogger را از آن درخواست کرد و رخداد واقع شده را برای بررسی بیشتر لاگ نمود؛ برای مثال چه کاربری مشکل پیدا کردهاست؟
context.HttpContext.RequestServices.GetService<ILoggerFactory>()? .CreateLogger("Microsoft.AspNetCore.RateLimitingMiddleware") .LogWarning("OnRejected: {RequestPath}", context.HttpContext.Request.Path);
طراحی فعلی میانافزار rate limiter، کمی محدود است. برای مثال «retry after»، تنها metadata مفیدی است که جهت بازگشت ارائه میدهد و همچنین مانند GitHub مشخص نمیکند که در لحظهی جاری چند درخواست دیگر را میتوان ارسال کرد و امکان دسترسی به اطلاعات آماری درونی آن وجود ندارد. اگر نیاز به یک چنین اطلاعاتی دارید شاید استفاده از میانافزار ثالث دیگری به نام AspNetCoreRateLimit برای شما مفیدتر باشد!
الگوریتمهای پشتیبانی شدهی توسط میانافزار rate limiter
در قسمت قبل با چند الگوریتم استاندارد طراحی میانافزارهای rate limiter آشنا شدیم که میانافزار توکار rate limiter موجود در ASP.NET Core 7x، اکثر آنها را پشتیبانی میکند:
- Concurrency limit: سادهترین نوع محدود سازی نرخ درخواستها است و کاری به زمان ندارد و فقط برای آن، تعداد درخواستهای همزمان مهم است. برای مثال پیاده سازی «مجاز بودن تنها 10 درخواست همزمان».
- Fixed window limit: توسط آن میتوان محدودیتهایی مانند «مجاز بودن تنها 60 درخواست در دقیقه» را اعمال کرد که به معنای امکان ارسال یک درخواست در هر ثانیه در هر دقیقه و یا حتی ارسال یکجای 60 درخواست در یک ثانیه است.
- Sliding window limit: این محدودیت بسیار شبیه به حالت قبل است اما به همراه قطعاتی که کنترل بیشتری را بر روی محدودیتها میسر میکند؛ مانند مجاز بودن 60 درخواست در هر دقیقه که فقط در این حالت یک درخواست در هر ثانیه مجاز باشد.
- Token bucket limit: امکان کنترل نرخ سیلان را میسر کرده و همچنین از درخواستهای انفجاری نیز پشتیبانی میکند (این مفاهیم در قسمت قبل بررسی شدند).
علاوه بر اینها امکان ترکیب گزینههای فوق توسط متد کمکی PartitionedRateLimiter.CreateChained نیز میسر است:
builder.Services.AddRateLimiter(options => { options.GlobalLimiter = PartitionedRateLimiter.CreateChained( PartitionedRateLimiter.Create<HttpContext, string>(httpContext => RateLimitPartition.GetFixedWindowLimiter(httpContext.ResolveClientIpAddress(), partition => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = 600, Window = TimeSpan.FromMinutes(1) })), PartitionedRateLimiter.Create<HttpContext, string>(httpContext => RateLimitPartition.GetFixedWindowLimiter(httpContext.ResolveClientIpAddress(), partition => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = 6000, Window = TimeSpan.FromHours(1) }))); // ... });
در این مثال فرضی، متد الحاقی ResolveClientIpAddress اهمیتی ندارد. بهتر است برای برنامهی خود از کلید پارتیشن بندی بهتر و معقولتری استفاده کنید.
امکان در صف قرار دادن درخواستها بجای رد کردن آنها
در تنظیمات مثالهای فوق، در کنار PermitLimit، میتوان QueueLimit را نیز مشخص کرد. به این ترتیب با رسیدن به PermitLimit، به تعداد QueueLimit، درخواستها در صف قرار میگیرند، بجای اینکه کاملا رد شوند:
PartitionedRateLimiter.Create<HttpContext, string>(httpContext => RateLimitPartition.GetFixedWindowLimiter(httpContext.ResolveClientIpAddress(), partition => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = 10, QueueLimit = 6, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, Window = TimeSpan.FromSeconds(1) })));
این تنظیم، تجربهی کاربری بهتری را برای استفاده کنندگان از برنامهی شما به همراه خواهد داشت؛ بجای رد قاطع درخواستهای ارسالی توسط آنها.
یک نکته: بهتر است QueueLimitهای بزرگی را انتخاب نکنید؛ خصوصا برای بازههای زمانی طولانی. چون یک مصرف کننده نیاز دارد تا سریع، پاسخی را دریافت کند و اگر اینطور نباشد، دوباره سعی خواهد کرد. تنها چند ثانیهی کوتاه در صف بودن برای کاربران معنا دارد.
امکان ایجاد سیاستهای محدود سازی سفارشی
اگر الگوریتمهای توکار میانافزار rate limiter برای کار شما مناسب نیستند، میتوانید با پیاده سازی <IRateLimiterPolicy<TPartitionKey، یک نمونهی سفارشی را ایجاد کنید. پیاده سازی این اینترفیس، نیاز به دو متد را دارد:
الف) متد GetPartition که بر اساس HttpContext جاری، یک rate limiter مخصوص را باز میگرداند.
ب) متد OnRejected که امکان سفارشی سازی response رد درخواستها را میسر میکند.
در مثال زیر پیاده سازی یک rate limiter سفارشی را مشاهده میکنید که نحوهی پارتیشن بندی آن بر اساس user-name کاربر اعتبارسنجی شده و یا host-name کاربر وارد نشدهی به سیستم است. در اینجا کاربر وارد شدهی به سیستم، محدودیت بیشتری دارد:
public class ExampleRateLimiterPolicy : IRateLimiterPolicy<string> { public RateLimitPartition<string> GetPartition(HttpContext httpContext) { if (httpContext.User.Identity?.IsAuthenticated == true) { return RateLimitPartition.GetFixedWindowLimiter(httpContext.User.Identity.Name!, partition => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = 1_000, Window = TimeSpan.FromMinutes(1), }); } return RateLimitPartition.GetFixedWindowLimiter(httpContext.Request.Headers.Host.ToString(), partition => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = 100, Window = TimeSpan.FromMinutes(1), }); } public Func<OnRejectedContext, CancellationToken, ValueTask>? OnRejected { get; } = (context, _) => { context.HttpContext.Response.StatusCode = 418; // I'm a 🫖 return new ValueTask(); }; }
options.AddPolicy<string, ExampleRateLimiterPolicy>("myPolicy");
امکان تعریف سیاستهای محدود سازی نرخ دسترسی به گروهی از endpoints
تا اینجا روشهای سراسری محدود سازی دسترسی به منابع برنامه را بررسی کردیم؛ اما ممکن است در برنامهای بخواهیم محدودیتهای متفاوتی را به گروههای خاصی از endpoints اعمال کنیم و یا شاید اصلا نخواهیم تعدادی از آنها را محدود کنیم:
builder.Services.AddRateLimiter(options => { options.AddFixedWindowLimiter("Api", options => { options.AutoReplenishment = true; options.PermitLimit = 10; options.Window = TimeSpan.FromMinutes(1); }); options.AddFixedWindowLimiter("Web", options => { options.AutoReplenishment = true; options.PermitLimit = 10; options.Window = TimeSpan.FromMinutes(1); }); // ... });
البته باید درنظر داشت که متدهای الحاقی Add داری را که در اینجا ملاحظه میکنید، محدود سازی را بر اساس نام درنظر گرفته شده انجام میدهند. یعنی درحقیقت یک محدودسازی سراسری بر اساس گروهی از endpoints هستند و امکان تعریف پارتیشنی را به ازای یک کاربر یا آدرس IP خاص، ندارند. اگر نیاز به اعمال این نوع پارتیشن بندی را دارید، باید از متدهای AddPolicy استفاده کنید:
options.AddPolicy("Api", httpContext => RateLimitPartition.GetFixedWindowLimiter(httpContext.ResolveClientIpAddress(), partition => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = 10, Window = TimeSpan.FromSeconds(1) }));
محدود سازی نرخ دسترسی به منابع در ASP.NET Core Minimal API
پس از تعریف نامی برای سیاستهای دسترسی، اکنون میتوان از آنها به صورت زیر جهت محدود سازی یک endpoint و یا گروهی از آنها استفاده کرد:
// Endpoint app.MapGet("/api/hello", () => "Hello World!").RequireRateLimiting("Api"); // Group app.MapGroup("/api/orders").RequireRateLimiting("Api");
// Endpoint app.MapGet("/api/hello", () => "Hello World!").DisableRateLimiting(); // Group app.MapGroup("/api/orders").DisableRateLimiting();
محدود سازی نرخ دسترسی به منابع در ASP.NET Core MVC
میتوان سیاستهای نرخ دسترسی تعریف شده را بر اساس نام آنها به کنترلرها و یا اکشن متدها اعمال نمود:
[EnableRateLimiting("Api")] public class Orders : Controller { [DisableRateLimiting] public IActionResult Index() { return View(); } [EnableRateLimiting("ApiListing")] public IActionResult List() { return View(); } }
و یا حتی میتوان این سیاستهای محدود سازی نرخ دسترسی را به تمام کنترلرها و صفحات razor نیز به صورت زیر اعمال کرد:
app.UseConfiguredEndpoints(endpoints => { endpoints.MapRazorPages() .DisableRateLimiting(); endpoints.MapControllers() .RequireRateLimiting("UserBasedRateLimiting"); });