public class Kala {
[Key]
public int Kala_id { get; set; }
[DisplayName("نام کالا")]
public string Name { get; set; }
[DisplayName("قیمت خرید")]
public double Fee_Kharid { get; set; }
public virtual Brand Brand { get; set; }
}
public class Brand
{
[Key]
public int Brand_id { get; set; }
public string Brand_Name { get; set; }
public virtual ICollection<Kala> Kalas { get; set; }
}
public class KalaViewModel
{
public int Kala_Id { get; set; }
public string Name { get; set; }
public double Fee_Kharid { get; set; }
public string Brand_Name { get; set; }
}
//Controller
[HttpGet]
public ActionResult Index()
{
var kala = _Kala_Service.GetAllKalas();
var brand = _Brand_Service.GetAllBrands();
var kalaviewmodel = EntityMapper.Map<List<KalaViewModel>>(kala, brand);
return View(kalaviewmodel);
}
protected override void Configure()
{
Mapper.CreateMap<Kala, KalaViewModel>();
Mapper.CreateMap<Brand, KalaViewModel>()
.ForMember(des => des.Kala_Id, op => op.Ignore())
.ForMember(des => des.Name, op => op.Ignore())
.ForMember(x => x.Fee_Kharid, opt => opt.Ignore());
}
Unobtrusive Ajax چیست؟
در حالت معمولی، با استفاده از متد ajax جیکوئری، کار ارسال غیرهمزمان اطلاعات، به سمت سرور صورت میگیرد. چون در این روش کدهای جیکوئری داخل صفحات برنامههای ما قرار میگیرند، به این روش، «روش چسبنده» میگویند. اما با استفاده از افزونهی «jquery.unobtrusive-ajax.min.js» مایکروسافت، میتوان این کدهای چسبنده را تبدیل به کدهای غیرچسنبده یا Unobtrusive کرد. در این حالت، پارامترهای متد ajax، به صورت ویژگیها (attributes) به شکل data-ajax به المانهای مختلف صفحه اضافه میشوند و به این ترتیب، افزونهی یاد شده به صورت خودکار با یافتن مقادیر ویژگیهای data-ajax، این المانها را تبدیل به المانهای ایجکسی میکند. در این حالت به کدهایی تمیزتر و عاری از متدهای چسبندهی ajax قرار گرفتهی در داخل صفحات وب خواهیم رسید.
روش طراحی Unobtrusive را در کتابخانههای معروفی مانند بوت استرپ هم میتوان مشاهده کرد.
پیشنیازهای فعال سازی Unobtrusive Ajax در ASP.NET Core 1.0
توزیع افزونهی «jquery.unobtrusive-ajax.min.js» مایکروسافت، از طریق bower صورت میگیرد که پیشتر در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 14 - فعال سازی اعتبارسنجی ورودیهای کاربران» با آن آشنا شدیم. در اینجا نیز برای دریافت آن، تنها کافی است فایل bower.json را به نحو ذیل تکمیل کرد:
{ "name": "asp.net", "private": true, "dependencies": { "bootstrap": "3.3.6", "jquery": "2.2.0", "jquery-validation": "1.14.0", "jquery-validation-unobtrusive": "3.2.6", "jquery-ajax-unobtrusive": "3.2.4" } }
[ { "outputFileName": "wwwroot/css/site.min.css", "inputFiles": [ "bower_components/bootstrap/dist/css/bootstrap.min.css", "content/site.css" ] }, { "outputFileName": "wwwroot/js/site.min.js", "inputFiles": [ "bower_components/jquery/dist/jquery.min.js", "bower_components/jquery-validation/dist/jquery.validate.min.js", "bower_components/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js", "bower_components/jquery-ajax-unobtrusive/jquery.unobtrusive-ajax.min.js", "bower_components/bootstrap/dist/js/bootstrap.min.js" ], "minify": { "enabled": true, "renameLocals": true }, "sourceMap": false } ]
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>@ViewBag.Title - My ASP.NET Application</title> <link href="~/css/site.min.css" rel="stylesheet" /> </head> <body> <div> <div> @RenderBody() </div> </div> <script src="~/js/site.min.js" type="text/javascript" asp-append-version="true"></script> @RenderSection("Scripts", required: false) </body> </html>
استفاده از معادلهای واقعی Unobtrusive Ajax در ASP.NET Core 1.0
واقعیت این است که HTML Helper ایجکسی حذف شدهی از ASP.NET Core 1.0، کاری بجز افزودن ویژگیهای data-ajax را که توسط افزونهی jquery.unobtrusive-ajax.min.js پردازش میشوند، انجام نمیدهد و این افزونه مستقل است از مباحث سمت سرور و به نگارش خاصی از ASP.NET گره نخورده است. بنابراین در اینجا تنها کاری را که باید انجام داد، استفاده از همان ویژگیهای اصلی است که این افزونه قادر به شناسایی آنها است.
خلاصهی آنها را جهت انتقال کدهای قدیمی و یا تهیهی کدهای جدید، در جدول ذیل میتوانید مشاهده کنید:
HTML attribute | AjaxOptions |
data-ajax-confirm | Confirm |
data-ajax-method | HttpMethod |
data-ajax-mode | InsertionMode |
data-ajax-loading-duration | LoadingElementDuration |
data-ajax-loading | LoadingElementId |
data-ajax-begin | OnBegin |
data-ajax-complete | OnComplete |
data-ajax-failure | OnFailure |
data-ajax-success | OnSuccess |
data-ajax-update | UpdateTargetId |
data-ajax-url | Url |
در ASP.NET Core 1.0، به علت حذف متدهای کمکی Ajax دیگر خبری از AjaxOptions نیست. اما اگر علاقمند به انتقال کدهای قدیمی به ASP.NET Core 1.0 هستید، معادلهای اصلی این پارامترها را میتوانید در ستون HTML attribute مشاهده کنید.
چند نکته:
- اگر قصد استفادهی از این ویژگیها را دارید، باید ویژگی "data-ajax="true را نیز حتما قید کنید تا سیستم Unobtrusive Ajax فعال شود.
- ویژگی data-ajax-mode تنها با ذکر data-ajax-update (و یا همان UpdateTargetId پیشین) معنا پیدا میکند.
- ویژگی data-ajax-loading-duration نیاز به ذکر data-ajax-loading (و یا همان LoadingElementId پیشین) را دارد.
- ویژگی data-ajax-mode مقادیر before، after و replace-with را میپذیرد. اگر قید نشود، کل المان با data دریافتی جایگزین میشود.
- سه callback قابل تعریف data-ajax-complete، data-ajax-failure و data-ajax-success، یک چنین پارامترهایی را از سمت سرور در اختیار کلاینت قرار میدهند:
parameters | Callback |
xhr, status | data-ajax-complete |
data, status, xhr | data-ajax-success |
xhr, status, error | data-ajax-failure |
برای مثال میتوان ویژگی data-ajax-success را به نحو ذیل در سمت کلاینت مقدار دهی کرد:
data-ajax-success = "myJsMethod"
function myJsMethod(data, status, xhr) { }
return Json(new { param1 = 1, param2 = 2, ... });
مثالهایی از افزودن ویژگیهای data-ajax به المانهای مختلف
در حالت استفاده از Form Tag Helpers که در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 12 - معرفی Tag Helpers» بررسی شدند، یک فرم ایجکسی، چنین تعاریفی را پیدا خواهد کرد:
با این ViewModel فرضی
using System.ComponentModel.DataAnnotations; namespace Core1RtmEmptyTest.ViewModels.Account { public class RegisterViewModel { [Required] [EmailAddress] [Display(Name = "Email")] public string Email { get; set; } } }
@using Core1RtmEmptyTest.ViewModels.Account @model RegisterViewModel @{ } <form method="post" asp-controller="TestAjax" asp-action="Index" asp-route-returnurl="@ViewBag.ReturnUrl" class="form-horizontal" role="form" data-ajax="true" data-ajax-loading="#Progress" data-ajax-success="myJsMethod"> <input asp-for="Email" class="form-control" /> <span asp-validation-for="Email" class="text-danger"></span> <button type="submit">ارسال</button> <div id="Progress" style="display: none"> <img src="images/loading.gif" alt="loading..." /> </div> </form> @section scripts{ <script type="text/javascript"> function myJsMethod(data, status, xhr) { alert(data.param1); } </script> }
این View از کنترلر ذیل استفاده میکند:
using Core1RtmEmptyTest.ViewModels.Account; using Microsoft.AspNetCore.Mvc; namespace Core1RtmEmptyTest.Controllers { public class TestAjaxController : Controller { public IActionResult Index() { return View(); } [HttpPost] public IActionResult Index([FromForm]RegisterViewModel vm) { var ajax = isAjax(); if (ajax) { // it's an ajax post } if (ModelState.IsValid) { //todo: save data return Json(new { param1 = 1, param2 = 2 }); } return View(); } private bool isAjax() { return Request?.Headers != null && Request.Headers["X-Requested-With"] == "XMLHttpRequest"; } } }
و یا Action Link ایجکسی نیز به صورت خلاصه به این نحو قابل تعریف است:
<div id="EmployeeInfo"> <a asp-controller="MyController" asp-action="MyAction" data-ajax="true" data-ajax-loading="#Progress" data-ajax-method="POST" data-ajax-mode="replace" data-ajax-update="#EmployeeInfo"> Get Employee-1 info </a> <div id="Progress" style="display: none"> <img src="images/loading.gif" alt="loading..." /> </div> </div>
نکتهای در مورد اکشن متدهای ایجکسی در ASP.NET Core 1.0
همانطور که در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 18 - کار با ASP.NET Web API»، قسمت «تغییرات Model binding پیش فرض، برای پشتیبانی از ASP.NET MVC و ASP.NET Web API» نیز ذکر شد:
public IActionResult Index([FromBody] MyViewModel vm) { return View(); }
فرض کنید که میخواهیم یک برنامه برای یک فروشگاه نوشیدنی (مانند coffee shop) بنویسیم ، این فروشگاه در ابتدای کار ممکن است ، منوی سادهای جهت ارائه به مشتری داشته باشد. برای مثال ممکن است که فقط 3 یا 4 محصول داشته باشد. بنابراین ممکن است ما برنامهای را که میخواهیم برای این مشتری بنویسیم به صورت زیر طراحی کنیم:
که بسیار طبیعی و درست میباشد. اما حالا در نظر بگیرید که این فروشگاه در آینده ممکن است محصولات خود را افزایش بدهد و یا حالاتی که ممکن است این محصولات با هم ادغام شوند را در نظر بگیرید. برای مثال ممکن است شما بخواهید که قهوهتان را با شیر نوش جان کنید و یا ....
بنابراین تعداد این حالات را در نظر بگیرید که در آینده ممکن است چقدر زیاد بشود:
خوب پس چهکاری ما میتوانیم برای نگهداری این برنامه انجام بدهیم؟ یکی از راههایی که ممکن است به فکر ما برسد این است که روش بالا روش احمقانه ای است. چرا ما باید به همهی این کلاسها نیاز داشته باشیم. ما میتوانیم که چاشنیها را در کلاس اصلی نگهداری کنیم و کلاس محصولاتمان را از کلاس اصلی به ارث ببریم اجازه دهید تا این کار را با هم انجام بدهیم
خوب با این روش ما n کلاس تشکیل شده در رویکرد اول را فقط به 5 کلاس تبدیل کردیم. خوب این روشی بسیار ایدهال به نظر میرسد. اما ممکن است در آینده که تعداد چاشنیهای ما بالا میرود و همچنین تعداد محصولاتمان نیز ممکن است بیشتر شود مجبور شویم که تعداد این کلاسها را بیشتر کنیم، و یا فکر کنید که ما میخواهیم هریک از چاشنیهایمان، یک قیمت را نسبت بدهیم. بنابراین مجبوریم که تمامی اینها را در کلاس پایه اضافه کنیم؛ بله درست است، ما با کلاس پایهی حجیمی روبرو میشویم که بیشتر خواص و یا متدهای آن برای زیر کلاسهای دیگر مناسبت نیستند. خوب آیا روش بهتری برای جلوگیری از این مشکل داریم؟ بلی.
خوب ما به این مسئله به این صورت نگاه میکنیم که شروع میکنیم با نوشیدنیها و آنها را با چاشنیها در زمان اجرا تزیین (Decorate) میکنیم؛ نه کامپایل.
برای مثال اگر مشتری ما یک نوشیدنی DarkRoast با Mocha و Whip خواست، سپس ما :
1- یک شی از DarkRoast ایجاد میکنیم .
2- آن را با یک شی از Mocha تزئین میکنیم.
3- آن را با یک شی از Whip - تزیین میکنیم.
4- متد Cost() را صدا میزنیم و یک Delegation را برای اضافه کردن قیمت چاشنیها در نظر میگیریم.
بسیار خوب؛ اما ما عملیات تزئین یک شی را چگونه انجام میدهیم و delegation ما چگونه عمل میکند .
یک اشاره : به شیء تزئین کننده، مانند یک Wrappers فکر کنید. اجاز بدهید ببینم که چه طور این کار را انجام میدهیم.
1- یک شی از DarkRoast ایجاد میکنیم.
3- آن را با یک شی از Whip تزیین میکنیم
4- حالا زمان محاسبه قیمت محصول برای مشتری فرا رسیده است. ما این کار را را با صدا زدن بیرونیترین Decorator(Whip) انجام میدهیم و شی whip به کمک Delegate مابقی توابع cost را صدا میزند.
public abstract class Beverage { string description ="unknow beverage"; public String getDescription(){ return description; } public abstract double cost(); } public abstract class CondimentDecorator extends Beverage { public abstract string getDescription(); } public class Espersso extends Beverage{ public Espersso() { description="Espersso"; } public double cost(){ return 1.99; } } public class HouseBelend extends Beverage { public HouseBelend() { description="HouseBelend"; } public double cost() { return .89; } } public class mocha extends condimateDecorator { Beverage beverage; public mocha(Beverage beverage) { this.beverage=beverage; } public string getDescription(){ return beverage.getdescription() + "Mocha"; } public double cost(){ return .20 +beverage.cost } } // Now Use These classes in Final form Beverage beverage=new Espersso(); //Customers want a coffe with double milk and whip beverage=new mocha(beverage); beverage=new mocha(meverage); beverage=new whip(beverage); system.out.println(beverage.getDescription() + "$" +beverage.cost());
سفارشی سازی RoleStore
ASP.NET Core Identity دو سرویس را جهت کار با نقشهای کاربران پیاده سازی کردهاست:
- سرویس RoleStore: کار آن دسترسی به ApplicationDbContext ایی است که در قسمت قبل سفارشی سازی کردیم و سپس ارائهی زیر ساختی به سرویس RoleManager جهت استفادهی از آن برای ذخیره سازی و یا تغییر و حذف نقشهای سیستم.
وجود Storeها از این جهت است که اگر علاقمند بودید، بتوانید از سایر ORMها و یا زیرساختهای ذخیره سازی اطلاعات برای کار با بانکهای اطلاعاتی استفاده کنید. در اینجا از همان پیاده سازی پیشفرض آن که مبتنی بر EF Core است استفاده میکنیم. به همین جهت است که وابستگی ذیل را در فایل project.json مشاهده میکنید:
"Microsoft.AspNetCore.Identity.EntityFrameworkCore": "1.1.0",
برای سفارشی سازی سرویسهای پایهی ASP.NET Core Identity، ابتدا باید سورس این مجموعه را جهت یافتن نگارشی از سرویس مدنظر که کلاسهای موجودیت را به صورت آرگومانهای جنریک دریافت میکند، پیدا کرده و سپس از آن ارث بری کنیم:
public class ApplicationRoleStore : RoleStore<Role, ApplicationDbContext, int, UserRole, RoleClaim>, IApplicationRoleStore
این ارث بری جهت تکمیل، نیاز به بازنویسی سازندهی RoleStore پایه را نیز دارد:
public ApplicationRoleStore( IUnitOfWork uow, IdentityErrorDescriber describer = null) : base((ApplicationDbContext)uow, describer)
در نگارش 1.1 این کتابخانه، بازنویسی متد CreateRoleClaim نیز اجباری است تا در آن مشخص کنیم، نحوهی تشکیل کلید خارجی و اجزای یک RoleClaim به چه نحوی است:
protected override RoleClaim CreateRoleClaim(Role role, Claim claim) { return new RoleClaim { RoleId = role.Id, ClaimType = claim.Type, ClaimValue = claim.Value }; }
در ادامه اگر به انتهای تعریف امضای کلاس دقت کنید، یک اینترفیس IApplicationRoleStore را نیز مشاهده میکنید. برای تشکیل آن بر روی نام کلاس سفارشی خود کلیک راست کرده و با استفاده از ابزارهای Refactoring کار Extract interface را انجام میدهیم؛ از این جهت که در سایر لایههای برنامه نمیخواهیم از تزریق مستقیم کلاس ApplicationRoleStore استفاده کنیم. بلکه میخواهیم اینترفیس IApplicationRoleStore را در موارد ضروری، به سازندههای کلاسهای تزریق نمائیم.
پس از تشکیل این اینترفیس، مرحلهی بعد، معرفی آنها به سیستم تزریق وابستگیهای ASP.NET Core است. چون تعداد تنظیمات مورد نیاز ما زیاد هستند، یک کلاس ویژه به نام IdentityServicesRegistry تشکیل شدهاست تا به عنوان رجیستری تمام سرویسهای سفارشی سازی شدهی ما عمل کند و تنها با فراخوانی متد AddCustomIdentityServices آن در کلاس آغازین برنامه، کار ثبت یکجای تمام سرویسهای ASP.NET Core Identity انجام شود و بیجهت کلاس آغازین برنامه شلوغ نگردد.
ثبت ApplicationDbContext طبق روش متداول آن در کلاس آغازین برنامه انجام شدهاست. سپس معرفی سرویس IUnitOfWork را که از ApplicationDbContext تامین میشود، در کلاس IdentityServicesRegistry مشاهده میکنید.
services.AddScoped<IUnitOfWork, ApplicationDbContext>();
services.AddScoped<IApplicationRoleStore, ApplicationRoleStore>(); services.AddScoped<RoleStore<Role, ApplicationDbContext, int, UserRole, RoleClaim>, ApplicationRoleStore>();
سپس RoleStore توکار ASP.NET Core Identity را نیز به ApplicationRoleStore خود هدایت کردهایم. به این ترتیب هر زمانیکه در کدهای داخلی این فریم ورک وهلهای از RoleStore جنریک آن درخواست میشود، دیگر از همان پیاده سازی پیشفرض خود استفاده نخواهد کرد و به پیاده سازی ما هدایت میشود.
این روشی است که جهت سفارشی سازی تمام سرویسهای پایهی ASP.NET Core Identity بکار میگیریم:
1) ارث بری از نگارش جنریک سرویس پایهی موجود و معرفی موجودیتهای سفارشی سازی شدهی خود به آن
2) سفارشی سازی سازندهی این کلاسها با سرویسهایی که تهیه کردهایم (بجای سرویسهای پیش فرض).
3) تکمیل متدهایی که باید به اجبار پس از این ارث بری پیاده سازی شوند.
4) استخراج یک اینترفیس از کلاس نهایی تشکیل شده (توسط ابزارهای Refactoring).
5) معرفی اینترفیس و همچنین نمونهی توکار این سرویس به سیستم تزریق وابستگیهای ASP.NET Core جهت استفادهی از این سرویس جدید سفارشی سازی شده.
سفارشی سازی RoleManager
در اینجا نیز همان 5 مرحلهای را که عنوان کردیم باید جهت تشکیل کلاس جدید ApplicationRoleManager پیگیری کنیم.
1) ارث بری از نگارش جنریک سرویس پایهی موجود و معرفی موجودیتهای سفارشی سازی شدهی خود به آن
public class ApplicationRoleManager : RoleManager<Role>, IApplicationRoleManager
2) سفارشی سازی سازندهی این کلاس با سرویسی که تهیه کردهایم (بجای سرویس پیش فرض).
public ApplicationRoleManager( IApplicationRoleStore store, IEnumerable<IRoleValidator<Role>> roleValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, ILogger<ApplicationRoleManager> logger, IHttpContextAccessor contextAccessor, IUnitOfWork uow) : base((RoleStore<Role, ApplicationDbContext, int, UserRole, RoleClaim>)store, roleValidators, keyNormalizer, errors, logger, contextAccessor)
الف) ذکر IApplicationRoleStore بجای RoleStore آن
ب) ذکر IUnitOfWork بجای ApplicationDbContext
3) تکمیل متدهایی که باید به اجبار پس از این ارث بری پیاده سازی شوند.
RoleManager پایه نیازی به پیاده سازی اجباری متدی ندارد.
4) استخراج یک اینترفیس از کلاس نهایی تشکیل شده (توسط ابزارهای Refactoring).
محتوای این اینترفیس را در IApplicationRoleManager مشاهده میکنید.
5) معرفی اینترفیس و همچنین نمونهی توکار این سرویس به سیستم تزریق وابستگیهای ASP.NET Core جهت استفادهی از این سرویس جدید سفارشی سازی شده.
services.AddScoped<IApplicationRoleManager, ApplicationRoleManager>(); services.AddScoped<RoleManager<Role>, ApplicationRoleManager>();
سفارشی سازی UserStore
در مورد مدیریت کاربران نیز دو سرویس Store و Manager را مشاهده میکنید. کار کلاس Store، کپسوله سازی لایهی دسترسی به دادهها است تا کتابخانههای ثالث، مانند وابستگی Microsoft.AspNetCore.Identity.EntityFrameworkCore بتوانند آنرا پیاده سازی کنند و کار کلاس Manager، استفادهی از این Store است جهت کار با بانک اطلاعاتی.
5 مرحلهای را که باید جهت تشکیل کلاس جدید ApplicationUserStore پیگیری کنیم، به شرح زیر هستند:
1) ارث بری از نگارش جنریک سرویس پایهی موجود و معرفی موجودیتهای سفارشی سازی شدهی خود به آن
public class ApplicationUserStore : UserStore<User, Role, ApplicationDbContext, int, UserClaim, UserRole, UserLogin, UserToken, RoleClaim>, IApplicationUserStore
2) سفارشی سازی سازندهی این کلاس با سرویسی که تهیه کردهایم (بجای سرویس پیش فرض).
public ApplicationUserStore( IUnitOfWork uow, IdentityErrorDescriber describer = null) : base((ApplicationDbContext)uow, describer)
الف) ذکر IUnitOfWork بجای ApplicationDbContext
3) تکمیل متدهایی که باید به اجبار پس از این ارث بری پیاده سازی شوند.
در اینجا نیز تکمیل 4 متد از کلاس پایه UserStore جنریک انتخاب شده، جهت مشخص سازی نحوهی انتخاب کلیدهای خارجی جداول سفارشی سازی شدهی مرتبط با جدول کاربران ضروری است:
protected override UserClaim CreateUserClaim(User user, Claim claim) { var userClaim = new UserClaim { UserId = user.Id }; userClaim.InitializeFromClaim(claim); return userClaim; } protected override UserLogin CreateUserLogin(User user, UserLoginInfo login) { return new UserLogin { UserId = user.Id, ProviderKey = login.ProviderKey, LoginProvider = login.LoginProvider, ProviderDisplayName = login.ProviderDisplayName }; } protected override UserRole CreateUserRole(User user, Role role) { return new UserRole { UserId = user.Id, RoleId = role.Id }; } protected override UserToken CreateUserToken(User user, string loginProvider, string name, string value) { return new UserToken { UserId = user.Id, LoginProvider = loginProvider, Name = name, Value = value }; }
4) استخراج یک اینترفیس از کلاس نهایی تشکیل شده (توسط ابزارهای Refactoring).
محتوای این اینترفیس را در IApplicationUserStore مشاهده میکنید.
5) معرفی اینترفیس و همچنین نمونهی توکار این سرویس به سیستم تزریق وابستگیهای ASP.NET Core جهت استفادهی از این سرویس جدید سفارشی سازی شده.
services.AddScoped<IApplicationUserStore, ApplicationUserStore>(); services.AddScoped<UserStore<User, Role, ApplicationDbContext, int, UserClaim, UserRole, UserLogin, UserToken, RoleClaim>, ApplicationUserStore>();
سفارشی سازی UserManager
5 مرحلهای را که باید جهت تشکیل کلاس جدید ApplicationUserManager پیگیری کنیم، به شرح زیر هستند:
1) ارث بری از نگارش جنریک سرویس پایهی موجود و معرفی موجودیتهای سفارشی سازی شدهی خود به آن
public class ApplicationUserManager : UserManager<User>, IApplicationUserManager
2) سفارشی سازی سازندهی این کلاس با سرویسی که تهیه کردهایم (بجای سرویس پیش فرض).
public ApplicationUserManager( IApplicationUserStore store, IOptions<IdentityOptions> optionsAccessor, IPasswordHasher<User> passwordHasher, IEnumerable<IUserValidator<User>> userValidators, IEnumerable<IPasswordValidator<User>> passwordValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, IServiceProvider services, ILogger<ApplicationUserManager> logger, IHttpContextAccessor contextAccessor, IUnitOfWork uow, IUsedPasswordsService usedPasswordsService) : base((UserStore<User, Role, ApplicationDbContext, int, UserClaim, UserRole, UserLogin, UserToken, RoleClaim>)store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
الف) ذکر IUnitOfWork بجای ApplicationDbContext (البته این مورد، یک پارامتر اضافی است که بر اساس نیاز این سرویس سفارشی، اضافه شدهاست)
تمام پارامترهای پس از logger به دلیل نیاز این سرویس اضافه شدهاند و جزو پارامترهای سازندهی کلاس پایه نیستند.
ب) استفادهی از IApplicationUserStore بجای UserStore پیشفرض
3) تکمیل متدهایی که باید به اجبار پس از این ارث بری پیاده سازی شوند.
UserManager پایه نیازی به پیاده سازی اجباری متدی ندارد.
4) استخراج یک اینترفیس از کلاس نهایی تشکیل شده (توسط ابزارهای Refactoring).
محتوای این اینترفیس را در IApplicationUserManager مشاهده میکنید.
5) معرفی اینترفیس و همچنین نمونهی توکار این سرویس به سیستم تزریق وابستگیهای ASP.NET Core جهت استفادهی از این سرویس جدید سفارشی سازی شده.
services.AddScoped<IApplicationUserManager, ApplicationUserManager>(); services.AddScoped<UserManager<User>, ApplicationUserManager>();
سفارشی سازی SignInManager
سرویس پایه SignInManager از سرویس UserManager جهت فراهم آوردن زیرساخت لاگین کاربران استفاده میکند.
5 مرحلهای را که باید جهت تشکیل کلاس جدید ApplicationSignInManager پیگیری کنیم، به شرح زیر هستند:
1) ارث بری از نگارش جنریک سرویس پایهی موجود و معرفی موجودیتهای سفارشی سازی شدهی خود به آن
public class ApplicationSignInManager : SignInManager<User>, IApplicationSignInManager
2) سفارشی سازی سازندهی این کلاس با سرویسی که تهیه کردهایم (بجای سرویس پیش فرض).
public ApplicationSignInManager( IApplicationUserManager userManager, IHttpContextAccessor contextAccessor, IUserClaimsPrincipalFactory<User> claimsFactory, IOptions<IdentityOptions> optionsAccessor, ILogger<ApplicationSignInManager> logger) : base((UserManager<User>)userManager, contextAccessor, claimsFactory, optionsAccessor, logger)
الف) استفادهی از IApplicationUserManager بجای UserManager پیشفرض
3) تکمیل متدهایی که باید به اجبار پس از این ارث بری پیاده سازی شوند.
SignInManager پایه نیازی به پیاده سازی اجباری متدی ندارد.
4) استخراج یک اینترفیس از کلاس نهایی تشکیل شده (توسط ابزارهای Refactoring).
محتوای این اینترفیس را در IApplicationSignInManager مشاهده میکنید.
5) معرفی اینترفیس و همچنین نمونهی توکار این سرویس به سیستم تزریق وابستگیهای ASP.NET Core جهت استفادهی از این سرویس جدید سفارشی سازی شده.
services.AddScoped<IApplicationSignInManager, ApplicationSignInManager>(); services.AddScoped<SignInManager<User>, ApplicationSignInManager>();
معرفی نهایی سرویسهای سفارشی سازی شده به ASP.NET Identity Core
تا اینجا سرویسهای پایهی این فریم ورک را جهت معرفی موجودیتهای سفارشی سازی شدهی خود سفارشی سازی کردیم و همچنین آنها را به سیستم تزریق وابستگیهای ASP.NET Core نیز معرفی نمودیم. مرحلهی آخر، ثبت این سرویسها در رجیستری ASP.NET Core Identity است:
services.AddIdentity<User, Role>(identityOptions => { }).AddUserStore<ApplicationUserStore>() .AddUserManager<ApplicationUserManager>() .AddRoleStore<ApplicationRoleStore>() .AddRoleManager<ApplicationRoleManager>() .AddSignInManager<ApplicationSignInManager>() // You **cannot** use .AddEntityFrameworkStores() when you customize everything //.AddEntityFrameworkStores<ApplicationDbContext, int>() .AddDefaultTokenProviders();
در اینجا متد AddIdentity یک سری تنظیمهای پیش فرضها این فریم ورک مانند اندازهی طول کلمهی عبور، نام کوکی و غیره را در اختیار ما قرار میدهد به همراه ثبت تعدادی سرویس پایه مانند نرمال ساز نامها و ایمیلها. سپس توسط متدهای AddUserStore، AddUserManager و ... ایی که مشاهده میکنید، سبب بازنویسی سرویسهای پیش فرض این فریم ورک به سرویسهای سفارشی خود خواهیم شد.
در این مرحلهاست که اگر Migration را اجرا کنید، کار میکند و خطای تبدیل نشدن کلاسها به یکدیگر را دریافت نخواهید کرد.
تشکیل مرحله استفادهی از ASP.NET Core Identity و ثبت اولین کاربر در بانک اطلاعاتی به صورت خودکار
روال متداول کار با امکانات کتابخانههای نوشته شدهی برای ASP.NET Core، ثبت سرویسهای پایهی آنها توسط متدهای Add است که نمونهی services.AddIdentity فوق دقیقا همین کار را انجام میدهد. مرحلهی بعد به app.UseIdentity میرسیم که کار ثبت میانافزارهای این فریم ورک را انجام میدهد. متد UseCustomIdentityServices کلاس IdentityServicesRegistry اینکار را انجام میدهد که از آن در کلاس آغازین برنامه استفاده شدهاست.
public static void UseCustomIdentityServices(this IApplicationBuilder app) { app.UseIdentity(); var identityDbInitialize = app.ApplicationServices.GetService<IIdentityDbInitializer>(); identityDbInitialize.Initialize(); identityDbInitialize.SeedData(); }
الف) متد Initialize آن، متد context.Database.Migrate را فراخوانی میکند. به همین جهت دیگر نیاز به اعمال دستی حاصل Migrations، به بانک اطلاعاتی نخواهد بود. متد Database.Migrate هر مرحلهی اعمال نشدهای را که باقی مانده باشد، به صورت خودکار اعمال میکند.
ب) متد SeedData آن، به نحو صحیحی یک Scope جدید را ایجاد کرده و توسط آن به ApplicationDbContext دسترسی پیدا میکند تا نقش Admin و کاربر Admin را به سیستم اضافه کند. همچنین به کاربر Admin، نقش Admin را نیز انتساب میدهد. تنظیمات این کاربران را نیز از فایل appsettings.json و مدخل AdminUserSeed آن دریافت میکند.
کدهای کامل این سری را در مخزن کد DNT Identity میتوانید ملاحظه کنید.
تبدیل یک View به رشته و بازگشت آن به همراه نتایج JSON حاصل از یک عملیات Ajax ایی در ASP.NET MVC
ممکن است بخواهیم در پاسخ یک تقاضای Ajax ایی، اگر عملیات در سمت سرور با موفقیت انجام شد، خروجی یک Controller action را به کاربر نهایی نشان دهیم. در
چنین سناریویی لازم است که بتوانیم خروجی یک action را
بصورت رشته برگردانیم. در این مقاله به این مسئله خواهیم پرداخت .
فرض کنید در یک سیستم وبلاگ ساده قصد داریم امکان کامنت گذاشتن بصورت Ajax را پیاده سازی کنیم. یک ایده عملی و کارآ
این است: بعد از اینکه کاربر متن کامنت را وارد کرد و دکمهی ارسال کامنت را زد،
تقاضا به سمت سرور ارسال شود و اگر سرور پیغام موفقیت را صادر کرد، متن نوشته شده
توسط کاربر را به کمک کدهای JavaScript و در همان سمت کلاینت بصورت یک
کادر کامنت جدید به محتوای صفحه اضافه کنیم. بنده در اینجا برای اینکه بتوانم اصل
موضوع مورد بحث را توضیح دهم، از یک سناریوی جایگزین استفاده میکنم؛ کاربر موقعیکه دکمه ارسال را زد، تقاضا به سرور ارسال میشود. سرور بعد از انجام عملیات، تحت یک
شی JSON هم نتیجهی انجام عملیات و هم
محتوای HTML نمایش کامنت جدید در صفحه را به سمت کلاینت
ارسال خواهد کرد و کلاینت در صورت موفقیت آمیز بودن عملیات، آن محتوا را به صفحه
اضافه میکند.
با توجه به توضیحات داده شده، ابتدا یک شیء نیاز داریم تا بتوانیم توسط آن نتیجهی عملیات Ajax ایی را بصورت JSON به سمت کلاینت ارسال کنیم:
public class MyJsonResult { public bool success { set; get; } public bool HasWarning { set; get; } public string WarningMessage { set; get; } public int errorcode { set; get; }
public string message {set; get; } public object data { set; get; } }
سپس به متدی نیاز داریم که کار تبدیل نتیجهی action را به رشته، انجام دهد:
public static string RenderViewToString(ControllerContext context, string viewPath, object model = null, bool partial = false) { ViewEngineResult viewEngineResult = null; if (partial) viewEngineResult = ViewEngines.Engines.FindPartialView(context, viewPath); else viewEngineResult = ViewEngines.Engines.FindView(context, viewPath, null); if (viewEngineResult == null) throw new FileNotFoundException("View cannot be found."); var view = viewEngineResult.View; context.Controller.ViewData.Model = model; string result = null; using(var sw = new StringWriter()) { var ctx = new ViewContext(context, view, context.Controller.ViewData, context.Controller.TempData, sw); view.Render(ctx, sw); result = sw.ToString(); } return result; }
فرض کنیم در سمت Controller هم از کدی شبیه به این استفاده میکنیم:
public JsonResult AddComment(CommentViewModel model) { MyJsonResult result = new MyJsonResult() { success = false; }; if (!ModelState.IsValid) { result.success = false; result.message = "لطفاً اطلاعات فرم را کامل وارد کنید"; return Json(result); } try { Comment theComment = model.toCommentModel(); //EF service factory Factory.CommentService.Create(theComment); Factory.SaveChanges(); result.data = Tools.RenderViewToString(this.ControllerContext, "/views/posts/_AComment", model, true); result.success = true; } catch (Exception ex) { result.success = false; result.message = "اشکال زمان اجرا"; } return Json(result); }
و در سمت کلاینت برای ارسال Form به صورت Ajax ایی خواهیم داشت:
@using (Ajax.BeginForm("AddComment", "posts", new AjaxOptions() { HttpMethod = "Post", OnSuccess = "AddCommentSuccess", LoadingElementId = "AddCommentLoading" }, new { id = "frmAddComment", @class = "form-horizontal" })) { @Html.HiddenFor(m => m.PostId) <label for="fname">@Texts.ContactName</label> <input type="text" id="fname" name="FullName" class="form-control" placeholder="@Texts.ContactName "> <label for="email">@Texts.Email</label> <input type="email" id="InputEmail" name="email" class="form-control" placeholder="@Texts.Email"> <br><textarea name="C_Content" cols="60" rows="10" class="form-control"></textarea><br> <input type="submit" value="@Texts.SubmitComments" name="" class="btn btn-primary"> <div class="loading-mask" style="display:none">@Texts.LoadingMessage</div> }
باید توجه شود Texts در اینجا یک Resource هست که به منظور نگهداری کلمات استفاده شده در سایت، برای زبانهای مختلف استفاده میشود (رجوع شود به مفهوم بومی سازی در Asp.net) .
و در قسمت script ها داریم:
<script type="text/javascript"> function AddCommentSuccess(jsData) { if (jsData && jsData.message) alert(jsData.message); if (jsData && jsData.success) { document.getElementById("frmAddComment").reset(); //افزودن کامنت جدید ساخته شده توسط کاربر به لیست کامنتهای صفحه $("#divAllComments").html(jsData.data + $("#divAllComments").html()); } } </script>
الف) اگر از jQuery Ajax استفاده میکنید، حتما باید استفاده از Url.Action را لحاظ کنید
برای نمونه اگر قسمتی از عملیات Ajaxایی برنامه شما به نحو زیر تعریف شده است :
$.ajax({ type: "POST", url: "/Home/EmployeeInfo", ...
در این حالت برنامه شما تنها در زمانیکه در ریشه یک دومین قرار گرفته باشد کار خواهد کرد. اگر برنامه شما در مسیری مانند http://www.site.com/myNewApp نصب شود، کلیه فراخوانیهای Ajax ایی آن دیگر کار نخواهند کرد چون مسیر url فوق به ریشه سایت اشاره میکند و نه مسیر جاری برنامه شما (در یک sub domain جدید).
به همین جهت در یک چنین حالتی حتما باید به کمک Url.Action مسیر یک اکشن متد را معرفی کرد تا به صورت خودکار بر اساس محل قرارگیری برنامه و تعاریف مسیریابی آن، Url صحیحی تولید شود.
@Url.Action("EmployeeInfo", "Home")
ب) دریافت Url مطلق از یک Url.Action
Urlهای تولید شده توسط Url.Action به صورت پیش فرض نسبی هستند (نسبت به محل نصب و قرارگیری برنامه تعریف میشوند). اگر نیاز به دریافت یک مسیر مطلق که با http برای مثال شروع میشود دارید، باید به نحو زیر عمل کرد:
@Url.Action("About", "Home", null, "http")
ج) استفاده از Url.Action در یک کنترلر
فرض کنید قصد تولید یک فید RSS را در کنترلری دارید. یکی از آیتمهایی که باید ارائه دهید، لینک به مطلب مورد نظر است. این لینک باید مطلق باشد همچنین در یک View هم قرار نداریم که به کمک @ بلافاصله به متد کمکی Url.Action دسترسی پیدا کنیم.
در کنترلرها، وهله جاری کلاس به شیء Url و متد Action آن به نحو زیر دسترسی دارد و خروجی نهایی آن یک رشته است:
var url = this.Url.Action(actionName: "Index", controllerName: "Post", protocol: "http", routeValues: new { id = item.Id });
د) استفاده از Url.Action در کلاسهای کمکی برنامه خارج از یک کنترلر
فرض کنید قصد تهیه یک Html Helper سفارشی را به کمک کدنویسی در یک کلاس مجزا دارید. در اینجا نیز نباید Urlها را دستی تولید کرد. در Html Helperهای سفارشی نیز میتوان به کمک متد UrlHelper.GenerateUrl، به همان امکانات Url.Action دسترسی یافت:
public static class Extensions { public static string MyLink(this HtmlHelper html, ...) { string url = UrlHelper.GenerateUrl(null, "actionName", "controllerName", null, html.RouteCollection, html.ViewContext.RequestContext, includeImplicitMvcValues: true); //...
PM> Install-Package DNTFrameworkCore.Web
- Refactor کردن فرمهای ثبت و ویرایش مرتبط با یک Aggregate، به یک PartialView که با یک ViewModel کار میکند. برای موجودیتهای ساده و پایه، همان Model/DTO، به عنوان Model متناظر با یک ویو یا به اصطلاح ViewModel استفاده میشود؛ ولی برای سایر موارد، از مدلی که نام آن با نام موجودیت + کلمه ModalViewModel یا FormViewModel تشکیل میشود، استفاده خواهیم کرد.
- یک فرم، در قالب یک پارشالویو، به صورت Ajaxای با استفاده از افزونه jquery-unobtrusive-ajax بارگذاری شده و به سرور ارسال خواهد شد.
- یک فرم براساس طراحی خود میتواند در قالب یک مودال باز شود، یا به منظور inline-editing آن را بارگذاری و به قسمتی از صفحه که مدنظرتان میباشد اضافه شود.
- وجود ویو Index به همراه پارشالویو _List برای نمایش لیستی و یک پارشالویو برای عملیات ثبت و ویرایش الزامی میباشد. البته اگر از مکانیزمی که در مطلب « طراحی یک گرید با jQuery Ajax و ASP.NET MVC به همراه پیاده سازی عملیات CRUD» مطرح شد، استفاده نمیکنید و نیاز دارید تا اطلاعات صفحهبندی شده، مرتب شده و فیلتر شدهای را در قالب JSON دریافت کنید، از اکشنمتد ReadPagedList کنترلر پایه استفاده کنید.
public class BlogsController : CrudController<IBlogService, int, BlogModel> { public BlogsController(IBlogService service) : base(service) { } protected override string CreatePermissionName => PermissionNames.Blogs_Create; protected override string EditPermissionName => PermissionNames.Blogs_Edit; protected override string ViewPermissionName => PermissionNames.Blogs_View; protected override string DeletePermissionName => PermissionNames.Blogs_Delete; protected override string ViewName => "_BlogModal"; }
<form asp-action="@(Model.IsNew() ? "Create" : "Edit")" asp-controller="Blogs" asp-modal-form="BlogForm"> <div> <input type="hidden" name="save-continue" value="true"/> <input asp-for="RowVersion" type="hidden"/> <input asp-for="Id" type="hidden"/> <div> <div> <label asp-for="Title"></label> <input asp-for="Title" autocomplete="off"/> <span asp-validation-for="Title"></span> </div> </div> <div> <div> <label asp-for="Url"></label> <input asp-for="Url" type="url"/> <span asp-validation-for="Url"></span> </div> </div> </div> ... </form>
<div> <a asp-modal-delete-link asp-model-id="@Model.Id" asp-modal-toggle="false" asp-controller="Blogs" asp-action="Delete" asp-if="!Model.IsNew()" asp-permission="@PermissionNames.Blogs_Delete" title="Delete Blog"> <i></i> </a> <a title="Refresh Blog" asp-if="!Model.IsNew()" asp-modal-link asp-modal-toggle="false" asp-controller="Blogs" asp-action="Edit" asp-route-id="@Model.Id"> <i></i> </a> <a title="New Blog" asp-modal-link asp-modal-toggle="false" asp-controller="Blogs" asp-action="Create"> <i></i> </a> <button type="button" data-dismiss="modal"> <i></i> Cancel </button> <button type="submit"> <i></i> Save Changes </button> </div>
public class RoleModalViewModel : RoleModel { public IReadOnlyList<LookupItem> PermissionList { get; set; } }
protected override IActionResult RenderView(RoleModel role) { var model = _mapper.Map<RoleModalViewModel>(role); model.PermissionList = ReadPermissionList(); return PartialView(ViewName, model); }
برای مدیریت سناریوهای Master-Detail به مانند قسمت مدیریت دسترسیها در تب Permissions فرم بالا، امکاناتی در زیرساخت تعبیه شده است ولی پیادهسازی آن را به عنوان یک تمرین و با توجه به سری مطالب «Editing Variable Length Reorderable Collections in ASP.NET MVC» به شما واگذار میکنم.
نکته تکمیلی: برای ارسال اطلاعات اضافی به ویو Index متناظر با یک موجودیت میتوانید متد RenderIndex را به شکل زیر بازنویسی کنید:
protected override IActionResult RenderIndex(IPagedQueryResult<RoleReadModel> model) { var indexModel = new RoleIndexViewModel { Items = model.Items, TotalCount = model.TotalCount, Permissions = ReadPermissionList() }; return Request.IsAjaxRequest() ? (IActionResult) PartialView(indexModel) : View(indexModel); }
مدل RoleIndexViewModel استفاده شده در تکه کد بالا نیز به شکل زیر خواهد بود:
public class RoleIndexViewModel : PagedQueryResult<RoleReadModel> { public IReadOnlyList<LookupItem> Permissions { get; set; } }
فرآیند بارگذاری یک پارشالویو در مودال
به عنوان مثال برای استفاده از مودالهای بوت استرپ، ایده کار به این شکل است که یک مودال را به شکل زیر در فایل Layout قرار دهید:
<div class="modal fade" @*tabindex="-1"*@ id="main-modal" data-keyboard="true" data-backdrop="static" role="dialog" aria-hidden="true"> <div class="modal-dialog modal-dialog-centered" role="document"> <div class="modal-content"> <div class="modal-body"> Loading... </div> </div> </div> </div>
سپس در زمان کلیک بروی یک دکمه Ajaxای، ابتدا main-modal را نمایش داده و بعد از دریافت پارشالویو از سرور، آن را با محتوای modal-content جایگزین میکنیم. به همین دلیل Tag Halperهای مطرح شده در مطلب جاری، callbackهای failure/complete/success متناظر با unobtrusive-ajax را نیز مقداردهی میکنند. برای این منظور نیاز است تا متدهای جاوااسکریپتی زیر نیز در سطح شیء window تعریف شده باشند:
/*---------------------------------- asp-modal-link ---------------------------*/ window.handleModalLinkLoaded = function (data, status, xhr) { prepareForm('#main-modal.modal form'); }; window.handleModalLinkFailed = function (xhr, status, error) { //.... }; /*---------------------------------- asp-modal-form ---------------------------*/ window.handleModalFormBegin = function (xhr) { $('#main-modal a').addClass('disabled'); $('#main-modal button').attr('disabled', 'disabled'); }; window.handleModalFormComplete = function (xhr, status) { $('#main-modal a').removeClass('disabled'); $('#main-modal button').removeAttr('disabled'); }; window.handleModalFormSucceeded = function (data, status, xhr) { if (xhr.getResponseHeader('Content-Type') === 'text/html; charset=utf-8') { prepareForm('#main-modal.modal form'); } else { hideMainModal(); } }; window.handleModalFormFailed = function (xhr, status, error, formId) { if (xhr.status === 400) { handleBadRequest(xhr, formId); } };
برای بررسی بیشتر، پیشنهاد میکنم پروژه DNTFrameworkCore.TestWebApp موجود در مخزن این زیرساخت را بازبینی کنید.
جیمیل هر ایمیلی را که به همراه آن یک فایل اجرایی پیوست شده باشد برگشت میزند. Zip کردن آن هم فایده ندارد چون محتویات فایلهای zip را هم بررسی میکند! فقط به نظر فرمت rar و همچنین 7z را بررسی نمیکند (احتمالا با مجوز آن مشکل دارد).
قویترین برنامه سورس بازی که این فرمت را پشتیبانی میکند، برنامه 7zip است و خوشبختانه محصور کنندههایی نیز جهت کار با کتابخانههای این برنامه برای دات نت فریم ورک موجود است. برای مثال:
مزیت استفاده از این کتابخانه این است که اغلب فرمتهای پر کاربرد را نیز پشتیبانی میکند (شامل zip ، gz ، rar و ...).
برای استفاده از آن به فایلهای 7z.dll و SevenZipSharp.dll نیاز خواهید داشت. 7z.dll از برنامه 7zip گرفته شده و SevenZipSharp.dll هم محصور کننده دات نتی آن است.
مثالی در مورد فشرده سازی با فرمت 7z با کمک کتابخانههای نامبرده شده:
using SevenZip;
using System.Windows.Forms;
using System;
class C7Z
{
public static void Compress7Z(string filePath, string outPath)
{
SevenZipCompressor.SetLibraryPath(String.Format(@"{0}\7z.dll", Application.StartupPath));
SevenZipCompressor cmp = new SevenZipCompressor
{
ArchiveFormat = OutArchiveFormat.SevenZip,
CompressionMethod = CompressionMethod.Lzma,
CompressionMode = CompressionMode.Create,
CompressionLevel = CompressionLevel.High,
VolumeSize = 0
};
cmp.CompressFiles(outPath, filePath);
}
}
C7Z.Compress7Z(@"C:\test\test.txt", @"C:\test\test.7z");
آشنایی اولیه با gRPC
syntax = "proto3"; package helloworld; service Greeter { rpc SayHello (HelloRequest) returns (HelloReply) {} } message HelloRequest { string name = 1; } message HelloReply { string message = 1; }
Google.Protobuf grpc Grpc.Tools
<ItemGroup> <Protobuf Include="helloworld.proto" /> </ItemGroup>
using System; using System.Collections.Generic; using System.Threading.Tasks; using Helloworld; using Grpc.Core; namespace ServerGrpc { class GreeterImpl : Greeter.GreeterBase { public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context) { System.Console.WriteLine("request made!"); return Task.FromResult(new HelloReply { Message = "Hello " + request.Name }); } } class Program { const int Port = 50051; public static void Main(string[] args) { Server server = new Server { Services = { Greeter.BindService(new GreeterImpl()) }, Ports = { new ServerPort("localhost", Port, ServerCredentials.Insecure) } }; server.Start(); Console.WriteLine("Greeter server listening on port " + Port); Console.WriteLine("Press any key to stop the server..."); Console.ReadKey(); server.ShutdownAsync().Wait(); } } }
obj/Debug/netcoreapp2.2(یا نسخهی دیگری که استفاده میکنید)
protoc helloworld.proto --go_out=plugins=grpc:.
package main import ( "fmt" "golang.org/x/net/context" "google.golang.org/grpc" "gosample/proto" ) func main() { initial() } func initial(){ conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure()) defer conn.Close() client := helloworld.NewGreeterClient(conn) data, _ := client.SayHello(context.Background(), &helloworld.HelloRequest{Name : "Ali"}) fmt.Println(data) }
ردیابی تغییرات در سمت کلاینت توسط Web API
فرض کنید میخواهیم از سرویسهای REST-based برای انجام عملیات CRUD روی یک Object graph استفاده کنیم. همچنین میخواهیم رویکردی در سمت کلاینت برای بروز رسانی کلاس موجودیتها پیاده سازی کنیم که قابل استفاده مجدد (reusable) باشد. علاوه بر این دسترسی دادهها توسط مدل Code-First انجام میشود.
در مثال جاری یک اپلیکیشن کلاینت (برنامه کنسول) خواهیم داشت که سرویسهای ارائه شده توسط پروژه Web API را فراخوانی میکند. هر پروژه در یک Solution مجزا قرار دارد، با این کار یک محیط n-Tier را شبیه سازی میکنیم.
مدل زیر را در نظر بگیرید.
همانطور که میبینید مدل مثال جاری مشتریان و شماره تماس آنها را ارائه میکند. میخواهیم مدلها و کد دسترسی به دادهها را در یک سرویس Web API پیاده سازی کنیم تا هر کلاینتی که به HTTP دسترسی دارد بتواند از آن استفاده کند. برای ساخت سرویس مذکور مراحل زیر را دنبال کنید.
- در ویژوال استودیو پروژه جدیدی از نوع ASP.NET Web Application بسازید و قالب پروژه را Web API انتخاب کنید. نام پروژه را به Recipe4.Service تغییر دهید.
- کنترلر جدیدی با نام CustomerController به پروژه اضافه کنید.
- کلاسی با نام BaseEntity ایجاد کنید و کد آن را مطابق لیست زیر تغییر دهید. تمام موجودیتها از این کلاس پایه مشتق خواهند شد که خاصیتی بنام TrackingState را به آنها اضافه میکند. کلاینتها هنگام ویرایش آبجکت موجودیتها باید این فیلد را مقدار دهی کنند. همانطور که میبینید این خاصیت از نوع TrackingState enum مشتق میشود. توجه داشته باشید که این خاصیت در دیتابیس ذخیره نخواهد شد. با پیاده سازی enum وضعیت ردیابی موجودیتها بدین روش، وابستگیهای EF را برای کلاینت از بین میبریم. اگر قرار بود وضعیت ردیابی را مستقیما از EF به کلاینت پاس دهیم وابستگیهای بخصوصی معرفی میشدند. کلاس DbContext اپلیکیشن در متد OnModelCreating به EF دستور میدهد که خاصیت TrackingState را به جدول موجودیت نگاشت نکند.
public abstract class BaseEntity { protected BaseEntity() { TrackingState = TrackingState.Nochange; } public TrackingState TrackingState { get; set; } } public enum TrackingState { Nochange, Add, Update, Remove, }
- کلاسهای موجودیت Customer و PhoneNumber را ایجاد کنید و کد آنها را مطابق لیست زیر تغییر دهید.
public class Customer : BaseEntity { public int CustomerId { get; set; } public string Name { get; set; } public string Company { get; set; } public virtual ICollection<Phone> Phones { get; set; } } public class Phone : BaseEntity { public int PhoneId { get; set; } public string Number { get; set; } public string PhoneType { get; set; } public int CustomerId { get; set; } public virtual Customer Customer { get; set; } }
- با استفاده از NuGet Package Manager کتابخانه Entity Framework 6 را به پروژه اضافه کنید.
- کلاسی با نام Recipe4Context ایجاد کنید و کد آن را مطابق لیست زیر تغییر دهید. در این کلاس از یکی از قابلیتهای جدید EF 6 بنام "Configuring Unmapped Base Types" استفاده کرده ایم. با استفاده از این قابلیت جدید هر موجودیت را طوری پیکربندی میکنیم که خاصیت TrackingState را نادیده بگیرند. برای اطلاعات بیشتر درباره این قابلیت EF 6 به این لینک مراجعه کنید.
public class Recipe4Context : DbContext { public Recipe4Context() : base("Recipe4ConnectionString") { } public DbSet<Customer> Customers { get; set; } public DbSet<Phone> Phones { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { // Do not persist TrackingState property to data store // This property is used internally to track state of // disconnected entities across service boundaries. // Leverage the Custom Code First Conventions features from Entity Framework 6. // Define a convention that performs a configuration for every entity // that derives from a base entity class. modelBuilder.Types<BaseEntity>().Configure(x => x.Ignore(y => y.TrackingState)); modelBuilder.Entity<Customer>().ToTable("Customers"); modelBuilder.Entity<Phone>().ToTable("Phones"); } }
- فایل Web.config پروژه را باز کنید و رشته اتصال زیر را به قسمت ConnectionStrings اضافه نمایید.
<connectionStrings> <add name="Recipe4ConnectionString" connectionString="Data Source=.; Initial Catalog=EFRecipes; Integrated Security=True; MultipleActiveResultSets=True" providerName="System.Data.SqlClient" /> </connectionStrings>
- فایل Global.asax را باز کنید و کد زیر را به متد Application_Start اضافه نمایید. این کد بررسی Entity Framework Model Compatibility را غیرفعال میکند و به JSON serializer دستور میدهد که self-referencing loop خواص پیمایشی را نادیده بگیرد. این حلقه بدلیل رابطه bidirectional بین موجودیتهای Customer و PhoneNumber بوجود میآید.
protected void Application_Start() { // Disable Entity Framework Model Compatibilty Database.SetInitializer<Recipe1Context>(null); // The bidirectional navigation properties between related entities // create a self-referencing loop that breaks Web API's effort to // serialize the objects as JSON. By default, Json.NET is configured // to error when a reference loop is detected. To resolve problem, // simply configure JSON serializer to ignore self-referencing loops. GlobalConfiguration.Configuration.Formatters.JsonFormatter .SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore; ... }
- کلاسی با نام EntityStateFactory بسازید و کد آن را مطابق لیست زیر تغییر دهید. این کلاس مقدار خاصیت TrackingState که به کلاینتها ارائه میشود را به مقادیر متناظر کامپوننتهای ردیابی EF تبدیل میکند.
public static EntityState Set(TrackingState trackingState) { switch (trackingState) { case TrackingState.Add: return EntityState.Added; case TrackingState.Update: return EntityState.Modified; case TrackingState.Remove: return EntityState.Deleted; default: return EntityState.Unchanged; } }
- در آخر کد کنترلر CustomerController را مطابق لیست زیر بروز رسانی کنید.
public class CustomerController : ApiController { // GET api/customer public IEnumerable<Customer> Get() { using (var context = new Recipe4Context()) { return context.Customers.Include(x => x.Phones).ToList(); } } // GET api/customer/5 public Customer Get(int id) { using (var context = new Recipe4Context()) { return context.Customers.Include(x => x.Phones).FirstOrDefault(x => x.CustomerId == id); } } [ActionName("Update")] public HttpResponseMessage UpdateCustomer(Customer customer) { using (var context = new Recipe4Context()) { // Add object graph to context setting default state of 'Added'. // Adding parent to context automatically attaches entire graph // (parent and child entities) to context and sets state to 'Added' // for all entities. context.Customers.Add(customer); foreach (var entry in context.ChangeTracker.Entries<BaseEntity>()) { entry.State = EntityStateFactory.Set(entry.Entity.TrackingState); if (entry.State == EntityState.Modified) { // For entity updates, we fetch a current copy of the entity // from the database and assign the values to the orginal values // property from the Entry object. OriginalValues wrap a dictionary // that represents the values of the entity before applying changes. // The Entity Framework change tracker will detect // differences between the current and original values and mark // each property and the entity as modified. Start by setting // the state for the entity as 'Unchanged'. entry.State = EntityState.Unchanged; var databaseValues = entry.GetDatabaseValues(); entry.OriginalValues.SetValues(databaseValues); } } context.SaveChanges(); } return Request.CreateResponse(HttpStatusCode.OK, customer); } [HttpDelete] [ActionName("Cleanup")] public HttpResponseMessage Cleanup() { using (var context = new Recipe4Context()) { context.Database.ExecuteSqlCommand("delete from phones"); context.Database.ExecuteSqlCommand("delete from customers"); return Request.CreateResponse(HttpStatusCode.OK); } } }
- در ویژوال استودیو پروژه جدیدی از نوع Console Application بسازید و نام آن را به Recipe4.Client تغییر دهید.
- فایل program.cs را باز کنید و کد آن را مطابق لیست زیر تغییر دهید.
internal class Program { private HttpClient _client; private Customer _bush, _obama; private Phone _whiteHousePhone, _bushMobilePhone, _obamaMobilePhone; private HttpResponseMessage _response; private static void Main() { Task t = Run(); t.Wait(); Console.WriteLine("\nPress <enter> to continue..."); Console.ReadLine(); } private static async Task Run() { var program = new Program(); program.ServiceSetup(); // do not proceed until clean-up completes await program.CleanupAsync(); program.CreateFirstCustomer(); // do not proceed until customer is added await program.AddCustomerAsync(); program.CreateSecondCustomer(); // do not proceed until customer is added await program.AddSecondCustomerAsync(); // do not proceed until customer is removed await program.RemoveFirstCustomerAsync(); // do not proceed until customers are fetched await program.FetchCustomersAsync(); } private void ServiceSetup() { // set up infrastructure for Web API call _client = new HttpClient { BaseAddress = new Uri("http://localhost:62799/") }; // add Accept Header to request Web API content negotiation to return resource in JSON format _client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue ("application/json")); } private async Task CleanupAsync() { // call the cleanup method from the service _response = await _client.DeleteAsync("api/customer/cleanup/"); } private void CreateFirstCustomer() { // create customer #1 and two phone numbers _bush = new Customer { Name = "George Bush", Company = "Ex President", // set tracking state to 'Add' to generate a SQL Insert statement TrackingState = TrackingState.Add, }; _whiteHousePhone = new Phone { Number = "212 222-2222", PhoneType = "White House Red Phone", // set tracking state to 'Add' to generate a SQL Insert statement TrackingState = TrackingState.Add, }; _bushMobilePhone = new Phone { Number = "212 333-3333", PhoneType = "Bush Mobile Phone", // set tracking state to 'Add' to generate a SQL Insert statement TrackingState = TrackingState.Add, }; _bush.Phones.Add(_whiteHousePhone); _bush.Phones.Add(_bushMobilePhone); } private async Task AddCustomerAsync() { // construct call to invoke UpdateCustomer action method in Web API service _response = await _client.PostAsync("api/customer/updatecustomer/", _bush, new JsonMediaTypeFormatter()); if (_response.IsSuccessStatusCode) { // capture newly created customer entity from service, which will include // database-generated Ids for all entities _bush = await _response.Content.ReadAsAsync<Customer>(); _whiteHousePhone = _bush.Phones.FirstOrDefault(x => x.CustomerId == _bush.CustomerId); _bushMobilePhone = _bush.Phones.FirstOrDefault(x => x.CustomerId == _bush.CustomerId); Console.WriteLine("Successfully created Customer {0} and {1} Phone Numbers(s)", _bush.Name, _bush.Phones.Count); foreach (var phoneType in _bush.Phones) { Console.WriteLine("Added Phone Type: {0}", phoneType.PhoneType); } } else Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase); } private void CreateSecondCustomer() { // create customer #2 and phone numbers _obama = new Customer { Name = "Barack Obama", Company = "President", // set tracking state to 'Add' to generate a SQL Insert statement TrackingState = TrackingState.Add, }; _obamaMobilePhone = new Phone { Number = "212 444-4444", PhoneType = "Obama Mobile Phone", // set tracking state to 'Add' to generate a SQL Insert statement TrackingState = TrackingState.Add, }; // set tracking state to 'Modifed' to generate a SQL Update statement _whiteHousePhone.TrackingState = TrackingState.Update; _obama.Phones.Add(_obamaMobilePhone); _obama.Phones.Add(_whiteHousePhone); } private async Task AddSecondCustomerAsync() { // construct call to invoke UpdateCustomer action method in Web API service _response = await _client.PostAsync("api/customer/updatecustomer/", _obama, new JsonMediaTypeFormatter()); if (_response.IsSuccessStatusCode) { // capture newly created customer entity from service, which will include // database-generated Ids for all entities _obama = await _response.Content.ReadAsAsync<Customer>(); _whiteHousePhone = _bush.Phones.FirstOrDefault(x => x.CustomerId == _obama.CustomerId); _bushMobilePhone = _bush.Phones.FirstOrDefault(x => x.CustomerId == _obama.CustomerId); Console.WriteLine("Successfully created Customer {0} and {1} Phone Numbers(s)", _obama.Name, _obama.Phones.Count); foreach (var phoneType in _obama.Phones) { Console.WriteLine("Added Phone Type: {0}", phoneType.PhoneType); } } else Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase); } private async Task RemoveFirstCustomerAsync() { // remove George Bush from underlying data store. // first, fetch George Bush entity, demonstrating a call to the // get action method on the service while passing a parameter var query = "api/customer/" + _bush.CustomerId; _response = _client.GetAsync(query).Result; if (_response.IsSuccessStatusCode) { _bush = await _response.Content.ReadAsAsync<Customer>(); // set tracking state to 'Remove' to generate a SQL Delete statement _bush.TrackingState = TrackingState.Remove; // must also remove bush's mobile number -- must delete child before removing parent foreach (var phoneType in _bush.Phones) { // set tracking state to 'Remove' to generate a SQL Delete statement phoneType.TrackingState = TrackingState.Remove; } // construct call to remove Bush from underlying database table _response = await _client.PostAsync("api/customer/updatecustomer/", _bush, new JsonMediaTypeFormatter()); if (_response.IsSuccessStatusCode) { Console.WriteLine("Removed {0} from database", _bush.Name); foreach (var phoneType in _bush.Phones) { Console.WriteLine("Remove {0} from data store", phoneType.PhoneType); } } else Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase); } else { Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase); } } private async Task FetchCustomersAsync() { // finally, return remaining customers from underlying data store _response = await _client.GetAsync("api/customer/"); if (_response.IsSuccessStatusCode) { var customers = await _response.Content.ReadAsAsync<IEnumerable<Customer>>(); foreach (var customer in customers) { Console.WriteLine("Customer {0} has {1} Phone Numbers(s)", customer.Name, customer.Phones.Count()); foreach (var phoneType in customer.Phones) { Console.WriteLine("Phone Type: {0}", phoneType.PhoneType); } } } else { Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase); } } }
- در آخر کلاسهای Customer, Phone و BaseEntity را به پروژه کلاینت اضافه کنید. چنین کدهایی بهتر است در لایه مجزایی قرار گیرند و بین لایههای مختلف اپلیکیشن به اشتراک گذاشته شوند.
اگر اپلیکیشن کلاینت را اجرا کنید با خروجی زیر مواجه خواهید شد.
شرح مثال جاری
با اجرای اپلیکیشن Web API شروع کنید. این اپلیکیشن یک MVC Web Controller دارد که پس از اجرا شما را به صفحه خانه هدایت میکند. در این مرحله سایت در حال اجرا است و سرویسها قابل دسترسی هستند.
سپس اپلیکیشن کنسول را باز کنید و روی خط اول کد فایل program.cs یک breakpoint قرار داده و آن را اجرا کنید. ابتدا آدرس سرویس را نگاشت میکنیم و از سرویس درخواست میکنیم که اطلاعات را با فرمت JSON بازگرداند.
سپس توسط متد DeleteAsync که روی آبجکت HttpClient تعریف شده است اکشن متد Cleanup را روی سرویس فراخوانی میکنیم. این فراخوانی تمام دادههای پیشین را حذف میکند.
در قدم بعدی یک مشتری بهمراه دو شماره تماس میسازیم. توجه کنید که برای هر موجودیت مشخصا خاصیت TrackingState را مقدار دهی میکنیم تا کامپوننتهای Change-tracking در EF عملیات لازم SQL برای هر موجودیت را تولید کنند.
سپس توسط متد PostAsync که روی آبجکت HttpClient تعریف شده اکشن متد UpdateCustomer را روی سرویس فراخوانی میکنیم. اگر به این اکشن متد یک breakpoint اضافه کنید خواهید دید که موجودیت مشتری را بعنوان یک پارامتر دریافت میکند و آن را به context جاری اضافه مینماید. با اضافه کردن موجودیت به کانتکست جاری کل object graph اضافه میشود و EF شروع به ردیابی تغییرات آن میکند. دقت کنید که آبجکت موجودیت باید Add شود و نه Attach.
قدم بعدی جالب است، هنگامی که از خاصیت DbChangeTracker استفاده میکنیم. این خاصیت روی آبجکت context تعریف شده و یک <IEnumerable<DbEntityEntry را با نام Entries ارائه میکند. در اینجا بسادگی نوع پایه EntityType را تنظیم میکنیم. این کار به ما اجازه میدهد که در تمام موجودیت هایی که از نوع BaseEntity هستند پیمایش کنیم. اگر بیاد داشته باشید این کلاس، کلاس پایه تمام موجودیتها است. در هر مرحله از پیمایش (iteration) با استفاده از کلاس EntityStateFactory مقدار خاصیت TrackingState را به مقدار متناظر در سیستم ردیابی EF تبدیل میکنیم. اگر کلاینت مقدار این فیلد را به Modified تنظیم کرده باشد پردازش بیشتری انجام میشود. ابتدا وضعیت موجودیت را از Modified به Unchanged تغییر میدهیم. سپس مقادیر اصلی را با فراخوانی متد GetDatabaseValues روی آبجکت Entry از دیتابیس دریافت میکنیم. فراخوانی این متد مقادیر موجود در دیتابیس را برای موجودیت جاری دریافت میکند. سپس مقادیر بدست آمده را به کلکسیون OriginalValues اختصاص میدهیم. پشت پرده، کامپوننتهای EF Change-tracking بصورت خودکار تفاوتهای مقادیر اصلی و مقادیر ارسالی را تشخیص میدهند و فیلدهای مربوطه را با وضعیت Modified علامت گذاری میکنند. فراخوانیهای بعدی متد SaveChanges تنها فیلدهایی که در سمت کلاینت تغییر کرده اند را بروز رسانی خواهد کرد و نه تمام خواص موجودیت را.
در اپلیکیشن کلاینت عملیات افزودن، بروز رسانی و حذف موجودیتها توسط مقداردهی خاصیت TrackingState را نمایش داده ایم.
متد UpdateCustomer در سرویس ما مقادیر TrackingState را به مقادیر متناظر EF تبدیل میکند و آبجکتها را به موتور change-tracking ارسال میکند که نهایتا منجر به تولید دستورات لازم SQL میشود.
نکته: در اپلیکیشنهای واقعی بهتر است کد دسترسی دادهها و مدلهای دامنه را به لایه مجزایی منتقل کنید. همچنین پیاده سازی فعلی change-tracking در سمت کلاینت میتواند توسعه داده شود تا با انواع جنریک کار کند. در این صورت از نوشتن مقادیر زیادی کد تکراری جلوگیری خواهید کرد و از یک پیاده سازی میتوانید برای تمام موجودیتها استفاده کنید.