Popover بوت استرپ برای کار با منابع remote طراحی نشدهاست و نیاز است توابع API آنرا به همراه jQuery Ajax ترکیب کرد تا به تصویر فوق رسید.
مرحلهی اول: اکشن متدی که یک partial view را باز میگرداند
فرض کنید اکشن متدی که لیست کاربران رای دادهی به یک مطلب را باز میگرداند، چنین شکلی را دارد:
public ActionResult RenderResults(string param1) { var users = new[] { new User{ Id = 1, Name = "Test 1", Rating = 3}, new User{ Id = 2, Name = "Test 2", Rating = 4}, new User{ Id = 3, Name = "Test 3", Rating = 5} }; return PartialView("_RenderResults", model: users); }
@using RemotePopOver.Models @model IList<User> <ul id="ratings1" data-title="Ratings" class="list-unstyled"> @foreach (var user in Model) { <li> @user.Name <span class="badge pull-right">@user.Rating</span> </li> } </ul>
مرحلهی دوم: دریافت اطلاعات partial view با استفاده از jQuery Ajax و سپس درج آن در یک popover
میخواهیم با حرکت ماوس بر روی دکمهی سفارشی ذیل، یک popover ظاهر شده و محتوای خودش را از اکشن متد فوق تامین کند.
<span id="remotePopover1" aria-hidden="true" data-param1="test" data-popover-content-url="@Url.Action("RenderResults", "Home")" class="glyphicon glyphicon-info-sign btn btn-info"></span>
در ادامه نحوهی استفادهی از این ویژگیها را در jQuery Ajax مشاهده میکنید:
@section Scripts { <script type="text/javascript"> $(document).ready(function () { $('body').on('mouseenter', 'span[data-popover-content-url]', function () { var el = $(this); $.ajax({ type: "POST", url: $(this).data("popover-content-url"), data: JSON.stringify({ param1: $(this).data("param1") }), contentType: "application/json; charset=utf-8", dataType: "json", // controller is returning a simple text, not json complete: function (xhr, status) { var data = xhr.responseText; if (status === 'error' || !data) { el.popover({ content: 'Error connecting server!', trigger: 'focus', html: true, container: 'body', placement: 'auto', title: 'Error!' }).popover('show'); } else { el.popover({ content: data, trigger: 'focus', html: true, container: 'body', placement: 'auto', title: $('<html />').html(data).find('#ratings1:first').data('title') }).popover('show'); } } }); }).on('mouseleave', 'span[data-popover-content-url]', function () { $(this).popover('hide'); }); }); </script> }
خروجی partial view به صورت json نیست. بنابراین باید اطلاعات نهایی آنرا در callback ویژهی complete دریافت کرد. مقدار data دریافتی، معادل اطلاعات رندر شدهی partial view است. به همین جهت آنرا به خاصیت content متد popver ارسال میکنیم. همچنین چون خروجی patrtial view به همراه html است، نیاز است خاصیت html متد popover نیز به true تنظیم شود. در خاصیت title، نحوهی دسترسی به مقدار data-title تنظیم شدهی در partial view را مشاهده میکنید.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید:
RemotePopOver.zip
امکان تعریف alias، قابلیت جدیدی نیست!
در نگارشهای پیشین زبان #C نیز میتوان برای نوعهای نامدار داتنت، alias/«نام مستعار» تعریف کرد؛ برای مثال:
using MyConsole = System.Console; MyConsole.WriteLine("Test console");
بنابراین دو حالت تعریف Namespace alias برای کوتاه سازی فضاهای نام طولانی و یا تعریف Type alias برای معرفی یک نام مستعار جدید برای نوعی مشخص، میسر است:
// Namespace alias using SuperJSON = System.Text.Json; var document = SuperJSON.JsonSerializer.Serialize("{}"); // Type alias using SuperJSON = System.Text.Json.JsonSerializer; var document = SuperJSON.Serialize("{}");
تنها کارکرد نامهای مستعار، کوتاه و زیبا سازی نامهای طولانی نیستند. برای مثال گاهی از اوقات ممکن است که بین نام نوعهای موجود در usingهای جاری، تداخل حاصل شود و برنامه کامپایل نشود. برای مثال فرض کنید که دو using زیر را تعریف کردهاید:
using UnityEngine; using System; Random rnd = new Random();
var rnd = new System.Random();
using Random = System.Random;
امکان تعریف alias برای هر نوعی در C# 12.0
محدودیت امکان تعریف alias برای نوعهای نامدار داتنت در C# 12.0 برطرف شده و اکنون میتوان برای انواع و اقسام نوعها مانند آرایهها، tuples و غیره نیز alias تعریف کرد:
using Ints = int[]; using DatabaseInt = int?; using OptionalFloat = float?; using Grade= decimal; using Point3D = (int, int, int); using Person = (string name, int age, string country); using unsafe P = char*; using Matrix = int[][]; Matrix aMatrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
یک نکته: امکان تعریف alias برای nullable reverence types وجود ندارد.
بررسی یک مثال C# 12.0
در اینجا محتویات یک فایل Program.cs یک برنامهی کنسول داتنت 8 را مشاهده میکنید:
using MyConsole = System.Console; using Person = (string name, int age, string country); Person person = new("User 1", 33, "Iran"); Console.WriteLine(person); PrintPerson(person); MyConsole.WriteLine("Test console"); static void PrintPerson(Person person) { MyConsole.WriteLine($"{person.name}, {person.age}, {person.country}"); }
(User 1, 33, Iran) User 1, 33, Iran Test console
چه زمانی بهتر است از قابلیت تعریف نامهای مستعار نوعها و یا فضاهای نام استفاده شود؟
اگر یک نام طولانی را بتوان به این صورت خلاصه کرد، مفید هستند؛ برای مثال ساده سازی تعریف یک لیست طولانی به صورت زیر:
using Companies = System.Collections.Generic.List<Company>; Companies GetCompanies() { // logic here } class Company { public string Name; public int Id; }
using EventHandlers = System.Collections.Generic.IEnumerable<System.Func<System.Threading.Tasks.Task>>;
و یا اگر بتوانند رفع تداخلی را حاصل کنند، بکارگیری آنها ضروری است (مانند مثال شیء Random ابتدای بحث) و یا اگر بتوانند از تکرار تعریف یک tuple جلوگیری کنند، ذکر آنها یک refactoring مثبت بهشمار میرود؛ مانند مثال زیر که در آن از تعریف نوع tuple ای، دوبار استفاده شدهاست:
using Country = (string Abbreviation, string Name); Country GetCountry(string abbreviation) { // Logic here } List<Country> GetCountries() { // Logic here }
using Ints = int[]; using DatabaseInt = int?; using OptionalFloat = float?;
میدان دید نامهای مستعار
به صورت پیشفرض، تمام نامهای مستعار تنها در داخل همان فایلی که تعریف شدهاند، قابل استفاده میباشند. از زمان C# 10.0 ، میتوان پیش از واژهی کلیدی using از واژهی کلیدی global نیز استفاده کرد تا تعریف آنها فقط در پروژهی جاری به صورت سراسری قابل دسترسی شود.
به همین جهت اگر نوعی قرار است در سایر پروژهها استفاده شود، بهتر است از global using استفاده نشده و از همان روشهای متداول تعریف records و یا classes استفاده شود.
اگر با MVC کار کرده باشید حتما با ModelBinding آن آشنا هستید؛ DefaultModelBinder توکار آن که در اکثر مواقع، باری زیادی را از روی دوش برنامه نویسان بر میدارد و کار را برای آنان راحتتر میکند.
اما در بعضی مواقع این مدل بایندر پیش فرض ممکن است پاسخگوی نیاز ما در
بایند کردن یک خصوصیت از یک مدل خاص نباشد، برای همین ما نیاز داریم که کمی آن را سفارشی سازی کنیم.
برای این کار ما دو راه داریم:
1) یک مدل بایندر جدید را با پیاده سازی IModelBinder تهیه کنیم. (در این حالت ما مجبوریم که مدل بایندر را از ابتدا جهت بایند
کردن کلیه مقادیر شی مدل خود، بازنویسی کنیم و در واقع امکان انتساب آنرا در سطح فقط یک خصوصیت نداریم.) (نحوه پیاده سازی قبلا در اینجا مطرح شده)
2) ModelBinder پیش فرض را جهت پاسخگویی به نیازمان توسعه دهیم. (که در این مطلب قصد آموزشش را داریم.)
فرض کنید که میخواهید بر اساس یک Enum در صفحه، یک DropDownFor معادل را قرار بدید که به طور خودکار رشته انتخاب شده را به یک خصوصیت مدل که از نوع بایت هست بایند بکند.
@Html.DropDownListFor(model => model.AccountType, new SelectList(Enum.GetNames(typeof(Enums.AccountType))))
public enum AccountType : byte { مدیر = 0, کاربر_حقیقی = 1, کاربر_حقوقی = 2, }
namespace MvcApplication1.Models { [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] public abstract class PropertyBindAttribute : Attribute { public abstract bool BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor); } public class ExtendedModelBinder : DefaultModelBinder { protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor) { if (propertyDescriptor.Attributes.OfType<PropertyBindAttribute>().Any()) { var modelBindAttr = propertyDescriptor.Attributes.OfType<PropertyBindAttribute>().FirstOrDefault(); if (modelBindAttr.BindProperty(controllerContext, bindingContext, propertyDescriptor)) return; } base.BindProperty(controllerContext, bindingContext, propertyDescriptor); } } }
در کد بالا ما تمام کلاس هایی را که از PropertyBindAttribute مشتق شده باشند را به DefaultModelBinder اضافه میکنیم. این کد فقط یک بار نوشته میشود و از این به بعد هر بایندر سفارشی که بسازیم به بایندر پیشفرض اضافه خواهد شد.
public class AccountTypeBindAttribute : PropertyBindAttribute { public override bool BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor) { if (propertyDescriptor.PropertyType == typeof(byte)) { HttpRequestBase request = controllerContext.HttpContext.Request; byte accountType = (byte)Enum.Parse(typeof(Enums.AccountType), request.Form["AccountType"]); propertyDescriptor.SetValue(bindingContext.Model, accountType); return true; } return false; } }
[AccountTypeBindAttribute] public byte AccountType { get; set; }
ModelBinders.Binders.DefaultBinder = new ExtendedModelBinder();
The field نوع کاربر : must be a number.
@Html.DropDownListFor(model => model.AccountType, new SelectList(Enum.GetNames(typeof(Enums.AccountType))),new Dictionary<string, object>() {{ "data-val", "false" }})
2- استفاده از {}less.
Dotless یک پیاده سازی از کتابخانه جاوا اسکریپتی LESS برای دات نت میباشد. پکیج نیوگت DotLess را نیز میتوانید از اینجا دریافت کنید. بعد از اضافه شدن فایلهای آن، یک ارجاع به dotless.core به پروژه تان اضافه خواهد شد. همچنین در فایل Web.Config در قسمت HttpHandler خط زیر اضافه خواهد شد:
<add type="dotless.Core.LessCssHttpHandler,dotless.Core" validate="false" path="*.LESS" verb="*" />
خط فوق یعنی به محض مواجه شدن با فایل LESS، پردازشگر فایلهای LESS وارد عمل میشود. همچنین خط زیر نیز جهت پیکربندی به قسمت configSections در فایل Web.Config اضافه میشود:
<section name="dotless" type="dotless.Core.configuration.DotlessConfigurationSectionHandler,dotless.Core" />
همچنین اگر مایل بودید میتوانید تنظیمات مربوط به فشرده سازی و caching را نیز فعال کنید:
<dotless minifyCss="false" cache="true" />
3- استفاده از افزونهی Web Essentials
Web Essentials برای کامپایل فایلهای LESS از کامپایلر node استفاده میکند. کار با این افزونه خیلی ساده است. کافی است پسوند فایل CSS موجود در پروژه تان را درون ویژوال استودیو، به less. تغییر دهید. با دوبار کلیک بر روی فایل، ویرایشگر فایلهای LESS برای شما نمایش داده میشود، همزمان نیز فایل یک فایل CSS و یک نسخه از فایل CSS را به صورت فشرده، برایتان تولید میکند. خب، هر بار که فایل LESS را تغییر دهید، Web Essentials به صورت خودکار فایلهای css. و min.css. را برایتان روز رسانی میکند.
خوب با کلیک بر روی فایل less، ویرایشگر فایلهای less نمایش داده میشود که با تغییر فایل css میتوانید پیش نمایش آنرا در سمت راست مشاهده کنید:
تعریف متغیر
با استفاده از syntax زیر میتوانید متغیرهای خود را تعریف کنید:
@variable-name: variableValue;
یکی از قابلیتهای جالب در حین مقداردهی متغیرها به خصوص زمانیکه مقدار یک کد رنگی باشد، نمایش کادر انتخاب رنگ است، این کادر بلافاصله بعد از نوشتن علامت # در ابتدای مقدار متغیر نمایش داده میشود:
به طور مثال با تعریف متغیر فوق هر جایی میتوانیم برای تعیین رنگ از آن استفاده کنیم:
@primary-color: #ff6a00; body { background-color: @primary-color; }
استفاده از توابع
LESS شامل تعداد زیادی توابع از پیش نوشته شده است که میتوانید به راحتی از آنها استفاده کنید، توابعی از جمله کار با رنگ ها، اعمال ریاضی و غیره. استفاده از آنها خیلی ساده است. به طور مثال در کد زیر از تابع percentage جهت تبدیل 0.5 به 50% استفاده کرده ایم:
.myClass { width: percentage(0.5); }
استخراج یک فایل
یکی دیگر از قابلیتهای Web Essentials استخراج(Extract) یک فایل میباشد به طور مثال فایل LESS شما شامل متغیرهای زیر است:
@primary-color: #7BA857; @primary-color-light: #B6DE8F; @primary-color-lighter: #D3EFC3; @primary-color-lightest: #EFFAE6; @secondary-color: #AE855C; @text-color-light: #666666; @text-color-dark: #0444;
به راحتی میتوانید تعاریف فوق را درون یک فایل LESS دیگر با نام colors.less قرار دهید:
تغییر تنظیمات پیش فرض Web Essentials
افزونه Web Essentials دارای یک قسمت جهت تغییر تنظیمات پیش فرض برای کار با LESS میباشد که با مراجعه به منوی Tools در ویژوال استودیو و سپس Options میتوانید آنها را تغییر دهید:
Auto-compile dependent files on save: توسط این گزینه میتوانیم تعیین کنیم که فایلهای که import کرده ایم تنها در صورتی که تغییر کرده و ذخیره شده باشند، در فایل CSS جاری کامپایل شوند.
Compile files on build: توسط این گزینه میتوانیم تعیین کنیم که فایلهای less در زمان Build پروژه کامپایل شوند.
Compile files on save: توسط این گزینه میتوانیم تعیین کنیم که فایلهای less در زمان ذخیره کردن پروژه کامپایل شوند.
Create source map files: اگر این گزینه True باشد فایل map. نیز تولید خواهد شد.
Custom output directory: اگر میخواهید خروجی در پوشهی موردنظر شما نمایش داده شود میتوانید آدرس آن را تعیین کنید.
Don't save raw compilation output: با فعال بودن این گزینه فایل CSS عادی ایجاد نخواهد شد.
Process source maps: توسط این گزینه میتوانید قابلیتهای ویرایشگر فایلهای source map را فعال یا غیرفعال کنید.
Strict Math: با فعال بودن این گزینه LESS تمام اعمال ریاضی درون فایل CSS را پردازش خواهد کرد.
Show preview pane: از این گزینه نیز جهت نمایش یا عدم نمایش preview window استفاده میشود.
در این مطلب قصد داریم علاوه بر طراحی زیرساختی برای راه اندازی هرچه سریعتر 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 با هم بررسی خواهیم کرد.
کدهای کامل این قسمت را میتوانید از اینجا دریافت کنید.
در مورد کار با jQuery Ajax و نحوه فراخوانی یک متد وب سرویس توسط آن، چند مطلب پیشتر ارائه شدند:
بررسی وجود نام کاربر با استفاده از jQuery Ajax در ASP.Net
و ...
تمام این مقالات یک ایراد مهم دارند که امروز با آن مواجه شدم و خلاصه آن به شرح زیر است:
پارامتر data متد Ajax جیکوئری را به صورت زیر در نظر بگیرید:
data: "{'username': '" + $('#<%= TextBox1.ClientID %>').val() + "'}",
\b Backspace (ascii code 08)
\f Form feed (ascii code 0C)
\n New line
\r Carriage return
\t Tab
\v Vertical tab
' Apostrophe or single quote
" Double quote
\ Backslash caracter
ابتدا ارجاعی از این اسکریپت باید به صفحه اضافه شود:
<script src="js/json2.js" type="text/javascript"></script>
var jsonText = JSON.stringify({ username: $('#<%= TextBox1.ClientID %>').val() });
...
data: jsonText,
به عنوان مثالی دیگر، اگر متد وب سرویس ما دو پارامتر داشت، jsonText به شکل زیر در خواهد آمد:
var jsonText = JSON.stringify({ param1: val1, param2: val2 });
using System; namespace CS6NewFeatures { class Person { public string FirstName { set; get; } public string LastName { set; get; } public int Age { set; get; } } class Program { static void Main(string[] args) { var person = new Person { FirstName = "User 1", LastName = "Last Name 1", Age = 50 }; var message = string.Format("Hello! My name is {0} {1} and I am {2} years old.", person.FirstName, person.LastName, person.Age); Console.Write(message); } } }
در C# 6 جهت رفع این مشکلات، راه حلی به نام String interpolation ارائه شدهاست و اگر افزونهی ReSharper یا یکی از افزونههای Roslyn را نصب کرده باشید، به سادگی امکان تبدیل کدهای قدیمی را به فرمت جدید آن خواهید یافت:
در این حالت کد قدیمی فوق، به کد ذیل تبدیل خواهد شد:
static void Main(string[] args) { var person = new Person { FirstName = "User 1", LastName = "Last Name 1", Age = 50 }; var message = $"Hello! My name is {person.FirstName} {person.LastName} and I am {person.Age} years old."; Console.Write(message); }
عملیاتی که در اینجا توسط کامپایلر صورت خواهد گرفت، تبدیل این کدهای جدید مبتنی بر String interpolation به همان string.Format قدیمی در پشت صحنهاست. بنابراین این قابلیت جدید C# 6 را به کدهای قدیمی خود نیز میتوانید اعمال کنید. فقط کافی است VS 2015 را نصب کرده باشید و دیگر شمارهی دات نت فریم ورک مورد استفاده مهم نیست.
امکان انجام محاسبات با String interpolation
زمانیکه $ در ابتدای رشته قرار گرفت، عبارات داخل {}ها توسط کامپایلر محاسبه و جایگزین میشوند. بنابراین میتوان چنین محاسباتی را نیز انجام داد:
var message2 = $"{Environment.NewLine}Test {DateTime.Now}, {3*2}"; Console.Write(message2);
تغییر فرمت عبارات نمایش داده شده توسط String interpolation
همانطور که با string.Format میتوان نمایش سه رقم جدا کنندهی هزارها را فعال کرد و یا تاریخی را به نحوی خاص نمایش داد، در اینجا نیز همان قابلیتها برقرار هستند و باید پس از ذکر یک : عنوان شوند:
var message3 = $"{Environment.NewLine}{1000000:n0} {DateTime.Now:dd-MM-yyyy}"; Console.Write(message3);
سفارشی سازی String interpolation
اگر متغیر رشتهای معرفی شدهی توسط $ را با یک var مشخص کنیم، نوع آن به صورت پیش فرض، از نوع string خواهد بود. برای نمونه در مثالهای فوق، message و message2 از نوع string تعریف میشوند. اما این رشتههای ویژه را میتوان از نوع IFormattable و یا FormattableString نیز تعریف کرد.
در حقیقت رشتههای آغاز شدهی با $ از نوع IFormattable هستند و اگر نوع متغیر آنها ذکر نشود، به صورت خودکار به نوع FormattableString که اینترفیس IFormattable را پیاده سازی میکند، تبدیل میشوند. بنابراین پیاده سازی این اینترفیس، امکان سفارشی سازی خروجی string interpolation را میسر میکند. برای نمونه میخواهیم در مثال message2، نحوهی نمایش تاریخ را سفارشی سازی کنیم.
class MyDateFormatProvider : IFormatProvider { readonly MyDateFormatter _formatter = new MyDateFormatter(); public object GetFormat(Type formatType) { return formatType == typeof(ICustomFormatter) ? _formatter : null; } class MyDateFormatter : ICustomFormatter { public string Format(string format, object arg, IFormatProvider formatProvider) { if (arg is DateTime) return ((DateTime)arg).ToString("MM/dd/yyyy"); return arg.ToString(); } } }
پس از پیاده سازی این سفارشی کنندهی تاریخ، نحوهی استفادهی از آن به صورت ذیل است:
static string formatMyDate(FormattableString formattable) { return formattable.ToString(new MyDateFormatProvider()); }
در ادامه برای اعمال این سفارشی سازی، فقط کافی است متد formatMyDate را به رشتهی مدنظر اعمال کنیم:
var message2 = formatMyDate($"{Environment.NewLine}Test {DateTime.Now}, {3*2}"); Console.Write(message2);
و اگر تنها میخواهید فرهنگ جاری را عوض کنید، از روش سادهی زیر استفاده نمائید:
public static string faIr(IFormattable formattable) { return formattable.ToString(null, new CultureInfo("fa-Ir")); }
نمونهی کاربردیتر آن اعمال InvariantCulture به String interpolation است:
static string invariant(FormattableString formattable) { return formattable.ToString(CultureInfo.InvariantCulture); }
یک نکته: همانطور که عنوان شد این قابلیت جدید با نگارشهای قبلی دات نت نیز سازگار است؛ اما این کلاسهای جدید را در این نگارشها نخواهید یافت. برای رفع این مشکل تنها کافی است این کلاسهای یاد شده را به صورت دستی در فضای نام اصلی آنها تعریف و اضافه کنید. یک مثال
غیرفعال سازی String interpolation
اگر میخواهید در رشتهای که با $ شروع شده، بجای محاسبهی عبارتی، دقیقا خود آنرا نمایش دهید (و { را escape کنید)، از {{}} استفاده کنید:
var message0 = $"Hello! My name is {person.FirstName} {{person.FirstName}}";
پردازش عبارات شرطی توسط String interpolation
همانطور که عنوان شد، امکان ذکر یک عبارت کامل هم در بین {} وجود دارد (محاسبات، ذکر یک عبارت LINQ، ذکر یک متد و امثال آن). اما در این میان اگر یک عبارت شرطی مدنظر بود، باید بین () قرار گیرد:
Console.Write($"{(person.Age>50 ? "old": "young")}");
سایتهای بسیاری خودشان را با این الگو وفق دادهاند. برای نمونه Twitter و Github از مفهوم pjax استفادهی وسیعی دارند. برای نمونه، layout یا master page یک سایت را درنظر بگیرید. به ازای مرور هر صفحه، یکبار باید تمام قسمتهای تکراری layout از سرور بارگذاری شوند. توسط pjax به سرور اعلام میکنیم، ما تنها نیاز به body صفحات را داریم و نه کل صفحه را. همچنین اگر مرورگر از جاوا اسکریپت استفاده نمیکند، لطفا کل صفحه را همانند گذشته بازگشت بده. به علاوه مسایل سمت کلاینت مانند تغییر آدرس مرورگر و تغییر عنوان صفحه نیز به صورت خودکار مدیریت شوند. این تکنیک را دقیقا در حین مرور مخزنهای کد Github میتوانید مشاهده کنید. فقط قسمتی که لیست فایلها را ارائه میدهد، از سرور دریافت میگردد و نه کل صفحه.
بکارگیری pjax در ASP.NET MVC
مطابق توضیحاتی که ارائه شد، برای پیاده سازی سازی pjax نیاز به دو فایل layout داریم. یکی برای حالت ajax ایی و دیگری برای حالت بارگذاری کامل صفحه. حالت ajax ایی آن تنها از رندرکردن body پشتیبانی میکند؛ و نه ارائه تمام قسمتهای صفحه مانند هدر، فوتر، منوها و غیره. بنابراین خواهیم داشت:
الف) تعریف فایلهای layout سازگار با pjax
ابتدا یک فایل جدید را به نام _PjaxLayout.cshtml به پوشهی Shared اضافه کنید؛ با این محتوا:
<title>@ViewBag.Title</title> @RenderBody()
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width" /> <title>@ViewBag.Title</title> <link href="~/Content/Site.css" rel="stylesheet" /> <script src="~/Scripts/jquery-1.8.2.min.js"></script> <script src="~/Scripts/jquery.pjax.js"></script> <script type="text/javascript"> $(function () { $(document).pjax('a[withpjax]', '#pjaxContainer', { timeout: 5000 }); }); </script> </head> <body> <div>Main layout ...</div> <div id="pjaxContainer"> @RenderBody() </div> </body> </html>
فایل layout اصلی سایت همانند قبل است. فقط RenderBody آن داخل یک div با id مساوی pjaxContainer قرار گرفته و از آن در فراخوانی افزونهی pjax استفاده شدهاست. همانطور که ملاحظه میکنید، مطابق تنظیمات ابتدای هدر layout، فقط لینکهایی که دارای ویژگی withpjax باشند، توسط pjax پردازش خواهند شد.
ب) تغییر فایل ViewStart برنامه
در فایل ViewStart، کار مقدار دهی layout پیش فرض صورت گرفتهاست. اکنون نیاز است این فایل را جهت معرفی layout دوم تعریف شده مخصوص pjax، اندکی ویرایش کنیم:
@{ if (Request.Headers["X-PJAX"] != null) { Layout = "~/Views/Shared/_PjaxLayout.cshtml"; } else { Layout = "~/Views/Shared/_Layout.cshtml"; } }
ج) آزمایش برنامه
using System.Web.Mvc; namespace PajxMvcApp.Controllers { public class HomeController : Controller { public ActionResult Index() { return View(); } public ActionResult About() { return View(); } } }
سپس View متد Index را به نحو ذیل تغییر دهید:
@{ ViewBag.Title = "Index"; } <h2>Index</h2> @Html.ActionLink(linkText: "About", actionName:"About", routeValues: null, controllerName:"Home", htmlAttributes: new { withpjax = "with-pjax"})
اکنون اگر برنامه را اجرا کنید، چنین خروجی را در برگهی network آن مشاهده خواهید کرد:
همانطور که ملاحظه میکنید، با کلیک بر روی لینک About، یک درخواست pjax ایی به سرور ارسال شدهاست؛ به همراه هدرهای ویژه آن. هنوز قسمتهای اصلی layout سایت مشخص هستند (و مجددا از سرور درخواست نشدهاند). آدرس صفحه عوض شدهاست. به علاوه قسمت body آن تنها تغییر کردهاست.
این مثال را از اینجا نیز میتوانید دریافت کنید
PajxMvcApp.zip
برای مطالعه بیشتر
A Faster Web With PJAX
Favour PJAX over dynamically loaded partial views
What is PJAX and why
Pjax.Mvc
Using pjax with ASP.Net MVC3
Getting started with PJAX with ASP.NET MVC
ASP.NET MVC with PAjax or PushState/ReplaceState and Ajax