آشنایی با mocking frameworks - قسمت دوم
الان یک مشکل وجود دارد که تقلید باعث تغییر در اصل کد ما میشود مثل همان آرگومان متد اصلی
و دوم اینکه ما نمیتوانیم هر چیزی را در کدمان پیاده میکنیم را بخواهیم یک اینترفیس بسازیم در این صورت باید مدت زیادی وقت بزاریم تا مرتب هر چه در کد هایمان است را تبدیل به اینترفیس کنیم
EF Code First #12
نصب و راه اندازی SharePoint 2007
از برنامه OneNote زیاد استفاده میکنم. عموما برای یادداشت برداری از سایتها؛ فقط کافی است یک صفحه از مرورگر خودتون را با Ctrl+A انتخاب و با Ctrl+C در حافظه کپی کنید. سپس با Ctrl+V در OneNote ، کل صفحه با همان فرمت اصلی و تمام تصاویر، جداول و غیره ذخیره خواهد شد. همچنین در OneNote 2010، دریافت تصاویر از سایتها به صورت asynchronous است (برخلاف نگارش 2007 آن) و حین دریافت تصاویر برنامه متوقف نمیشود.
اما اگر سرویس Windows search که کار indexing را انجام میدهد خاموش باشد:
- احتمالا OneNote شما بالای 400 مگ رم مصرف خواهد کرد.
- مرتبا هنگ میکند.
- عموما CPU Usage ایی بالای 50 درصد به صورت مداوم خواهید داشت.
- جستجوی آن دیگر درست کار نمیکند و بسیار بسیار کند خواهد بود.
Command چیست ؟
اینترفیس ICommand:
نام عضو | توضیحات |
Bool CanExecute(object parameter) | این تابع پارامتری از نوع object را دریافت میکند و یک مقدار bool را به خروجی تابع میفرستد. اگر مقدار خروجی متد، true باشدcommand اجرا خواهد شد و در غیر اینصورت اتفاقی رخ نخواهد داد. اغلب ازDelegate ها به عنوان پارامتر این تابع استفاده میشود؛Delegate های پیش ساختهای همچون Action,Predicate,Func |
Event EventHandler CanExecuteChanged | این رویداد برای آگاه سازی UI که به Command متصل است، استفاده میشود .بر اساس خروجی تابع CanExecute، این رویداد اتفاق میافتد. |
Void Execute(Object parameter) | این متد کار اصلی را در Commandها انجام میدهد. این متد تنها زمانی اجرا میشود که متدCanExecute مقدار true را بازگرداند. این تابع پارامتری را از نوع object دریافت میکند، اما عموما ما یکDelegate را به آن ارسال میکنیم. Delegate ارجاعی را به متدی، در خود نگاه میدارد که انتظار داریم در صورت اجرایcommand ، اجرا شود. |
چرا اینترفیس ICommand :
ساخت UI مورد نیاز :
گام دوم:
<Grid DataContext="{Binding Source={StaticResource calculatorVM}}" Background="#FFCCCC"> <Grid.RowDefinitions> <RowDefinition Height="80"/> <RowDefinition/> <RowDefinition Height="80"/> <RowDefinition Height="44"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition/> </Grid.ColumnDefinitions> <Label Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="4" FontSize="25" VerticalAlignment="Top" HorizontalAlignment="Center" Foreground="Blue" Content="ICommand Sample"/> <Label Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Margin="10,0,0,0" VerticalAlignment="Bottom" FontSize="20" Content="First Input"/> <Label Grid.Row="0" Grid.Column="2" Grid.ColumnSpan="2" Margin="10,0,0,0" VerticalAlignment="Bottom" FontSize="20" Content="Second Input"/> <TextBox Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Margin="10,0,0,0" FontSize="20" HorizontalAlignment="Left" Height="30" Width="150" TextAlignment="Center" Text="{Binding FirstValue, Mode=TwoWay}"/> <TextBox Grid.Row="1" Grid.Column="2" Grid.ColumnSpan="2" Margin="10,0,0,0" FontSize="20" HorizontalAlignment="Left" Height="30" Width="150" TextAlignment="Center" Text="{Binding SecondValue, Mode=TwoWay}"/> <Rectangle Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="4" Fill="LightBlue"/> <Button Grid.Row="2" Grid.Column="0" Content="+" Margin="10,0,0,0" HorizontalAlignment="Left" Height="50" Width="50" FontSize="30" Command="{Binding AddCommand}"/> <Label Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2" FontSize="25" Margin="10,0,0,0" HorizontalAlignment="Left" Height="50" Content="Result : "/> <TextBlock Grid.Row="3" Grid.Column="2" Grid.ColumnSpan="2" FontSize="20" Margin="10,0,0,0" Background="BlanchedAlmond" TextAlignment="Center" HorizontalAlignment="Left" Height="36" Width="150" Text="{Binding Output}"/> </Grid>
xmlns:myview="clr-namespace:ICommnadSample.Views"
<Grid> <myview:CalculatorView/> </Grid>
گام سوم :
private double firstValue; private double secondValue; private double output; public double FirstValue { get { return firstValue; } set { firstValue = value; OnPropertyChanged("FirstValue"); } } public double SecondValue { get { return secondValue; } set { secondValue = value; OnPropertyChanged("SecondValue"); } } public double Output { get { return output; } set { output = value; OnPropertyChanged("Output"); } }
گام چهارم:
public class PlusCommand : ICommand { private CalculatorViewModel calculatorViewModel; public PlusCommand(CalculatorViewModel vm) { calculatorViewModel = vm; } public bool CanExecute(object parameter) { return true; } public void Execute(object parameter) { calculatorViewModel.Add(); } public event EventHandler CanExecuteChanged; }
گام پنجم:
private PlusCommand plusCommand; public CalculatorViewModel() { plusCommand = new PlusCommand(this); }
گام ششم:
xmlns:vm="clr-namespace:ICommnadSample.ViewModels"
<UserControl.Resources> <vm:CalculatorViewModel x:Key="calculatorVM" /> </UserControl.Resources>
گام هفتم:
public void Add() { Output = firstValue + secondValue; }
گام هشتم:
public ICommand AddCommand { get { return plusCommand; } }
نحوهی استفاده:
Command="{Binding AddCommand}"
public class ViewModelBase:INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
گام دهم:
public class CalculatorViewModel : ViewModelBase
OnPropertyChanged("FirstValue");
public class CalculatorViewModel : ViewModelBase { private double firstValue; private double secondValue; private double output; private PlusCommand plusCommand; public CalculatorViewModel() { plusCommand = new PlusCommand(this); } public double FirstValue { get { return firstValue; } set { firstValue = value; OnPropertyChanged("FirstValue"); } } public double SecondValue { get { return secondValue; } set { secondValue = value; OnPropertyChanged("SecondValue"); } } public double Output { get { return output; } set { output = value; OnPropertyChanged("Output"); } } public ICommand AddCommand { get { return plusCommand; } } internal void Add() { Output = firstValue + secondValue; } }
حال میتوانید برنامه را اجرا و تست کنید:
برای درک بهتر عملیات انقیاد دادها میتوانید به این مقاله مراجعه کنید.
Generic Host چیست؟
Generic Host یکی از ویژگیهای جدید ASP.NET Core 2.1 است. هدف آن جداسازی HTTP pipeline برنامه، از Web Host API آن است. یکی از مزایای اینکار، امکان استفادهی از آن نه فقط در پروژههای وب، بلکه در پروژههای کنسول نیز میباشد. به این ترتیب میتوان کارهای غیر HTTP را از برنامهی وب مجزا کرد تا به کارآیی بیشتری رسید و برای این منظور اینترفیس IHostedService را که در فضای نام Microsoft.Extensions.Hosting قرار دارد، برای ثبت کارهای پسزمینهی خارج از اعمال web host جاری، ارائه دادهاند:
namespace Microsoft.Extensions.Hosting { public interface IHostedService { Task StartAsync(CancellationToken cancellationToken); Task StopAsync(CancellationToken cancellationToken); } }
یک مثال: معرفی کار پسزمینهای که هر دو ثانیه یکبار انجام میشود
در SampleHostedService زیر، عبارت Hosted service executing به همراه زمان جاری، هر دو ثانیه یکبار لاگ میشود و اگر برنامه را توسط دستور dotnet run اجرا کنید، میتوانید خروجی آنرا در کنسول، مشاهده کنید:
using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace MvcTest { public class SampleHostedService : IHostedService { private readonly ILogger<SampleHostedService> _logger; public SampleHostedService(ILogger<SampleHostedService> logger) { _logger = logger; } public async Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation("Starting Hosted service"); while (!cancellationToken.IsCancellationRequested) { _logger.LogInformation("Hosted service executing - {0}", DateTime.Now); await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken); } } public Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation("Stopping Hosted service"); return Task.CompletedTask; } } }
namespace MvcTest { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddSingleton<IHostedService, SampleHostedService>();
services.AddHostedService<SampleHostedService>();
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
Startup.cs(82,56): error CS0104: 'IHostingEnvironment' is an ambiguous reference between 'Microsoft.AspNetCore.Hosting.IHostingEnvironment' and 'Microsoft.Extensions.Hosting.IHostingEnvironment'
مشکلات پیاده سازی کار پسزمینهی SampleHostedService فوق
هر چند اگر مثال فوق را اجرا کنید، خروجی مناسبی را دریافت خواهید کرد، اما دارای این اشکال مهم نیز هست:
D:\MvcTest>dotnet run info: MvcTest.SampleHostedService[0] Starting Hosted service info: MvcTest.SampleHostedService[0] Hosted service executing - 02/19/2019 14:45:10 info: MvcTest.SampleHostedService[0] Hosted service executing - 02/19/2019 14:45:12 info: MvcTest.SampleHostedService[0] Hosted service executing - 02/19/2019 14:45:14 Ctrl+C Application is shutting down... Hosting environment: Development Content root path: D:\MvcTest Now listening on: https://localhost:5001 Now listening on: http://localhost:5000 Application started. Press Ctrl+C to shut down.
از دیدگاه ASP.NET Core، یک کار پس زمینه زمانی خاتمه یافته محسوب میشود که متد StartAsync، مقدار Task.CompletedTask را بازگرداند؛ در غیراینصورت، در حال اجرا درنظر گرفته میشود و چون در پیاده سازی فوق این نکته رعایت نشدهاست، این Task همواره در حال اجرا و خاتمه نیافته محسوب میشود و نوبت به مابقی کارها نخواهد رسید. همچنین در قسمت StopAsync نیز بهتر است یک فیلد CancellationTokenSource تعریف شدهی در سطح کلاس را مورد استفاده قرار داد و متد Cancel آنرا فراخوانی کرد تا اطلاع رسانی صحیحی را به متد StartAsync در مورد خاتمهی برنامه، انجام دهد.
برای این منظور و جهت ساده سازی و پیاده سازی تمام این نکات، از اینترفیس خام IHostedService، یک کلاس abstract به نام BackgroundService نیز در فضای نام Microsoft.Extensions.Hosting پیش بینی شدهاست:
namespace Microsoft.Extensions.Hosting { public abstract class BackgroundService : IHostedService, IDisposable { protected BackgroundService(); public virtual void Dispose(); public virtual Task StartAsync(CancellationToken cancellationToken); public virtual Task StopAsync(CancellationToken cancellationToken); protected abstract Task ExecuteAsync(CancellationToken stoppingToken); } }
namespace MvcTest { public class PrinterHostedService : BackgroundService { private readonly ILogger<SampleHostedService> _logger; public PrinterHostedService(ILogger<SampleHostedService> logger) { _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("Starting Hosted service"); while (!stoppingToken.IsCancellationRequested) { _logger.LogInformation("Hosted service executing - {0}", DateTime.Now); await Task.Delay(TimeSpan.FromSeconds(2), stoppingToken); } } } }
namespace MvcTest { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddHostedService<PrinterHostedService>();
D:\MvcTest>dotnet run Hosting environment: Development infoContent root path: D:\MvcTest Now listening on: https://localhost:5001 Now listening on: http://localhost:5000 Application started. Press Ctrl+C to shut down. : MvcTest.SampleHostedService[0] Starting Hosted service info: MvcTest.SampleHostedService[0] Hosted service executing - 02/19/2019 15:00:23 info: MvcTest.SampleHostedService[0] Hosted service executing - 02/19/2019 15:00:25 info: MvcTest.SampleHostedService[0] Hosted service executing - 02/19/2019 15:00:27 Application is shutting down... ^C
یک نکته: تزریق وابستگی DbContext برنامه در یک سرویس کار پسزمینه
IHostedServiceها با طول عمر singleton به سیستم تزریق وابستگیها معرفی میشوند. در این حالت اگر سرویسهایی با طول عمر transient و یا scoped را به آنها تزریق کنید، دیگر طول عمر مدنظر شما را نداشته و آنها هم به صورت singleton عمل خواهند کرد. هر چند خود سیستم تزریق وابستگیهای NET Core. با صدور استثنائی، از این مساله جلوگیری میکند (در این مورد در مطالب «مهارتهای تزریق وابستگیها در برنامههای NET Core. - قسمت چهارم - پرهیز از الگوی Service Locator در برنامههای وب» و همچنین «قسمت سوم - رهاسازی منابع سرویسهای IDisposable» بیشتر بحث شدهاست). یک چنین مواردی را به صورت زیر با تزریق IServiceScopeFactory و ساخت صریح یک Scope میتوان مدیریت کرد:
using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; public abstract class ScopedBackgroundService : BackgroundService { private readonly IServiceScopeFactory _serviceScopeFactory; public ScopedBackgroundService(IServiceScopeFactory serviceScopeFactory) { _serviceScopeFactory = serviceScopeFactory; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { using (var scope = _serviceScopeFactory.CreateScope()) { await ExecuteInScope(scope.ServiceProvider, stoppingToken); } } public abstract Task ExecuteInScope(IServiceProvider serviceProvider, CancellationToken stoppingToken); }
طراحی سرویس کارهای پسزمینهی زمانبندی شده
ASP.NET Core، متد ExecuteAsync را یکبار بیشتر اجرا نمیکند. بنابراین پیاده سازی تایمری که بخواهد برای مثال ارسال ایمیلهای خبرنامهی سایت را هر روز ساعت 11 شب انجام دهد، به خود ما واگذار شدهاست. برای پیاده سازی بهتر این تایمر میتوان از کتابخانهی NCrontab که توسط نویسندهی کتابخانهی معروف ELMAH تهیه شدهاست، استفاده کرد که با برنامههای NET Core. نیز سازگاری دارد:
dotnet add package ncrontab
┌───────────── minute (0 - 59) │ ┌───────────── hour (0 - 23) │ │ ┌───────────── day of month (1 - 31) │ │ │ ┌───────────── month (1 - 12) │ │ │ │ ┌───────────── day of week (0 - 6) (Sunday to Saturday; │ │ │ │ │ 7 is also Sunday on some systems) │ │ │ │ │ │ │ │ │ │ * * * * *
* * * * * * - - - - - - | | | | | | | | | | | +--- day of week (0 - 6) (Sunday=0) | | | | +----- month (1 - 12) | | | +------- day of month (1 - 31) | | +--------- hour (0 - 23) | +----------- min (0 - 59) +------------- sec (0 - 59)
using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using NCrontab; using static NCrontab.CrontabSchedule; public abstract class ScheduledScopedBackgroundService : ScopedBackgroundService { private CrontabSchedule _schedule; private DateTime _nextRun; protected abstract string Schedule { get; } public ScheduledScopedBackgroundService(IServiceScopeFactory serviceScopeFactory) : base(serviceScopeFactory) { _schedule = CrontabSchedule.Parse(Schedule, new ParseOptions { IncludingSeconds = true }); _nextRun = _schedule.GetNextOccurrence(DateTime.Now); } public override async Task ExecuteInScope(IServiceProvider serviceProvider, CancellationToken stoppingToken) { do { var now = DateTime.Now; if (now > _nextRun) { await ScheduledExecuteInScope(serviceProvider, stoppingToken); _nextRun = _schedule.GetNextOccurrence(DateTime.Now); } await Task.Delay(1000, stoppingToken); //1 second delay } while (!stoppingToken.IsCancellationRequested); } public abstract Task ScheduledExecuteInScope(IServiceProvider serviceProvider, CancellationToken stoppingToken); }
روش استفادهی از آن برای تعریف یک وظیفهی جدید نیز به صورت زیر است:
using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; public class MyScheduledTask : ScheduledScopedBackgroundService { private readonly ILogger<MyScheduledTask> _logger; public MyScheduledTask( IServiceScopeFactory serviceScopeFactory, ILogger<MyScheduledTask> logger) : base(serviceScopeFactory) { _logger = logger; } protected override string Schedule => "*/10 * * * * *"; //Runs every 10 seconds public override Task ScheduledExecuteInScope(IServiceProvider serviceProvider, CancellationToken stoppingToken) { _logger.LogInformation("MyScheduledTask executing - {0}", DateTime.Now); return Task.CompletedTask; } }
روش معرفی آن به سیستم نیز مانند قبل است:
namespace MvcTest { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddHostedService<MyScheduledTask>();
D:\MvcTest>dotnet run Hosting environment: Development Content root path: D:\MvcTest Now listening on: https://localhost:5001 Now listening on: http://localhost:5000 Application started. Press Ctrl+C to shut down. info: MyScheduledTask[0] MyScheduledTask executing - 02/19/2019 19:18:50 info: MyScheduledTask[0] MyScheduledTask executing - 02/19/2019 19:19:00 info: MyScheduledTask[0] MyScheduledTask executing - 02/19/2019 19:19:10 Application is shutting down... ^C
OutputCaching باعث میشود خروجیِ یک اکشن متد در حافظه نگهداری شود. با اعمال این نوع کشینگ، ASP.NET در خواستهای بعدی به این اکشن را تنها با بازگرداندن همان مقدار قبلی ِ نگهداری شده در کش، پاسخ میدهد. در حقیقت با OutputCaching از تکرار چند باره کد درون یک اکشن در فراخوانیهای مختلف جلوگیری کردهایم. کش کردن باعث میشود که کارایی و سرعت سایت افزایش یابد؛ اما باید دقت کنیم که چه موقع و چرا از کَش کردن استفاده میکنیم و چه موقع باید از این کار امتناع کرد.
فواید کَش کردن
- انجام عملیات هزینه دار فقط یکبار صورت میگیرد. (هزینه از لحاظ فشار روی حافظه سرور و کاهش سرعت بالا آمدن سایت)
- بار روی سرور در زمانهای پیک کاهش مییابد.
- سرعت بالا آمدن سایت بیشتر میشود.
چه زمانی باید کَش کرد؟
- وقتی محتوای نمایشی برای همه کاربران یکسان است.
- وقتی محتوای نمایشی برای نمایش داده شدن، فشار زیادی روی سرور تحمیل میکند.
- وقتی محتوای نمایشی به شکل مکرر در طول روز باید نمایش داده شود.
- وقتی محتوای نمایشی به طور مکرر آپدیت نمیشود. (در مورد تعریف کیفیت "مکرر"، برنامه نویس بهترین تصمیم گیرنده است)
طرح مساله
فرض کنید صفحه اول سایت شما دارای بخشهای زیر است :
خلاصه اخبار بخش علمی، خلاصه اخبار بخش فرهنگی ، ده کامنت آخر، لیستی از کتگوریهای موجود در سایت.
روشهای مختلفی برای کوئری گرفتن وجود دارد، به عنوان مثال ما به کمک یک یا چند کوئری و توسط یک ViewModel جامع، میخواهیم اطلاعات را به سمت ویو ارسال کنیم. پس در اکشن متد Index ، حجم تقریبا کمی از اطلاعات را باید به کمک کوئری(کوئری های) تقریبا پیچیده ای دریافت کنیم و اینکار به ازای هر ریکوئست هزینه دارد و فشار به سرور وارد خواهد شد. از طرفی میدانیم صفحه اول ممکن است در طول یک یا چند روز تغییر نکند و همچنین شاید در طول یکساعت چند بار تغییر کند! به هر حال در جایی از سایت قرار داریم که کوئری (کوئری های) مورد نظر زیاد صدا زده میشوند ، در حقیقت صفحه اول احتمالا بیشترین فشار ترافیکی را در بین صفحات ما دارد، البته این فقط یک احتمال است و ما دقیقا از این موضوع اطلاع نداریم.
یکی از راههای انجام یک کش موفق و دانستن لزوم کش کردن، این است که دقیقا بدانیم ترافیک سایت روی چه صفحه ای بیشتر است. در واقع باید بدانیم در کدام صفحه "هزینهی اجرای عملیات موجود در کد" بیشترین است.
فشار ترافیکی(ریکوئستهای زیاد) و آپدیتهای روزانهی دیتابیس را، در دو کفه ترازو قرار دهید؛ چه کار باید کرد؟ این تصمیمی است که شما باید بگیرید. نگرانی خود را در زمینه آپدیتهای روزانه و ساعتی کمتر کنید؛ در ادامه راهی را معرفی میکنیم که آپدیتهای هر از گاهِ شما، در پاسخِ ریکوئستها دیده شوند. کمی کفهی کش کردن را سنگین کنید.
به هر حال، فعال کردن قابلیت کش کردن برای یک اکشن، بسیار ساده است، کافیست ویژگی ( attribute ) آن را بالای اکشن بنویسید :
[OutputCache(Duration = "60", Location = OutputCacheLocation.Server)] public ActionResult Index() { //کوئری یا کوئریهای لازم برای استفاده در صفحه اصلی و تبدیل آن به یک ویو مدل جامع }
[OutputCache(CacheProfile = "FirstPageIndex",Location=OutputCacheLocation.Server)] public ActionResult Index() { //کوئری یا کوئریهای لازم برای استفاده در صفحه اصلی و تبدیل آن به یک ویو مدل جامع }
دو روش فوق برای کش کردن خروجی Index از لحاظ عملکرد یکسان است، به شرطی که در حالت دوم در وب کانفیگ و در بخش system.web آن ، یک پروفایل ایجاد کنیم کنیم :
<caching> <outputCacheSettings> <outputCacheProfiles> <add name="FirstPageIndex" duration="60"/> </outputCacheProfiles> </outputCacheSettings> </caching>
در حالت دوم ما یک پروفایل برای کشینگ ساخته ایم و در ویژگی بالای اکشن متد، آن پروفایل را صدا زده ایم. از لحاظ منطقی در حالت دوم، چون امکان استفاده مکرر از یک پروفایل در جاهای مختلف فراهم شده، روش بهتری است. محل ذخیره کش نیز در هر دو حالت سرور تعریف شده است.
برای تست عملیات کشینگ، کافیست یک BreakPoint درون Index قرار دهید و برنامه را اجرا کنید. پس از اجرا، برنامه روی Break Point میایستد و اگر F5 را بزنیم، سایت بالا میآید. بار دیگر صفحه را رفرش کنیم، اگر این "بار دیگر" در کمتر از 60 ثانیه پس از رفرش قبلی اتفاق افتاده باشد برنامه روی Break Point متوقف نخواهد شد، چون خروجی اکشن، در کش بر روی سرور ذخیره شده است و این یعنی ما فشار کمتری به سرور تحمیل کرده ایم، صفحه با سرعت بالاتری در دسترس خواهد بود.
ما از تکرار اجرای کد جلوگیری کرده ایم و عدم اجرای کد بهترین نوع بهینه سازی برای یک سایت است. [اسکات الن، پلورال سایت]
چطور زمان مناسب برای کش کردن یک اکشن را انتخاب کنیم؟
- کشینگ با زمان کوتاه؛ فرض کنید زمان کش را روی 1 ثانیه تنظیم کرده اید. این یعنی اگر ریکوئست هایی به یک اکشن ارسال شود و همه در طول یک ثانیه اتفاق بیفتد، آن اکشن فقط برای بار اول اجرا میشود، و در بارهای بعد(در طول یک ثانیه) فقط محتوای ذخیره شده در آن یک اجرا، بدون اجرای جدید، نمایش داده میشود. پس سرور شما فقط به یک ریکوئست در ثانیه در طول روز جواب خواهد داد و ریکوئستهای تقریبا همزمان دیگر، در طول همان ثانیه، از نتایج آن ریکوئست (اگر موجود باشد) استفاده خواهند کرد
- کشینگ با زمان طولانی؛ ما در حقیقت با اینکار منابع سرور را حفاظت میکنیم، چون عملیاتِ هزینه دار(مثل کوئریهای حجیم) تنها یکبار در طول زمان کشینگ اجرا خواهند شد. مثلا اگر تنظیم زمان روی عدد 86400 تنظیم شود(یک روز کامل)، پس از اولین ریکوئست به اکشن مورد نظر، تا 24 ساعت بعد، این اکشن اجرا نخواهد شد و فقط خروجی آن نمایش داده خواهد شد. آیا دلیلی دارد که یک کوئری هزینه دار را که قرار نیست خروجی اش در طول روز تغییر کند به ازای هر ریکوئست یک بار اجرا کنیم؟
اگر اطلاعات موجود در دیتابیس را تغییر دهیم چه کار کنیم که کشینگ رفرش شود؟
فرض کنید در همان مثال ابتدای این مقاله، شما یک پست به دیتابیس اضافه کرده اید، اما چون مثلا duration مربوط به کشینگ را روی 86400 تعریف کرده اید تا 24 ساعت از زمان ریکوئست اولیه نگذرد، سایت آپدیت نخواهد شد و محتوا همان چیزهای قبلی باقی خواهند ماند. اما چاره چیست؟
کافیست در بخش ادمین، وقتی که یک پست ایجاد میکنید یا پستی را ویرایش میکند در اکشنهای مرتبط با Create یا Edit یا Delete چنین کدی را پس از فرمان ذخیره تغییرات در دیتابیس، بنویسید:
Response.RemoveOutputCacheItem(Url.Action("index", "home"));
واضح است که ما داریم کشینگ مرتبط با یک اکشن متد مشخص را پاک میکنیم. با اینکار در اولین ریکوئست پس از تغییرات اعمال شده در دیتابیس، ASP.NET MVC چون میبیند کَشی برای این اکشن وجود ندارد، متد را اجرا میکند و کوئریهای درونش را خواهد دید و اولین ریکوئست پیش از کَش شدن را انجام خواهد داد. با اینکار کشینگ ریست شده است و پس از این ریکوئست و استخراج اطلاعات جدید، زمان کشینگ صفر شده و آغاز میشود.
میتوانید یک دکمه در بخش ادمین سایت طراحی کنید که هر موقع دلتان خواست کلیه کشها را به روش فوق پاک کنید! تا اپلیکیشن منتظر ریکوئستهای جدید بماند و کشها دوباره ایجاد شوند.
جمع بندی
ویژگی OutputCatch دارای پارامترهای زیادیست و در این مقاله فقط به توضیح عملکرد این اتریبیوت اکتفا شده است. بطور کلی این مبحث ظاهر ساده ای دارد، ولی نحوه استفاده از کشینگ کاملا وابسته به هوش برنامه نویس است و پیچیدگیهای مرتبط با خود را دارد. در واقع خیلی مشکل است که بتوانید یک زمان مناسب برای کش کردن تعیین کنید. باید برنامه خود را در یک محیط شبیه سازی تحت بار قرار دهید و به کمک اندازه گیری و محاسبه به یک قضاوت درست از میزان زمان کش دست پیدا کنید. گاهی متوجه خواهید شد، از مقدار زیادی از حافظه سیستم برای کش کردن استفاده کرده اید و در حقیقت آنقدر ریکوئست ندارید که احتیاج به این هزینه کردن باشد.
یکی از روشهای موثر برای دستیابی به زمان بهینه برای کش کردن استفاده از CacheProfile درون وب کانفیگ است. وقتی از کشینگ استفاده میکنید، در همان ابتدا مقدار زمانی مشخص برای آن در نظر نگرفته اید(در حقیقت مقدار زمان مشخصی نمیدانید) پس مجبور به آزمون و خطا و تست و اندازه گیری هستید تا بدانید چه مقدار زمانی را برای چه پروفایلی قرار دهید. مثلا پروفایل هایی به شکل زیر تعریف کرده اید و نام آنها را به اکشنهای مختلف نسبت داده اید. به راحتی میتوانید از طریق دستکاری وب کانفیگ مقادیر آن را تغییر دهید تا به حالت بهینه برسید، بدون آنکه کد خود را دستکاری کنید.
<caching> <outputCacheSettings> <outputCacheProfiles> <add name="Long" duration="86400"/> <add name="Average" duration="43600"/> <add name="Short" duration="600"/> </outputCacheProfiles> </outputCacheSettings> </caching>
برای مطالعه جزئیات بیشتر در مورد OutputCaching مقالات زیر منابع مناسبی هستند.
public class HomeViewModel { public string Id { get; set; } public string Message { get; set; } public DateTime DateTime { get; set; } }
اکنون به پوشهی Views بروید و فایل Index.cshtml را به این صورت تغییر دهید:
@model AspNetCoreDependencyInjection.Models.HomeViewModel @{ ViewData["Title"] = "Home"; } <div> <div> <div> <p> <b>Id : </b><span>@Model.Id</span> <br /> <b>Date And Time : </b><span> @Model.DateTime </span> <br/> <b>Message : </b><span>@Model.Message</span> </p> </div> </div> </div>
using AspNetCoreDependencyInjection.Services; namespace AspNetCoreDependencyInjection.ServicesImplentaions { public class MessageServiceAA { public string Message() { return "A message from MessageServiceAA"; } } }
namespace AspNetCoreDependencyInjection.Helpers { public class GuidProvider { private readonly Guid _serviceGuid; public GuidProvider() { _serviceGuid = Guid.NewGuid(); } public Guid GetNewGuid() => Guid.NewGuid(); public string GetGuidAsFormatedString(string prefix = "") => getFormatedGuid(_serviceGuid, prefix); private string getFormatedGuid(Guid guid, string prefix = "") { var guidString = guid.GetHashCode().ToString("x"); if (string.IsNullOrEmpty(prefix) == false) guidString = new StringBuilder($"{prefix}-").Append(guidString).ToString(); return guidString; } } }
حالا درون کنترل HomeController، این تغییرات را انجام میدهیم:
private readonly ILogger<HomeController> _logger; private readonly MessageServiceAA _messageService; private readonly GuidProvider _ guidProvider; public HomeController(ILogger<HomeController> logger) { _logger = logger; _messageService = new MessageServiceAA(); _guidProvider = new GuidProvider(); } public IActionResult Index() { var model = new HomeViewModel() { Id = _ guidProvider.GetGuidAsFormatedString(), Message = _messageService.Message(), DateTime = DateTime.Now, }; return View(model); }
همانطور که میبینید، در کد بالا، کنترلر HomeController، به دو شیء از کلاسها و یا سرویسهای GuidProvider و MessageServiceAA به صورت مستقیم وابسته شدهاست و با هر تغییری در هر کدام از این سرویسها، باید دوباره کامپایل شود. علاوه بر این اگر بخواهیم پیاده سازیهای مختلفی را برای هر کدام از این موارد، ارائه دهیم، به مشکل بر میخوریم. خب بیاید تغییراتی را در کد بالا بدهیم تا مشکلات ذکر شده را حل کنیم.
برای این منظور پوشهای را به نام Services میسازیم و اینترفیسی را
به نام IMessageBrokerA ایجاد میکنیم و سپس کاری میکنیم که MessageServiceAA از این
اینترفیس ارث بری کند:
namespace AspNetCoreDependencyInjection.Services { public interface IMessageServiceA { string Message(); } }
و حالا میخواهیم با
استفاده از تزریق وابستگی، وابستگی کنترلر HomeController را از کلاس MessageBrokerAA لغو کرده و آن را به اینترفیس IMessageBrokerA (انتزاع) وابسته کنیم. در
اینجا ما از تکنیک تزریق درون سازنده یا Constructor Injection استفاده میکنیم.
تزریق درون سازنده
در این تکنیک، ما لیستی از وابستگیهای مورد نیاز را به عنوان پارامترهای ورودی سازندهی کلاس، تعریف میکنیم:private readonly ILogger<HomeController> _logger; private readonly IMessageServiceA _messageService; private readonly GuidProvider _guidHelper; public HomeController(ILogger<HomeController> logger , IMessageServiceA messageService) { _logger = logger; _messageService = messageService; _messageService = new MessageServiceAA(); _guidHelper = new GuidProvider(); }
- IServiceCollection : برای ثبت سرویسها
- IServiceProvider : برای واکشی سرویسها
در ASP.NET Core معمولترین مکان برای ثبت کردن سرویسها درون Container، به صورت پیش فرض درون کلاس Startup و درون متد ConfigureServices انجام میگیرد.
به صورت پیش فرض کلاس Startup دو متد دارد:
- ConfigureServices : برای پیکربندی و ثبت سرویسهای درونی DI Container استفاده میشود.
- Configure : برای تنظیمات pipeline میان افزارها ( Middlewares ) بکار میرود.
در اینجا پیاده سازی پیش فرض کلاس Startup را میبینیم که البته کدهای درون متد Configure را برای درگیر نکردن ذهن شما، مخفی کردهایم:
public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // کدها جهت خوانایی بیشتر مخفی شده اند } }
همانطورکه میبینید، متد ConfigureService پارامتر IServiceCollection را میگیرد که به وسیلهی WebHost در زمان اجرای برنامه، مقدار دهی میشود.
تعداد زیادی Extension method برای IServiceCollection وجود دارند که برای پشتیبانی از ثبت کردن سرویسهای مختلف در سناریوهای گوناگون به کار میروند. در اینجا ما از نسخهی 3.1 چارچوب ASP.NET Core استفاده میکنیم. برای همین هم برای ثبت سرویسهای پیش فرض فریمورک MVC از متد توسعهی services.AddControllersWithViews() استفاده میکنیم. متد توسعهی AddControllersWithViews() سرویسهایی را که معمولا در فریم ورک MVC استفاده میشوند، درون IServiceCollection ثبت میکند. در نسخههای قبلی چارچوب ASP.NET Core، مانند نسخههای 2.1 و 2.2 برای این کار از متد توسعهی AddMvc() استفاده میشد.
در Microsoft Dependency Injection Container ، معمولا ترتیب ثبت سرویسها مهم نیست.
خب، اولین سرویس اختصاصی برنامهی خودمان را با چرخهی حیات Transient و زیر سرویس پیشین، به شکل زیر ثبت میکنیم :
public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); services.AddTransient<IMessageServiceA, MessageServiceAA>(); }
public static IServiceCollection AddTransient<TService, TImplementation>(this IServiceCollection services)
در اینجا وقتی ما برای IMessageServiceA ، پیاده سازی MessageServiceA را ثبت میکنیم، از این به بعد DI Container، هر زمانیکه در لیست پارامترهای سازندهی یک کلاس، IMessageServiceA را مشاهده کند، بررسی میکند که چه کلاسی به عنوانی پیاده سازی این اینترفیس ثبت شدهاست، سپس از آن نمونه سازی میکند و درون سازندهی مورد نظر تزریق میکند. خب، حالا برنامه را دوباره اجرا کنید؛ میبینید که برنامه اجرا میشود.