protected override bool ShouldRender() { return shouldRender; }
اطلاعات بیشتر و حالتهای دیگر
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Post>() .HasOne(p => p.Blog) .WithMany(b => b.Posts) .OnDelete(DeleteBehavior.SetNull); }
در بعضی از شرایط پیش رفته، ممکن است نمونه سازی از یک Implementation Type، نیاز به دخالت مستقیم ما را داشته باشد. Implementation Factory کنترل بیشتری بر چگونگی استفادهی از Implementation Typeها را به ما ارائه میدهد. در هنگام ثبت سرویسی که Implementation Factory را در اختیار ما قرار میدهد، ما یک Delegate را برای فراخوانی سرویس استفاده میکنیم. این Delegate مسئول ساخت یک نمونه از Service Type است. برای مثال وقتی از الگوهای builder یا factory برای ساخت یک شیء استفاده میکنید، شاید نیاز باشد که Implementation Factory را به صورت دستی پیاده سازی کنید. اولین قدم این است که کدتان را در صورت امکان چنان refactor کنید تا DI Container بتواند آن را به صورت خودکار بسازد؛ ولی اینکار همیشه ممکن نیست. برای مثال بعضی از برنامه نویسان ترجیح میدهند یک Config را مستقیما از IOptionMonitor بگیرند و بعد در هر جائیکه خواستند، بجای تزریق IOptionMonitor به سرویس، مستقیما از همان سرویس ثبت شده استفاده کنند:
services.AddSingleton<ILiteDbConfig>(sp =>sp.GetRequiredService<IOptionsMonitor<LiteDbConfig>>().CurrentValue);
یک کاربرد بالقوهی دیگر برای Implementation Factory ، استفاده از Composite Pattern است. هر چند Microsoft DI Container به صورت پیش فرض از Composite Pattern پشتیبانی نمیکند، ولی ما میتوانیم آنرا پیاده سازی کنیم. فرض کنید که قبلا به ازای انجام کاری، به کاربر یک ایمیل را میفرستادیم؛ ولی حالا مالک محصول میآید و میگوید که علاوه بر ایمیل، باید پیامک هم بفرستید و ما یا این سرویس پیامک را از قبل داریم و یا باید آن را بسازیم که فرض میکنیم از قبل آن را داریم. برای این کار ما یک اینترفیس کلیتر به نام INotificationService میسازیم که دو سرویس IEmailNotificationService و ISmsNotificaitonService از آن ارث بری میکنند:
public interface INotificationService { void SendMessage(string msg, int userId); }
public class CompositeNotificationService : INotificationService { private readonly IEnumerable<INotificationService> _notificationServices; public CompositeNotificationService(IEnumerable<INotificationService> notificationServices) { _notificationServices = notificationServices; } public void SendMessage(string msg, int userId) { foreach (var notificationServicei in _notificationServices) { notificationServicei.SendMessage(msg, userId); } } }
services.AddScoped<IEmailNotificationService, EmailNotificationService>(); services.AddScoped<ISMSNotificationService, SMSNotificationService>(); services.AddSingleton<INotificationService>(sp => new CompositeNotificationService( new INotificationService[] { sp.GetRequiredService<IEmailNotificationService>() , sp.GetRequiredService<ISMSNotificationService>() } ));
وهله سازی سفارشی
در مثال بعدی نشان میدهیم که چطور میتوانیم از Implementation Factory برای برگرداندن پیادهسازی سرویسهایی که Service Provider امکان ساخت
خودکار آنها را ندارد، استفاده کنیم. فرض کنید یک کلاس Account داریم که از IAccount ارث بری میکند
و برای ساخت آن باید از متدهای IAccountBuilder که فرآیند ساخت را انجام میدهند، استفاده کنیم. بنابراین امکان ساخت مستقیم یک
شیء از IAccount وجود ندارد. در این حالت بدین صورت عمل میکنیم:
services.AddTransient<IAccountBuilder, AccountBuilder>(); services.AddScoped<IAccount>(sp => { var builder = sp.GetService<IAccountBuilder>(); builder.WithAccountType(AccountType.Guest); return builder.Build(); });
ثبت یک نمونه برای چندین اینترفیس
ممکن است بنا به دلایلی مجبور باشیم یک implementation Type را برای چند سرویس (اینترفیس) به ثبت برسانیم. در این حالت
نمونهی شیء ساخته شده، توسط هر کدام از اینترفیسها قابل استفاده است. فرض کنید یک سرویس Greeting
داریم که پیش از این فقط اینترفیس IHomeGreetingService را پیاده سازی میکرد؛ ولی بنا به دلایلی تصمیم گرفتیم که سرویسی
جامعتر را با نیازمندیهای دیگری نیز تعریف کنیم و GreetingService آن را پیاده سازی کند:
public class GreetingService : IHomeGreetingService , IGreetingService
{ // code here }
احتمالا اولین چیزی که به ذهنمان میرسد این است:
services.TryAddSingleton<IHomeGreetingService, GreetingService>(); services.TryAddSingleton<IGreetingService, GreetingService>();
مشکل روش بالا این است که دو نمونه از GreetingService ساخته میشود و درون حافظه باقی میماند و در حقیقت برای هر اینترفیس، یک نوع جداگانه از GreetingService ثبت میشود؛ در حالیکه ما میخواهیم هر دو اینترفیس فقط از یک نمونه از شیء GreetingService استفاده کنند. دو راه حل برای این مشکل وجود دارد:
var greetingService = new GreetingService(Environment); services.TryAddSingleton<IHomeGreetingService>(greetingService); services.TryAddSingleton<IGreetingService>(greetingService);
در اینجا سازندهی کلاس GreetingService فقط به environment نیاز داشت که یکی از سرویسهای پایهی فریم ورک هست و در این مرحله در دسترس است. به صورت کلی مشکل روش بالا این است که ما مسئول نمونه سازی از سرویس GreetingService هستیم! اگر GreetingService برای ساخته شدن به سرویسها یا ورودی هایی نیاز داشته باشد که فقط در زمان اجرا در دسترس باشند، ما امکان نمونه سازی آنها را نداریم؛ علاوه بر این نمیتوان از روشهای بالای برای حالتهای Scoped یا Transient استفاده کرد.
روش بعدی همان روش استفاده
از Implementation Factory است که در ادامه آن را میبینید:
services.TryAddSingleton<GreetingService>(); services.TryAddSingleton<IHomeGreetingService>(sp => sp.GetRequiredService<GreetingService>()); services.TryAddSingleton<IGreetingService>(sp => sp.GetRequiredService<GreetingService>());
در این روش خود DI Container مسئول نمونه سازی از GreetingService است. علاوه بر این میتوان با استفاده از روش فوق از طول حیاتهای Scoped و Transient هم استفاده کرد؛ در حالیکه در روش قبلی این کار امکان پذیر نبود.
Open Generics Service
گاهی از اوقات میخواهید
سرویسهایی را ثبت کنید که از اینترفیسی جنریک ارث بری میکنند. هر نوع جنریک در
زمان اجرا، نوع مخصوص به خود را واکشی میکند. ثبت کردن دستی این سرویسها میتواند خسته کننده باشد. برای همین مایکروسافت در DI Container خود قابلیت ثبت و واکشی سرویسهای جنریک را نیز در
اختیار ما گذاشتهاست. بیایید نگاهی به سرویس ILogger<T> بیندازیم. این یک سرویس درونی فریمورک است و میتواند به ازای هر نوع، کارهای
مربوط به ثبت log را
انجام بدهد و در پروژهها معمولا از این اینترفیس برای ثبت لاگها در سطح
کنترلر و سرویسها استفاده میشود:
public interface ILogger<out TCategoryName> : ILogger { }
در حالت عادی اگر سرویسی
مشابه سرویس فوق را داشته باشیم، برای ثبت کردن هر سرویس با نوع جنریک اختصاصی آن،
مجبوریم به صورت دستی آن را درون DI Container ثبت کنیم؛ مثلا باید به این صورت عمل کنیم:
services.TryAddScoped<ILogger<HomeController>,Logger<HomeController>>();
services.TryAddScoped(typeof(ILogger<>) , typeof(Logger<>));
دسته بندی سرویسها درون متدهای مختلف و پاکسازی متد ConfigurationService
تا اینجای کار ما سرویسهای مختلفی را به روشهای مختلفی ثبت کردهایم. حتی در همین آموزش ساده، تعداد زیاد سرویسهای ثبت شده، باعث شلوغی و در هم ریختگی کدهای ما میشوند که خوانایی و در ادامه اشکال زدایی و توسعهی کدها را برای ما سختتر میکنند. سادهترین کار برای دسته بندی کدها، استفاده از متدهای private محلی یا استفاده از متدهای توسعهای(الحاقی) است که در اینجا مثالی از استفاده از متدهای توسعهای را آوردهام:namespace AspNetCoreDependencyInjection.Extensions { public static class DICRegisterationExetnsion { /// <summary> /// مثال ثبت برای اپن جنریت /// </summary> /// <param name="services"></param> public static void OpenGenericRegisterationExample(this IServiceCollection services) { services.TryAddScoped<ILogger<HomeController>, Logger<HomeController>>(); services.TryAddScoped(typeof(ILogger<>), typeof(Logger<>)); } /// <summary> /// ثبت تنظیمات به روشهای مختلف /// </summary> public static void RegisterConfiguration(this IServiceCollection services, IConfiguration configuration) { services.AddSingleton(services => new AppConfig { ApplicationName = configuration["ApplicationName"], GreetingMessage = configuration["GreetingMessage"], AllowedHosts = configuration["AllowedHosts"] }); services.AddSingleton(services => new AccountTypeBalanceConfig( new List<(AccountType, long)> { (AccountType.Guest , Convert.ToInt64 (configuration["AccountInitialBalance.Guest"]) ) , (AccountType.Silver , Convert.ToInt64 (configuration["AccountInitialBalance.Silver"]) ) , (AccountType.Gold , Convert.ToInt64 (configuration["AccountInitialBalance.Gold"]) ) , (AccountType.Platinum , Convert.ToInt64 (configuration["AccountInitialBalance.Platinum"]) ) , (AccountType.Titanium , Convert.ToInt64 (configuration["AccountInitialBalance.Titanium"]) ) , }) ); services.AddSingleton(services => new LiteDbConfig { ConnectionString = configuration["LiteDbConfig:ConnectionString"], }); services.Configure<UserOptionConfig>(configuration.GetSection("UserOptionConfig")); } } }
حالا در کلاس ConfigureServices ، درون متدStartup ، به این صورت از این متدهای توسعهای استفاده میکنیم:
services.RegisterConfiguration(this.Configuration); services.OpenGenericRegisterationExample();
میتوانید کد منبع این آموزش را در اینجا ببینید.
ابتدا مثال کامل ذیل را در نظر بگیرید:
using System; using System.Collections.Generic; using System.Data.Entity; using System.Data.Entity.Migrations; using System.Linq; using System.Linq.Expressions; namespace Sample { public abstract class BaseEntity { public int Id { set; get; } } public class Receipt : BaseEntity { public int TotalPrice { set; get; } } public class MyContext : DbContext { public DbSet<Receipt> Receipts { get; set; } } public class Configuration : DbMigrationsConfiguration<MyContext> { public Configuration() { AutomaticMigrationsEnabled = true; AutomaticMigrationDataLossAllowed = true; } protected override void Seed(MyContext context) { if (!context.Receipts.Any()) { for (int i = 0; i < 20; i++) { context.Receipts.Add(new Receipt { TotalPrice = i }); } } base.Seed(context); } } public static class EFUtils { public static IList<T> LoadEntities<T>(this DbContext ctx, Expression<Func<T, bool>> predicate) where T : class { return ctx.Set<T>().Where(predicate).ToList(); } public static IList<T> LoadData<T>(this DbContext ctx, Func<T, bool> predicate) where T : class { return ctx.Set<T>().Where(predicate).ToList(); } } public static class Test { public static void RunTests() { startDB(); using (var context = new MyContext()) { var list1 = context.LoadEntities<Receipt>(x => x.TotalPrice == 10); var list2 = context.LoadData<Receipt>(x => x.TotalPrice == 20); } } private static void startDB() { Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, Configuration>()); // Forces initialization of database on model changes. using (var context = new MyContext()) { context.Database.Initialize(force: true); } } } }
نکته اصلی مورد بحث، کلاس کمکی EFUtils است که در آن دو متد الحاقی LoadEntities و LoadData تعریف شدهاند. در متد LoadEntities، امضای متد شامل Expression Func است و در متد LoadData فقط Func ذکر شده است.
در ادامه اگر برنامه را توسط فراخوانی متد RunTests اجرا کنیم، به نظر شما خروجی SQL حاصل از list1 و list2 چیست؟
احتمالا شاید عنوان کنید که هر دو یک خروجی SQL دارند (با توجه به اینکه بدنه متدهای LoadEntities و LoadData دقیقا/یا به نظر یکی هستند) اما یکی از پارامتر 10 استفاده میکند و دیگری از پارامتر 20. تفاوت دیگری ندارند.
اما ... اینطور نیست!
خروجی SQL متد LoadEntities در متد RunTests به صورت زیر است:
SELECT [Extent1].[Id] AS [Id], [Extent1].[TotalPrice] AS [TotalPrice] FROM [dbo].[Receipts] AS [Extent1] WHERE 10 = [Extent1].[TotalPrice]
SELECT [Extent1].[Id] AS [Id], [Extent1].[TotalPrice] AS [TotalPrice] FROM [dbo].[Receipts] AS [Extent1]
چرا؟
Func اشارهگری است به یک متد و Expression Func بیانگر ساختار درختی عبارت lambda نوشته شده است. این ساختار درختی صرفا بیان میکند که عبارت lambda منتسب، چه کاری را قرار است یا میتواند انجام دهد؛ بجای انجام واقعی آن.
public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate); public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate);
بنابراین هرچند بدنه دو متد LoadEntities و LoadData به ظاهر یکی هستند، اما بر اساس نوع ورودی Where ایی که دریافت میکنند، اگر Expression Func باشد، EF فرصت آنالیز و ترجمه عبارت ورودی را خواهد یافت اما اگر Func باشد، ابتدا باید کل اطلاعات را به صورت یک لیست IEnumerable دریافت و سپس سمت کلاینت، خروجی نهایی را فیلتر کند.
اگر برنامه را اجرا کنید نهایتا هر دو لیست یک و دو، بر اساس شرط عنوان شده عمل خواهند کرد و فیلتر خواهند شد. اما در حالت اول این فیلتر شدن سمت بانک اطلاعاتی است و در حالت دوم کل اطلاعات بارگذاری شده و سپس سمت کاربر فیلتر میشود (که کارآیی پایینی دارد).
نتیجه گیری
به امضای متد Where ایی که در حال استفاده است دقت کنید. همینطور در مورد Sum ، Count و یا موارد مشابه دیگری که predicate قبول میکنند.
ارتباطات بلادرنگ و SignalR
لینک مقاله
من دقیقا مثال بالا را پیاده سازی کردم اما جواب نداد
قبلا شنیده بودم که از کلاس Sqldepedency تنها میتوان در برنامههای ویندوزی استفاده کرد و نه وب !
و در برنامههای مبتنی بر وب تنها از SqlCachedepedency
آیا هنوز هم اینگونه هست ؟
بررسی روش آپلود فایلها در ASP.NET Core
private async Task UploadFiles(InputFileChangeEventArgs e) { foreach (var file in e.GetMultipleFiles()) { var fileData = new FileToBeSaveVM(); var buffers = new byte[file.Size]; await file.OpenReadStream().ReadAsync(buffers); fileData.FileName = file.Name; fileData.FileSize = file.Size; fileData.FileType = file.ContentType; fileData.Extension = Path.GetExtension(file.Name); fileData.ImageBytes = buffers; lstFileToBeSaves.fileToBeSaves.Add(fileData); } } public class FileToBeSaveVM { public byte[] ImageBytes { get; set; } public string FileName { get; set; } public string FileType { get; set; } public string Extension { get; set; } public long FileSize { get; set; } }
public async Task UploadFileAsync(List<FileToBeSaveVM> files, string uploadFolder) { var folderDirectory=createUploadDir(uploadFolder); foreach (var file in files) { string fileExtenstion = Path.GetExtension(file.FileName); string fileuniqName =$"{Guid.NewGuid()}{fileExtenstion}"; string fileName = Path.Combine(folderDirectory, fileuniqName); string url=$"{uploadFolder}/{fileuniqName}"; using (var fileStream = File.Create(fileName)) { await fileStream.WriteAsync(file.ImageBytes); } }
مکانیزم Eventing
PM> Install-Package DNTFrameworkCore
public class TaskEditingBusinessEventHandler : BusinessEventHandler<EditingBusinessEvent<TaskModel, int>> { private readonly ILogger<TaskEditingBusinessEventHandler> _logger; public TaskEditingBusinessEventHandler(ILogger<TaskEditingBusinessEventHandler> logger) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public override Task<Result> Handle(EditingBusinessEvent<TaskModel, int> @event) { foreach (var model in @event.Models) { _logger.LogInformation($"Title changed from: {model.OriginalValue.Title} to: {model.NewValue.Title}"); } return Task.FromResult(Ok()); } }
کار با پیادهسازی واسط جنریک IBusinessEventHandler یا ارثبری از کلاس جنریک BusinessEventHandler آغاز میشود؛ سپس نیاز است Type Parameter متناظر را نیز مشخص کنیم. برای این منظور در تکه کد بالا از رخداد جنریک EditingBusinessEvent استفاده شده است. همچنین همانطور که ملاحظه میکنید، نیاز است نوع Model مورد نظر نیز مشخص شده باشد؛ در اینجا از TaskModel به عنوان Model/DTO عملیات CUD موجودیت Task استفاده شده است.
رخدادهای Creating/Created/Deleting/Deleted دارای خصوصیتی بنام Models هستند که نوع آن IEnumerable<TModel> میباشد. ولی این خصوصیت در رخدادهای Editing/Edited از نوع IEnumerable<ModifiedModel<TModel>> میباشد؛ در این صورت به مقادیر موجود در بانک اطلاعاتی و همچنین مقادیری که توسط استفاده کننده از سرویس جاری به عنوان آرگومان به متد ویرایش ارسال شده است، دسترسی خواهیم داشت.
public class ModifiedModel<TValue> { public TValue NewValue { get; set; } public TValue OriginalValue { get; set; } }
استفاده از سرویسهای موجودیتها
OOP : Everything is an object CRUD-based thinking : Everything is CRUD
public class ItemCategoryCreatedBusinessEventHandler : IBusinessEventHandler<CreatedBusinessEvent<ItemCategoryModel, int>> { private readonly ISaleMethodService _saleMethodService; public TaskEditingBusinessEventHandler(ISaleMethodService saleMethodService) { _saleMethodService = saleMethodService ?? throw new ArgumentNullException(nameof(saleMethodService)); } public override Task<Result> Handle(CreatedBusinessEvent<ItemCategoryModel, int> @event) { var methods = _saleMethodService.FindAsnc(); foreach (var method in methods) { foreach (var model in @event.Models) { method.ItemCategories.Add(new SaleMethodItemCategoryModel { ItemCategoryId = model.Id, TrackingState = TrackingState.Added; }); } } return _saleMethodService.EditAsync(methods); }
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; } }
حال میتوانید برنامه را اجرا و تست کنید:
برای درک بهتر عملیات انقیاد دادها میتوانید به این مقاله مراجعه کنید.
API Versioning
- URI-based versioning
- Header-based versioning
- Media type-based versioning
پیاده سازی URI-based versioning
public class ItemViewModel { public int Id { get; set; } public string Name { get; set; } public string Country { get; set; } } public class ItemV2ViewModel : ItemViewModel { public double Price { get; set; } }
public class ItemsController : ApiController { [ResponseType(typeof(ItemViewModel))] public IHttpActionResult Get(int id) { var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country = "Japan" }; return Ok(viewModel); } } public class ItemsV2Controller : ApiController { [ResponseType(typeof(ItemV2ViewModel))] public IHttpActionResult Get(int id) { var viewModel = new ItemV2ViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 }; return Ok(viewModel); } }
config.Routes.MapHttpRoute("ItemsV2", "api/v2/items/{id}", new { controller = "ItemsV2", id = RouteParameter.Optional }); config.Routes.MapHttpRoute("Items", "api/items/{id}", new { controller = "Items", id = RouteParameter.Optional }); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } );
config.Routes.MapHttpRoute( "defaultVersioned", "api/v{version}/{controller}/{id}", new { id = RouteParameter.Optional }, new { version = @"\d+" }); config.Routes.MapHttpRoute( "DefaultApi", "api/{controller}/{id}", new { id = RouteParameter.Optional } );
با این تنظیمات فعلا به مسیریابی ورژن بندی شدهای دست نیافتهایم. زیرا فعلا به هیچ طریق به Web API اشاره نکردهایم که به چه صورت از این پارامتر version برای پیدا کردن کنترلر ورژن بندی شده استفاده کند و به همین دلیل این دو مسیریابی نوشته شده در عمل نتیجه یکسانی را خواهند داشت. برای رفع مشکل مطرح شده باید فرآیند پیش فرض انتخاب کنترلر را کمی شخصی سازی کنیم.
IHttpControllerSelector مسئول پیدا کردن کنترلر مربوطه با توجه به درخواست رسیده میباشد. شکل زیر مربوط است به مراحل ساخت کنترلر بر اساس درخواست رسیده:
به جای پیاده سازی مستقیم این اینترفیس، از پیاده سازی کننده پیش فرض موجود (DefaultHttpControllerSelector) اسفتاده کرده و HttpControllerSelector جدید ما از آن ارث بری خواهد کرد.
public class VersionFinder { private static bool NeedsUriVersioning(HttpRequestMessage request, out string version) { var routeData = request.GetRouteData(); if (routeData != null) { object versionFromRoute; if (routeData.Values.TryGetValue(nameof(version), out versionFromRoute)) { version = versionFromRoute as string; if (!string.IsNullOrWhiteSpace(version)) { return true; } } } version = null; return false; } private static int VersionToInt(string versionString) { int version; if (string.IsNullOrEmpty(versionString) || !int.TryParse(versionString, out version)) return 0; return version; } public static int GetVersionFromRequest(HttpRequestMessage request) { string version; return NeedsUriVersioning(request, out version) ? VersionToInt(version) : 0; } }
کلاس VersionFinder برای یافتن ورژن رسیده در درخواست جاری مورد استفاده قرار خواهد گرفت. با استفاده از متد NeedsUriVersioning بررسی صورت میگیرد که آیا در این درخواست پارامتری به نام version وجود دارد یا خیر که درصورت موجود بودن، مقدار آن واکشی شده و درون پارامتر out قرار میگیرد. در متد GetVersionFromRequest بررسی میشود که اگر خروجی متد NeedsUriVersioning برابر با true باشد، با استفاده از متد VersionToInt مقدار به دست آمده را به int تبدیل کند.
public class VersionAwareControllerSelector : DefaultHttpControllerSelector { public VersionAwareControllerSelector(HttpConfiguration configuration) : base(configuration) { } public override string GetControllerName(HttpRequestMessage request) { var controllerName = base.GetControllerName(request); var version = VersionFinder.GetVersionFromRequest(request); return version > 0 ? GetVersionedControllerName(request, controllerName, version) : controllerName; } private string GetVersionedControllerName(HttpRequestMessage request, string baseControllerName, int version) { var versionControllerName = $"{baseControllerName}v{version}"; HttpControllerDescriptor descriptor; if (GetControllerMapping().TryGetValue(versionControllerName, out descriptor)) { return versionControllerName; } throw new HttpResponseException(request.CreateErrorResponse( HttpStatusCode.NotFound, $"No HTTP resource was found that matches the URI {request.RequestUri} and version number {version}")); } }
متد GetControllerName وظیفه بازگشت دادن نام کنترلر را برعهده دارد. ما نیز با لغو رفتار پیش فرض این متد و تهیه نام ورژن بندی شده کنترلر و معرفی این پیاده سازی از IHttpControllerSelector به شکل زیر میتوانیم به Web API بگوییم که به چه صورت از پارامتر version موجود در درخواست استفاده کند.
config.Services.Replace(typeof(IHttpControllerSelector), new VersionAwareControllerSelector(config));
حال با اجرای برنامه به نتیجه زیر خواهیم رسید:
راه حل دوم: برای زمانیکه Attribute Routing مورد استفاده شما است میتوان به راحتی با تعریف قالبهای مناسب مسیریابی، API ورژن بندی شده را ارائه دهید.
[RoutePrefix("api/v1/Items")] public class ItemsController : ApiController { [ResponseType(typeof(ItemViewModel))] [Route("{id:int}")] public IHttpActionResult Get(int id) { var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country = "Japan" }; return Ok(viewModel); } } [RoutePrefix("api/V2/Items")] public class ItemsV2Controller : ApiController { [ResponseType(typeof(ItemV2ViewModel))] [Route("{id:int}")] public IHttpActionResult Get(int id) { var viewModel = new ItemV2ViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 }; return Ok(viewModel); } }
اگر توجه کرده باشید در مثال ما، نامهای کنترلرها متفاوت از هم میباشند. اگر بجای در نظر گرفتن نامهای مختلف برای یک کنترلر در ورژنهای مختلف، آن را با یک نام یکسان درون namespaceهای مختلف احاطه کنیم یا حتی آنها را درون Class Libraryهای جدا نگهداری کنیم، به مشکل "یافت شدن چندین کنترلر که با درخواست جاری مطابقت دارند" برخواهیم خورد. برای حل این موضوع به راه حل سوم توجه کنید.
راه حل سوم: استفاده از یک NamespaceControllerSelector که پیاده سازی دیگری از اینترفیس IHttpControllerSelector میباشد. فرض بر این است که قالب پروژه به شکل زیر میباشد:
کار با پیاده سازی اینترفیس IHttpRouteConstraint آغاز میشود:
public class VersionConstraint : IHttpRouteConstraint { public VersionConstraint(string allowedVersion) { AllowedVersion = allowedVersion.ToLowerInvariant(); } public string AllowedVersion { get; private set; } public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection) { object value; if (values.TryGetValue(parameterName, out value) && value != null) { return AllowedVersion.Equals(value.ToString().ToLowerInvariant()); } return false; } }
کلاس بالا در واقع برای اعمال محدودیت خاصی که در ادامه توضیح داده میشود، پیاده سازی شده است.
متد Match آن وظیفه چک کردن برابری مقدار کلید parameterName موجود در درخواست را با مقدار allowedVersion ای که API از آن پشتیبانی میکند، برعهده دارد. با استفاده از این Constraint مشخص کردهایم که دقیقا چه زمانی باید Route نوشته شده انتخاب شود.
به روش استفاده از این Constraint توجه کنید:
namespace UriBasedVersioning.Namespace.Controllers.V1 { using Models.V1; RoutePrefix("api/{version:VersionConstraint(v1)}/items")] public class ItemsController : ApiController { [ResponseType(typeof(ItemViewModel))] [Route("{id}")] public IHttpActionResult Get(int id) { var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country = "Japan" }; return Ok(viewModel); } } } namespace UriBasedVersioning.Namespace.Controllers.V2 { using Models.V2; RoutePrefix("api/{version:VersionConstraint(v2)}/items")] public class ItemsController : ApiController { [ResponseType(typeof(ItemViewModel))] [Route("{id}")] public IHttpActionResult Get(int id) { var viewModel = new ItemViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 }; return Ok(viewModel); } } }
با توجه به کد بالا میتوان دلیل استفاده از VersionConstraint را هم درک کرد؛ از آنجایی که ما دو Route شبیه به هم داریم، لذا باید مشخص کنیم که در چه شرایطی و کدام یک از این Routeها انتخاب شود. خوب، اگر برنامه را اجرا کرده و یکی از APIهای بالا را تست کنید، با خطا مواجه خواهید شد؛ زیرا فعلا این Constraint به سیستم Web API معرفی نشده است. تنظیمات زیر را انجام دهید:
var constraintsResolver = new DefaultInlineConstraintResolver(); constraintsResolver.ConstraintMap.Add(nameof(VersionConstraint), typeof (VersionConstraint)); config.MapHttpAttributeRoutes(constraintsResolver);
مرحله بعدی کار، پیاده سازی IHttpControllerSelector میباشد:
public class NamespaceControllerSelector : IHttpControllerSelector { private readonly HttpConfiguration _configuration; private readonly Lazy<Dictionary<string, HttpControllerDescriptor>> _controllers; public NamespaceControllerSelector(HttpConfiguration config) { _configuration = config; _controllers = new Lazy<Dictionary<string, HttpControllerDescriptor>>(InitializeControllerDictionary); } public HttpControllerDescriptor SelectController(HttpRequestMessage request) { var routeData = request.GetRouteData(); if (routeData == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } var controllerName = GetControllerName(routeData); if (controllerName == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } var version = GetVersion(routeData); if (version == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } var controllerKey = string.Format(CultureInfo.InvariantCulture, "{0}.{1}", version, controllerName); HttpControllerDescriptor controllerDescriptor; if (_controllers.Value.TryGetValue(controllerKey, out controllerDescriptor)) { return controllerDescriptor; } throw new HttpResponseException(HttpStatusCode.NotFound); } public IDictionary<string, HttpControllerDescriptor> GetControllerMapping() { return _controllers.Value; } private Dictionary<string, HttpControllerDescriptor> InitializeControllerDictionary() { var dictionary = new Dictionary<string, HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase); var assembliesResolver = _configuration.Services.GetAssembliesResolver(); var controllersResolver = _configuration.Services.GetHttpControllerTypeResolver(); var controllerTypes = controllersResolver.GetControllerTypes(assembliesResolver); foreach (var controllerType in controllerTypes) { var segments = controllerType.Namespace.Split(Type.Delimiter); var controllerName = controllerType.Name.Remove(controllerType.Name.Length - DefaultHttpControllerSelector.ControllerSuffix.Length); var controllerKey = string.Format(CultureInfo.InvariantCulture, "{0}.{1}", segments[segments.Length - 1], controllerName); if (!dictionary.Keys.Contains(controllerKey)) { dictionary[controllerKey] = new HttpControllerDescriptor(_configuration, controllerType.Name, controllerType); } } return dictionary; } private static T GetRouteVariable<T>(IHttpRouteData routeData, string name) { object result; if (routeData.Values.TryGetValue(name, out result)) { return (T)result; } return default(T); } private static string GetControllerName(IHttpRouteData routeData) { var subroute = routeData.GetSubRoutes().FirstOrDefault(); var dataTokenValue = subroute?.Route.DataTokens.First().Value; var controllerName = ((HttpActionDescriptor[])dataTokenValue)?.First().ControllerDescriptor.ControllerName.Replace("Controller", string.Empty); return controllerName; } private static string GetVersion(IHttpRouteData routeData) { var subRouteData = routeData.GetSubRoutes().FirstOrDefault(); return subRouteData == null ? null : GetRouteVariable<string>(subRouteData, "version"); } }
سورس اصلی کلاس بالا از این آدرس قابل دریافت است. در تکه کد بالا بخشی که مربوط به چک کردن تکراری بودن آدرس میباشد، برای ساده سازی کار حذف شده است. ولی نکتهی مربوط به SubRoutes که برای واکشی مقادیر پارامترهای مرتبط با Attribute Routing میباشد، اضافه شده است. روال کار به این صورت است که ابتدا RouteData موجود در درخواست را واکشی کرده و با استفاده از متدهای GetControllerName و GetVersion، پارامترهای controller و version را جستجو میکنیم. بعد با استفاده از مقادیر به دست آمده، controllerKey را تشکیل داده و درون کنترلرهای موجود در برنامه به دنبال کنترلر مورد نظر خواهیم گشت.
کارکرد متد InitializeControllerDictionary :
همانطور که میدانید به صورت پیشفرض Web API توجهی به فضای نام مرتبط با کنترلرها ندارد. از طرفی برای پیاده سازی اینترفیس IHttpControllerSelector نیاز است متدی با امضای زیر را داشته باشیم:
public IDictionary<string, HttpControllerDescriptor> GetControllerMapping()
لذا در کلاس پیاده سازی شده، خصوصیتی به نام _controllers را که از به صورت Lazy و از نوع بازگشتی متد بالا میباشد، تعریف کردهایم. متد InitializeControllerDictionary کار آماده سازی دادههای مورد نیاز خصوصیت _controllers میباشد. به این صورت که تمام کنترلرهای موجود در برنامه را واکشی کرده و این بار کلیدهای مربوط به دیکشنری را با استفاده از نام کنترلر و آخرین سگمنت فضای نام آن، تولید و درون دیکشنری مورد نظر ذخیره کردهایم.
حال تنظیمات زیر را اعمال کنید:
public static class WebApiConfig { public static void Register(HttpConfiguration config) { var constraintsResolver = new DefaultInlineConstraintResolver(); constraintsResolver.ConstraintMap.Add(nameof(VersionConstraint), typeof (VersionConstraint)); config.MapHttpAttributeRoutes(constraintsResolver); config.Services.Replace(typeof(IHttpControllerSelector), new NamespaceControllerSelector(config)); } }
این بار برنامه را اجرا کرده و APIهای مورد نظر را تست کنید؛ بله بدون مشکل کار خواهد کرد.
نکته تکمیلی: سورس مذکور در سایت کدپلکس برای حالتی که از Centralized Routes استفاده میکنید آماده شده است. روش مذکور در این مقاله هم فقط قسمت Duplicate Routes آن را کم دارد که میتوانید اضافه کنید. پیاده سازی دیگری را از این راه حل هم میتوانید داشته باشید.
پیاده سازی Header-based versioning
public class HeaderVersionConstraint : IHttpRouteConstraint { private const string VersionHeaderName = "api-version"; public HeaderVersionConstraint(int allowedVersion) { AllowedVersion = allowedVersion; } public int AllowedVersion { get; } public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection) { if (!request.Headers.Contains(VersionHeaderName)) return false; var version = request.Headers.GetValues(VersionHeaderName).FirstOrDefault(); return VersionToInt(version) == AllowedVersion; } private static int VersionToInt(string versionString) { int version; if (string.IsNullOrEmpty(versionString) || !int.TryParse(versionString, out version)) return 0; return version; } }
public sealed class HeaderVersionedRouteAttribute : RouteFactoryAttribute { public HeaderVersionedRouteAttribute(string template) : base(template) { Order = -1; } public int Version { get; set; } public override IDictionary<string, object> Constraints => new HttpRouteValueDictionary { {"", new HeaderVersionConstraint(Version)} }; }
[RoutePrefix("api/items")] public class ItemsController : ApiController { [ResponseType(typeof(ItemViewModel))] [HeaderVersionedRoute("{id}", Version = 1)] public IHttpActionResult Get(int id) { var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country = "Japan" }; return Ok(viewModel); } } [RoutePrefix("api/items")] public class ItemsV2Controller : ApiController { [ResponseType(typeof(ItemV2ViewModel))] [HeaderVersionedRoute("{id}", Version = 2)] public IHttpActionResult Get(int id) { var viewModel = new ItemV2ViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 }; return Ok(viewModel); } }
پیاده سازی Media type-based versioning
GET /api/Items HTTP 1.1 Accept: application/vnd.mediatype.versioning-v1+json GET /api/Items HTTP 1.1 Accept: application/vnd.mediatype.versioning-v2+json
public class MediaTypeVersionConstraint : IHttpRouteConstraint { private const string VersionMediaType = "vnd.mediatype.versioning"; public MediaTypeVersionConstraint(int allowedVersion) { AllowedVersion = allowedVersion; } public int AllowedVersion { get; } public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection) { if (!request.Headers.Accept.Any()) return false; var acceptHeaderVersion = request.Headers.Accept.FirstOrDefault(x => x.MediaType.Contains(VersionMediaType)); //Accept: application/vnd.mediatype.versioning-v1+json if (acceptHeaderVersion == null || !acceptHeaderVersion.MediaType.Contains("-v") || !acceptHeaderVersion.MediaType.Contains("+")) return false; var version = acceptHeaderVersion.MediaType.Between("-v", "+"); return VersionToInt(version)==AllowedVersion; } }