public class Customer { public string Email { get; private set; } public string Name { get; private set; } private Customer(email, name) { Email = email; Name = name; } public static Result<Customer> New(string email, string name, INewCustomerPolicy policy) { var isUnique = policy.IsUnique(email); if (!isUnique) { return Result.Fail<Customer>("Customer with this email already exists."); } var customer = new Customer(email, name); //customer.AddDomainEvent(new CustomerRegistered(customer)); return Result.Ok(customer); } }
در ادامه مطالب مربوط به برنامه نویسی تابعی، قصد دارم بیشتر وارد کد شویم و مباحث عنوان شده را در دنیای کد پیاده سازی کنیم. هدف این قسمت، refactor کردن کد موجود به یک معماری immutable هست. پیشتر درباره immutable ها صحبت کردیم. ابتدا برای یکسان سازی ادبیات مورد استفاده، چند کلمه را مجددا تعریف خواهیم کرد:
- Immutability: عدم توانایی تغییر داده
- State: دادههایی که در طول زمان تغییر میکنند
- Side Effect: تغییری که روی دادهها اتفاق میافتد
در قطعه کد زیر سعی شدهاست تفاوت یک کلاس Stateless و stateful را به سادگی نشان دهیم:
//Stateful public class UserProfile { private User _user; private string _address; public void UpdateUser(int userId, string name) { _user = new User(userId, name); } } //Stateless public class User { public User(int id, string name) { Id = id; Name = name; } public int Id { get; } public string Name { get; } }
چرا Immutable بودن مهم است؟
هر عمل mutable معادل کدی غیر شفاف است. در واقع وابستگی هر عملی که انجام میدهیم به state، باعث میشود که شرایط ناپایداری را در کد داشته باشیم. به طور مثال در یک عملیات چند نخی تصور کنید که چندین نخ به طور همزمان میتوانند state را تغییر دهند و مدیریت این قضیه باعث به وجود آمدن کدهایی ناخوانا و تحمیل پیچیدگی بیشتر به کد خواهد شد.
در واقع انتظار داریم که به ازای یک ورودی بر اساس بدنهی متد، یک خروجی داشته باشیم؛ ولی در واقعیت تاثیری که اجرای متد بر روی state کل کلاس خواهد گذاشت، از دید ما پنهان است و باعث به وجود آمدن مشکلات بعدی خواهد شد. برای مثال قطعه کد بالا را به صورت Honest بازنویسی میکنیم:
public class UserProfile { private readonly User _user; private readonly string _address; public UserProfile(User user,string address) { _user = user; _address = address; } public UserProfile UpdateUser(int userId, string name) { var newUser = new User(userId, name); return new UserProfile(newUser,_address); } } public class User { public User(int id, string name) { Id = id; Name = name; } public int Id { get; } public string Name { get; } }
در این مثال متد UpdateUser به جای void، یک شی از جنس کلاس UserProfile را بر میگرداند. کلاس UserProfile هم برای وهله سازی نیاز به یک شیء از جنس User و Address را دارد. بنابراین مطمئن هستیم که مقدار دهی شدهاند. نکته دیگر در قطعه کد بالا این است که به ازای هر بار فراخوانی متد، یک شیء جدید بدون وابستگی به وهله سازی اشیاء دیگر، برگردانده میشود.
Immutable
بودن باعث میشود:
- خوانایی کد افزایش پیدا کند
- جای واحدی برای Validate کردن داشته باشیم
- به صورت ذاتی Thread Safe باشیم
در مورد محدودیتهایی که در کار با اشیاء Immutable باید در نظر داشته باشیم، میتوان به مصرف بالای رم و سی پی یو، اشاره کرد. در واقع به نسبت حالت mutate، تعداد اشیاء بیشتری ساخته خواهند شد. در فریمورک دات نت برای کار با اشیا immutable امکاناتی در نظر گرفته شده که این هزینه را کاهش میدهند. به طور مثال میتوانیم از کلاس ImmutableList استفاده کنیم و از ایجاد اشیاء اضافهتر و تحمیل بار اضافی به GC جلوگیری کنیم. یک مثال:
//Create Immutable List ImmutableList<string> list = ImmutableList.Create<string>(); ImmutableList<string> list2 = list.Add("Salam"); //Builder ImmutableList<string>.Builder builder = ImmutableList.CreateBuilder<string>(); builder.Add("avali"); builder.Add("dovomi"); builder.Add("sevomi"); ImmutableList<string> immutableList = builder.ToImmutable();
چطور با side effect کنار بیایم؟
یکی از الگوهای رایج برای این کار، مفهوم جدا سازی Command/Query است. به طور ساده تمامی عملیاتی را که تاثیر گذار هستند، به صورت Command در نظر میگیریم. Command ها معمولا هیچ نوعی را بازگشت نمیدهند و همینطور بر عکس این قضیه برای Query ها صادق است. اشتباه رایج درباره این الگو، محدود کردن این الگو به معماریهای خاصی مانند Domain Driven میباشد؛ در صورتیکه الزامی برای رعایت این الگو در سایر معماریها وجود ندارد.
به مثال زیر دقت کنید. سعی کردم قسمتهای Command و Query را از هم جدا کنم:
در واقع هر برنامه میتواند شامل دو قسمت باشد:
قسمتی که در آن منطق تجاری برنامه پیاده سازی میشود و باید به صورت Immutable
باشد که یک خروجی را تولید میکند و قسمت دیگر برنامه که خروجی تولید شده را برای ذخیره
سازی وضعیت سیستم استفاده میکند.
در واقع یک هسته Immutable، ورودی را دریافت کرده و خروجیهای مورد نیاز را تولید میکند و همه اینها در دل یک پوستهMutable پیاده سازی میشوند که ما در اینجا به آن اصطلاحا Mutable Shell میگوییم.
برای مسائلی که در بالا صحبت شد، نمونهای را آماده کردهام. این نمونه به طور ساده یک سیستم مدیریت نوبت است که نوبتها را در فایلی ذخیره و بازیابی میکند ( mutate ) و منطق مربوط به نوبتها و زمان ویزیت آن میتواند به صورت immutable پیاده سازی شود. این کد در دو حالت functional و غیر functional پیاده سازی شده تا به خوبی تفاوت آن را در حالت قبل و بعد از برنامه نویسی تابعی بتوانیم درک کنیم. به جهت خوانایی بیشتر و دسترسی به کدها، آنها را روی گیتهاب قرار داده و شما میتوانید از اینجا سورس کد مورد نظر را بررسی کنید. سعی شده در این مثال تمامی مواردی که در این قسمت ذکر شد را پیاده سازی کنیم. امیدوارم که مطالب مربوط به برنامه نویسی تابعی یا functional programming توانسته باشد دیدگاه جدیدی را به کدهایی که مینویسیم بدهد. در قسمتهای بعدی به مواردی مانند مدیریت exception ها و کار با null ها و ... خواهیم پرداخت.
برای شروع همانند سایر مطالب میتوانید از این پست جهت راه اندازی هاست سرویسهای Web Api استفاده نمایید. برای فعال سازی قابلیت batching Request نیاز به یک MessageHandler داریم تا بتوانند درخواستهایی از این نوع را پردازش نمایند. خوشبختانه به صورت پیش فرض این Handler پیاده سازی شدهاست و ما فقط باید آن را با استفاده از متد MapHttpBatchRoute به بخش مسیر یابی (Route Handler) پروژه معرفی نماییم.
public class Startup { public void Configuration(IAppBuilder appBuilder) { var config = new HttpConfiguration(); config.Routes.MapHttpBatchRoute( routeName: "Batch", routeTemplate: "api/$batch", batchHandler: new DefaultHttpBatchHandler(GlobalConfiguration.DefaultServer)); config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "Default", routeTemplate: "{controller}/{action}/{name}", defaults: new { name = RouteParameter.Optional } ); config.Formatters.Clear(); config.Formatters.Add(new JsonMediaTypeFormatter()); config.Formatters.JsonFormatter.SerializerSettings.Formatting = Newtonsoft.Json.Formatting.Indented; config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); config.EnsureInitialized(); appBuilder.UseWebApi(config); } }
مهمترین نکتهی آن استفاده از DefaultHttpBatchHandler و معرفی آن به بخش batchHandler مسیریابی است. کلاس DefaultHttpBatchHandler برای وهله سازی نیاز به آبجکت سروری که سرویسهای WebApi در آن هاست شدهاند دارد که با دستور GlobalConfiguration.DefaultServer به آن دسترسی خواهید داشت. در صورتی که HttpServer خاص خود را دارید به صورت زیر عمل نمایید:
var config = new HttpConfiguration(); HttpServer server = new HttpServer(config);
using System.Net.Http; using System.Net.Http.Formatting; public class Program { private static void Main(string[] args) { string baseAddress = "http://localhost:8080"; var client = new HttpClient(); var batchRequest = new HttpRequestMessage(HttpMethod.Post, baseAddress + "/api/$batch") { Content = new MultipartContent("mixed") { new HttpMessageContent(new HttpRequestMessage(HttpMethod.Post, baseAddress + "/api/Book/Add") { Content = new ObjectContent<string>("myBook", new JsonMediaTypeFormatter()) }), new HttpMessageContent(new HttpRequestMessage(HttpMethod.Get, baseAddress + "/api/Book/GetAll")) } }; var batchResponse = client.SendAsync(batchRequest).Result; MultipartStreamProvider streamProvider = batchResponse.Content.ReadAsMultipartAsync().Result; foreach (var content in streamProvider.Contents) { var response = content.ReadAsHttpResponseMessageAsync().Result; } } }
برای دریافت پاسخ این گونه درخواستها نیز از متد الحاقی ReadAsMultipartAsync استفاده میشود که امکان پیمایش بر بدنهی پیام دریافتی را میدهد.
مدیریت ترتیب درخواست ها
شاید این سوال به ذهن شما نیز خطور کرده باشد که ترتیب پردازش این گونه پیامها چگونه خواهد بود؟ به صورت پیش فرض ترتیب اجرای درخواستها حائز اهمیت است. بعنی تا زمانیکه پردازش درخواست اول به اتمام نرسد، کنترل اجرای برنامه، به درخواست بعدی نخواهد رسید که این مورد بیشتر زمانی رخ میدهد که قصد دریافت اطلاعاتی را داشته باشید که قبل از آن باید عمل Persist در پایگاه داده اتفاق بیافتد. اما در حالاتی غیر از این میتوانید این گزینه را غیر فعال کرده تا تمام درخواستها به صورت موازی پردازش شوند که به طور قطع کارایی آن نسبت به حالت قبلی بهینهتر است.
برای غیر فعال کردن گزینهی ترتیب اجرای درخواستها، به صورت زیر عمل نمایید:
config.Routes.MapHttpBatchRoute( routeName: "WebApiBatch", routeTemplate: "api/$batch", batchHandler: new DefaultHttpBatchHandler(GlobalConfiguration.DefaultServer) { ExecutionOrder = BatchExecutionOrder.NonSequential });
سفارشی سازی RoleStore
ASP.NET Core Identity دو سرویس را جهت کار با نقشهای کاربران پیاده سازی کردهاست:
- سرویس RoleStore: کار آن دسترسی به ApplicationDbContext ایی است که در قسمت قبل سفارشی سازی کردیم و سپس ارائهی زیر ساختی به سرویس RoleManager جهت استفادهی از آن برای ذخیره سازی و یا تغییر و حذف نقشهای سیستم.
وجود Storeها از این جهت است که اگر علاقمند بودید، بتوانید از سایر ORMها و یا زیرساختهای ذخیره سازی اطلاعات برای کار با بانکهای اطلاعاتی استفاده کنید. در اینجا از همان پیاده سازی پیشفرض آن که مبتنی بر EF Core است استفاده میکنیم. به همین جهت است که وابستگی ذیل را در فایل project.json مشاهده میکنید:
"Microsoft.AspNetCore.Identity.EntityFrameworkCore": "1.1.0",
برای سفارشی سازی سرویسهای پایهی ASP.NET Core Identity، ابتدا باید سورس این مجموعه را جهت یافتن نگارشی از سرویس مدنظر که کلاسهای موجودیت را به صورت آرگومانهای جنریک دریافت میکند، پیدا کرده و سپس از آن ارث بری کنیم:
public class ApplicationRoleStore : RoleStore<Role, ApplicationDbContext, int, UserRole, RoleClaim>, IApplicationRoleStore
این ارث بری جهت تکمیل، نیاز به بازنویسی سازندهی RoleStore پایه را نیز دارد:
public ApplicationRoleStore( IUnitOfWork uow, IdentityErrorDescriber describer = null) : base((ApplicationDbContext)uow, describer)
در نگارش 1.1 این کتابخانه، بازنویسی متد CreateRoleClaim نیز اجباری است تا در آن مشخص کنیم، نحوهی تشکیل کلید خارجی و اجزای یک RoleClaim به چه نحوی است:
protected override RoleClaim CreateRoleClaim(Role role, Claim claim) { return new RoleClaim { RoleId = role.Id, ClaimType = claim.Type, ClaimValue = claim.Value }; }
در ادامه اگر به انتهای تعریف امضای کلاس دقت کنید، یک اینترفیس IApplicationRoleStore را نیز مشاهده میکنید. برای تشکیل آن بر روی نام کلاس سفارشی خود کلیک راست کرده و با استفاده از ابزارهای Refactoring کار Extract interface را انجام میدهیم؛ از این جهت که در سایر لایههای برنامه نمیخواهیم از تزریق مستقیم کلاس ApplicationRoleStore استفاده کنیم. بلکه میخواهیم اینترفیس IApplicationRoleStore را در موارد ضروری، به سازندههای کلاسهای تزریق نمائیم.
پس از تشکیل این اینترفیس، مرحلهی بعد، معرفی آنها به سیستم تزریق وابستگیهای ASP.NET Core است. چون تعداد تنظیمات مورد نیاز ما زیاد هستند، یک کلاس ویژه به نام IdentityServicesRegistry تشکیل شدهاست تا به عنوان رجیستری تمام سرویسهای سفارشی سازی شدهی ما عمل کند و تنها با فراخوانی متد AddCustomIdentityServices آن در کلاس آغازین برنامه، کار ثبت یکجای تمام سرویسهای ASP.NET Core Identity انجام شود و بیجهت کلاس آغازین برنامه شلوغ نگردد.
ثبت ApplicationDbContext طبق روش متداول آن در کلاس آغازین برنامه انجام شدهاست. سپس معرفی سرویس IUnitOfWork را که از ApplicationDbContext تامین میشود، در کلاس IdentityServicesRegistry مشاهده میکنید.
services.AddScoped<IUnitOfWork, ApplicationDbContext>();
services.AddScoped<IApplicationRoleStore, ApplicationRoleStore>(); services.AddScoped<RoleStore<Role, ApplicationDbContext, int, UserRole, RoleClaim>, ApplicationRoleStore>();
سپس RoleStore توکار ASP.NET Core Identity را نیز به ApplicationRoleStore خود هدایت کردهایم. به این ترتیب هر زمانیکه در کدهای داخلی این فریم ورک وهلهای از RoleStore جنریک آن درخواست میشود، دیگر از همان پیاده سازی پیشفرض خود استفاده نخواهد کرد و به پیاده سازی ما هدایت میشود.
این روشی است که جهت سفارشی سازی تمام سرویسهای پایهی ASP.NET Core Identity بکار میگیریم:
1) ارث بری از نگارش جنریک سرویس پایهی موجود و معرفی موجودیتهای سفارشی سازی شدهی خود به آن
2) سفارشی سازی سازندهی این کلاسها با سرویسهایی که تهیه کردهایم (بجای سرویسهای پیش فرض).
3) تکمیل متدهایی که باید به اجبار پس از این ارث بری پیاده سازی شوند.
4) استخراج یک اینترفیس از کلاس نهایی تشکیل شده (توسط ابزارهای Refactoring).
5) معرفی اینترفیس و همچنین نمونهی توکار این سرویس به سیستم تزریق وابستگیهای ASP.NET Core جهت استفادهی از این سرویس جدید سفارشی سازی شده.
سفارشی سازی RoleManager
در اینجا نیز همان 5 مرحلهای را که عنوان کردیم باید جهت تشکیل کلاس جدید ApplicationRoleManager پیگیری کنیم.
1) ارث بری از نگارش جنریک سرویس پایهی موجود و معرفی موجودیتهای سفارشی سازی شدهی خود به آن
public class ApplicationRoleManager : RoleManager<Role>, IApplicationRoleManager
2) سفارشی سازی سازندهی این کلاس با سرویسی که تهیه کردهایم (بجای سرویس پیش فرض).
public ApplicationRoleManager( IApplicationRoleStore store, IEnumerable<IRoleValidator<Role>> roleValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, ILogger<ApplicationRoleManager> logger, IHttpContextAccessor contextAccessor, IUnitOfWork uow) : base((RoleStore<Role, ApplicationDbContext, int, UserRole, RoleClaim>)store, roleValidators, keyNormalizer, errors, logger, contextAccessor)
الف) ذکر IApplicationRoleStore بجای RoleStore آن
ب) ذکر IUnitOfWork بجای ApplicationDbContext
3) تکمیل متدهایی که باید به اجبار پس از این ارث بری پیاده سازی شوند.
RoleManager پایه نیازی به پیاده سازی اجباری متدی ندارد.
4) استخراج یک اینترفیس از کلاس نهایی تشکیل شده (توسط ابزارهای Refactoring).
محتوای این اینترفیس را در IApplicationRoleManager مشاهده میکنید.
5) معرفی اینترفیس و همچنین نمونهی توکار این سرویس به سیستم تزریق وابستگیهای ASP.NET Core جهت استفادهی از این سرویس جدید سفارشی سازی شده.
services.AddScoped<IApplicationRoleManager, ApplicationRoleManager>(); services.AddScoped<RoleManager<Role>, ApplicationRoleManager>();
سفارشی سازی UserStore
در مورد مدیریت کاربران نیز دو سرویس Store و Manager را مشاهده میکنید. کار کلاس Store، کپسوله سازی لایهی دسترسی به دادهها است تا کتابخانههای ثالث، مانند وابستگی Microsoft.AspNetCore.Identity.EntityFrameworkCore بتوانند آنرا پیاده سازی کنند و کار کلاس Manager، استفادهی از این Store است جهت کار با بانک اطلاعاتی.
5 مرحلهای را که باید جهت تشکیل کلاس جدید ApplicationUserStore پیگیری کنیم، به شرح زیر هستند:
1) ارث بری از نگارش جنریک سرویس پایهی موجود و معرفی موجودیتهای سفارشی سازی شدهی خود به آن
public class ApplicationUserStore : UserStore<User, Role, ApplicationDbContext, int, UserClaim, UserRole, UserLogin, UserToken, RoleClaim>, IApplicationUserStore
2) سفارشی سازی سازندهی این کلاس با سرویسی که تهیه کردهایم (بجای سرویس پیش فرض).
public ApplicationUserStore( IUnitOfWork uow, IdentityErrorDescriber describer = null) : base((ApplicationDbContext)uow, describer)
الف) ذکر IUnitOfWork بجای ApplicationDbContext
3) تکمیل متدهایی که باید به اجبار پس از این ارث بری پیاده سازی شوند.
در اینجا نیز تکمیل 4 متد از کلاس پایه UserStore جنریک انتخاب شده، جهت مشخص سازی نحوهی انتخاب کلیدهای خارجی جداول سفارشی سازی شدهی مرتبط با جدول کاربران ضروری است:
protected override UserClaim CreateUserClaim(User user, Claim claim) { var userClaim = new UserClaim { UserId = user.Id }; userClaim.InitializeFromClaim(claim); return userClaim; } protected override UserLogin CreateUserLogin(User user, UserLoginInfo login) { return new UserLogin { UserId = user.Id, ProviderKey = login.ProviderKey, LoginProvider = login.LoginProvider, ProviderDisplayName = login.ProviderDisplayName }; } protected override UserRole CreateUserRole(User user, Role role) { return new UserRole { UserId = user.Id, RoleId = role.Id }; } protected override UserToken CreateUserToken(User user, string loginProvider, string name, string value) { return new UserToken { UserId = user.Id, LoginProvider = loginProvider, Name = name, Value = value }; }
4) استخراج یک اینترفیس از کلاس نهایی تشکیل شده (توسط ابزارهای Refactoring).
محتوای این اینترفیس را در IApplicationUserStore مشاهده میکنید.
5) معرفی اینترفیس و همچنین نمونهی توکار این سرویس به سیستم تزریق وابستگیهای ASP.NET Core جهت استفادهی از این سرویس جدید سفارشی سازی شده.
services.AddScoped<IApplicationUserStore, ApplicationUserStore>(); services.AddScoped<UserStore<User, Role, ApplicationDbContext, int, UserClaim, UserRole, UserLogin, UserToken, RoleClaim>, ApplicationUserStore>();
سفارشی سازی UserManager
5 مرحلهای را که باید جهت تشکیل کلاس جدید ApplicationUserManager پیگیری کنیم، به شرح زیر هستند:
1) ارث بری از نگارش جنریک سرویس پایهی موجود و معرفی موجودیتهای سفارشی سازی شدهی خود به آن
public class ApplicationUserManager : UserManager<User>, IApplicationUserManager
2) سفارشی سازی سازندهی این کلاس با سرویسی که تهیه کردهایم (بجای سرویس پیش فرض).
public ApplicationUserManager( IApplicationUserStore store, IOptions<IdentityOptions> optionsAccessor, IPasswordHasher<User> passwordHasher, IEnumerable<IUserValidator<User>> userValidators, IEnumerable<IPasswordValidator<User>> passwordValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, IServiceProvider services, ILogger<ApplicationUserManager> logger, IHttpContextAccessor contextAccessor, IUnitOfWork uow, IUsedPasswordsService usedPasswordsService) : base((UserStore<User, Role, ApplicationDbContext, int, UserClaim, UserRole, UserLogin, UserToken, RoleClaim>)store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
الف) ذکر IUnitOfWork بجای ApplicationDbContext (البته این مورد، یک پارامتر اضافی است که بر اساس نیاز این سرویس سفارشی، اضافه شدهاست)
تمام پارامترهای پس از logger به دلیل نیاز این سرویس اضافه شدهاند و جزو پارامترهای سازندهی کلاس پایه نیستند.
ب) استفادهی از IApplicationUserStore بجای UserStore پیشفرض
3) تکمیل متدهایی که باید به اجبار پس از این ارث بری پیاده سازی شوند.
UserManager پایه نیازی به پیاده سازی اجباری متدی ندارد.
4) استخراج یک اینترفیس از کلاس نهایی تشکیل شده (توسط ابزارهای Refactoring).
محتوای این اینترفیس را در IApplicationUserManager مشاهده میکنید.
5) معرفی اینترفیس و همچنین نمونهی توکار این سرویس به سیستم تزریق وابستگیهای ASP.NET Core جهت استفادهی از این سرویس جدید سفارشی سازی شده.
services.AddScoped<IApplicationUserManager, ApplicationUserManager>(); services.AddScoped<UserManager<User>, ApplicationUserManager>();
سفارشی سازی SignInManager
سرویس پایه SignInManager از سرویس UserManager جهت فراهم آوردن زیرساخت لاگین کاربران استفاده میکند.
5 مرحلهای را که باید جهت تشکیل کلاس جدید ApplicationSignInManager پیگیری کنیم، به شرح زیر هستند:
1) ارث بری از نگارش جنریک سرویس پایهی موجود و معرفی موجودیتهای سفارشی سازی شدهی خود به آن
public class ApplicationSignInManager : SignInManager<User>, IApplicationSignInManager
2) سفارشی سازی سازندهی این کلاس با سرویسی که تهیه کردهایم (بجای سرویس پیش فرض).
public ApplicationSignInManager( IApplicationUserManager userManager, IHttpContextAccessor contextAccessor, IUserClaimsPrincipalFactory<User> claimsFactory, IOptions<IdentityOptions> optionsAccessor, ILogger<ApplicationSignInManager> logger) : base((UserManager<User>)userManager, contextAccessor, claimsFactory, optionsAccessor, logger)
الف) استفادهی از IApplicationUserManager بجای UserManager پیشفرض
3) تکمیل متدهایی که باید به اجبار پس از این ارث بری پیاده سازی شوند.
SignInManager پایه نیازی به پیاده سازی اجباری متدی ندارد.
4) استخراج یک اینترفیس از کلاس نهایی تشکیل شده (توسط ابزارهای Refactoring).
محتوای این اینترفیس را در IApplicationSignInManager مشاهده میکنید.
5) معرفی اینترفیس و همچنین نمونهی توکار این سرویس به سیستم تزریق وابستگیهای ASP.NET Core جهت استفادهی از این سرویس جدید سفارشی سازی شده.
services.AddScoped<IApplicationSignInManager, ApplicationSignInManager>(); services.AddScoped<SignInManager<User>, ApplicationSignInManager>();
معرفی نهایی سرویسهای سفارشی سازی شده به ASP.NET Identity Core
تا اینجا سرویسهای پایهی این فریم ورک را جهت معرفی موجودیتهای سفارشی سازی شدهی خود سفارشی سازی کردیم و همچنین آنها را به سیستم تزریق وابستگیهای ASP.NET Core نیز معرفی نمودیم. مرحلهی آخر، ثبت این سرویسها در رجیستری ASP.NET Core Identity است:
services.AddIdentity<User, Role>(identityOptions => { }).AddUserStore<ApplicationUserStore>() .AddUserManager<ApplicationUserManager>() .AddRoleStore<ApplicationRoleStore>() .AddRoleManager<ApplicationRoleManager>() .AddSignInManager<ApplicationSignInManager>() // You **cannot** use .AddEntityFrameworkStores() when you customize everything //.AddEntityFrameworkStores<ApplicationDbContext, int>() .AddDefaultTokenProviders();
در اینجا متد AddIdentity یک سری تنظیمهای پیش فرضها این فریم ورک مانند اندازهی طول کلمهی عبور، نام کوکی و غیره را در اختیار ما قرار میدهد به همراه ثبت تعدادی سرویس پایه مانند نرمال ساز نامها و ایمیلها. سپس توسط متدهای AddUserStore، AddUserManager و ... ایی که مشاهده میکنید، سبب بازنویسی سرویسهای پیش فرض این فریم ورک به سرویسهای سفارشی خود خواهیم شد.
در این مرحلهاست که اگر Migration را اجرا کنید، کار میکند و خطای تبدیل نشدن کلاسها به یکدیگر را دریافت نخواهید کرد.
تشکیل مرحله استفادهی از ASP.NET Core Identity و ثبت اولین کاربر در بانک اطلاعاتی به صورت خودکار
روال متداول کار با امکانات کتابخانههای نوشته شدهی برای ASP.NET Core، ثبت سرویسهای پایهی آنها توسط متدهای Add است که نمونهی services.AddIdentity فوق دقیقا همین کار را انجام میدهد. مرحلهی بعد به app.UseIdentity میرسیم که کار ثبت میانافزارهای این فریم ورک را انجام میدهد. متد UseCustomIdentityServices کلاس IdentityServicesRegistry اینکار را انجام میدهد که از آن در کلاس آغازین برنامه استفاده شدهاست.
public static void UseCustomIdentityServices(this IApplicationBuilder app) { app.UseIdentity(); var identityDbInitialize = app.ApplicationServices.GetService<IIdentityDbInitializer>(); identityDbInitialize.Initialize(); identityDbInitialize.SeedData(); }
الف) متد Initialize آن، متد context.Database.Migrate را فراخوانی میکند. به همین جهت دیگر نیاز به اعمال دستی حاصل Migrations، به بانک اطلاعاتی نخواهد بود. متد Database.Migrate هر مرحلهی اعمال نشدهای را که باقی مانده باشد، به صورت خودکار اعمال میکند.
ب) متد SeedData آن، به نحو صحیحی یک Scope جدید را ایجاد کرده و توسط آن به ApplicationDbContext دسترسی پیدا میکند تا نقش Admin و کاربر Admin را به سیستم اضافه کند. همچنین به کاربر Admin، نقش Admin را نیز انتساب میدهد. تنظیمات این کاربران را نیز از فایل appsettings.json و مدخل AdminUserSeed آن دریافت میکند.
کدهای کامل این سری را در مخزن کد DNT Identity میتوانید ملاحظه کنید.
معرفی کتابخانهی DNTPersianUtils.Core
Cannot implicitly convert type 'System.DateTime?' to 'System.DateTime'. An explicit conversion exists
نحوه استفاده از ViewModel در ASP.NET MVC
public class ProjectViewModel{ public ProjectDto Project{ get; set; } public ProjectProgramDto ProjectProgram { get; set} public IEnumerable<SelectListItem> PriorityListItems { get; set; } }
ویژگی های پیشرفته ی AutoMapper - قسمت دوم
کار زیر را میتونی انجام بدی:
public class Anbar_KalaViewModel { public Anbar Anbar { get; set; } public Kala Kala{ get; set; } public int Tedad { get; set; } } //Class Configure Mapper.CreateMap<Anbar_Kala,Anbar_KalaViewModel>();
سفارشی سازی نرمال سازها
اگر به طراحی جداول ASP.NET Core Identity دقت کنید، تعدادی فیلد اضافی حاوی کلمهی Normalized را هم مشاهده خواهید کرد. برای مثال:
در جدول کاربران، فیلدهای Email و UserName به همراه دو فیلد اضافهی NormalizedEmail و NormalizedUserName وجود دارند.
مقدار دهی و مدیریت این فیلدهای ویژه به صورت خودکار توسط کلاسی به نام UpperInvariantLookupNormalizer صورت میگیرد:
public class UpperInvariantLookupNormalizer : ILookupNormalizer
همانطور که در مطلب «نرمال سازی اطلاعات کاربران در حین ثبت نام» نیز عنوان شد، برای مثال ایمیلهای جیمیل را میتوان با چندین حالت مختلف ثبت کرد و یک کاربر به این صورت میتواند شرط یکتا بودن آدرس ایمیلهای تنظیم شدهی در کلاس IdentityServicesRegistry را دور بزند:
identityOptionsUser.RequireUniqueEmail = true;
چون تنها یک اینترفیس ILookupNormalizer وجود دارد، باید بر اساس محتوای کلیدی که به آن ارسال میشود:
public override string Normalize(string key)
پس از تدارک کلاس CustomNormalizer، تنها کاری را که باید در جهت معرفی و جایگرینی آن انجام داد، تغییر ذیل در کلاس IdentityServicesRegistry است:
services.AddScoped<ILookupNormalizer, CustomNormalizer>(); services.AddScoped<UpperInvariantLookupNormalizer, CustomNormalizer>();
بنابراین دیگر نیازی نیست تا در حین ثبتنام نسبت به تمیزسازی ایمیلها و یا نامهای کاربری اقدام کنیم. سرویس ILookupNormalizer در پشت صحنه به صورت خودکار در تمام مراحل ثبت نام و به روز رسانیها توسط ASP.NET Core Identity استفاده میشود.
سفارشی سازی UserValidator
ASP.NET Core Identity به همراه یک سرویس توکار اعتبارسنج کاربران است که با پیاده سازی اینترفیس IUserValidator ارائه شدهاست:
public class UserValidator<TUser> : IUserValidator<TUser> where TUser : class
بنابراین اگر قصد تهیهی یک IUserValidator جدید را داشته باشیم، از تمام تنظیمات و بررسیهای پیش فرض سرویس توکار UserValidator فوق محروم میشویم. به همین جهت برای سفارشی سازی این سرویس، از خود کلاس UserValidator ارث بری کرده و سپس base.ValidateAsync آنرا فراخوانی میکنیم. با اینکار سبب خواهیم شد تا تمام اعتبارسنجیهای پیشفرض ASP.NET Core Identity اعمال شده و پس از آن منطقهای سفارشی اعتبارسنجی خود را که در کلاس CustomUserValidator قابل مشاهده هستند، اضافه میکنیم.
public override async Task<IdentityResult> ValidateAsync(UserManager<User> manager, User user) { // First use the built-in validator var result = await base.ValidateAsync(manager, user).ConfigureAwait(false); var errors = result.Succeeded ? new List<IdentityError>() : result.Errors.ToList(); // Extending the built-in validator validateEmail(user, errors); validateUserName(user, errors); return !errors.Any() ? IdentityResult.Success : IdentityResult.Failed(errors.ToArray()); }
پس از تدارک کلاس CustomUserValidator، تنها کاری را که باید در جهت معرفی و جایگرینی آن انجام داد، تغییر ذیل در کلاس IdentityServicesRegistry است:
services.AddScoped<IUserValidator<User>, CustomUserValidator>(); services.AddScoped<UserValidator<User>, CustomUserValidator>();
سفارشی سازی PasswordValidator
مراحل سفارشی سازی اعتبارسنج کلمات عبور نیز همانند تهیهی CustomUserValidator فوق است.
ASP.NET Core Identity به همراه یک سرویس توکار اعتبارسنج کلمات عبور کاربران است که با پیاده سازی اینترفیس IPasswordValidator ارائه شدهاست:
public class PasswordValidator<TUser> : IPasswordValidator<TUser> where TUser : class
private static void setPasswordOptions(PasswordOptions identityOptionsPassword, SiteSettings siteSettings) { identityOptionsPassword.RequireDigit = siteSettings.PasswordOptions.RequireDigit; identityOptionsPassword.RequireLowercase = siteSettings.PasswordOptions.RequireLowercase; identityOptionsPassword.RequireNonAlphanumeric = siteSettings.PasswordOptions.RequireNonAlphanumeric; identityOptionsPassword.RequireUppercase = siteSettings.PasswordOptions.RequireUppercase; identityOptionsPassword.RequiredLength = siteSettings.PasswordOptions.RequiredLength; }
"PasswordOptions": { "RequireDigit": false, "RequiredLength": 6, "RequireLowercase": false, "RequireNonAlphanumeric": false, "RequireUppercase": false },
بنابراین در اینجا نیز ارائهی یک پیاده سازی خام از IPasswordValidator سبب خواهد شد تا تمام اعتبارسنجیهای توکار کلاس PasswordValidator اصلی را از دست بدهیم. به همین جهت کار را با ارث بری از همین کلاس توکار شروع کرده و ابتدا متد base.ValidateAsync آنرا فراخوانی میکنیم تا مطمئن شویم، مدخل PasswordOptions تنظیمات یاد شده، حتما پردازش خواهند شد. سپس منطق سفارشی خود را اعمال میکنیم.
برای مثال در کلاس CustomPasswordValidator تهیه شده، به مدخل PasswordsBanList فایل appsettings.json مراجعه شده و کاربران را از انتخاب کلمات عبوری به شدت ساده، منع میکند.
پس از تدارک کلاس CustomPasswordValidator، تنها کاری را که باید در جهت معرفی و جایگرینی آن انجام داد، تغییر ذیل در کلاس IdentityServicesRegistry است:
services.AddScoped<IPasswordValidator<User>, CustomPasswordValidator>(); services.AddScoped<PasswordValidator<User>, CustomPasswordValidator>();
پردازش نتایج اعتبارسنجها
این اعتبارسنجها در خروجیهای IdentityResult تمام متدهای ASP.NET Core Identity ظاهر میشوند. بنابراین فراخوانی سادهی UpdateUserAsync اشتباه است و حتما باید خروجی آنرا جهت پردازش IdentityResult آن بررسی کرد. به همین جهت تعدادی متد الحاقی به کلاس IdentityExtensions اضافه شدهاند تا کارکردن با IdentityResult را سادهتر کنند.
public static void AddErrorsFromResult(this ModelStateDictionary modelStat, IdentityResult result)
public static string DumpErrors(this IdentityResult result, bool useHtmlNewLine = false)
استفادهی از این متدهای الحاقی را در کنترلرهای برنامه میتوانید مشاهده کنید.
استفادهی از اعتبارسنجها جهت انجام Remote validation
اگر به RegisterController دقت کنید، اکشن متدهای ValidateUsername و ValidatePassword قابل مشاهده هستند:
[AjaxOnly, HttpPost, ValidateAntiForgeryToken] [ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)] public async Task<IActionResult> ValidateUsername(string username, string email)
[AjaxOnly, HttpPost, ValidateAntiForgeryToken] [ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)] public async Task<IActionResult> ValidatePassword(string password, string username)
IPasswordValidator<User> passwordValidator, IUserValidator<User> userValidator,
به این ترتیب کاربران در حین ثبت نام، راهنمای بهتری را جهت انتخاب کلمات عبور و نام کاربری مشاهده خواهند کرد و این بررسیها نیز Ajax ایی هستند و پیش از ارسال فرم نهایی به سرور اتفاق میافتند.
برای فعالسازی Remote validation، علاوه بر ثبت اسکریپتهای Ajax ایی، خواص کلاس RegisterViewModel نیز از ویژگی Remote استفاده میکنند:
[Required(ErrorMessage = "(*)")] [Display(Name = "نام کاربری")] [Remote("ValidateUsername", "Register", AdditionalFields = nameof(Email) + "," + ViewModelConstants.AntiForgeryToken, HttpMethod = "POST")] [RegularExpression("^[a-zA-Z_]*$", ErrorMessage = "لطفا تنها از حروف انگلیسی استفاده نمائید")] public string Username { get; set; }
یک نکته: برای اینکه Remote Validation را به همراه ValidateAntiForgeryToken استفاده کنیم، تنها کافی است نام فیلد مخفی آنرا به لیست AdditionalFields به نحوی که مشاهده میکنید، اضافه کنیم.
کدهای کامل این سری را در مخزن کد DNT Identity میتوانید ملاحظه کنید.
بر اساس مستندات دات نت Core 2.0، در فایل Startup.cs، الزاما نیازی به تنظیمات ذیل
public Startup(IHostingEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) .AddEnvironmentVariables(); Configuration = builder.Build(); }
public IConfiguration Configuration { get; } public Startup(IConfiguration configuration) { Configuration = configuration; }
public static void Main(string[] args) { BuildWebHost(args).Run(); } public static IWebHost BuildWebHost(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>() .Build();
در واقع متد CreateDefaultBuilder این کار را انجام میدهد. البته همچنان امکان تنظیمات سفارشی نیز موجود است.
public static IWebHost BuildWebHost(string[] args) { return WebHost.CreateDefaultBuilder() .ConfigureAppConfiguration((ctx, cfg) => { cfg.SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("config.json", true) // require the json file! .AddEnvironmentVariables(); }) .ConfigureLogging((ctx, logging) => { }) // No logging .UseStartup<Startup>() .Build(); }
لازم به ذکر است سرویس IConfiguration از ابتدا در سیستم ثبت شده است و جهت دسترسی به تنظیمات میتوان در قسمتهای مختلف برنامه آن را تزریق نمود.
مهاجرت از SQL Membership به ASP.NET Identity
فرض کنید ما یک سیستم Authentication مثل Membership داریم با 2 تا نقش. حالا میخواهیم نقش A به فرم F1 و بعضی از متدهاش دسترسی داشته و در فرم F2، نقش B به آن دسترسی داشته باشه. خوب تا اینجا فکر کنم پیاده سازیش راحت باشه. ولی مساله از جایی پیچیده میشه که ما بخواهیم در زمان اجرا این تنظیمات رو عوض کنیم مثلا: در فرم F1، نقش A دسترسیش به متدها و حتی به کل فرم تغییر کنه و یا اجازه دسترسی به فرم B بهش داده بشه و این شرایط رو برای چندین نقش و یا در سطح کاربر داشت توصیه چیه؟ از چه تکنولوژی و یا راهکاری میشه به این مقصود رسید.
خیلی ممنون