قلم +M
EF Code First #4
آشنایی با Code first migrations
ویژگی Code first migrations برای اولین بار در EF 4.3 ارائه شد و هدف آن سهولت هماهنگ سازی کلاسهای مدل برنامه با بانک اطلاعاتی است؛ به صورت خودکار یا با تنظیمات دقیق دستی.
همانطور که در قسمتهای قبل نیز به آن اشاره شد، تا پیش از EF 4.3، پنج روال جهت آغاز به کار با بانک اطلاعاتی در EF code first وجود داشت و دارد:
1) در اولین بار اجرای برنامه، در صورتیکه بانک اطلاعاتی اشاره شده در رشته اتصالی وجود خارجی نداشته باشد، نسبت به ایجاد خودکار آن اقدام میگردد. اینکار پس از وهله سازی اولین DbContext و همچنین صدور یک کوئری به بانک اطلاعاتی انجام خواهد شد.
2) DropCreateDatabaseAlways : همواره پس از شروع برنامه، ابتدا بانک اطلاعاتی را drop کرده و سپس نمونه جدیدی را ایجاد میکند.
3) DropCreateDatabaseIfModelChanges : اگر EF Code first تشخیص دهد که تعاریف مدلهای شما با بانک اطلاعاتی مشخص شده توسط رشته اتصالی، هماهنگ نیست، آنرا drop کرده و نمونه جدیدی را تولید میکند.
4) با مقدار دهی پارامتر متد System.Data.Entity.Database.SetInitializer به نال، میتوان فرآیند آغاز خودکار بانک اطلاعاتی را غیرفعال کرد. در این حالت شخص میتواند تغییرات انجام شده در کلاسهای مدل برنامه را به صورت دستی به بانک اطلاعاتی اعمال کند.
5) میتوان با پیاده سازی اینترفیس IDatabaseInitializer، یک آغاز کننده بانک اطلاعاتی سفارشی را نیز تولید کرد.
اکثر این روشها در حین توسعه یک برنامه یا خصوصا جهت سهولت انجام آزمونهای خودکار بسیار مناسب هستند، اما به درد محیط کاری نمیخورند؛ زیرا drop یک بانک اطلاعاتی به معنای از دست دادن تمام اطلاعات ثبت شده در آن است. برای رفع این مشکل مهم، مفهومی به نام «Migrations» در EF 4.3 ارائه شده است تا بتوان بانک اطلاعاتی را بدون تخریب آن، بر اساس اطلاعات تغییر کردهی کلاسهای مدل برنامه، تغییر داد. البته بدیهی است زمانیکه توسط NuGet نسبت به دریافت و نصب EF اقدام میشود، همواره آخرین نگارش پایدار که حاوی اطلاعات و فایلهای مورد نیاز جهت کار با «Migrations» است را نیز دریافت خواهیم کرد.
تنظیمات ابتدایی Code first migrations
در اینجا قصد داریم همان مثال قسمت قبل را ادامه دهیم. در آن مثال از یک نمونه سفارشی سازی شده DropCreateDatabaseAlways استفاده شد.
نیاز است از منوی Tools در ویژوال استودیو، گزینه Library package manager آن، گزینه package manager console را انتخاب کرد تا کنسول پاورشل NuGet ظاهر شود.
اطلاعات مرتبط با پاورشل EF، به صورت خودکار توسط NuGet نصب میشود. برای مثال جهت مشاهده آنها به مسیر packages\EntityFramework.4.3.1\tools در کنار پوشه پروژه خود مراجعه نمائید.
در ادامه در پایین صفحه، زمانیکه کنسول پاورشل NuGet ظاهر میشود، ابتدا باید دقت داشت که قرار است فرامین را بر روی چه پروژهای اجرا کنیم. برای مثال اگر تعاریف DbContext را به یک اسمبلی و پروژه class library مجزا انتقال دادهاید، گزینه Default project را در این قسمت باید به این پروژه مجزا، تغییر دهید.
سپس در خط فرمان پاور شل، دستور enable-migrations را وارد کرده و دکمه enter را فشار دهید.
پس از اجرای این دستور، یک سری اتفاقات رخ خواهد داد:
الف) پوشهای به نام Migrations به پروژه پیش فرض مشخص شده در کنسول پاورشل، اضافه میشود.
ب) دو کلاس جدید نیز در آن پوشه تعریف خواهند شد به نامهای Configuration.cs و یک نام خودکار مانند number_InitialCreate.cs
ج) در کنسول پاور شل، پیغام زیر ظاهر میگردد:
Detected database created with a database initializer. Scaffolded migration '201205050805256_InitialCreate'
corresponding to current database schema. To use an automatic migration instead, delete the Migrations
folder and re-run Enable-Migrations specifying the -EnableAutomaticMigrations parameter.
با توجه به اینکه در مثال قسمت سوم، از آغاز کننده سفارشی سازی شده DropCreateDatabaseAlways استفاده شده بود، اطلاعات آن در جدول سیستمی dbo.__MigrationHistory در بانک اطلاعاتی برنامه موجود است (تصویری از آنرا در قسمت اول این سری مشاهده کردید). سپس با توجه به ساختار بانک اطلاعاتی جاری، دو کلاس خودکار زیر را ایجاد کرده است:
namespace EF_Sample02.Migrations
{
using System;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Linq;
internal sealed class Configuration : DbMigrationsConfiguration<EF_Sample02.Sample2Context>
{
public Configuration()
{
AutomaticMigrationsEnabled = false;
}
protected override void Seed(EF_Sample02.Sample2Context context)
{
// This method will be called after migrating to the latest version.
// You can use the DbSet<T>.AddOrUpdate() helper extension method
// to avoid creating duplicate seed data. E.g.
//
// context.People.AddOrUpdate(
// p => p.FullName,
// new Person { FullName = "Andrew Peters" },
// new Person { FullName = "Brice Lambson" },
// new Person { FullName = "Rowan Miller" }
// );
//
}
}
}
namespace EF_Sample02.Migrations
{
using System.Data.Entity.Migrations;
public partial class InitialCreate : DbMigration
{
public override void Up()
{
CreateTable(
"Users",
c => new
{
Id = c.Int(nullable: false, identity: true),
Name = c.String(),
LastName = c.String(),
Email = c.String(),
Description = c.String(),
Photo = c.Binary(),
RowVersion = c.Binary(nullable: false, fixedLength: true, timestamp: true, storeType: "rowversion"),
Interests_Interest1 = c.String(maxLength: 450),
Interests_Interest2 = c.String(maxLength: 450),
AddDate = c.DateTime(nullable: false),
})
.PrimaryKey(t => t.Id);
CreateTable(
"Projects",
c => new
{
Id = c.Int(nullable: false, identity: true),
Title = c.String(maxLength: 50),
Description = c.String(),
RowVesrion = c.Binary(nullable: false, fixedLength: true, timestamp: true, storeType: "rowversion"),
AddDate = c.DateTime(nullable: false),
AdminUser_Id = c.Int(),
})
.PrimaryKey(t => t.Id)
.ForeignKey("Users", t => t.AdminUser_Id)
.Index(t => t.AdminUser_Id);
}
public override void Down()
{
DropIndex("Projects", new[] { "AdminUser_Id" });
DropForeignKey("Projects", "AdminUser_Id", "Users");
DropTable("Projects");
DropTable("Users");
}
}
}
در این کلاس خودکار، نحوه ایجاد جداول بانک اطلاعاتی تعریف شدهاند. در متد تحریف شده Up، کار ایجاد بانک اطلاعاتی و در متد تحریف شده Down، دستورات حذف جداول و قیود ذکر شدهاند.
به علاوه اینبار متد Seed را در کلاس مشتق شده از DbMigrationsConfiguration، میتوان تحریف و مقدار دهی کرد.
علاوه بر اینها جدول سیستمی dbo.__MigrationHistory نیز با اطلاعات جاری مقدار دهی میگردد.
فعال سازی گزینههای مهاجرت خودکار
برای استفاده از این کلاسها، ابتدا به فایل Configuration.cs مراجعه کرده و خاصیت AutomaticMigrationsEnabled را true کنید:
internal sealed class Configuration : DbMigrationsConfiguration<EF_Sample02.Sample2Context>
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
}
پس از آن EF به صورت خودکار کار استفاده و مدیریت «Migrations» را عهدهدار خواهد شد. البته برای این منظور باید نوع آغاز کننده بانک اطلاعاتی را از DropCreateDatabaseAlways قبلی به نمونه جدید MigrateDatabaseToLatestVersion نیز تغییر دهیم:
//Database.SetInitializer(new Sample2DbInitializer());
Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample2Context, Migrations.Configuration>());
یک نکته:
کلاس Migrations.Configuration که باید در حین وهله سازی از MigrateDatabaseToLatestVersion قید شود (همانند کدهای فوق)، از نوع internal sealed معرفی شده است. بنابراین اگر این کلاس را در یک اسمبلی جداگانه قرار دادهاید، نیاز است فایل را ویرایش کرده و internal sealed آنرا به public تغییر دهید.
روش دیگر معرفی کلاسهای Context و Migrations.Configuration، حذف متد Database.SetInitializer و استفاده از فایل app.config یا web.config است به نحو زیر ( در اینجا حرف ` اصطلاحا back tick نام دارد. فشردن دکمه ~ در حین تایپ انگلیسی):
<entityFramework>
<contexts>
<context type="EF_Sample02.Sample2Context, EF_Sample02">
<databaseInitializer
type="System.Data.Entity.MigrateDatabaseToLatestVersion`2[[EF_Sample02.Sample2Context, EF_Sample02],
[EF_Sample02.Migrations.Configuration, EF_Sample02]], EntityFramework"
/>
</context>
</contexts>
</entityFramework>
آزمودن ویژگی مهاجرت خودکار
اکنون برای آزمایش این موارد، یک خاصیت دلخواه را به کلاس Project به نام public string SomeProp اضافه کنید. سپس برنامه را اجرا نمائید.
در ادامه به بانک اطلاعاتی مراجعه کرده و فیلدهای جدول Projects را بررسی کنید:
CREATE TABLE [dbo].[Projects](
---...
[SomeProp] [nvarchar](max) NULL,
---...
بله. اینبار فیلد SomeProp بدون از دست رفتن اطلاعات و drop بانک اطلاعاتی، به جدول پروژهها اضافه شده است.
عکس العمل ویژگی مهاجرت خودکار در مقابل از دست رفتن اطلاعات
در ادامه، خاصیت public string SomeProp را که در قسمت قبل به کلاس پروژه اضافه کردیم، حذف کنید. اکنون مجددا برنامه را اجرا نمائید. برنامه بلافاصله با استثنای زیر متوقف خواهد شد:
Automatic migration was not applied because it would result in data loss.
از آنجائیکه حذف یک خاصیت مساوی است با حذف یک ستون در جدول بانک اطلاعاتی، امکان از دست رفتن اطلاعات در این بین بسیار زیاد است. بنابراین ویژگی مهاجرت خودکار دیگر اعمال نخواهد شد و این مورد به نوعی یک محافظت خودکار است که درنظر گرفته شده است.
البته در EF Code first این مساله را نیز میتوان کنترل نمود. به کلاس Configuration اضافه شده توسط پاورشل مراجعه کرده و خاصیت AutomaticMigrationDataLossAllowed را به true تنظیم کنید:
internal sealed class Configuration : DbMigrationsConfiguration<EF_Sample02.Sample2Context>
{
public Configuration()
{
this.AutomaticMigrationsEnabled = true;
this.AutomaticMigrationDataLossAllowed = true;
}
این تغییر به این معنا است که خودمان صریحا مجوز حذف یک ستون و اطلاعات مرتبط به آنرا صادر کردهایم.
پس از این تغییر، مجددا برنامه را اجرا کنید. ستون SomeProp به صورت خودکار حذف خواهد شد، اما اطلاعات رکوردهای موجود تغییری نخواهند کرد.
استفاده از Code first migrations بر روی یک بانک اطلاعاتی موجود
تفاوت یک دیتابیس موجود با بانک اطلاعاتی تولید شده توسط EF Code first در نبود جدول سیستمی dbo.__MigrationHistory است.
به این ترتیب زمانیکه فرمان enable-migrations را در یک پروژه EF code first متصل به بانک اطلاعاتی قدیمی موجود اجرا میکنیم، پوشه Migration در آن ایجاد خواهد شد اما تنها حاوی فایل Configuration.cs است و نه فایلی شبیه به number_InitialCreate.cs .
بنابراین نیاز است به صورت صریح به EF اعلام کنیم که نیاز است تا جدول سیستمی dbo.__MigrationHistory و فایل number_InitialCreate.cs را نیز تولید کند. برای این منظور کافی است دستور زیر را در خط فرمان پاورشل NuGet پس از فراخوانی enable-migrations اولیه، اجرا کنیم:
add-migration Initial -IgnoreChanges
با بکارگیری پارامتر IgnoreChanges، متد Up در فایل number_InitialCreate.cs تولید نخواهد شد. به این ترتیب نگران نخواهیم بود که در اولین بار اجرای برنامه، تعاریف دیتابیس موجود ممکن است اندکی تغییر کند.
سپس دستور زیر را جهت به روز رسانی جدول سیستمی dbo.__MigrationHistory اجرا کنید:
update-database
پس از آن جهت سوئیچ به مهاجرت خودکار، خاصیت AutomaticMigrationsEnabled = true را در فایل Configuration.cs همانند قبل مقدار دهی کنید.
مشاهده دستوارت SQL به روز رسانی بانک اطلاعاتی
اگر علاقمند هستید که دستورات T-SQL به روز رسانی بانک اطلاعاتی را نیز مشاهده کنید، دستور Update-Database را با پارامتر Verbose آغاز نمائید:
Update-Database -Verbose
و اگر تنها نیاز به مشاهده اسکریپت تولیدی بدون اجرای آنها بر روی بانک اطلاعاتی مدنظر است، از پارامتر Script باید استفاده کرد:
update-database -Script
نکتهای در مورد جدول سیستمی dbo.__MigrationHistory
تنها دلیلی که این جدول در SQL Server البته (ونه برای مثال در SQL Server CE) به صورت سیستمی معرفی میشود این است که «جلوی چشم نباشد»! به این ترتیب در SQL Server management studio در بین سایر جداول معمولی بانک اطلاعاتی قرار نمیگیرد. اما برای EF تفاوتی نمیکند که این جدول سیستمی است یا خیر.
همین سیستمی بودن آن ممکن است بر اساس سطح دسترسی کاربر اتصالی به بانک اطلاعاتی مساله ساز شود. برای نمونه ممکن است schema کاربر متصل dbo نباشد. همینجا است که کار به روز رسانی این جدول متوقف خواهد شد.
بنابراین اگر قصد داشتید خواص سیستمی آنرا لغو کنید، تنها کافی است دستورات T-SQL زیر را در SQL Server اجرا نمائید:
SELECT * INTO [TempMigrationHistory]
FROM [__MigrationHistory]
DROP TABLE [__MigrationHistory]
EXEC sp_rename [TempMigrationHistory], [__MigrationHistory]
ساده سازی پروسه مهاجرت خودکار
کل پروسهای را که در این قسمت مشاهده کردید، به صورت ذیل نیز میتوان خلاصه کرد:
using System;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Data.Entity.Migrations.Infrastructure;
using System.IO;
namespace EF_Sample02
{
public class Configuration<T> : DbMigrationsConfiguration<T> where T : DbContext
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
AutomaticMigrationDataLossAllowed = true;
}
}
public class SimpleDbMigrations
{
public static void UpdateDatabaseSchema<T>(string SQLScriptPath = "script.sql") where T : DbContext
{
var configuration = new Configuration<T>();
var dbMigrator = new DbMigrator(configuration);
saveToFile(SQLScriptPath, dbMigrator);
dbMigrator.Update();
}
private static void saveToFile(string SQLScriptPath, DbMigrator dbMigrator)
{
if (string.IsNullOrWhiteSpace(SQLScriptPath)) return;
var scriptor = new MigratorScriptingDecorator(dbMigrator);
var script = scriptor.ScriptUpdate(sourceMigration: null, targetMigration: null);
File.WriteAllText(SQLScriptPath, script);
Console.WriteLine(script);
}
}
}
سپس برای استفاده از آن خواهیم داشت:
SimpleDbMigrations.UpdateDatabaseSchema<Sample2Context>();
در این کلاس ذخیره سازی اسکریپت تولیدی جهت به روز رسانی بانک اطلاعاتی جاری در یک فایل نیز درنظر گرفته شده است.
تا اینجا مهاجرت خودکار را بررسی کردیم. در قسمت بعدی Code-Based Migrations را ادامه خواهیم داد.
- تمام تعاریف بومی سازی مورد نیاز برنامه در یک تک فایل SharedResource.fa.resx قرار میگیرند. این فایل نیز در یک اسمبلی مستقل از برنامهی اصلی اضافه میشود.
- با استفاده از تزریق سرویس IStringLocalizer میتوان به کلیدهای فایل SharedResource.fa.resx در هر قسمتی از برنامهی Web API دسترسی یافت.
- در این بین اگر کلیدی یافت نشد، خطایی با ذکر دقیق جزئیات منبع جستجو شده، لاگ میشود.
- کلیدهای بومی سازی data annotations نیز قابل دریافت از فایل SharedResource.fa.resx میباشند.
در ادامه روش پیاده سازی یک چنین امکاناتی را بررسی میکنیم.
قرار دادن فایل منبع اشتراکی در اسمبلی ExternalResources
پس از ایجاد پروژهی ابتدایی Web API به نام Core3xSharedResource.WebApi، یک اسمبلی جدید را برای مثال به نام Core3xSharedResource.ExternalResources تعریف کرده و در داخل آن پوشهی جدید Resources را تعریف میکنیم. به این پوشه، فایل منبع جدیدی را به نام SharedResource.fa.resx اضافه میکنیم. در کنار آن باید یک کلاس خالی به نام SharedResource.cs نیز وجود داشته باشد.
کار با ین فایل (و یا فایلهای دیگری مانند SharedResource.en.resx) همانند تمام فایلهای منبع استاندارد است و نکتهی خاصی را به همراه ندارد.
معرفی فایل منبع اشتراکی به سرویسهای بومی سازی برنامه
پس از ایجاد و تکمیل فایل منبع اشتراکی، برای معرفی آن به برنامه، ابتدا کلاس جدید LocalizationConfig را تعریف کرده و در آن متد جدید AddCustomLocalization را به صورت زیر معرفی میکنیم:
public static class LocalizationConfig { public static IMvcBuilder AddCustomLocalization(this IMvcBuilder mvcBuilder, IServiceCollection services) { mvcBuilder.AddDataAnnotationsLocalization(options => { const string resourcesPath = "Resources"; string baseName = $"{resourcesPath}.{nameof(SharedResource)}"; var location = new AssemblyName(typeof(SharedResource).GetTypeInfo().Assembly.FullName).Name; options.DataAnnotationLocalizerProvider = (type, factory) => { // to use `SharedResource.fa.resx` file return factory.Create(baseName, location); }; }); services.AddLocalization(); services.AddScoped<IStringLocalizer>(provider => provider.GetRequiredService<IStringLocalizer<SharedResource>>()); services.AddScoped<ISharedResourceService, SharedResourceService>(); return mvcBuilder; } }
- سپس با استفاده از متد AddLocalization، سرویسهای پایهی بومی سازی ASP.NET Core به برنامه اضافه میشوند. برای مثال پس از این تعریف اگر در هر جائی از برنامه سرویس <IStringLocalizer<SharedResource را تزریق کنید، میتوان به مداخل فایل منبع اشتراکی، دسترسی یافت.
- در ادامه امکان تزریق سرویس غیرجنریک IStringLocalizer را نیز میسر کردهایم که تعاریف خودش را از همان سرویس توکار <IStringLocalizer<SharedResource دریافت میکند. مزیت اینکار، فراهم شدن امکانات بومی سازی، برای مثال در کتابخانههایی مانند Fluent Validation است که دقیقا از سرویس غیرجنریک IStringLocalizer برای دریافت منابع استفاده میکنند.
- در آخر تعریف یک سرویس سفارشی را نیز مشاهده میکنید که در ادامهی بحث تکمیل خواهد شد.
هدف از متد AddCustomLocalization فوق، خلوت کردن فایل startup برنامه است. این متد به صورت زیر مورد استفاده قرار میگیرد:
public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddHttpContextAccessor(); services.AddControllers().AddCustomLocalization(services); }
پس از آن نیاز است میانافزار بومی سازی را نیز فعال کرد. متد UseCustomRequestLocalization زیر، اینکار را انجام میدهد:
public static class LocalizationConfig { public static IApplicationBuilder UseCustomRequestLocalization(this IApplicationBuilder app) { var requestLocalizationOptions = new RequestLocalizationOptions { DefaultRequestCulture = new RequestCulture(new CultureInfo("fa-IR")), SupportedCultures = new[] { new CultureInfo("en-US"), new CultureInfo("fa-IR") }, SupportedUICultures = new[] { new CultureInfo("en-US"), new CultureInfo("fa-IR") } }; app.UseRequestLocalization(requestLocalizationOptions); return app; } }
public class Startup { public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseHttpsRedirection(); app.UseCustomRequestLocalization(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } }
تعریف مدل برنامه به همراه ویژگیهای بومی سازی شده
در اینجا تعریف RegisterModel را مشاهده میکنید که ErrorMessageهای آن هرچند به ظاهر یک رشتهی معمولی هستند، اما در عمل از فایل منبع اشتراکی خوانده میشوند:
using System.ComponentModel.DataAnnotations; namespace Core3xSharedResource.Models.Account { public class RegisterModel { [Required(ErrorMessage = "Please enter an email address")] // -->> from the shared resources [EmailAddress(ErrorMessage = "Please enter a valid email address")] // -->> from the shared resources public string Email { get; set; } } }
فایل resx ما دارای یک چنین کلیدهایی است:
<?xml version="1.0" encoding="utf-8"?> <root> <data name="<b>Hello</b><i> {0}</i>" xml:space="preserve"> <value><b>سلام</b><i> {0}</i></value> </data> <data name="About Title" xml:space="preserve"> <value>درباره</value> </data> <data name="DNT" xml:space="preserve"> <value>.NET Tips</value> </data> <data name="SiteName" xml:space="preserve"> <value>DNT</value> </data> <data name="Please enter an email address" xml:space="preserve"> <value>لطفا ایمیلی را وارد کنید</value> </data> <data name="Please enter a valid email address" xml:space="preserve"> <value>لطفا ایمیل معتبری را وارد کنید</value> </data> </root>
آزمایش برنامه
اکنون برنامهی Web API، برای آزمایش آمادهاست. برای مثال در کنترلر زیر، سرویس عمومی IStringLocalizer به سازندهی کلاس تزریق شدهاست و سپس قصد بازگشت مقدار کلید «About Title» را دارد. همچنین خطاهای بومی شدهی مدل برنامه را نیز بررسی میکنیم:
using Core3xSharedResource.Models.Account; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Localization; namespace Core3xSharedResource.WebApi.Controllers { [ApiController] [Route("[controller]")] public class NormalIStringLocalizerController : ControllerBase { private readonly IStringLocalizer _localizer; public NormalIStringLocalizerController(IStringLocalizer localizer) { _localizer = localizer; } [HttpGet] public ActionResult<string> Get() { var localizedString = _localizer["About Title"]; if (localizedString.ResourceNotFound) { return NotFound($"The localization resource with ID:`{localizedString.Name}` not found. SearchedLocation: `{localizedString.SearchedLocation}`."); } return localizedString.Value; } [HttpPost] public ActionResult<RegisterModel> Post(RegisterModel model) { return model; } } }
حالت get را در تصویر فوق مشاهده میکنید. در Web API برای تنظیم زبان مورد استفاده میتوان از هدری به نام Accept-Language استفاده کرد که برای مثال در اینجا به fa تنظیم شدهاست و نتیجهی آن مراجعه به فایل SharedResource.fa.resx خواهد بود. اگر en-us وارد شود، نیاز خواهد بود تا فایل منبع اشتراکی دیگری را تعریف کنید. البته اگر این هدر تنظیم نشود، با توجه به تنظیمات متد UseCustomRequestLocalization، مقدار پیشفرض آن همان fa-IR خواهد بود.
حالت post را نیز در تصویر زیر میتوان مشاهده کرد:
در اینجا چون ایمیل وارد نشده، هر دو خطای تنظیم شدهی در مدل برنامه را دریافت کردهایم و این خطاها نیز فارسی هستند. به این معنا که بومی سازی data annotations نیز به درستی کار میکند.
تعریف یک سرویس عمومی برای محصور سازی قابلیتهای بومی سازی، در برنامههای Web API
در ادامه تعریف سرویس SharedResourceService را مشاهده میکنید که ثبت آنرا پیشتر انجام دادیم:
using System; using System.Collections.Generic; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using Microsoft.AspNetCore.Http; namespace Core3xSharedResource.Services { public interface ISharedResourceService { string this[string index] { get; } IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures); string GetString(string name, params object[] arguments); string GetString(string name); } public class SharedResourceService : ISharedResourceService { private readonly IStringLocalizer _sharedLocalizer; private readonly ILogger<SharedResourceService> _logger; private readonly IHttpContextAccessor _httpContextAccessor; public SharedResourceService( IStringLocalizer sharedHtmlLocalizer, IHttpContextAccessor httpContextAccessor, ILogger<SharedResourceService> logger ) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _sharedLocalizer = sharedHtmlLocalizer ?? throw new ArgumentNullException(nameof(sharedHtmlLocalizer)); _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); } public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures) { return _sharedLocalizer.GetAllStrings(includeParentCultures); } public string this[string index] => GetString(index); public string GetString(string name, params object[] arguments) { var result = _sharedLocalizer.GetString(name, arguments); logError(name, result); return result; } private void logError(string name, LocalizedString result) { if (result.ResourceNotFound) { var acceptLanguage = _httpContextAccessor?.HttpContext?.Request?.Headers["Accept-Language"]; _logger.LogError($"The localization resource with Accept-Language:`{acceptLanguage}` & ID:`{name}` not found. SearchedLocation: `{result.SearchedLocation}`."); } } public string GetString(string name) { var result = _sharedLocalizer.GetString(name); logError(name, result); return result; } } }
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Core3xSharedResource.zip
اعمال کنترل دسترسی پویا در پروژههای ASP.NET Core با استفاده از AuthorizationPolicyProvider سفارشی
در مطلب «سفارشی سازی ASP.NET Core Identity - قسمت پنجم - سیاستهای دسترسی پویا» به طور مفصل به قضیه کنترل دسترسی پویا در ASP.NET Core Identity پرداخته شدهاست؛ در این مطلب روش دیگری را بررسی خواهیم کرد.
مشخص میباشد که بدون وابستگی به روش خاصی، خیلی ساده میتوان به شکل زیر عمل کرد:
services.AddAuthorization(options => { options.AddPolicy("View Projects", policy => policy.RequireClaim(CustomClaimTypes.Permission, "projects.view")); });
[Authorize("View Projects")] public IActionResult Index(int siteId) { return View(); }
Using a large range of policies (for different room numbers or ages, for example), so it doesn’t make sense to add each individual authorization policy with an AuthorizationOptions.AddPolicy call.
کار با پیاده سازی واسط IAuthorizationPolicyProvider شروع میشود؛ یا شاید ارث بری از DefaultAuthorizationPolicyProvider رجیستر شدهی در سیستم DI و توسعه آن هم کافی باشد.
public class AuthorizationPolicyProvider : DefaultAuthorizationPolicyProvider { public AuthorizationPolicyProvider(IOptions<AuthorizationOptions> options) : base(options) { } public override Task<AuthorizationPolicy> GetPolicyAsync(string policyName) { if (!policyName.StartsWith(PermissionAuthorizeAttribute.PolicyPrefix, StringComparison.OrdinalIgnoreCase)) { return base.GetPolicyAsync(policyName); } var permissionNames = policyName.Substring(PermissionAuthorizeAttribute.PolicyPrefix.Length).Split(','); var policy = new AuthorizationPolicyBuilder() .RequireClaim(CustomClaimTypes.Permission, permissionNames) .Build(); return Task.FromResult(policy); } }
متد GetPolicyAsync موظف به یافتن و بازگشت یک Policy ثبت شده میباشد؛ با این حال میتوان با بازنویسی آن و با استفاده از وهلهای از AuthorizationPolicyBuilder، فرآیند تعریف سیاست درخواست شده را که احتمالا در تنظیمات آغازین پروژه تعریف نشده و پیشوند مدنظر را نیز دارد، خوکار کرد. در اینجا امکان ترکیب کردن چندین دسترسی را هم خواهیم داشت که برای این منظور میتوان دسترسیهای مختلف را به صورت comma separated به سیستم معرفی کرد.
نکتهی مهم در تکه کد بالا مربوط است به PolicyPrefix که با استفاده از آن مشخص کردهایم که برای هر سیاست درخواستی، این فرآیند را طی نکند و موجب اختلال در سیستم نشود.
پس از پیاده سازی واسط مطرح شده، لازم است این پیاده سازی جدید را به سیستم DI هم معرفی کنید:
services.AddSingleton<IAuthorizationPolicyProvider, AuthorizationPolicyProvider>();
خوب، تا اینجا فرآیند تعریف سیاستها به صورت خودکار انجام شد. در ادامه نیاز است با تعریف یک فیلتر Authorization، بتوان لیست دسترسیهای مورد نظر برای اکشنی خاص را نیز مشخص کرد تا در متد GetPolicyAsync فوق، کار ثبت خودکار سیاست دسترسی متناظر با آنرا توسط فراخوانی متد policyBuilder.RequireClaim، انجام دهد تا دیگر نیازی به تعریف دستی و جداگانهی آن، در کلاس آغازین برنامه نباشد. برای این منظور به شکل زیر عمل خواهیم کرد:
public class PermissionAuthorizeAttribute : AuthorizeAttribute { internal const string PolicyPrefix = "PERMISSION:"; /// <summary> /// Creates a new instance of <see cref="AuthorizeAttribute"/> class. /// </summary> /// <param name="permissions">A list of permissions to authorize</param> public PermissionAuthorizeAttribute(params string[] permissions) { Policy = $"{PolicyPrefix}{string.Join(",", permissions)}"; } }
همانطور که مشخص میباشد، رشته PERMISSION به عنوان پیشوند رشته تولیدی از لیست اسامی دسترسیها، استفاده شدهاست و در پراپرتی Policy قرار داده شدهاست. این بار برای کنترل دسترسی میتوان به شکل زیر عمل کرد:
[PermissionAuthorize(PermissionNames.Projects_View)] public IActionResult Get(FilteredQueryModel query) { //... } [PermissionAuthorize(PermissionNames.Projects_Create)] public IActionResult Post(ProjectModel model) { //... }
برای مثال در اولین فراخوانی فیلتر PermissionAuthorize فوق، مقدار ثابت PermissionNames.Projects_View به عنوان یک Policy جدید به متد GetPolicyAsync کلاس AuthorizationPolicyProvider سفارشی ما ارسال میشود. چون دارای پیشوند «:PERMISSION» است، مورد پردازش قرار گرفته و توسط متد policyBuilder.RequireClaim به صورت خودکار به سیستم معرفی و ثبت خواهد شد.
همچنین راه حل مطرح شده برای مدیریت دسترسیهای پویا، در gist به اشتراک گذاشته شده «موجودیتهای مرتبط با مدیریت دسترسیهای پویا» را نیز مد نظر قرار دهید.
دسترسی سریع به مقادیر خواص توسط Reflection.Emit
فرض کنید تعاریف کلاس User به صورت زیر است:
public class User { public int Id { set; get; } }
حال اگر یک متد پویا ایجاد کنیم که بجای هر بار Reflection جهت دریافت مقدار Id، خود متد get_Id را مستقیما صدا بزند، چه خواهد شد؟
پیاده سازی این نکته را در ادامه ملاحظه میکنید:
using System; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Reflection.Emit; namespace FastReflectionTests { /// <summary> /// کلاسی برای اندازه گیری زمان اجرای عملیات /// </summary> public class Benchmark : IDisposable { Stopwatch _watch; string _name; public static Benchmark Start(string name) { return new Benchmark(name); } private Benchmark(string name) { _name = name; _watch = new Stopwatch(); _watch.Start(); } public void Dispose() { _watch.Stop(); Console.WriteLine("{0} Total seconds: {1}" , _name, _watch.Elapsed.TotalSeconds); } } public class User { public int Id { set; get; } } class Program { public static Func<object, object> GetFastGetterFunc(string propertyName, Type ownerType) { var propertyInfo = ownerType.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public); if (propertyInfo == null) return null; var getter = ownerType.GetMethod("get_" + propertyInfo.Name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy); if (getter == null) return null; var dynamicGetterMethod = new DynamicMethod( name: "_", returnType: typeof(object), parameterTypes: new[] { typeof(object) }, owner: propertyInfo.DeclaringType, skipVisibility: true); var il = dynamicGetterMethod.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); // Load input to stack il.Emit(OpCodes.Castclass, propertyInfo.DeclaringType); // Cast to source type // نکته مهم در اینجا فراخوانی نهایی متد گت بدون استفاده از ریفلکشن است il.Emit(OpCodes.Callvirt, getter); //calls its get method if (propertyInfo.PropertyType.IsValueType) il.Emit(OpCodes.Box, propertyInfo.PropertyType);//box il.Emit(OpCodes.Ret); return (Func<object, object>)dynamicGetterMethod.CreateDelegate(typeof(Func<object, object>)); } static void Main(string[] args) { //تهیه لیستی از دادهها جهت آزمایش var list = new List<User>(); for (int i = 0; i < 1000000; i++) { list.Add(new User { Id = i }); } // دسترسی به اطلاعات لیست به صورت متداول از طریق ریفلکشن معمولی var idProperty = typeof(User).GetProperty("Id"); using (Benchmark.Start("Normal reflection")) { foreach (var item in list) { var id = idProperty.GetValue(item, null); } } // دسترسی از طریق روش سریع دستیابی به اطلاعات خواص var fastIdProperty = GetFastGetterFunc("Id", typeof(User)); using (Benchmark.Start("Fast Property")) { foreach (var item in list) { var id = fastIdProperty(item); } } } } }
از کلاس Benchmark برای نمایش زمان انجام عملیات دریافت مقادیر Id از یک لیست، به دو روش Reflection متداول و روش صدا زدن مستقیم متد get_Id استفاده شده است.
در متد GetFastGetterFunc، ابتدا به متد get_Id خاصیت Id دسترسی پیدا خواهیم کرد. سپس یک متد پویا ایجاد میکنیم تا این get_Id را مستقیما صدا بزند. حاصل کار را به صورت یک delegate بازگشت میدهیم. شاید عنوان کنید که در اینجا هم حداقل در ابتدای کار متد، یک Reflection اولیه وجود دارد. پاسخ این است که مهم نیست؛ چون در یک برنامه واقعی، تهیه delegates در زمان آغاز برنامه انجام شده و حاصل کش میشود. بنابراین در زمان استفاده نهایی، به هیچ عنوان با سربار Reflection مواجه نخواهیم بود.
خروجی آزمایش فوق بر روی سیستم معمولی من به صورت زیر است:
Normal reflection Total seconds: 2.0054177 Fast Property Total seconds: 0.0552056
چند پروژه که از این روش استفاده میکنند
Dapper
AutoMapper
fastJson
در سورس این کتابخانهها روشهای فراخوانی مستقیم متدهای set نیز پیاده سازی شدهاند که جهت تکمیل بحث میتوان به آنها مراجعه نمود.
ماخذ اصلی
این کشف و استفاده خاص، از اینجا شروع و عمومیت یافته است و پایه تمام کتابخانههایی است که پیشوند fast را به خود دادهاند:
2000% faster using dynamic method calls
آشنایی با AOP Interceptors
ابزارهایی جهت تولید AOP Interceptors
متداولترین کامپوننتهای خارجی که جهت تولید AOP Interceptors مورد استفاده قرار میگیرند، همان IOC Containers معروف هستند مانند StructureMap، Ninject، MS Unity و غیره.
سایر ابزارهای تولید AOP Interceptors، از روش تولید Dynamic proxies بهره میگیرند. به این ترتیب مزین کنندههایی پویا، در زمان اجرا، کدهای شما را محصور خواهند کرد. (نمونهای از آنرا شاید در حین کار با ORMهای مختلف دیده باشید).
نگاهی به فرآیند Interception
زمانیکه از یک IOC Container در کدهای خود استفاده میکنید، مراحلی چند رخ خواهند داد:
الف) کد فراخوان، از IOC Container، یک شیء مشخص را درخواست میکند. عموما اینکار با درخواست یک اینترفیس صورت میگیرد؛ هرچند محدودیتی نیز وجود نداشته و امکان درخواست یک کلاس از نوعی مشخص نیز وجود دارد.
ب) در ادامه IOC Container به لیست اشیاء قابل ارائه توسط خود نگاه کرده و در صورت وجود، وهله سازی شیء درخواست شده را انجام و نهایتا شیء مطلوب را بازگشت خواهد داد.
ج) سپس، کد فراخوان، وهله دریافتی را مورد پردازش قرار داده و شروع به استفاده از متدها و خواص آن خواهد نمود.
اکنون با اضافه کردن Interception به این پروسه، چند مرحله دیگر نیز در این بین به آن اضافه خواهند شد:
الف) در اینجا نیز در ابتدا کد فراخوان، درخواست وهلهای را بر اساس اینترفیسی خاص به IOC Container ارائه میدهد.
ب) IOC Container نیز سعی در وهله سازی درخواست رسیده بر اساس تنظیمات اولیه خود میکند.
ج) اما در این حالت IOC Container تشخیص میدهد، نوعی که باید بازگشت دهد، علاوه بر وهله سازی، نیاز به مزین سازی توسط Aspects و پیاده سازی Interceptors را نیز دارد. بنابراین نوع مورد انتظار را در صورت وجود، به یک Dynamic Proxy، بجای بازگشت مستقیم به فراخوان ارائه میدهد.
د) در ادامه Dynamic Proxy، نوع مورد انتظار را توسط Interceptors محصور کرده و به فراخوان بازگشت میدهد.
ه) اکنون فراخوان، در حین استفاده از امکانات شیء وهله سازی شده، به صورت خودکار مراحل مختلف اجرای یک Aspect را که در قسمت قبل بررسی شدند، سبب خواهد شد.
نحوه ایجاد Interceptors
برای ایجاد یک Interceptor دو مرحله باید انجام شود:
الف) پیاده سازی یک اینترفیس
ب) اتصال آن به کدهای اصلی برنامه
در ادامه قصد داریم از یک IOC Container معروف به نام StructureMap در یک برنامه کنسول استفاده کنیم. برای دریافت آن نیاز است دستور پاورشل ذیل را در کنسول نوگت ویژوال استودیو فراخوانی کنید:
PM> Install-Package structuremap
البته باید دقت داشت که برای استفاده از StructureMap نیاز است به خواص پروژه مراجعه و سپس حالت Client profile را به Full profile تغییر داد تا برنامه قابل کامپایل باشد.
using System; using StructureMap; namespace AOP00 { public interface IMyType { void DoSomething(string data, int i); } public class MyType : IMyType { public void DoSomething(string data, int i) { Console.WriteLine("DoSomething({0}, {1});", data, i); } } class Program { static void Main(string[] args) { ObjectFactory.Initialize(x => { x.For<IMyType>().Use<MyType>(); }); var myType = ObjectFactory.GetInstance<IMyType>(); myType.DoSomething("Test", 1); } } }
در اینجا یک اینترفیس نمونه و پیاده سازی آنرا ملاحظه میکنید. همچنین نحوه آغاز تنظیمات StructureMap و نحوه دریافت یک وهله متناظر با IMyType نیز بیان شدهاند.
نکتهی مهمی که در اینجا باید به آن دقت داشت، وضعیت شیء myType حین فراخوانی متد myType.DoSomething است. شیء myType در اینجا، دقیقا یک وهلهی متداول از کلاس myType است و هیچگونه دخل و تصرفی در نحوه اجرای آن صورت نگرفته است.
خوب! تا اینجای کار را احتمالا پیشتر نیز دیده بودید. در ادامه قصد داریم یک Interceptor را طراحی و مراحل چهارگانه اجرای یک Aspect را در اینجا بررسی کنیم.
در ادامه نیاز خواهیم داشت تا یک Dynamic proxy را نیز مورد استفاده قرار دهیم؛ از این جهت که StructureMap تنها دارای Interceptorهای وهله سازی اطلاعات است و نه Method Interceptor. برای دسترسی به Method Interceptors نیاز به یک Dynamic proxy نیز میباشد. در اینجا از Castle.Core استفاده خواهیم کرد:
PM> Install-Package Castle.Core
سپس کلاس ذیل را به پروژه جاری اضافه کنید:
using System; using Castle.DynamicProxy; namespace AOP00 { public class LoggingInterceptor : IInterceptor { public void Intercept(IInvocation invocation) { try { Console.WriteLine("Logging On Start."); invocation.Proceed(); //فراخوانی متد اصلی در اینجا صورت میگیرد Console.WriteLine("Logging On Success."); } catch (Exception ex) { Console.WriteLine("Logging On Error."); throw; } finally { Console.WriteLine("Logging On Exit."); } } } }
اکنون برای معرفی این کلاس به برنامه کافی است سطرهای ذیل را اندکی ویرایش کنیم:
static void Main(string[] args) { ObjectFactory.Initialize(x => { var dynamicProxy = new ProxyGenerator(); x.For<IMyType>().Use<MyType>(); x.For<IMyType>().EnrichAllWith(myTypeInterface => dynamicProxy.CreateInterfaceProxyWithTarget(myTypeInterface, new LoggingInterceptor())); }); var myType = ObjectFactory.GetInstance<IMyType>(); myType.DoSomething("Test", 1); }
برای مثال اکنون با فراخوانی متد myType.DoSomething، ابتدا کنترل برنامه به پروکسی پویای تشکیل شده توسط Castle.Core منتقل میشود. در اینجا هنوز هم متد DoSomething فراخوانی نشده است. ابتدا وارد بدنه متد public void Intercept خواهیم شد. سپس سطر invocation.Proceed، فراخوانی واقعی متد DoSomething اصلی را انجام میدهد. در ادامه باز هم فرصت داریم تا مراحل موفقیت، خطا یا خروج را لاگ کنیم.
تنها زمانیکه کار متد public void Intercept به پایان میرسد، سطر پس از فراخوانی متد myType.DoSomething اجرا خواهد شد.
در این حالت اگر برنامه را اجرا کنیم، چنین خروجی را نمایش میدهد:
Logging On Start. DoSomething(Test, 1); Logging On Success. Logging On Exit.
برای اینکه فراخوانی قسمت On Error را نیز ملاحظه کنید، یک استثنای عمدی را در متد DoSomething قرار داده و مجددا برنامه را اجرا کنید.
<Comp1>This is a content coming from the parent</Comp1>
معرفی مفهوم Render Fragment
برای درج محتوای تامین شدهی توسط کامپوننت والد در یک کامپوننت فرزند، از ویژگی به نام Render Fragment استفاده میشود. مثالی جهت توضیح جزئیات آن:
در ابتدا یک کامپوننت والد جدید را در مسیر Pages\LearnBlazor\ParentComponent.razor به صورت زیر تعریف میکنیم:
@page "/ParentComponent" <h1 class="text-danger">Parent Child Component</h1> <ChildComponent Title="This title is passed as a parameter from the Parent Component"> A `Render Fragment` from the parent! </ChildComponent> <ChildComponent Title="This is the second child component"></ChildComponent> @code { }
- سپس دوبار کامپوننت فرضی ChildComponent به همراه پارامتر Title و یک محتوای جدید قرار گرفتهی در بین تگهای آن، در صفحه تعریف شدهاند.
- بار دومی که ChildComponent در صفحه قرار گرفتهاست، به همراه محتوای جدیدی در بین تگهای خود نیست.
برای دسترسی به این کامپوننت از طریق منوی برنامه، مدخل منوی آنرا به کامپوننت Shared\NavMenu.razor اضافه میکنیم:
<li class="nav-item px-3"> <NavLink class="nav-link" href="ParentComponent"> <span class="oi oi-list-rich" aria-hidden="true"></span> Parent/Child Relation </NavLink> </li>
<div> <div class="alert alert-info">@Title</div> <div class="alert alert-success"> @if (ChildContent == null) { <span> Hello, from Empty Render Fragment </span> } else { <span>@ChildContent</span> } </div> </div> @code { [Parameter] public string Title { get; set; } [Parameter] public RenderFragment ChildContent { get; set; } }
- خاصیت عمومی Title که توسط ویژگی Parameter مزین شدهاست، امکان تنظیم مقدار مشخصی را توسط کامپوننت دربرگیرندهی ChildComponent میسر میکند.
- در اینجا پارامتر عمومی دیگری نیز تعریف شدهاست که اینبار از نوع ویژهی RenderFragment است. توسط آن میتوان به محتوایی که در کامپوننت والد ChildComponent در بین تگهای آن تنظیم شدهاست، دسترسی یافت. همچنین اگر این محتوا توسط کامپوننت والد تنظیم نشده باشد، مانند دومین باری که ChildComponent در صفحه قرار گرفتهاست، میتوان با بررسی نال بودن آن، یک محتوای پیشفرض را نمایش داد.
با این خروجی:
روش دیگری برای فراخوانی Event Call Back ها
در قسمت قبل روش انتقال اطلاعات را از کامپوننتهای فرزند، به والد مشاهده کردیم. فراخوانی آنها در سمت Child Component نیاز به یک متد اضافی داشت و همچنین تنها یک پارامتر را هم ارسال کردیم. برای ساده سازی این عملیات از روش زیر نیز میتوان استفاده کرد:
<button class="btn btn-danger" @onclick="@(() => OnClickBtnMethod.InvokeAsync((1, "A message from child!")))"> Show a message from the child! </button> @code { // ... [Parameter] public EventCallback<(int, string)> OnClickBtnMethod { get; set; } }
- همچنین فراخوانی OnClickBtnMethod.InvokeAsync را نیز در محل تعریف onclick@ بدون نیازی به یک متد اضافی، مشاهده میکنید. نکتهی مهم آن، قرار دادن این قطعه کد داخل ()@ است تا ابتدا و انتهای کدهای #C مشخص شود؛ وگرنه کامپایل نمیشود.
در سمت کامپوننت والد برای دسترسی به OnClickBtnMethod که اینبار یک tuple را ارسال میکند، میتوان به صورت زیر عمل کرد:
@page "/ParentComponent" <h1 class="text-danger">Parent Child Component</h1> <ChildComponent OnClickBtnMethod="ShowMessage" Title="This title is passed as a parameter from the Parent Component"> A `Render Fragment` from the parent! </ChildComponent> <ChildComponent Title="This is the second child component"> <p><b>@MessageText</b></p> </ChildComponent> @code { string MessageText = ""; private void ShowMessage((int Value, string Message) args) { MessageText = args.Message; } }
امکان تعریف چندین RenderFragment
تا اینجا یک RenderFragment را در کامپوننت فرزند تعریف کردیم. امکان تعریف چندین RenderFragment در ChildComponent.razor نیز وجود دارند:
@code { // ... [Parameter] public RenderFragment ChildContent { get; set; } [Parameter] public RenderFragment DangerChildContent { get; set; } }
<ChildComponent OnClickBtnMethod="ShowMessage" Title="This title is passed as a parameter from the Parent Component"> <ChildContent> A `Render Fragment` from the parent! </ChildContent> <DangerChildContent> A danger content from the parent! </DangerChildContent> </ChildComponent>
از آنجائیکه ذکر این تگها اختیاری است، نیاز است در ChildComponent.razor بر اساس null بودن آنها، تصمیم به رندر محتوایی پیشفرض گرفت:
@if(DangerChildContent == null) { @if (ChildContent == null) { <span> Hello, from Empty Render Fragment </span> } else { <span>@ChildContent</span> } } else { <span>@DangerChildContent</span> }
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-08.zip
<input dir="ltr" class="form-control input-validation-error" type="email" data-val="true" data-val-email="'آدرس ایمیل' is not a valid email address." data-val-remote="این آدرس ایمیل هم اکنون مورد استفادهاست" data-val-remote-url="/Home/ValidateUniqueEmail" data-val-required="'آدرس ایمیل' must not be empty." id="Email" name="Email" >
افزودن آدرس ایمیل به مدل کاربران
به همان مدل قسمت قبل، قصد داریم خاصیت آدرس ایمیل را هم اضافه کنیم:
using System.ComponentModel.DataAnnotations; namespace FluentValidationSample.Models { public class UserModel { [Display(Name = "نام کاربری")] public string Username { get; set; } [Display(Name = "سن")] public int Age { get; set; } [Display(Name = "سابقه کار")] public int Experience { get; set; } [DataType(DataType.EmailAddress)] [Display(Name = "آدرس ایمیل")] public string Email { get; set; } } }
ایجاد سرویسی برای بررسی منحصربفرد بودن آدرس ایمیل
در ادامه قصد داریم سرویسی را ایجاد کنیم که برای مثال با بانک اطلاعاتی ارتباط برقرار کرده (در اینجا جهت سهولت ارائه، از یک آرایه استفاده شدهاست) و مشخص میکند که آیا ایمیل دریافتی پیشتر استفاده شدهاست یا خیر:
using System.Linq; namespace FluentValidationSample.Services { public interface IUsersService { bool IsUniqueEmail(string emailAddress); } public class UsersService : IUsersService { public bool IsUniqueEmail(string emailAddress) { string[] registedEmails = { "email@site.com", "test@gmail.com" }; return !registedEmails.Contains(emailAddress); } } }
namespace FluentValidationSample.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddScoped<IUsersService, UsersService>();
ایجاد اعتبارسنج سمت سرور بررسی منحصربفرد بودن آدرس ایمیل
در ادامه یک PropertyValidator جدید را ایجاد میکنیم تا بتوان توسط آن مقدار ایمیل دریافتی را در سمت سرور، تعیین اعتبار کرد:
using FluentValidation.Validators; using FluentValidationSample.Services; namespace FluentValidationSample.ModelsValidations { public class UniqueEmailValidator : PropertyValidator { private readonly IUsersService _usersService; public UniqueEmailValidator(IUsersService usersService) : base("این آدرس ایمیل هم اکنون مورد استفادهاست") { _usersService = usersService; } protected override bool IsValid(PropertyValidatorContext context) { return context.PropertyValue != null && _usersService.IsUniqueEmail((string)context.PropertyValue); } } }
پس از آن جهت سهولت استفادهی از آن، یک متد الحاقی جدید را نیز به نام UniqueEmail به نحو زیر تعریف میکنیم:
using FluentValidation; using FluentValidationSample.Services; namespace FluentValidationSample.ModelsValidations { public static class CustomValidatorExtensions { public static IRuleBuilderOptions<T, string> UniqueEmail<T>( this IRuleBuilder<T, string> ruleBuilder, IUsersService usersService) { return ruleBuilder.SetValidator(new UniqueEmailValidator(usersService)); } } }
یک نکته: اگر دقت کرده باشید، فضای نام این اعتبارسنج در این قسمت FluentValidationSample.ModelsValidations شدهاست:
علت اینجا است که اعتبارسنج تعریف شده نیاز دارد هم از مدلها استفاده کند و هم از سرویس کاربران. سرویس کاربران هم از مدلها استفاده میکند. به همین جهت اگر تعاریف اعتبارسنجی را داخل پروژهی مدلها قرار دهیم، یک وابستگی حلقوی رخ خواهد داد (وابستگی مدلها به سرویسها و برعکس). بنابراین بهتر است اعتبارسنجها را به یک پروژهی مجزا منتقل کنیم تا از بروز این cyclic dependency جلوگیری شود.
اعمال اعتبارسنجی منحصربفرد بودن ایمیل دریافتی به اعتبارسنج UserModel
پس از تهیهی متد الحاقی UniqueEmail، آنرا به RuleFor مخصوص خاصیت ایمیل اضافه میکنیم. در اینجا نیز تزریق وابستگی سرویس سفارشی IUsersService به سازندهی کلاس اعتبارسنج مجاز است:
using FluentValidation; using FluentValidationSample.Models; using FluentValidationSample.Services; namespace FluentValidationSample.ModelsValidations { public class UserModelValidator : AbstractValidator<UserModel> { public UserModelValidator(IUsersService usersService) { RuleFor(x => x.Username).NotNull(); RuleFor(x => x.Age).NotNull(); RuleFor(x => x.Experience).LowerThan(nameof(UserModel.Age)).NotNull(); RuleFor(x => x.Email).EmailAddress().NotNull().UniqueEmail(usersService); } } }
ایجاد متادیتای مورد نیاز جهت unobtrusive java script validation در سمت سرور
در ادامه نیاز است ویژگیهای data-val خاص unobtrusive java script validation را توسط FluentValidation ایجاد کنیم:
using FluentValidation; using FluentValidation.AspNetCore; using FluentValidation.Internal; using FluentValidation.Resources; using FluentValidation.Validators; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; namespace FluentValidationSample.ModelsValidations { public class RemoteClientValidator : ClientValidatorBase { public string RemoteUrl { set; get; } public RemoteClientValidator(PropertyRule rule, IPropertyValidator validator) : base(rule, validator) { } public override void AddValidation(ClientModelValidationContext context) { MergeAttribute(context.Attributes, "data-val", "true"); MergeAttribute(context.Attributes, "data-val-remote", GetErrorMessage(context)); MergeAttribute(context.Attributes, "data-val-remote-url", RemoteUrl); } private string GetErrorMessage(ClientModelValidationContext context) { var formatter = ValidatorOptions.MessageFormatterFactory().AppendPropertyName(Rule.GetDisplayName()); string messageTemplate; try { messageTemplate = Validator.Options.ErrorMessageSource.GetString(null); } catch (FluentValidationMessageFormatException) { messageTemplate = ValidatorOptions.LanguageManager.GetStringForValidator<NotEmptyValidator>(); } return formatter.BuildMessage(messageTemplate); } } }
<input dir="ltr" class="form-control input-validation-error" type="email" data-val="true" data-val-email="'آدرس ایمیل' is not a valid email address." data-val-remote="این آدرس ایمیل هم اکنون مورد استفادهاست" data-val-remote-url="/Home/ValidateUniqueEmail" data-val-required="'آدرس ایمیل' must not be empty." id="Email" name="Email" >
افزودن اعتبارسنجهای تعریف شده به تنظیمات برنامه
پس از تعریف UniqueEmailValidator و RemoteClientValidator، روش افزودن آنها به تنظیمات FluentValidation به صورت زیر است:
namespace FluentValidationSample.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddScoped<IUsersService, UsersService>(); services.AddControllersWithViews().AddFluentValidation( fv => { fv.RegisterValidatorsFromAssembly(Assembly.GetExecutingAssembly()); fv.RegisterValidatorsFromAssemblyContaining<RegisterModelValidator>(); fv.RunDefaultMvcValidationAfterFluentValidationExecutes = false; fv.ConfigureClientsideValidation(clientSideValidation => { // ... clientSideValidation.Add( validatorType: typeof(UniqueEmailValidator), factory: (context, rule, validator) => new RemoteClientValidator(rule, validator) { RemoteUrl = "/Home/ValidateUniqueEmail" }); }); } ); }
namespace FluentValidationSample.Web.Controllers { public class HomeController : Controller { private readonly IUsersService _usersService; public HomeController(IUsersService usersService) { _usersService = usersService; } // ... public IActionResult ValidateUniqueEmail(string email) { return Ok(_usersService.IsUniqueEmail(email)); } } }
تعریف کدهای جاوا اسکریپتی مورد نیاز
پیش از هرکاری، اسکریپتهای فایل layout برنامه باید چنین تعریفی را داشته باشند:
<script src="~/lib/jquery/dist/jquery.min.js"></script> <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script> <script src="~/lib/jquery-validation/dist/jquery.validate.js"></script> <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script> <script src="~/js/site.js" asp-append-version="true"></script>
آزمایش برنامه
View این قسمت نیز همانند قسمت قبل است که فقط یک آدرس ایمیل به آن اضافه شدهاست:
برای آزمایش آن اگر برای مثال یکی از آدرسهای ایمیل از پیش تعریف شدهی در متد IsUniqueEmail سرویس کاربران را وارد کنیم، با خطای اعتبارسنجی سمت کلاینت فوق روبرو خواهیم شد.
کدهای کامل این سری را تا این قسمت از اینجا میتوانید دریافت کنید: FluentValidationSample-part04.zip