Parallel Programming در Vb.Net
اینکه من همه کد ها رو به صورت Strored Procedure بنویسم و بعد فقط اون ها رو در EF استفاده کنم اشتباه؟
از این بابت میگم اشتباه چون فکر می کنم با این کار از قدرت EF استفاده نمی کنم.
RadioButtonList در ASP.NET MVC
من که کدی نمیبینم اینجا برای دیباگ و بررسی: آناتومی یک گزارش خطای خوب
ولی احتمالا نام خاصیت ViewModel شما با نام گروهی که تعریف کردید یکی نیست چون در مثال اول بین پارامتر string tech و کلاس زیر فرقی نیست:
public class SomeClass { public string Tech {set; get;} }
برای شروع ابتدا یک اینترفیس تعریف میکنیم:
public interface IFilesService { DirectoryInfo GetDirectoryInfo(string directoryPath); void DeleteFile(string fileName); void WritePersonInFile(string fileName,string name, string lastName, byte age); }
کلاس اصلی:
class FilesServices:IFilesService { public DirectoryInfo GetDirectoryInfo(string directoryPath) { return new DirectoryInfo(directoryPath); } public void DeleteFile(string fileName) { File.Delete(fileName); Console.WriteLine("the file has been deleted"); } public void WritePersonInFile(string fileName, string name, string lastName, byte age) { var text = $"my name is {name} {lastName} with {age} years old from dotnettips.info"; File.WriteAllText(fileName,text); } }
class FilesServicesProxy:IFilesService { private readonly IFilesService _filesService; public FilesServicesProxy() { _filesService=new FilesServices(); } public DirectoryInfo GetDirectoryInfo(string directoryPath) { var existing = Directory.Exists(directoryPath); if (!existing) Directory.CreateDirectory(directoryPath); return _filesService.GetDirectoryInfo(directoryPath); } public void DeleteFile(string fileName) { if(!File.Exists(fileName)) Console.WriteLine("Please enter a valid path"); else _filesService.DeleteFile(fileName); } public void WritePersonInFile(string fileName, string name, string lastName, byte age) { if (!Directory.Exists(fileName.Remove(fileName.LastIndexOf("\\")))) { Console.WriteLine("File Path is not valid"); return; } if (name.Trim().Length == 0) { Console.WriteLine("first name must enter"); return; } if (lastName.Trim().Length == 0) { Console.WriteLine("last name must enter"); return; } if (age<18) { Console.WriteLine("your age is illegal"); return; } if (name.Trim().Length < 3) { Console.WriteLine("first name must be more than 2 letters"); return; } if (lastName.Trim().Length <5) { Console.WriteLine("last name must be more than 4 letters"); return; } _filesService.WritePersonInFile(fileName,name,lastName,age); Console.WriteLine("the file has been written"); } }
در نهایت در بدنه اصلی با تست چندین حالت مختلف، همه متدها را داریم:
static void Main(string[] args) { IFilesService filesService=new FilesServicesProxy(); filesService.WritePersonInFile("c:\\myfakepath\\a.txt","ali","yeganeh",26); var directory = filesService.GetDirectoryInfo("d:\\myrightpath\\"); var fileName = Path.Combine(directory.FullName, "dotnettips.txt"); filesService.WritePersonInFile(fileName, "al", "yeganeh", 26); filesService.WritePersonInFile(fileName, "ali", "yeganeh", 12); filesService.WritePersonInFile(fileName, "ali", "yeganeh", 26); filesService.DeleteFile("c:\\myfakefile.txt"); filesService.DeleteFile(fileName); }
File Path is not valid first name must be more than 2 letters your age is illegal the file has been written Please enter a valid path the file has been deleted
Blazor 5x - قسمت 31 - احراز هویت و اعتبارسنجی کاربران Blazor WASM - بخش 1 - انجام تنظیمات اولیه
using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Text.Json; namespace BlazorWasm.Client.Utils { public class JwtInfo { public IEnumerable<Claim> Claims { set; get; } public DateTime? ExpirationDateUtc { set; get; } public bool IsExpired { set; get; } public IEnumerable<string> Roles { set; get; } } /// <summary> /// From the Steve Sanderson’s Mission Control project: /// https://github.com/SteveSandersonMS/presentation-2019-06-NDCOslo/blob/master/demos/MissionControl/MissionControl.Client/Util/ServiceExtensions.cs /// </summary> public static class JwtParser { public static JwtInfo ParseClaimsFromJwt(string jwt) { var claims = new List<Claim>(); var payload = jwt.Split('.')[1]; var jsonBytes = getBase64WithoutPadding(payload); foreach (var keyValue in JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes)) { if (keyValue.Value is JsonElement element && element.ValueKind == JsonValueKind.Array) { foreach (var itemValue in element.EnumerateArray()) { claims.Add(new Claim(keyValue.Key, itemValue.ToString())); } } else { claims.Add(new Claim(keyValue.Key, keyValue.Value.ToString())); } } var roles = getRoles(claims); var expirationDateUtc = getDateUtc(claims, "exp"); var isExpired = getIsExpired(expirationDateUtc); return new JwtInfo { Claims = claims, Roles = roles, ExpirationDateUtc = expirationDateUtc, IsExpired = isExpired }; } private static IList<string> getRoles(IList<Claim> claims) => claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value).ToList(); private static byte[] getBase64WithoutPadding(string base64) { switch (base64.Length % 4) { case 2: base64 += "=="; break; case 3: base64 += "="; break; } return Convert.FromBase64String(base64); } private static bool getIsExpired(DateTime? expirationDateUtc) => !expirationDateUtc.HasValue || !(expirationDateUtc.Value > DateTime.UtcNow); private static DateTime? getDateUtc(IList<Claim> claims, string type) { var exp = claims.SingleOrDefault(claim => claim.Type == type); if (exp == null) { return null; } var expValue = getTimeValue(exp.Value); if (expValue == null) { return null; } var dateTimeEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); return dateTimeEpoch.AddSeconds(expValue.Value); } private static long? getTimeValue(string claimValue) { if (long.TryParse(claimValue, out long resultLong)) return resultLong; if (float.TryParse(claimValue, out float resultFloat)) return (long)resultFloat; if (double.TryParse(claimValue, out double resultDouble)) return (long)resultDouble; return null; } } }
برای بهبود این وضعیت میتوان مرحلهی دومی را نیز به این فرآیند لاگین افزود؛ پس از اینکه مشخص شد کاربر وارد شدهی به سایت، دارای اکانتی در IDP ما است، کدی را به آدرس ایمیل او ارسال میکنیم. اگر این ایمیل واقعا متعلق به این شخص است، بنابراین قادر به دسترسی به آن، خواندن و ورود آن به برنامهی ما نیز میباشد. این اعتبارسنجی دو مرحلهای را میتوان به عملیات لاگین متداول از طریق ورود نام کاربری و کلمهی عبور در IDP ما نیز اضافه کرد.
تنظیم میانافزار Cookie Authentication
مرحلهی اول ایجاد گردش کاری اعتبارسنجی دو مرحلهای، فعالسازی میانافزار Cookie Authentication در برنامهی IDP است. برای این منظور به کلاس Startup آن مراجعه کرده و AddCookie را اضافه میکنیم:
namespace DNT.IDP { public class Startup { public const string TwoFactorAuthenticationScheme = "idsrv.2FA"; public void ConfigureServices(IServiceCollection services) { // ... services.AddAuthentication() .AddCookie(authenticationScheme: TwoFactorAuthenticationScheme) .AddGoogle(authenticationScheme: "Google", configureOptions: options => { options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; options.ClientId = Configuration["Authentication:Google:ClientId"]; options.ClientSecret = Configuration["Authentication:Google:ClientSecret"]; }); }
اصلاح اکشن متد Login برای هدایت کاربر به صفحهی ورود اطلاعات کد موقتی
تا این مرحله، در اکشن متد Login کنترلر Account، اگر کاربر، اطلاعات هویتی خود را صحیح وارد کند، به سیستم وارد خواهد شد. برای لغو این عملکرد پیشفرض، کدهای HttpContext.SignInAsync آنرا حذف کرده و با Redirect به اکشن متد نمایش صفحهی ورود کد موقتی ارسال شدهی به آدرس ایمیل کاربر، جایگزین میکنیم.
namespace DNT.IDP.Controllers.Account { [SecurityHeaders] [AllowAnonymous] public class AccountController : Controller { [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Login(LoginInputModel model, string button) { // ... if (ModelState.IsValid) { if (await _usersService.AreUserCredentialsValidAsync(model.Username, model.Password)) { var user = await _usersService.GetUserByUsernameAsync(model.Username); var id = new ClaimsIdentity(); id.AddClaim(new Claim(JwtClaimTypes.Subject, user.SubjectId)); await HttpContext.SignInAsync(scheme: Startup.TwoFactorAuthenticationScheme, principal: new ClaimsPrincipal(id)); await _twoFactorAuthenticationService.SendTemporaryCodeAsync(user.SubjectId); var redirectToAdditionalFactorUrl = Url.Action("AdditionalAuthenticationFactor", new { returnUrl = model.ReturnUrl, rememberLogin = model.RememberLogin }); // request for a local page if (Url.IsLocalUrl(model.ReturnUrl)) { return Redirect(redirectToAdditionalFactorUrl); } if (string.IsNullOrEmpty(model.ReturnUrl)) { return Redirect("~/"); } // user might have clicked on a malicious link - should be logged throw new Exception("invalid return URL"); } await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials")); ModelState.AddModelError("", AccountOptions.InvalidCredentialsErrorMessage); } // something went wrong, show form with error var vm = await BuildLoginViewModelAsync(model); return View(vm); }
- سپس بر اساس Id این کاربر، یک ClaimsIdentity تشکیل میشود.
- در ادامه با فراخوانی متد SignInAsync بر روی این ClaimsIdentity، یک کوکی رمزنگاری شده را با scheme تعیین شده که با authenticationScheme تنظیم شدهی در کلاس آغازین برنامه تطابق دارد، ایجاد میکنیم.
await HttpContext.SignInAsync(scheme: Startup.TwoFactorAuthenticationScheme, principal: new ClaimsPrincipal(id));
public interface ITwoFactorAuthenticationService { Task SendTemporaryCodeAsync(string subjectId); Task<bool> IsValidTemporaryCodeAsync(string subjectId, string code); }
- متد IsValidTemporaryCodeAsync، کد دریافت شدهی از کاربر را با نمونهی موجود در بانک اطلاعاتی مقایسه و اعتبار آنرا اعلام میکند.
ایجاد اکشن متد AdditionalAuthenticationFactor و View مرتبط با آن
پس از ارسال کد موقتی به کاربر، کاربر را به صورت خودکار به اکشن متد جدید AdditionalAuthenticationFactor هدایت میکنیم تا این کد موقتی را که به صورت ایمیل (و یا در اینجا با مشاهدهی لاگ برنامه)، دریافت کردهاست، وارد کند. همچنین returnUrl را نیز به این اکشن متد جدید ارسال میکنیم تا بدانیم پس از ورود موفق کد موقتی توسط کاربر، او را باید در ادامهی این گردش کاری به کجا هدایت کنیم. بنابراین قسمت بعدی کار، ایجاد این اکشن متد و تکمیل View آن است.
ViewModel ای که بیانگر ساختار View مرتبط است، چنین تعریفی را دارد:
using System.ComponentModel.DataAnnotations; namespace DNT.IDP.Controllers.Account { public class AdditionalAuthenticationFactorViewModel { [Required] public string Code { get; set; } public string ReturnUrl { get; set; } public bool RememberLogin { get; set; } } }
سپس اکشن متد AdditionalAuthenticationFactor در حالت Get، این View را نمایش میدهد و در حالت Post، اطلاعات آنرا از کاربر دریافت خواهد کرد:
namespace DNT.IDP.Controllers.Account { public class AccountController : Controller { [HttpGet] public IActionResult AdditionalAuthenticationFactor(string returnUrl, bool rememberLogin) { // create VM var vm = new AdditionalAuthenticationFactorViewModel { RememberLogin = rememberLogin, ReturnUrl = returnUrl }; return View(vm); } [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> AdditionalAuthenticationFactor( AdditionalAuthenticationFactorViewModel model) { if (!ModelState.IsValid) { return View(model); } // read identity from the temporary cookie var info = await HttpContext.AuthenticateAsync(Startup.TwoFactorAuthenticationScheme); var tempUser = info?.Principal; if (tempUser == null) { throw new Exception("2FA error"); } // ... check code for user if (!await _twoFactorAuthenticationService.IsValidTemporaryCodeAsync(tempUser.GetSubjectId(), model.Code)) { ModelState.AddModelError("code", "2FA code is invalid."); return View(model); } // login the user AuthenticationProperties props = null; if (AccountOptions.AllowRememberLogin && model.RememberLogin) { props = new AuthenticationProperties { IsPersistent = true, ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration) }; } // issue authentication cookie for user var user = await _usersService.GetUserBySubjectIdAsync(tempUser.GetSubjectId()); await _events.RaiseAsync(new UserLoginSuccessEvent(user.Username, user.SubjectId, user.Username)); await HttpContext.SignInAsync(user.SubjectId, user.Username, props); // delete temporary cookie used for 2FA await HttpContext.SignOutAsync(Startup.TwoFactorAuthenticationScheme); if (_interaction.IsValidReturnUrl(model.ReturnUrl) || Url.IsLocalUrl(model.ReturnUrl)) { return Redirect(model.ReturnUrl); } return Redirect("~/"); }
- فراخوانی HttpContext.SignInAsync با اسکیمای مشخص شده، یک کوکی رمزنگاری شده را در اکشن متد Login ایجاد میکند. اکنون در اینجا با استفاده از متد HttpContext.AuthenticateAsync و ذکر همان اسکیما، میتوانیم به محتوای این کوکی رمزنگاری شده دسترسی داشته باشیم و از طریق آن، Id کاربر را استخراج کنیم.
- اکنون که این Id را داریم و همچنین Code موقتی نیز از طرف کاربر ارسال شدهاست، آنرا به متد IsValidTemporaryCodeAsync که پیشتر در مورد آن توضیح دادیم، ارسال کرده و اعتبارسنجی میکنیم.
- در آخر این کوکی رمزنگاری شده را با فراخوانی متد HttpContext.SignOutAsync، حذف و سپس یک کوکی جدید را بر اساس اطلاعات هویت کاربر، توسط متد HttpContext.SignInAsync ایجاد و ثبت میکنیم تا کاربر بتواند بدون مشکل وارد سیستم شود.
View متناظر با آن نیز در فایل src\IDP\DNT.IDP\Views\Account\AdditionalAuthenticationFactor.cshtml، به صورت زیر تعریف شدهاست تا کد موقتی را به همراه آدرس بازگشت پس از ورود آن، به سمت سرور ارسال کند:
@model AdditionalAuthenticationFactorViewModel <div> <div class="page-header"> <h1>2-Factor Authentication</h1> </div> @Html.Partial("_ValidationSummary") <div class="row"> <div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title">Input your 2FA code</h3> </div> <div class="panel-body"> <form asp-route="Login"> <input type="hidden" asp-for="ReturnUrl" /> <input type="hidden" asp-for="RememberLogin" /> <fieldset> <div class="form-group"> <label asp-for="Code"></label> <input class="form-control" placeholder="Code" asp-for="Code" autofocus> </div> <div class="form-group"> <button class="btn btn-primary">Submit code</button> </div> </fieldset> </form> </div> </div> </div> </div>
آزمایش برنامه جهت بررسی اعتبارسنجی دو مرحلهای
پس از طی این مراحل، اعتبارسنجی دو مرحلهای در برنامه فعال شدهاست. اکنون برای آزمایش آن، برنامهها را اجرا میکنیم. پس از لاگین، صفحهی زیر نمایش داده میشود:
همچنین کد موقتی این مرحله را نیز در لاگهای برنامه مشاهده میکنید:
پس از ورود آن، کار اعتبارسنجی نهایی آن انجام شده و سپس بلافاصله به برنامهی MVC Client هدایت میشویم.
اضافه کردن اعتبارسنجی دو مرحلهای به قسمت ورود از طریق تامین کنندههای هویت خارجی
دقیقا همین مراحل را نیز به اکشن متد Callback کنترلر ExternalController اضافه میکنیم. در این اکشن متد، تا قسمت کدهای مشخص شدن user آن که از اکانت خارجی وارد شدهاست، با قبل یکی است. پس از آن تمام کدهای لاگین شخص به برنامه را از اینجا حذف و به اکشن متد جدید AdditionalAuthenticationFactor در همین کنترلر منتقل میکنیم.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشهی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشهی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آنرا اجرا کنید تا برنامهی IDP راه اندازی شود.
- در آخر به پوشهی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحهی login نام کاربری را User 1 و کلمهی عبور آنرا password وارد کنید.
در این مطلب قصد داریم علاوه بر طراحی زیرساختی برای راه اندازی هرچه سریعتر ServiceLayer، طراحی ای برای مکانیزم Validation به عنوان یک Cross Cutting Concern، نیز ارائه داده و آن را پیاده سازی کنیم.
پیش نیازها:
- قبلا در سایت در مورد لایه بندی نرم افزار و ServiceLayer مطلب منتشر شده است؛ لذا مطالعه این سری مقالات برگرفته از کتاب Professional ASP.NET Design Patterns جزء پیش نیازهای این مطلب میباشد.
- دوره Aspect oriented programming
- مطالب مربوط به کتابخانه FluentValidation
- دوره بررسی مفاهیم معکوس سازی وابستگیها و ابزارهای مرتبط با آن
- دوره AutoMapper
ServiceLayer در معماری لایهای، در برگیرنده ApplicationService هایی میباشد که به عنوان مدخل ورودی (Entry Point) برنامه، در معرض دید لایه Presentation قرار گرفته و داده را به فرمت مورد نیاز Presentation در اختیارش قرار خواهند داد.
این سرویسها DTOها را به عنوان پارامتر دریافت کرده و DTO هایی را به عنوان خروجی برگشت خواهند داد. مباحثی مانند Logging، Caching، Business Validation Authorization و مدیریت تراکنشها را میتوان در این لایه در نظر گرفت.
در ادامه اگر واژه «سرویس» به کار گرفته میشود منظور ما ApplicationServiceها میباشند.
کار را با ارائه یکسری واسط و کلاس پایه برای عملیات CRUD در سرویسها به صورت زیر پیش میبریم.
قرار است به صورت قراردادی، تمام سرویسهای ما واسط زیر را پیاده سازی کرده باشند. این مورد در مباحث تعریف Policyهای مربوط به StructureMap مفید خواهد بود.
namespace MvcFramework.Framework.Application.Services { public interface IApplicationService : ITransientDependency { } }
دو واسط دیگر برای اعمال طول عمر اشیاء به صورت قراردادی در StructureMap به شکل زیر در نظر گرفته شدهاند.
namespace MvcFramework.Framework.Dependency { public interface ISingletonDependency { } } namespace MvcFramework.Framework.Dependency { public interface ITransientDependency { } }
و با پیاده سازی یک LifeCyclePolicy از دو واسط بالا به شکل زیر استفاده خواهیم کرد.
namespace MvcFramework.Framework.Dependency { public class LifeCyclePolicy : IInstancePolicy { public void Apply(Type pluginType, Instance instance) { if (typeof(ISingletonDependency).IsAssignableFrom(instance.ReturnedType)) instance.SetLifecycleTo<SingletonLifecycle>(); else if (typeof(ITransientDependency).IsAssignableFrom(instance.ReturnedType)) instance.SetLifecycleTo<TransientLifecycle>(); } } }
به این صورت تنظیم طول عمر اشیاء ساخته شده توسط StructureMap این بار به صورت قرادادی بوده و لازم به ذکر تک تک این موارد در تنظیمات اولیه مربوط به Container آن نیست.
کلاس پایهای را که پیاده ساز واسط IApplicationService میباشد، برای مقابله با عدم نگارش پذیری واسطها، به شکل زیر در نظر میگیریم.
namespace MvcFramework.Framework.Application.Services { public abstract class ApplicationService : IApplicationService { } }
بسته به نیاز پروژه خودتان میتوانید اعضای مشترک بین سرویسها را در دل این کلاس قرار دهید.
در ادامه واسط ICrudApplicationSevie را به شکل زیر طراحی خواهیم کرد.
namespace MvcFramework.Framework.Application.Services { public interface ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel> : ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, PagedListRequest, PagedListResponse<TModel, PagedListRequest>, DynamicListRequest> where TEditModel : class, IEditModel where TModel : class, IModel where TDeleteModel : class, IDeleteModel { } public interface ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, in TDynamicListRequest> : ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, PagedListRequest, PagedListResponse<TModel, PagedListRequest>, TDynamicListRequest> where TEditModel : class, IEditModel where TModel : class, IModel where TDeleteModel : class, IDeleteModel where TDynamicListRequest : DynamicListRequest { } public interface ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, in TPagedListRequest, TPagedListResponse> : ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, TPagedListRequest, TPagedListResponse, DynamicListRequest> where TEditModel : class, IEditModel where TModel : class, IModel where TDeleteModel : class, IDeleteModel where TPagedListRequest : PagedListRequest, new() where TPagedListResponse : PagedListResponse<TModel, TPagedListRequest> { } public interface ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, in TPagedListRequest, TPagedListResponse, in TDynamicListRequest> : IApplicationService where TEditModel : class, IEditModel where TModel : class, IModel where TDeleteModel : class, IDeleteModel where TPagedListRequest : PagedListRequest, new() where TPagedListResponse : PagedListResponse<TModel, TPagedListRequest> where TDynamicListRequest : DynamicListRequest { void Create(TCreateModel model); void Create(IList<TCreateModel> models); Task CreateAsync(TCreateModel model); Task CreateAsync(IList<TCreateModel> models); IList<TModel> GetList(); DynamicListResponse GetDynamicList(TDynamicListRequest request); TPagedListResponse GetPagedList(TPagedListRequest request); IList<LookupItem> GetLookup(); TModel GetById(long id); TEditModel GetForEdit(long id); bool Exists(long id); Task<IList<TModel>> GetListAsync(); Task<DynamicListResponse> GetDynamicListAsync(TDynamicListRequest request); Task<TPagedListResponse> GetPagedListAsync(TPagedListRequest request); Task<IList<LookupItem>> GetLookupAsync(); Task<TModel> GetByIdAsync(long id); Task<TEditModel> GetForEditAsync(long id); Task<bool> ExistsAsync(long id); void Edit(TEditModel model); void Edit(IList<TEditModel> models); Task EditAsync(TEditModel model); Task EditAsync(IList<TEditModel> models); void Delete(TDeleteModel model); void Delete(IList<TDeleteModel> models); Task DeleteAsync(TDeleteModel model); Task DeleteAsync(IList<TDeleteModel> models); } }
سرویسی که نیاز دارد از عملیات CRUD نیز پشتیبانی داشته باشد، بهتر است واسط آن از یک چنین واسطی که در بالا معرفی شد، ارث بری کند.
مدلها و واسطهای پیش فرضی را که در واسط بالا از آنها استفاده شده است، در زیر مشاهده میکنید:
واسط IModel
namespace MvcFramework.Framework.Application.Models { public interface IModel { long Id { get; set; } } }
واسط IEditModel
namespace MvcFramework.Framework.Application.Models { public interface IEditModel : IModel { byte[] RowVersion { get; set; } } }
واسط IDeleteModel
namespace MvcFramework.Framework.Application.Models { public interface IDeleteModel : IModel { byte[] RowVersion { get; set; } } }
کلاس LookupItem
namespace MvcFramework.Framework.Application.Models { public class LookupItem { public string Value { get; set; } public string Text { get; set; } public bool Selected { get; set; } } }
کلاس PagedListRequest
namespace MvcFramework.Framework.Application.Models { public class PagedListRequest : IShouldNormalize { public long TotalCount { get; set; } public int PageNumber { get; set; } public int PageSize { get; set; } /// <summary> /// Sorting information. /// Should include sorting field and optionally a direction (ASC or DESC) /// Can contain more than one field separated by comma (,). /// </summary> /// <example> /// Examples: /// "Name" /// "Name DESC" /// "Name ASC, Age DESC" /// </example> public string SortBy { get; set; } public void Normalize() { if (PageNumber < 1) PageNumber = 1; if (PageSize < 0) PageSize = 10; if (SortBy.IsEmpty()) SortBy = "Id DESC"; } } }
در این طراحی دو شکل از GetPagedList در نظر گرفته شده است؛ یکی با ورودی و خروجی داینامیک مثلا جهت استفاده برای نمایش اطلاعات در کندو گرید که در ادامه با آن بیشتر آشنا خواهید شد و دیگری هم برای زمانیکه نیاز دارید اطلاعات صفحه بندی شدهای را در اختیار داشته باشید. کلاس بالا برای پیاده سازی شکل دومی که صحبت شد، استفاده میشود. پیاده سازی واسط IShouldNormalize باعث خواهد شد که قبل از اجرای خود متد، این نوع پارامترها با استفاده از یک Interceptor شناسایی شده و متد Normalize آنها اجرا شود.
کلاس PagedListResponse
namespace MvcFramework.Framework.Application.Models { public class PagedListResponse<TModel, TPagedListRequest> where TPagedListRequest : PagedListRequest, new() where TModel : IModel { public PagedListResponse() { Result = new List<TModel>(); Request = new TPagedListRequest(); } public IList<TModel> Result { get; set; } public TPagedListRequest Request { get; set; } } }
کلاس بالا به عنوان نوع خروجی متد GetPagedList مورد استفاده قرار میگرد. وجود خصوصیتی از نوع PagedListRequest هم برای مواردی مانند صفحه بندی نیز میتواند مفید باشد.
کلاسهای DynamicListRequest و DynamicListResponse برگرفته از کتابخانه Kendo.DynamicLinq می باشند.
کلاس Entity
namespace MvcFramework.Framework.Domain.Entities { public abstract class Entity { #region Properties public long Id { get; set; } public byte[] RowVersion { get; set; } public EntityChangeState State { get; set; } #endregion #region Public Methods [SuppressMessage("ReSharper", "BaseObjectGetHashCodeCallInGetHashCode")] [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")] public override int GetHashCode() { if (IsTransient()) return base.GetHashCode(); unchecked { var hash = this.GetRealType().GetHashCode(); return (hash * 31) ^ Id.GetHashCode(); } } public virtual bool IsTransient() { return Id == 0; } public override bool Equals(object obj) { var other = obj as Entity; if (ReferenceEquals(other, null)) return false; if (ReferenceEquals(this, other)) return true; var typeOfThis = this.GetRealType(); var typeOfOther = other.GetRealType(); if (typeOfThis != typeOfOther) return false; if (IsTransient() || other.IsTransient()) return false; return Id.Equals(other.Id); } public override string ToString() { return $"[{this.GetRealType().Name} : {Id}]"; } #endregion #region Operators public static bool operator ==(Entity left, Entity right) { return Equals(left, right); } public static bool operator !=(Entity left, Entity right) { return !(left == right); } #endregion } }
در این کلاس یکسری خصوصیات پایه ای مانند Id و متدهای مشترک بین Entityها قرار گرفته شده است. این کلاس پایه تمام Entityهای سیستم میباشد.
پیاده سازی پیش فرض از واسط ICrudApplicationService به شکل زیر میباشد.
namespace MvcFramework.Framework.Application.Services { public abstract class CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel> : CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel, PagedListRequest, PagedListResponse<TModel, PagedListRequest>, DynamicListRequest> where TEntity : Entity where TCreateModel : class where TEditModel : class, IEditModel where TModel : class, IModel where TDeleteModel : class, IDeleteModel { protected CrudApplicationService(IUnitOfWork unitOfWork, IMapper mapper) : base(unitOfWork, mapper) { } } public abstract class CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel, TDynamicListRequest> : CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel, PagedListRequest, PagedListResponse<TModel, PagedListRequest>, TDynamicListRequest> where TEntity : Entity where TCreateModel : class where TEditModel : class, IEditModel where TModel : class, IModel where TDeleteModel : class, IDeleteModel where TDynamicListRequest : DynamicListRequest { protected CrudApplicationService(IUnitOfWork unitOfWork, IMapper mapper) : base(unitOfWork, mapper) { } } public abstract class CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel, TPagedListRequest, TPagedListResponse> : CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel, TPagedListRequest, TPagedListResponse, DynamicListRequest> where TEntity : Entity where TCreateModel : class where TEditModel : class, IEditModel where TModel : class, IModel where TDeleteModel : class, IDeleteModel where TPagedListRequest : PagedListRequest, new() where TPagedListResponse : PagedListResponse<TModel, TPagedListRequest>, new() { protected CrudApplicationService(IUnitOfWork unitOfWork, IMapper mapper) : base(unitOfWork, mapper) { } } public abstract class CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel, TPagedListRequest, TPagedListResponse, TDynamicListRequest> : ApplicationService, ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, TPagedListRequest, TPagedListResponse, TDynamicListRequest> where TEntity : Entity where TCreateModel : class where TEditModel : class, IEditModel where TModel : class, IModel where TDeleteModel : class, IDeleteModel where TPagedListRequest : PagedListRequest, new() where TPagedListResponse : PagedListResponse<TModel, TPagedListRequest>, new() where TDynamicListRequest : DynamicListRequest { #region Constructor protected CrudApplicationService(IUnitOfWork unitOfWork, IMapper mapper) { Guard.ArgumentNotNull(unitOfWork, nameof(unitOfWork)); Guard.ArgumentNotNull(mapper, nameof(mapper)); UnitOfWork = unitOfWork; Mapper = mapper; EntitySet = UnitOfWork.Set<TEntity>(); } #endregion #region Properties protected IQueryable<TEntity> UnTrackedEntitySet => EntitySet.AsNoTracking(); protected IUnitOfWork UnitOfWork { get; } protected IMapper Mapper { get; } protected IDbSet<TEntity> EntitySet { get; } #endregion #region ICrudApplicationService Members #region Methods [Transactional] public virtual void Create(TCreateModel model) { Guard.ArgumentNotNull(model, nameof(model)); var entity = Mapper.Map<TEntity>(model); EntitySet.Add(entity); UnitOfWork.SaveChanges(); } [Transactional] public virtual void Create(IList<TCreateModel> models) { Guard.ArgumentNotEmpty(models, nameof(models)); var entities = Mapper.Map<IList<TEntity>>(models); UnitOfWork.AddRange(entities); UnitOfWork.SaveChanges(); } [Transactional] public virtual Task CreateAsync(TCreateModel model) { Guard.ArgumentNotNull(model, nameof(model)); var entity = Mapper.Map<TEntity>(model); EntitySet.Add(entity); return UnitOfWork.SaveChangesAsync(); } [Transactional] public virtual Task CreateAsync(IList<TCreateModel> models) { Guard.ArgumentNotEmpty(models, nameof(models)); var entities = Mapper.Map<IList<TEntity>>(models); UnitOfWork.AddRange(entities); return UnitOfWork.SaveChangesAsync(); } [Transactional] public virtual void Edit(TEditModel model) { Guard.ArgumentNotNull(model, nameof(model)); var entity = Mapper.Map<TEntity>(model); UnitOfWork.MarkAsChanged(entity); UnitOfWork.SaveChanges(); } [Transactional] public virtual void Edit(IList<TEditModel> models) { Guard.ArgumentNotNull(models, nameof(models)); Guard.ArgumentNotEmpty(models, nameof(models)); var entities = Mapper.Map<IList<TEntity>>(models); UnitOfWork.UpdateRange(entities); UnitOfWork.SaveChanges(); } [Transactional] public virtual Task EditAsync(TEditModel model) { Guard.ArgumentNotNull(model, nameof(model)); var entity = Mapper.Map<TEntity>(model); UnitOfWork.MarkAsChanged(entity); return UnitOfWork.SaveChangesAsync(); } [Transactional] public virtual Task EditAsync(IList<TEditModel> models) { Guard.ArgumentNotNull(models, nameof(models)); Guard.ArgumentNotEmpty(models, nameof(models)); var entities = Mapper.Map<IList<TEntity>>(models); UnitOfWork.UpdateRange(entities); return UnitOfWork.SaveChangesAsync(); } public virtual IList<TModel> GetList() { return EntitySet.ProjectToList<TModel>(Mapper.ConfigurationProvider); } public virtual DynamicListResponse GetDynamicList(TDynamicListRequest request) { Guard.ArgumentNotNull(request, nameof(request)); var query = ApplyFiltering(request); return query.ProjectTo<TModel>().ToListResponse(request); } public virtual TPagedListResponse GetPagedList(TPagedListRequest request) { Guard.ArgumentNotNull(request, nameof(request)); var query = ApplyFiltering(request); request.TotalCount = query.LongCount(); query = ApplySorting(query, request); query = ApplyPaging(query, request); var result = query.ProjectToList<TModel>(Mapper.ConfigurationProvider); return new TPagedListResponse { Result = result, Request = request }; } public virtual IList<LookupItem> GetLookup() { return EntitySet.ProjectToList<LookupItem>(Mapper.ConfigurationProvider); } public virtual TModel GetById(long id) { Guard.ArgumentInRange(id, 1, long.MaxValue, nameof(id)); var entity = EntitySet.Where(a => a.Id == id).ProjectToFirstOrDefault<TModel>(Mapper.ConfigurationProvider); if (entity == null) throw new EntityNotFoundException($"Couldn't Find Entity {id} When GetById"); return entity; } public virtual TEditModel GetForEdit(long id) { Guard.ArgumentInRange(id, 1, long.MaxValue, nameof(id)); var entity = EntitySet.Where(a => a.Id == id).ProjectToFirstOrDefault<TEditModel>(Mapper.ConfigurationProvider); if (entity == null) throw new EntityNotFoundException($"Couldn't Find Entity {id} When GetForEdit"); return entity; } public virtual bool Exists(long id) { Guard.ArgumentInRange(id, 1, long.MaxValue, nameof(id)); return EntitySet.Any(a => a.Id == id); } public virtual async Task<IList<TModel>> GetListAsync() { return await EntitySet.ProjectToListAsync<TModel>(Mapper.ConfigurationProvider); } public virtual Task<DynamicListResponse> GetDynamicListAsync(TDynamicListRequest request) { Guard.ArgumentNotNull(request, nameof(request)); var query = ApplyFiltering(request); return query.ProjectTo<TModel>().ToListResponseAsync(request); } public virtual async Task<TPagedListResponse> GetPagedListAsync(TPagedListRequest request) { Guard.ArgumentNotNull(request, nameof(request)); var query = ApplyFiltering(request); request.TotalCount = await query.LongCountAsync().ConfigureAwait(false); query = ApplySorting(query, request); query = ApplyPaging(query, request); var result = await query.ProjectToListAsync<TModel>(Mapper.ConfigurationProvider).ConfigureAwait(false); return new TPagedListResponse { Result = result, Request = request }; } public virtual async Task<IList<LookupItem>> GetLookupAsync() { return await EntitySet.ProjectToListAsync<LookupItem>(Mapper.ConfigurationProvider); } public virtual async Task<TModel> GetByIdAsync(long id) { Guard.ArgumentInRange(id, 1, long.MaxValue, nameof(id)); var entity = await UnTrackedEntitySet.Where(a => a.Id == id) .ProjectToFirstOrDefaultAsync<TModel>(Mapper.ConfigurationProvider); if (entity == null) throw new EntityNotFoundException($"Couldn't Find Entity {id} When GetByIdAsync"); return entity; } public virtual async Task<TEditModel> GetForEditAsync(long id) { Guard.ArgumentInRange(id, 1, long.MaxValue, nameof(id)); var entity = await UnTrackedEntitySet.Where(a => a.Id == id) .ProjectToFirstOrDefaultAsync<TEditModel>(Mapper.ConfigurationProvider); if (entity == null) throw new EntityNotFoundException($"Couldn't Find Entity {id} When GetForEditAsync"); return entity; } public virtual Task<bool> ExistsAsync(long id) { Guard.ArgumentInRange(id, 1, long.MaxValue, nameof(id)); return EntitySet.AnyAsync(a => a.Id == id); } [Transactional] public virtual void Delete(TDeleteModel model) { Guard.ArgumentNotNull(model, nameof(model)); var entity = Mapper.Map<TEntity>(model); UnitOfWork.MarkAsDeleted(entity); UnitOfWork.SaveChanges(); } [Transactional] public virtual void Delete(IList<TDeleteModel> models) { Guard.ArgumentNotEmpty(models, nameof(models)); Guard.ArgumentNotEmpty(models, nameof(models)); var entities = Mapper.Map<IList<TEntity>>(models); UnitOfWork.RemoveRange(entities); UnitOfWork.SaveChanges(); } [Transactional] public virtual Task DeleteAsync(TDeleteModel model) { Guard.ArgumentNotNull(model, nameof(model)); var entity = Mapper.Map<TEntity>(model); UnitOfWork.MarkAsDeleted(entity); return UnitOfWork.SaveChangesAsync(); } [Transactional] public virtual Task DeleteAsync(IList<TDeleteModel> models) { Guard.ArgumentNotEmpty(models, nameof(models)); Guard.ArgumentNotEmpty(models, nameof(models)); var entities = Mapper.Map<IList<TEntity>>(models); UnitOfWork.RemoveRange(entities); return UnitOfWork.SaveChangesAsync(); } #endregion #endregion #region Protected Methods /// <summary> /// Apply Filtering To GetDynamicList /// </summary> /// <param name="request"></param> /// <returns></returns> protected virtual IQueryable<TEntity> ApplyFiltering(TDynamicListRequest request) { Guard.ArgumentNotNull(request, nameof(request)); return UnTrackedEntitySet; } /// <summary> /// Apply Filtering To GetPagedList and GetPagedListAsync /// </summary> /// <param name="request"></param> /// <returns></returns> protected virtual IQueryable<TEntity> ApplyFiltering(TPagedListRequest request) { Guard.ArgumentNotNull(request, nameof(request)); return UnTrackedEntitySet; } /// <summary> /// Apply Sorting To GetPagedList and GetPagedListAsync /// </summary> /// <param name="query">query</param> /// <param name="request">PagedListRequest</param> /// <returns></returns> protected virtual IQueryable<TEntity> ApplySorting(IQueryable<TEntity> query, TPagedListRequest request) { Guard.ArgumentNotNull(request, nameof(request)); Guard.ArgumentNotNull(query, nameof(query)); return !request.SortBy.IsEmpty() ? query.OrderBy(request.SortBy) : query.OrderByDescending(e => e.Id); } /// <summary> /// Apply Paging To GetPagedList and GetPagedListAsync /// </summary> /// <param name="request">PagedListRequest</param> /// <param name="query">query</param> /// <returns></returns> protected virtual IQueryable<TEntity> ApplyPaging(IQueryable<TEntity> query, TPagedListRequest request) { Guard.ArgumentNotNull(request, nameof(request)); Guard.ArgumentNotNull(query, nameof(query)); return request != null ? query.Page((request.PageNumber - 1) * request.PageSize, request.PageSize) : query; } #endregion } }
همه متدهای این کلاس پایه، قابلیت override شدن را دارند. به عنوان مثال یکسری متد با دسترسی protected مثلا ApplyFiltering هم برای بازنویسی نحوه فیلتر کردن خروجی GetPagedList میتوانند در SubClassها مورد استفاده قرار گیرند. برای مباحث مرتب سازی هم از کتابخانه System.Linq.Dynamic استفاده شده است.
برای مکانیزم Validation خودکار هم از کتابخانه FluentValidatoin کمک گرفته شده است و با استفاده از Interceptor زیر در صورت یافتن Validator مربوط به Model ورودی، عملیات اعتبارسنجی انجام میگرد و در صورت معتبر نبودن، استثنایی صادر خواهد شد که حاوی اطلاعات مربوط به جزئیات خطاها نیز میباشد.
ValidatorInterceptor
namespace MvcFramework.Framework.Aspects.Validation { public class ValidatorInterceptor : ISyncInterceptionBehavior { private readonly IValidatorFactory _validatorFactory; public ValidatorInterceptor(IValidatorFactory validatorFactory) { _validatorFactory = validatorFactory; } public IMethodInvocationResult Intercept(ISyncMethodInvocation methodInvocation) { var argumentValues = methodInvocation.Arguments.Select(a => a.Value).ToArray(); var validator = new MethodInvocationValidator(_validatorFactory, methodInvocation.MethodInfo, argumentValues); validator.Validate(); return methodInvocation.InvokeNext(); } } }
کتابخانه جانبی دیگری برای AOP توسط تیم StructureMap به نام StructureMap.DynamicInterception ارائه شده است. نمونهی استفاده از آن، در بالا مشخص میباشد. در اینجا انتقال مسئولیت اعتبارسنجی پارامترهای متدی که قرار است Intercept شود، به کلاسی به نام MethodInvocationValidator سپرده شدهاست.
کلاس MethodInvocationValidator
namespace MvcFramework.Framework.Aspects.Validation { internal class MethodInvocationValidator { #region Constructor public MethodInvocationValidator(IValidatorFactory validatorFactory, MethodInfo method, object[] parameterValues) { Guard.ArgumentNotNull(method, nameof(method)); Guard.ArgumentNotNull(parameterValues, nameof(parameterValues)); Guard.ArgumentNotNull(validatorFactory, nameof(validatorFactory)); _method = method; _parameterValues = parameterValues; _validatorFactory = validatorFactory; _parameters = method.GetParameters(); _parametersToBeNormalized = new List<IShouldNormalize>(); } #endregion #region Public Methods public void Validate() { if (!CheckShouldBeValidate()) return; foreach (var parameterValue in _parameterValues) ValidateMethodParameter(parameterValue); foreach (var parameterToBeNormalized in _parametersToBeNormalized) parameterToBeNormalized.Normalize(); } #endregion #region Fields private readonly MethodInfo _method; private readonly object[] _parameterValues; private readonly ParameterInfo[] _parameters; private readonly IValidatorFactory _validatorFactory; private readonly List<IShouldNormalize> _parametersToBeNormalized; #endregion #region Private Methods private bool CheckShouldBeValidate() { if (!_method.IsPublic) return false; if (IsValidationDisabled()) return false; if (_parameters.IsNullOrEmpty()) return false; if (_parameters.Length != _parameterValues.Length) throw new Exception("Method parameter count does not match with argument count!"); return true; } private bool IsValidationDisabled() { if (_method.IsDefined(typeof(EnableValidationAttribute), true)) return false; return ReflectionHelper .GetSingleAttributeOfMemberOrDeclaringTypeOrDefault<DisableValidationAttribute>(_method) != null; } private void ValidateMethodParameter(object parameterValue) { if (parameterValue == null) return; var parameterValueList = parameterValue as IEnumerable<object>; if (parameterValueList != null) { var valueList = parameterValueList.ToList(); ValidateMethodParameterValues(valueList); } else { ValidateMethodParameterValues(new List<object> { parameterValue }); } if (parameterValue is IShouldNormalize) _parametersToBeNormalized.Add(parameterValue as IShouldNormalize); } private void ValidateMethodParameterValues(List<object> valueList) { var ruleSet = GetRuleSet(_method); var validator = _validatorFactory.GetValidator(valueList.First().GetType()); if (validator == null) return; foreach (var item in valueList) ValidateWithReflection(validator, item, ruleSet); } private static string GetRuleSet(MemberInfo method) { const string @default = "default"; var attribute = method.GetCustomAttribute<ValidateWithRuleAttribute>(); if (attribute == null) return @default; var rules = new List<string> { @default }; rules.AddRange(attribute.RuleSetNames); return string.Join(",", rules).TrimEnd(','); } private static void ValidateAndThrow<T>(IValidator<T> validator, T argument, string ruleSet) { validator.ValidateAndThrow(argument, ruleSet); } private void ValidateWithReflection(IValidator validator, object argument, string ruleSet) { GetType().GetMethod(nameof(ValidateAndThrow), BindingFlags.Static | BindingFlags.NonPublic) .MakeGenericMethod(argument.GetType()) .Invoke(null, new[] { validator, argument, ruleSet }); } #endregion } }
در متد Validate آن ابتدا چک میشود که آیا اعتبارسنجی میبایستی انجام شود یا خیر. سپس تک تک آرگومانهای ارسالی را با استفاده از متد ValidateMethodParameter وارد مکانیزم اعتبارسنجی میکند. در داخل این متد ابتدا نوع آرگومان تشخیص داده شده و این مقادیر به متد ValidateMethodParameterValues ارسال شده و داخل آن ابتدا Validator مرتبط را یافته و آن را به متد ValidateWithReflection ارسال میکند. در این بین متد GetRuleSets وظیفه واکشی اسامی RuleSet هایی که بر روی متد مورد نظر تنظیم شده اند را دارد؛ برای مواقعی که از یک ویومدل برای ویرایش، درج و حذف استفاده کنید، در این صورت با توجه به اینکه برای یک ویومدل یک Validator خواهید داشت، امکانات RuleSet مربوط به FluentValidation کارساز خواهند بود. به این صورت که برای هر کدام از عملیات حذف، ویرایش و درج، RuleSet مناسب را تعریف کرده و با استفاده از ValidateWithRuleAttribute برروی متدهای مورد نظر، این ruleها در سیستم اعتبارسنجی ارائه شده اعمال خواهند شد.
با توجه به اینکه متد ValidateAndThrow در واسط IValidator<T> تعریف شدهاست و از آنجاییکه ما نوع داده مدل مورد نظر را هم نداریم لازم است با استفاده از MakeGenericMethod به صورت داینامیک نوع داده T را مشخص کنیم و فراخوانی متد استاتیک ValidatorWithThrow<T> را با Reflection انجام دهیم.
در ادامه لازم است ValidatorInterceptor معرفی شده را به StructureMap نیز معرفی کنیم. برای این منظور به شکل زیر عمل خواهیم کرد.
namespace MvcFramework.Framework { public class FrameworkRegistry : Registry { public FrameworkRegistry() { For<IValidatorFactory>().Singleton().Use<StructureMapValidatorFactory>(); Scan(scan => { scan.TheCallingAssembly(); scan.WithDefaultConventions(); scan.LookForRegistries(); }); Policies.Interceptors(new DynamicProxyInterceptorPolicy(f => typeof(IApplicationService).IsAssignableFrom(f), typeof(ValidatorInterceptor),typeof(TransactionInterceptor))); } } }
در کد بالا با استفاده از DynamicProxyInterceptorPolicy، یک Policy را برای Intercept کردن متدهای مربوط به کلاس هایی که پیاده ساز IApplicationService میباشند، معرفی کردهایم.
کار اعتبارسنجی هم به پایان رسید؛ در زیر استفاده از سرویس پایه معرفی شده را میتوانید مشاهده کنید.
namespace MyApp.ServiceLayer.Roles { public interface IRoleApplicationService : ICrudApplicationService<RoleViewModel, RoleCreateViewModel, RoleEditViewModel, RoleDeleteViewModel, RolePagedListRequest, RoleListViewModel> { } } namespace MyApp.ServiceLayer.Roles { public class RoleApplicationService : CrudApplicationService<Role, RoleViewModel, RoleCreateViewModel, RoleEditViewModel, RoleDeleteViewModel, RolePagedListRequest, RoleListViewModel>, IRoleApplicationService { #region Constructor public RoleApplicationService(IUnitOfWork unitOfWork, IMapper mapper) : base(unitOfWork, mapper) { } #endregion } }
نکته: در این لایه بندی نکات مربوط به مطلب «پیاده سازی ماژولار Autofac» نیز با استفاده از StructureMap اعمال شده است. بدین ترتیب در هر لایه یک Registry مربوط به StructureMap ایجاد شده است. به شکل زیر:
FrameworkRegistry
namespace MyApp.Framework { public class FrameworkRegistry : Registry { public FrameworkRegistry() { For<IValidatorFactory>().Singleton().Use<StructureMapValidatorFactory>(); Scan(scan => { scan.TheCallingAssembly(); scan.WithDefaultConventions(); scan.AssembliesFromApplicationBaseDirectory(); scan.AddAllTypesOf<IRunOnEndTask>(); scan.AddAllTypesOf<IRunOnOwinStartupTask>(); scan.AddAllTypesOf<IRunOnStartTask>(); scan.AddAllTypesOf<IRunOnBeginRequestTask>(); scan.AddAllTypesOf<IRunOnErrorTask>(); scan.AddAllTypesOf<IRunOnEndRequestTask>(); scan.LookForRegistries(); }); Policies.Interceptors(new DynamicProxyInterceptorPolicy(f => typeof(IApplicationService).IsAssignableFrom(f), typeof(ValidatorInterceptor)/*, typeof(TransactionInterceptor)*/)); } } }
DataLayerRegistry
namespace MyApp.DataLayer { public class DataLayerRegistry : Registry { public DataLayerRegistry() { Scan(scan => { scan.TheCallingAssembly(); scan.WithDefaultConventions(); scan.AssembliesFromApplicationBaseDirectory(); scan.AddAllTypesOf<IRunOnStartTask>(); }); //todo:use container per request (Nested Containers) instead of HttpContextLifeCycle For<IUnitOfWork>().Use<ApplicationDbContext>(); } } }
ServiceLayerRegistry
namespace MyApp.ServiceLayer { public class ServiceLayerRegistry : Registry { #region Constructor public ServiceLayerRegistry() { Scan(scan => { scan.TheCallingAssembly(); scan.WithDefaultConventions(); scan.AssembliesFromApplicationBaseDirectory(); scan.AddAllTypesOf<IRunOnEndTask>(); scan.AddAllTypesOf<IRunOnOwinStartupTask>(); scan.AddAllTypesOf<IRunOnStartTask>(); scan.AddAllTypesOf<IRunOnBeginRequestTask>(); scan.AddAllTypesOf<IRunOnErrorTask>(); scan.AddAllTypesOf<IRunOnEndRequestTask>(); scan.Assembly(typeof(DataLayerRegistry).Assembly); scan.LookForRegistries(); scan.AddAllTypesOf<Profile>().NameBy(item => item.FullName); scan.AddAllTypesOf<IHaveCustomMappings>().NameBy(item => item.FullName); }); FluentValidationConfig(); AutoMapperConfig(); } #endregion #region Private Methods private void AutoMapperConfig() { For<MapperConfiguration>().Singleton().Use("MapperConfig", ctx => { var config = new MapperConfiguration(cfg => { cfg.CreateMissingTypeMaps = true; AddProfiles(ctx, cfg); AddIHaveCustomMappings(ctx, cfg); AddMapFrom(cfg); }); config.AssertConfigurationIsValid(); return config; }); For<IMapper>().Singleton().Use(ctx => ctx.GetInstance<MapperConfiguration>().CreateMapper(ctx.GetInstance)); } private void FluentValidationConfig() { AssemblyScanner.FindValidatorsInAssembly(Assembly.GetExecutingAssembly()) .ForEach(result => { For(result.InterfaceType) .Singleton() .Use(result.ValidatorType); }); } private static void AddMapFrom(IProfileExpression cfg) { var types = typeof(RoleViewModel).Assembly.GetExportedTypes(); var maps = (from t in types from i in t.GetInterfaces() where i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IMapFrom<>) && !t.IsAbstract && !t.IsInterface select new { Source = i.GetGenericArguments()[0], Destination = t }).ToArray(); foreach (var map in maps) cfg.CreateMap(map.Source, map.Destination); } private static void AddProfiles(IContext ctx, IMapperConfigurationExpression cfg) { var profiles = ctx.GetAllInstances<Profile>().ToList(); foreach (var profile in profiles) cfg.AddProfile(profile); } private static void AddIHaveCustomMappings(IContext ctx, IMapperConfigurationExpression cfg) { var mappings = ctx.GetAllInstances<IHaveCustomMappings>().ToList(); foreach (var mapping in mappings) mapping.CreateMappings(cfg); } #endregion } }
WebRegistry
namespace MyApp.Web { public class WebRegistry : Registry { public WebRegistry() { Scan(scan => { scan.TheCallingAssembly(); scan.WithDefaultConventions(); scan.AssembliesFromApplicationBaseDirectory(); scan.AddAllTypesOf<IRunOnEndTask>(); scan.AddAllTypesOf<IRunOnOwinStartupTask>(); scan.AddAllTypesOf<IRunOnStartTask>(); scan.AddAllTypesOf<IRunOnBeginRequestTask>(); scan.AddAllTypesOf<IRunOnErrorTask>(); scan.AddAllTypesOf<IRunOnEndRequestTask>(); scan.Assembly(typeof(ServiceLayerRegistry).Assembly); scan.LookForRegistries(); }); } } }
در این طراحی، لایه Web یا همان Presentation به DataLayer و DomainClasses هیچ ارجاعی ندارد.
در قسمت بعد استفاده از این سرویس را در یک برنامه ASP.NET MVC با هم بررسی خواهیم کرد.
کدهای کامل این قسمت را میتوانید از اینجا دریافت کنید.
تمام توسعه دهندههای JavaScript با واژهی کلیدی var آشنایی دارند؛ اما TypeScript واژههای کلیدی let و const را نیز اضافه کردهاست (که جزئی از ES 6 نیز میباشند). تفاوت مهم بین var و let، در میدان دید متغیرهای تعریف شدهی توسط آنها خلاصه میشود. پیشتر در سری مباحث بررسی ES 6، مطلب «متغیرها در ES 6» را نیز بررسی کردیم که در TypeScript نیز صادق میباشند؛ با این تفاوت که TypeScript را میتوان به ES 5 نیز کامپایل کرد و بدون مشکل با تمام مرورگرهای موجود، اجرا نمود.
- متغیرهایی که با var تعریف میشوند، به صورت سراسری در متدی که تعریف شدهاند، قابل دسترسی هستند؛ حتی اگر 5 سطح داخل ifهای تو در تو تعریف شده باشند. اما let و const تنها در block و قطعهای که معرفی شدهاند، معتبر بوده و خارج از آن تعریف نشدهاند. در اینجا یک block توسط {} معرفی میشود.
- متغیرهای از نوع var به دلیل مفهومی به نام hoisting توسط runtime جاوا اسکریپت، به بالاترین سطح متد منتقل میشوند. به همین دلیل عمق تعریف آنها، اثری در دسترسی به این متغیرها ندارد. اما hoisting در مورد let و const اعمال نمیشود.
- متغیرهای var را میتوان چندبار مجددا تعریف کرد (هرچند این روش توصیه نمیشود؛ اما مجاز است). یک چنین تعریف مجددی با متغیرهای از نوع let و const مجاز نیست.
برای توضیحات بیشتر به مثال ذیل دقت کنید:
function ScopeTest() { if (true) { var foo = 'use anywhere'; let bar = 'use in this block'; } console.log(foo); // works! console.log(bar); // error! }
نوعهای پایهی TypeScript
نوعهای پایهی TypeScript شامل موارد ذیل هستند:
Boolean: برای ذخیره سازی true یا false.
let isDone: boolean = false;
let decimal: number = 6; let hex: number = 0xf00d; let binary: number = 0b1010; let octal: number = 0o744;
let name: string = "bob"; name = 'smith';
let list: number[] = [1, 2, 3];
enum Color {Red, Green, Blue}; let c: Color = Color.Green;
let notSure: any = 4; notSure = "maybe a string instead"; notSure = false; // okay, definitely a boolean
function warnUser(): void { alert("This is my warning message"); }
یک نکته: قابلیت Template string در ES 6 نیز در TypeScript پشتیبانی میشود.
مفهوم Type Inference در TypeScript
در TypeScript الزاما نیازی نیست تا نوع متغیری را صریحا مشخص کرد. در اینجا اگر نوع متغیری را در ابتدای کار تعریف نکنید، نوع آن در اولین باری که مقدار دهی میشود، مشخص خواهد شد که به آن Type Inference نیز میگویند.
let myString= 'this is a string'; myString= 42; // error!
و یا در مثال ذیل، نوع خروجی متد ReturnNumber به صورت صریح مشخص نشدهاست:
function ReturnNumber() { return 42; } let anotherString = 'this is also a string'; anotherString = ReturnNumber(); // error!
تعریف صریح نوعها در TypeScript با استفاده از Type Annotations
برای لغو Type Inference و تعیین صریح نوعها در TypeScript میتوان به صورت زیر عمل کرد:
let myString : string = 'this is a string'; myString = 42; // error! function ReturnNumber() : number { return 42; } let anotherString : string = 'this is also a string'; anotherString = ReturnNumber(); // error!
نوعهای شمارشی در TypeScript
Enums در بسیاری از زبانهای برنامه نویسی متداول وجود دارند و هدف از آنها، دادن نامهایی بهتر و مشخص، به مجموعهای از مقادیر است:
enum Category { Biography, Poetry, Fiction }; // 0, 1, 2
اگر میخواهید این مقادیر با اعداد دیگری شروع شوند (بجای صفر پیش فرض)، میتوان مقدار اولین نام را به صورت صریحی مشخص کرد:
enum Category { Biography = 1, Poetry, Fiction }; // 1, 2, 3
enum Category{ Biography = 5, Poetry = 8, Fiction = 9 }; // 5, 8, 9
let favoriteCategory: Category = Category.Biography;
در این حالت اگر مقدار favoriteCategory را چاپ کنیم، خروجی عددی 5 نمایش داده میشود:
console.log(favoriteCategory); // 5
let categoryString= Category[favoriteCategory]; // Biography
آرایهها در TypeScript
در حالت عمومی، آرایهها در TypeScript همانند جاوا اسکریپت تعریف میشوند؛ البته به همراه تعدادی استثناء. در TypeScript سه روش برای تعریف آرایهها وجود دارند:
الف) در مثال زیر آرایهای از رشتهها تعریف شدهاست. در اینجا نوع آرایه به همراه [] مشخص میشود:
let strArray1: string[] = ['here', 'are', 'strings'];
let strArray2: Array<string> = ['more', 'strings', 'here'];
let anyArray: any[] = [42, true, 'banana'];
نوع Tuples در TypeScript
Tuples در TypeScript نوع خاصی از آرایهها هستند که نوع مقادیر اعضای آنها به صورت صریح مشخص میشوند:
let myTuple: [number, string] = [25, 'truck'];
اکنون برای دسترسی به مقادیر این المانها، همانند کار با آرایههای معمولی، از ایندکسهای آرایهی تعریف شده استفاده میشود:
let firstElement= myTuple[0]; // 25 let secondElement= myTuple[1]; // truck
یک نکته: میتوان به آرایهی تعریف شده، عناصر جدیدی را نیز افزود؛ با این شرط که نوع آنها، یکی از نوعهای مشخص شدهی در تعریف Tuple باشند:
// other elements can have numbers or strings myTuple[2] = 100; myTuple[2] = 'this works!';
مفهوم Type assertions در TypeScript
حتما با مفهوم cast و تبدیل نوعهای مختلف به یکدیگر، در زبانهای دیگر برنامه نویسی آشنا هستید. در TypeScript نیز این مفهوم تحت عنوان Type assertions پشتیبانی میشود و دو روش برای تعریف آن وجود دارد:
الف) تعریف cast توسط angle-bracket syntax که در آن نوع مدنظر داخل یک <> قرار میگیرد:
let someValue: any = "this is a string"; let strLength: number = (<string>someValue).length;
ب) تعریف cast توسط as syntax به نحو ذیل:
let someValue: any = "this is a string"; let strLength: number = (someValue as string).length;
ما در ViewModel دسترسی مستقیمی به هیچ یک از اشیاء موجود در View نداریم (و درستش هم همین است). الان فرض کنید که میخواهیم از طریق ViewModel یک View را ببندیم؛ مثلا متد Close آن پنجره را فراخوانی کنیم. به عبارتی در حالت کلی میخواهیم یکی از متدهای تعریف شده یکی از عناصر بصری موجود در View را از طریق ViewModel فراخوانی نمائیم.
برای حل این مساله از فایلهای همان SDK مرتبط با Expression blend استفاده خواهیم کرد.
ابتدا ارجاعاتی را به اسمبلیهای System.Windows.Interactivity.dll و Microsoft.Expression.Interactions.dll اضافه میکنیم.
سپس دو فضای نام مرتبط هم باید اضافه شوند:
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
یک مثال عملی:
قصد داریم از طریق ViewModel ، پنجرهای را ببندیم. کدهای XAML این مثال را در ادامه مشاهده خواهید کرد:
<Window x:Class="WpfCallMethodActionSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
xmlns:vm="clr-namespace:WpfCallMethodActionSample.ViewModels"
Name="ThisWindow"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<vm:MainWindowViewModel x:Key="vmMainWindowViewModel" />
</Window.Resources>
<Grid DataContext="{Binding Source={StaticResource vmMainWindowViewModel}}">
<Button Content="Save & Close" VerticalAlignment="Top" Margin="5">
<i:Interaction.Triggers>
<!--فراخوانی متدی در ویوو مدل-->
<i:EventTrigger EventName="Click">
<ei:CallMethodAction
TargetObject="{Binding}"
MethodName="SaveButtonClicked" />
</i:EventTrigger>
<!--فراخوانی متدی در شیء جاری از طریق ویوو مدل-->
<i:EventTrigger SourceObject="{Binding}" EventName="CloseMainWindow">
<ei:CallMethodAction
TargetObject="{Binding ElementName=ThisWindow}"
MethodName="Close"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
</Grid>
</Window>
همچنین ViewModel تعریف شده نیز همین چند سطر زیر است:
using System;
namespace WpfCallMethodActionSample.ViewModels
{
public class MainWindowViewModel
{
public void SaveButtonClicked()
{
close();
}
public event EventHandler CloseMainWindow;
private void close()
{
if (CloseMainWindow != null) CloseMainWindow(this, EventArgs.Empty);
}
}
}
توضیحات:
اگر به ViewModel دقت کنید خبری از DelegateCommand در آن نیست. بله، به کمک ترکیبی از EventTrigger و CallMethodAction میتوان جایگزینی را جهت DelegateCommand معرفی شده در قسمتهای قبل این سری مباحث MVVM ارائه داد.
EventTrigger در اینجا به این معنا است که اگر EventName ذکر شده رخ داد، آنگاه این اعمال را انجام بده. مثلا در اینجا CallMethodAction را فراخوانی کن.
CallMethodAction در اسمبلی Microsoft.Expression.Interactions.dll تعریف شده است و تنها متدی از نوع void و بدون پارامتر را میتواند به صورت خودکار فراخوانی کند (محدودیت مهم آن است).
اینکه این متد کجا قرار دارد، توسط TargetObject آن مشخص میشود. اگر TargetObject را مساوی Binding قرار دادیم، یعنی به دنبال متدی که در DataContext گرید وجود دارد بگرد. به عبارتی به صورت خودکار به SaveButtonClicked تعریف شده در ViewModel ما متصل خواهد شد و آنرا فراخوانی میکند.
تا اینجا رخداد Click دکمه تعریف شده را به متد SaveButtonClicked موجود در ViewModel سیم کشی کردیم.
در مرحله بعد میخواهیم از طریق ViewModel ، متدی را در View فراخوانی کنیم. نکته آن هم پیشتر ذکر شد؛ TargetObject صحیحی را باید انتخاب کرد. در اینجا برای پنجره جاری نام ThisWindow تعریف شده است و از طریق تعریف:
TargetObject="{Binding ElementName=ThisWindow}"
به CallMethodAction خواهیم گفت که قرار است متد Close را در شیء ThisWindow فراخوانی کنی.
همچنین نحوه تعریف EventTrigger ما هم در اینجا برعکس شده است:
<i:EventTrigger SourceObject="{Binding}" EventName="CloseMainWindow">
قبلا به دنبال مثلا رخداد Click یک دکمه بودیم، اکنون با توجه به SourceObject تعریف شده، در ViewModel به دنبال این رخداد که برای نمونه در اینجا CloseMainWindow نام گرفته خواهیم گشت.
بنابراین View اینبار به رخداد CloseMainWindow تعریف شده در ViewModel سیم کشی خواهد شد. اکنون اگر این رخداد در ViewModel فراخوانی شود، CallMethodAction متناظر فعال شده و متد Close پنجره را فراخوانی میکند.