public abstract class BaseEntity { [ColumnInfo("کد",pWidth:70)] public int Id { get; set; } [ColumnInfo("",pIsVisible:false,pIsEditable:false)] [NotMapped] public bool IsDeleted { get; set; } }
تا اینجا در قسمت سوم، نحوهی قراردادن یک کامپوننت را در کامپوننتی دیگر، توسط مقدار دهی خاصیت directives مزین کنندهی Component بررسی کردیم. همینقدر که یک کامپوننت دارای selector باشد، قابلیت قرارگرفتن در یک کامپوننت دیگر را دارد. اما چگونه باید بین این کامپوننتها ارتباط برقرار کرد؟
تهیه کامپوننت نمایش ستارهای امتیازهای محصولات
مثال نمایش لیست محصولات سری جاری، دارای ستون «5Star Rating» است. در این قسمت میخواهیم بجای نمایش عددی این امتیازها، کامپوننتی را طراحی کنیم که نماش ستارهای آنها را سبب شود. این کامپوننت باید بتواند یک مقدار ورودی، یا همان عدد امتیاز محصول را از کامپوننت دربرگیرندهی آن دریافت کند. همچنین میخواهیم اگر کاربر بر روی این ستارهها کلیک کرد، کامپوننت در برگیرنده را نیز مطلع سازیم.
در این مثال در فایل product-list.component.html چنین سطری تعریف شدهاست:
<td>{{ product.starRating }}</td>
با توجه به اینکه کامپوننت نمایش ستارهای امتیازها، قابلیت استفادهی مجدد را دارد و الزامی ندارد که حتما در لیست محصولات، بکار گرفته شود، بهتر است محل تعریف آنرا به خارج از پوشهی products فعلی منتقل کنیم. برای مثال میتوان پوشهی app\shared را برای آن و تمامی کامپوننتهای با قابلیت استفادهی مجدد ایجاد کرد.
برای شروع، فایل جدید App\shared\star.component.ts را اضافه کنید؛ با کدهای کامل ذیل:
import { Component, OnChanges, Input, Output, EventEmitter } from 'angular2/core'; @Component({ selector: 'ai-star', templateUrl: 'app/shared/star.component.html', styleUrls: ['app/shared/star.component.css'] }) export class StarComponent implements OnChanges { @Input() rating: number; starWidth: number; @Output() ratingClicked: EventEmitter<string> = new EventEmitter<string>(); ngOnChanges(): void { this.starWidth = this.rating * 86 / 5; } onClick() { this.ratingClicked.emit(`The rating ${this.rating} was clicked!`); } }
سپس مسیر template و مسیر فایل css ویژهی آن، در تزئین کنندهی Component مشخص شدهاند. محتوای کامل این دو فایل را در ذیل مشاهده میکنید:
الف) محتوای فایل App\shared\star.component.html
<div class="crop" [style.width.px]="starWidth" [title]="rating" (click)='onClick()'> <div style="width: 86px"> <span class="glyphicon glyphicon-star"></span> <span class="glyphicon glyphicon-star"></span> <span class="glyphicon glyphicon-star"></span> <span class="glyphicon glyphicon-star"></span> <span class="glyphicon glyphicon-star"></span> </div> </div>
.crop { overflow: hidden; } div { cursor: pointer; }
معرفی مقدماتی life cycle hooks در قسمت قبل صورت گرفت. در اینجا چون نیاز است به ازای هر بار رندر شدن این کامپوننت، عرض آن متفاوت باشد، بنابراین نیاز است راهی را پیدا کنیم تا بتوان مقدار خاصیت starWidth را متغیر کرد. به همین منظور از hook مخصوص این تغییرات یا همان OnChanges استفاده میشود. بنابراین باید کلاس این کامپوننت، اینترفیس OnChanges را پیاده سازی کند. پس از آن، importهای لازم جهت تعریف OnChanges به ابتدای فایل اضافه شده و همچنین متد ngOnChanges نیز جهت تکمیل کار پیاده سازی اینترفیس OnChanges، به کلاس جاری اضافه میشود.
کار متد ngOnChanges، تبدیل عدد امتیاز یک محصول، به عرض div نمایش ستارهها است.
مکانیزم کار رخداد ngOnChanges و دریافت اطلاعات از والد
متد ngOnChanges، تنها به خواص ویژهای به نام «input properties» واکنش نشان میدهد. اگر یک کامپوننت تو در توی قرار گرفتهی در یک کامپوننت دیگر، بخواهد اطلاعاتی را از والد خود دریافت کند، باید خاصیتی را در معرض دید آن دربرگیرنده قرار دهد. این کار توسط decorator ویژهای به نام ()Input@ انجام میشود.
به همین جهت است که پیش از خاصیت rating در کلاس StarComponent، شاهد درج مزین کنندهی ویژهی ()Input@ هستیم:
export class StarComponent implements OnChanges { @Input() rating: number;
پس از آن، کامپوننت دربرگیرنده یا والد، این خاصیت ورودی ویژه را از طریق روش property binding متداول، مقدار دهی میکند:
[rating]='product.starRating'
بدیهی است در اینجا چون خاصیت starWidth از نوع ورودی تعریف نشدهاست، قابلیت property binging فوق را در کامپوننت والد، ندارد.
اکنون به ازای هر بار نمایش این کامپوننت فرزند، خاصیت rating ورودی آن مقدار دهی شده و مقدار آن در رخداد ngOnChanges قابل دسترسی و استفاده خواهد بود. اینجا است که میتوان از این مقدار تغییر یافته، جهت ترجمهی آن به عرض div نمایش ستارهها، استفاده کرد.
ارسال دادهها از کامپوننت فرزند به کامپوننت والد
تا اینجا با استفاده از «خواص ورودی» امکان دسترسی به مقادیر ارسالی از طرف والد را در کامپوننت فرزند، پیدا کردیم. عکس آن نیز امکان پذیر است؛ اما توسط رخدادها.
کامپوننت فرزند، با استفاده از decorator ویژهی دیگری به نام ()Output@ امکان ارسال رخدادها را به کامپوننت والد پیدا میکند:
export class StarComponent implements OnChanges { @Input() rating: number; starWidth: number; @Output() ratingClicked: EventEmitter<string> = new EventEmitter<string>();
در مثال جاری اگر کاربر بر روی div ستارههای نمایش داده شده کلیک کند، اتصال به آن از طریق event binging متداول انجام میشود (متد جدید onClick به رخداد click متصل شدهاست):
<div class="crop" [style.width.px]="starWidth" [title]="rating" (click)='onClick()'>
onClick() { this.ratingClicked.emit(`The rating ${this.rating} was clicked!`); }
تا اینجا مرحلهی تنظیمات رخدادها در کامپوننت فرزند صورت گرفت. ابتدا خاصیتی از نوع Output تعریف شد. سپس در کدهای قالب این کامپوننت جدید، متد onClick به رخداد click متصل گردید و سپس در کدهای مدیریت کنندهی این متد، متد ratingClicked.emit جهت ارسال اطلاعات نهایی به والد، فراخوانی گردید.
اکنون در کامپوننت والد، باید این مراحل برای دریافت اطلاعات از کامپوننت فرزند خود، طی شوند:
الف) ابتدا نام خاصیت مزین شدهی با Output، به عنوان مقصد event binding مشخص میشود و سپس متدی در کلاس کامپوننت والد، به آن متصل میگردد:
(ratingClicked)='onRatingClicked($event)'
ب) در ادامه، تعریف این متد جدید متصل شده را به کلاس ProductListComponent اضافه میکنیم:
onRatingClicked(message: string): void { this.pageTitle = 'Product List: ' + message; }
به این ترتیب با کلیک بر روی div هر کامپوننت نمایش ستارهای امتیازها، خاصیت pageTitle درج شدهی در صفحه تغییر میکند.
استفاده از کامپوننت نمایش ستارهای امتیازها
نکات کلی افزودن این کامپوننت جدید، تفاوتی با مطالب عنوان شدهی در قسمت سوم، در حین بررسی مراحل افزودن دایرکتیو نمایش لیست محصولات، به کامپوننت ریشهی سایت ندارد و یکی هستند.
برای افزودن و استفاده از این کامپوننت جدید، ابتدا قالب product-list.component.html را گشوده و سپس سطر نمایش عددی امتیاز یک محصول را به نحو ذیل تغییر میدهیم:
<td> <ai-star [rating]='product.starRating' (ratingClicked)='onRatingClicked($event)'> </ai-star> </td>
سپس باید به کلاس کامپوننت لیست محصولات (کامپوننت در برگیرنده) اعلام کرد که این کامپوننت جدید را باید از کجا پیدا کند. برای این منظور فایل product-list.component.ts را گشوده و خاصیت directives این کامپوننت را مقدار دهی میکنیم:
import { Component, OnInit } from 'angular2/core'; import { IProduct } from './product'; import { ProductFilterPipe } from './product-filter.pipe'; import { StarComponent } from '../shared/star.component'; @Component({ selector: 'pm-products', templateUrl: 'app/products/product-list.component.html', styleUrls: ['app/products/product-list.component.css'], pipes: [ProductFilterPipe], directives: [StarComponent] })
نمونهای از اجرای برنامه را در تصویر ذیل مشاهده میکنید:
در اینجا ستون امتیازهای محصولات با کامپوننت نمایش ستارهای این امتیازها جایگزین شدهاست و همچنین با کلیک بر روی یکی از آنها، عنوان panel جاری تغییر کردهاست.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MVC5Angular2.part6.zip
خلاصهی بحث
در اینجا نحوهی طراحی API عمومی یک کامپوننت را بررسی کردیم. تا زمانیکه خواص کلاس یک کامپوننت به نحو متداولی تعریف میشوند، میدان دید آنها محدود است به قالب تعریف شدهی متناظر با آنها. اگر نیاز است خاصیتی خارج از این قالب و به صورت عمومی در کامپوننت دربرگیرندهی دیگری در دسترس قرار گیرد، آنرا با مزین کنندهی ()Input@ مشخص میکنیم و اگر قرار است این کامپوننت فرزند، اطلاعاتی را به کامپوننت والد ارسال کند، اینکار را توسط رخدادها و با تعریف ویژگی ()Output@ و EventEmitter انجام میدهد. نوع آرگومان جنریک EventEmitter، تعیین کنندهی نوع اطلاعاتی است که قرار است به کامپوننت دربرگیرنده ارسال شوند.
پس از تعریف کامپوننت فرزند، برای تعریف آن در کامپوننت والد، از نام selector آن به عنوان یک المان جدید HTML استفاده میشود و سپس با استفاده از property binding، اطلاعات لازم، به خاصیت از نوع ()Input@ کامپوننت فرزند ارسال میگردد. از event binding برای دریافت رخدادها از کامپوننت فرزند استفاده میشود. در اینجا هر رخدادی که توسط مزین کنندهی ()Output@ تعریف شده باشد، میتواند به عنوان مقصد event binding تعریف شود و اگر نیاز است به رخدادهای property binding از والد به فرزند، گوش فرا داد، میتوان اینترفیس OnChanges را در کلاس کامپوننت فرزند پیاده سازی کرد.
مثال - نمایش درصد پیشرفت عملیات توسط SignalR
نکتهای در مورد نگارشهای مختلف SignalR
اگر برنامه شما قرار است دات نت 4 را پشتیبانی کند، آخرین نگارش SignalR که با آن سازگار است، نگارش 1.1.3 میباشد. بنابراین اگر دستور ذیل را اجرا کنید:
PM> Install-Package Microsoft.AspNet.SignalR
اگر دستور ذیل را اجرا کنید، SiganlR 1.x را نصب میکند که با دات نت 4 به بعد سازگار است:
PM> Install-Package Microsoft.AspNet.SignalR -Version 1.1.3
با اینکار Microsoft.AspNet.SignalR.JS نیز به صورت خودکار نصب میگردد و به این ترتیب کلاینت جاوا اسکریپتی SiganlR نیز در برنامه قابل استفاده خواهد بود.
تنظیمات فایل Global.asax.cs
سطر فراخوانی متد RouteTable.Routes.MapHubs باید در ابتدای متد Application_Start فایل Global.asax.cs قرار گیرد (پیش از هر تنظیم دیگری). تفاوتی هم نمیکند که برنامه وب فرم است یا MVC. به این ترتیب مسیریابیهای SignalR تنظیم شده و مسیر http://localhost/signalr/hubs قابل استفاده خواهد بود.
تنظیمات اسکریپتهای سمت کلاینت مورد نیاز
پس از نصب بسته SignalR، سه اسکریپت ذیل باید به ابتدای صفحه وب اضافه شوند تا کلاینتهای جاوا اسکریپتی SignalR بتوانند با سرور ارتباط برقرار کنند:
<script src="Scripts/jquery-1.6.4.min.js" type="text/javascript"></script> <script src="Scripts/jquery.signalR-1.1.3.min.js" type="text/javascript"></script> <script src="signalr/hubs" type="text/javascript"></script>
تعریف کلاس Hub برنامه
using Microsoft.AspNet.SignalR; namespace WebFormsSample03.Common { public class ProgressHub : Hub { /// <summary> /// این متد استاتیک تعریف شده تا در برنامه به صورت مستقیم قابل استفاده باشد /// یا میشد اصلا این متد تعریف نشود و از همان دریافت زمینه هاب در کنترلر استفاده گردد /// </summary> public static void UpdateProgressBar(int value, string connectionId) { var ctx = GlobalHost.ConnectionManager.GetHubContext<ProgressHub>(); ctx.Clients.Client(connectionId).updateProgressBar(value); //فراخوانی یک متد در سمت کلاینت } } }
البته تعریف این متد در اینجا ضروری نبود. حتی میشد بدنه کلاس هاب را خالی تعریف کرد و متد GetHubContext را مستقیما داخل یک کنترلر فراخوانی نمود.
متد UpdateProgressBar، مقدار value را به تنها یک کلاینت که Id آن مساوی connectionId دریافتی است، ارسال میکند. این کلاینت باید یک callback جاوا اسکریپتی را جهت تامین متد پویای updateProgressBar تدارک ببیند.
کلاس Web API کنترلر دریافت فایلها
فرقی نمیکند که برنامه شما از نوع وب فرم است یا MVC. امکانات Web API در هر دو نوع پروژه، قابل دسترسی است (همان ایده یک ASP.NET واحد).
بنابراین نیاز است یک کنترلر وب API جدید را به پروژه اضافه کرده و محتوای آن را به شکل ذیل تغییر دهیم:
using System.Threading; using System.Web.Http; using WebFormsSample03.Common; namespace WebFormsSample03 { public class DownloadRequest { public string Url { set; get; } public string ConnectionId { set; get; } } public class DownloaderController : ApiController { public void Post([FromBody]DownloadRequest data) { //todo: start downloading the data.Url .... ProgressHub.UpdateProgressBar(10, data.ConnectionId); Thread.Sleep(2000); ProgressHub.UpdateProgressBar(40, data.ConnectionId); Thread.Sleep(3000); ProgressHub.UpdateProgressBar(64, data.ConnectionId); Thread.Sleep(2000); ProgressHub.UpdateProgressBar(77, data.ConnectionId); Thread.Sleep(2000); ProgressHub.UpdateProgressBar(92, data.ConnectionId); Thread.Sleep(3000); ProgressHub.UpdateProgressBar(99, data.ConnectionId); Thread.Sleep(2000); ProgressHub.UpdateProgressBar(100, data.ConnectionId); } } }
using System; using System.Web.Http; using System.Web.Routing; namespace WebFormsSample03 { public class Global : System.Web.HttpApplication { protected void Application_Start(object sender, EventArgs e) { // Register the default hubs route: ~/signalr RouteTable.Routes.MapHubs(); RouteTable.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); } } }
همچنین در اینجا با توجه به مسیریابی تعریف شده، باید اطلاعات را به آدرس api/Downloader از نوع Post ارسال کرد.
تعریف کلاینت متصل به Hub
در سمت سرور، متد پویای updateProgressBar فراخوانی شده است. اکنون باید این متد را در سمت کلاینت پیاده سازی کنیم:
<form id="form1" runat="server"> <div> <input id="txtUrl" value="http://www.site.com/file.rar" type="text" /> <input id="send" type="button" value="start download ..." /> <br /> <div id="bar" style="border: #000 1px solid; width:300px;"></div> </div> </form> <script type="text/javascript"> $(function () { $.connection.hub.logging = true; //اطلاعات بیشتری را در جاوا اسکریپت کنسول مرورگر لاگ میکند var progressHub = $.connection.progressHub; //این نام مستعار پیشتر توسط ویژگی نام هاب تنظیم شده است progressHub.client.updateProgressBar = function (value) { //متدی که در اینجا تعریف شده دقیقا مطابق نام متد پویایی است که در هاب تعریف شده است //به این ترتیب سرور میتواند کلاینت را فراخوانی کند $("#bar").html(GaugeBar.generate(value)); }; $.connection.hub.start() // فاز اولیه ارتباط را آغاز میکند .done(function () { $("#send").click(function () { $("#send").attr('disabled', 'disabled'); var myClientId = $.connection.hub.id; // اکنون اتصال برقرار است به سرور $.ajax({ type: "POST", contentType: "application/json", url: "/api/Downloader", data: JSON.stringify({ Url: $("#txtUrl").val(), ConnectionId: myClientId }) }).success(function () { $("#send").removeAttr('disabled'); }).fail(function () { // }); }); }); }); </script>
در ابتدای کار صفحه، اتصال به progressHub برقرار میشود. اگر دقت کنید، نام این هاب با حروف کوچک در اینجا (در سمت کلاینت) آغاز میگردد.
سپس با تعریف یک callback به نام progressHub.client.updateProgressBar، پیامهای دریافتی از طرف سرور را به یک افزونه progress bar جیکوئری، برای نمایش ارسال میکند.
کار اتصال به رویداد کلیک دکمهی آغاز دریافت فایل، در متد done باید انجام شود. این callback زمانی فراخوانی میگردد که کار اتصال به سرور با موفقیت صورت گرفته باشد.
سپس در ادامه توسط jQuery Ajax، اطلاعات Url و همچنین Id کلاینت را به مسیر api/Downloader یا همان web api controller ارسال میکنیم.
کدهای کامل این مثال را از اینجا نیز میتوانید دریافت نمائید:
WebFormsSample03.zip
FluentValidation #2
NotNull | اطمینان از اینکه خاصیت مورد نظر Null نباشد |
NotEmpty | اطمینان از اینکه خاصیت مورد نظر Null یا رشته خالی نباشد (یا مقدار پیش فرض نباشد، مثلا 0 برای int) |
NotEqual | اطمینان از اینکه خاصیت مورد نظر برابر مقدار تعیین شده نباشد (یا برابر مقدار خاصیت دیگری نباشد) |
Equal | اطمینان از اینکه خاصیت مور نظر برابر مقدار تعیین شده باشد (یا برابر مقدار خاصیت دیگری باشد) |
Length | اطمینان از اینکه طول رشتهی خاصیت مورد نظر در محدوده خاصی باشد |
LessThan | اطمینان از اینکه مقدار خاصیت مورد نظر کوچکتر از مقدار تعیین شده باشد (یا کوچکتر از خاصیت دیگری) |
LessThanOrEqual | اطمینان از اینکه مقدار خاصیت مورد نظر کوچکتر یا مساوی مقدار تعیین شده باشد (یا کوچکتر مساوی مقدار خاصیت دیگری) |
GreaterThan | اطمینان از اینکه مقدار خاصیت مورد نظر بزرگتر از مقدار تعیین شده باشد (یا بزرگتر از مقدار خاصیت دیگری) |
GreaterThanOrEqual | اطمینان از اینکه مقدار خاصیت مورد نظر بزرگتر مساوی مقدار تعیین شده باشد (یا بزرگتر مساوی مقدار خاصیت دیگری) |
Matches | اطمینان از اینکه مقدار خاصیت مورد نظر با عبارت باقائده (Regular Expression) تنظیم شده مطابقت داشته باشد |
Must | اعتبارسنجی یک predicate با استفاده از Lambada Expressions. اگر عبارت Lambada مقدار true برگرداند اعتبارسنجی با موفقیت انجام شده و اگر false برگرداند، اعتبارسنجی با شکست مواجه شده است. |
اطمینان از اینکه مقدار خاصیت مورد نظر یک آدرس ایمیل معتبر باشد | |
CreditCard | اطمینان از اینکه مقدار خاصیت مورد نظر یک Credit Card باشد |
RuleFor(customer => customer.Surname).NotEqual(customer => customer.Forename);
RuleFor(customer => customer.Surname).NotNull().WithMessage("Please ensure that you have entered your Surname");
اعتبارسنجی تنها در مواقع خاص
RuleFor(customer => customer.CustomerDiscount).GreaterThan(0).When(customer => customer.IsPreferredCustomer);
When(customer => customer.IsPreferred, () => { RuleFor(customer => customer.CustomerDiscount).GreaterThan(0); RuleFor(customer => customer.CreditCardNumber).NotNull(); });
تعیین نحوه برخورد با اعتبارسنجیهای زنجیره ای
RuleFor(x => x.Surname).NotNull().NotEqual("foo");
RuleFor(x => x.Surname).Cascade(CascadeMode.StopOnFirstFailure).NotNull().NotEqual("foo");
public class PersonValidator : AbstractValidator<Person> { public PersonValidator() { // First set the cascade mode CascadeMode = CascadeMode.StopOnFirstFailure; // Rule definitions follow RuleFor(...) RuleFor(...) } }
راه اول ایجاد یک کلاس که از PropertyValidator مشتق میشود. برای توضیح نحوه استفاده از این راه، تصور کنید که میخواهیم یک اعتبارسنج سفارشی درست کنیم تا چک کند که یک لیست حتماً کمتر از 10 آیتم داخل خود داشته باشد. در این صورت کدی که بایستی نوشته شود به صورت زیر خواهد بود:
using System.Collections.Generic; using FluentValidation.Validators; public class ListMustContainFewerThanTenItemsValidator<T> : PropertyValidator { public ListMustContainFewerThanTenItemsValidator() : base("Property {PropertyName} contains more than 10 items!") { } protected override bool IsValid(PropertyValidatorContext context) { var list = context.PropertyValue as IList<T>; if(list != null && list.Count >= 10) { return false; } return true; } }
برای استفاده از این Validator سفارشی نیز میتوان از متد SetValidator به صورت زیر استفاده نمود:
public class PersonValidator : AbstractValidator<Person> { public PersonValidator() { RuleFor(person => person.Pets).SetValidator(new ListMustContainFewerThanTenItemsValidator<Pet>()); } }
راه دیگر استفاده از آن تعریف یک Extension Method میباشد که در این صورت میتوان از آن به صورت زنجیره ای مانند دیگر Validatorها استفاده نمود:
public static class MyValidatorExtensions { public static IRuleBuilderOptions<T, IList<TElement>> MustContainFewerThanTenItems<T, TElement>(this IRuleBuilder<T, IList<TElement>> ruleBuilder) { return ruleBuilder.SetValidator(new ListMustContainFewerThanTenItemsValidator<TElement>()); } }
public class PersonValidator : AbstractValidator<Person> { public PersonValidator() { RuleFor(person => person.Pets).MustContainFewerThanTenItems(); } }
راه دوم استفاده از متد Custom میباشد. برای توضیح نحوه استفاه از این متد مثال قبل (چک کردن تعداد آیتمهای لیست) را به صورت زیر بازنویسی میکنیم:
public class PersonValidator : AbstractValidator<Person> { public PersonValidator() { Custom(person => { return person.Pets.Count >= 10 ? new ValidationFailure("More than 10 pets is not allowed.") : null; }); } }
public class PersonValidator : AbstractValidator<Person> { public PersonValidator() { RuleFor(person => person.Pets).Must(HaveFewerThanTenPets).WithMessage("More than 9 pets is not allowed"); } private bool HaveFewerThanTenPets(IList<Pet> pets) { return pets.Count < 10; } }
پ.ن.
در این دو مقاله سعی شد تا ویژگیهای FluentValidation به صورت انتزاعی توضیح داده شود. در قسمت بعد نحوه استفاده از این کتابخانه در یک برنامه ASP.NET MVC نشان داده خواهد شد.
با توجه به پیشرفتی که در حوزه اپلیکشنهای وابسته به فریمورک دات نت بوجود آمده، ولی شاید حرکت عملی بزرگی از سمت تولیدکندگان در حوزه کامپکت صورت نگرفته و همچنان شاهد فرمانروایی سیستم عاملهایی چون Windows Compact 6.0 با استفاده از دات نت فریمورکهایی نهایت با نسخه 3.5 هستیم. البته میتوان ارزانتر بودن در خارج و مسئله تحریم در داخل را هم در نظر داشت و نمونه عینی این مورد را میتوان در دستگاههای وارد شده در حوزه Compact، دید. البته شرکتهای تولید کننده خارجی که عمدتا در کشورهای جنوب شرق و شرق آسیا هستند، جزو شرکتهای مطرح در این زمینه هستند که بازارهای خوبی هم در کشورهای توسعه یافتهای چون آمریکا پیدا کردهاند.
در این بین برای عقب نماندن از تکنولوژیهای جدید بوجود آمده در حوزه دات نت مانند WCF این مقاله کمکی هر چند کوچک برای استفاده از این قابلیت موثر در فریمورک کامپکت میتواند باشد.
پیشنیازهای لازم:- Microsoft Visual Studio 2008 + Service Pack 1
- نصب Power Toys for .NET Compact Framework 3.5
پیاده سازی سرویس (بر روی سیستمی غیر از ویندوز کامپکت):
در ویژوال استودیو 2008 سرویس پک یک، پروژه ای از نوعclass library را ایجاد کرده و سرویسی تستی را برای استفاده ایجاد میکنیم:
[ServiceContract(Namespace = "http://samples.wcf.cfnet.sample")] public interface ICalculator { [OperationContract] int Add(int a, int b); }
و پیاده سازی آن:
public class CalculatorService : ICalculator { public static int count; public int Add(int a, int b) { count++; Console.WriteLine(string.Format("{3}\tReceived 'Add({0}, {1})' returning {2}", a, b, a + b, count)); return a + b; }
سرور سرویس:
برای هاست این سرویس از یک برنامهی کنسول که در سلوشن ایجاد میکنیم استفاده میکنیم. البته امکانهای دیگر برای هاست سرویس در هر پروسس دات نتی را میتوان یاد آور شد. برای هاست کردن شروع یک سرویس WCF باید یک IP درون شبکه را که قابل دسترسی از سمت ویندوز کامپکت بوده و به سیستم انتساب داده شده، دریافت و استفاده کنیم:
var addressList = Dns.GetHostEntry(Dns.GetHostName()); string hostIP = addressList.AddressList.Single(x=>x.ToString().StartsWith("192.168.10.")).ToString(); Uri address = new Uri(string.Format("http://{0}:8000/Calculator", hostIP));
در قطعه بالا IP در رنج مناسب و قابل دسترسی انتخاب میشود چون ویندوز کامپکت (فارق از اینکه در شبیه ساز باشد یا واقعی) از طریق شبکه به سرور دسترسی پیدا میکند باید IP مناسب انتساب داده شده انتخاب شود.
ServiceHost serviceHost = new ServiceHost(typeof(CalculatorService),address); serviceHost.AddServiceEndpoint(typeof(ICalculator), new BasicHttpBinding(), "Calculator");
در ادامه یک سرویس هاست را new کرده و سرویس و بایندینگ را به آن در سازنده پاس میدهیم.
var serviceMetadataBehavior = new ServiceMetadataBehavior { HttpGetEnabled = true }; serviceHost.Description.Behaviors.Add(serviceMetadataBehavior);
این قسمت برای ادامه کارکرد سرویس لازم نیست ولی در ادامهی مقاله برای تولید کدهای سمت کلاینت باید این قابلیت فعال باشد و پس از آن دیگر احتیاجی نیست و میتوان این چند خط کد را کامنت کرد.
serviceHost.Open(); Console.WriteLine("CalculatorService is running at " + address.ToString()); Console.WriteLine("Press <ENTER> to terminate"); Console.ReadLine(); serviceHost.Close();
و در نهایت، شروع سرویس با فرمان Open و خاتمه آن با فرمان Close .
کلاینت سرویس (در داخل ویندوز کامپکت):
همراه با ارائه دات نت فریمورک 3.5 برای کار با سرویس WCF که از آن یک نسخهی ارائه شده برای کامپکت نیز تهیه شدهاست، ابزاری مانند netcfSvcUtil.exe که در SDK نسخهی کامپکت موجود است و کاربرد هندل کردن بعضی از موارد مانند تولید کد پروکسیهای سمت کلاینت را دارد که در ادامه طرز استفاده از آن را بررسی خواهیم کرد. بعد از اجرای سرویس WCF با رفتار HttpGetEnabled = true برای بررسی سریع کارکرد صحیح سرویس، آدرس آن را در مرورگر میبینیم. تصویر زیر نتیجهی آن در مرورگر است:
در خط فرمان به آدرس مربوط به این ابزار رفته (بسته به نسخهی سیستم عامل ممکن است در پوشههای زیر یافت شود ( :
(Windows Drive)\Program Files (x86)\Microsoft.NET\SDK\CompactFramework\v3.5\bin (Windows Drive)\Program Files\Microsoft.NET\SDK\CompactFramework\v3.5\bin
و فرمان زیر را اجرا میکنیم:
netcfSvcUtil.exe /language:C# /target:code /directory:D:\GeneratedCode\CF\CaculatorService http://192.168.10.189:8000/BooksService.svc?wsdl
البته ذکر IP شبکه در اینجا الزامی نیست؛ زیرا در صورت استفاده از آدرسهای داخلی سیستم، این فرمان به مشکلی بر نخواهد خورد. در این فرمان تولید کد با زبان c# و تولید کد که بصورت پیش فرض نیز وجود دارد و محل ذخیره سازی کدهای تولیدی را مشخص میکنیم و بعد از اجرای این فرمان، باید دو فایل در مسیر اشاره شده در فرمان تولید شود که اساس کار ما در سمت کلاینت خواهد بود:
کلاینت سرویس نیز با استفاده کدهای تولیدی بصورت زیر آماده سازی و اجرا میشود:
var addressList = Dns.GetHostEntry(Dns.GetHostName()); var localAddress = addressList.AddressList.Single(x => x.ToString().StartsWith("192.168.10.")).ToString();
دوباره IP مناسب در شبکه جاری استخراج میشود. بایندیگ مورد نیاز برای ارتباط با سرور ساخته میشود:
var binding = CalculatorClient.CreateDefaultBinding();
نکتهای که دراین قسمت باید مدنظر قرار گیرد این است که در زمان تولید کدها اگر از localhost یا 127.0.0.1 و یا آدرسهای دیگر انتساب داده شده به سرور استفاده کرده باشید در متد CreateDefaultBinding از همان آدرس استفاده میشود و برای اصلاح آن بصورت زیر عمل میکنیم:
string remoteAddress = CalculatorClient.EndpointAddress.Uri.ToString(); remoteAddress = remoteAddress.Replace("localhost", serviceAddress.Text);
یک EndpointAddress با استفاده از این آدرس ساخته و بههمراه بایندینگ، یک آبجکت از جنس CalculatorClient که در کدهای تولیدی داریم میسازیم:
CalculatorClient _client = new CalculatorClient(binding, endpoint);
برای تست نیز تنها متد این سرویس را با یک جفت عدد، صدا میزنیم:
var result = _client.Add(82, 18).ToString(CultureInfo.InvariantCulture);
به این ترتیب خروجی مورد نظر زیر را در کنسول سرویس مشاهده خواهیم کرد:
آموزش سیلورلایت 4 - قسمتهای 21 تا 27
در یک پروژه WCF RIA Services کلیه اعمال کار با بانک اطلاعاتی در همان سمت سرور و توسط سرویسهایی که اضافه میکنید انجام خواهد شد. فقط صدا زدن متدهای این سرویسها است که در سمت کاربر و برنامه سیلولایتی صورت خواهد گرفت (در همان ViewModel).
از آنجائیکه قسمت عمده مدل سیستم حین نمایش از همان جداول بانک اطلاعاتی شما تشکیل خواهد شد که در سمت سرور در سرویسهای تعریف شده قابل دسترسی میشود، روش WCF RIA Services تعریف مجدد اینها را در سمت کاربر به صورت خودکار انجام میدهد. به همراه replicate کردن تمام مسایل اعتبار سنجی و غیره به سمت کاربر.
using System; namespace CS9Features { class Program { static void Main(string[] args) { Console.WriteLine("Hello World!"); } } }
تبدیل قالب پیشفرض برنامههای کنسول به یک Top level program
در C# 9.0 میتوان تمام سطرهای فوق را به دو سطر زیر تقلیل داد و خلاصه کرد:
using System; Console.WriteLine("Hello World!");
روش استفاده از متدهای async در Top level programs
زمانیکه نقطهی آغازین برنامه را تبدیل به یک top level program کردیم، دیگر دسترسی مستقیمی را به متد Main نداریم تا آنرا async Task دار معرفی کنیم و پس از آن بتوانیم به سادگی با متدهای async کار کنیم. برای رفع این مشکل، کامپایلر فقط کافی است یک await را در قطعه کد شما پیدا کند. خودش به صورت خودکار متد Main غیرهمزمانی را جهت اجرای کدها، تشکیل میدهد. به همین جهت برای کار با کدهای async در اینجا، نیاز به تنظیم خاصی نیست و قطعه کد زیر که در آن متد MyMethodAsync را اجرا میکند، بدون مشکل کامپایل و اجرا خواهد شد:
using System; using System.Threading.Tasks; await MyMethodAsync(); Console.WriteLine("Hello World!"); static async Task MyMethodAsync() { await Task.Yield(); }
روش دسترسی به args در Top level programs
همانطور که در قطعه کد ابتدایی این مطلب مشخص است، متد Main به همراه پارامتر string[] args نیز هست. اما اکنون در Top level programs که فاقد متد Main هستند، چگونه میتوان به این آرگومانهای ارسالی توسط کاربر دسترسی یافت؟
پاسخ: پارامتر args نیز هنوز در اینجا قابل دسترسی است؛ فقط به ظاهر مخفی است:
using System; Console.WriteLine(args[0]);
بعضی از برنامههای کنسول در انتهای متد Main خود برای مثال return 0 و یا return 1 را دارند؛ که اولی به معنای موفقیت عملیات و دومی به معنای شکست عملیات است. در top level programs نیز میتوان این returnها را در انتهای کار قید کرد:
using System; Console.WriteLine($"Hello world!"); return 1;
// <Program>$ using System; using System.Runtime.CompilerServices; [CompilerGenerated] internal static class <Program>$ { private static int <Main>$(string[] args) { Console.WriteLine("Hello world!"); return 1; } }
امکان تعریف کلاسها و متدها در Top level programs
در تک فایل program.cs برنامه، در حین کار با Top level programs محدودیتی از لحاظ تعریف متدها، کلاسها و غیره نیست؛ یک مثال:
using System; var greeter = new Greeter(); var helloTeacher = greeter.Greet("teacher"); var helloStudents = SayHello("students"); Console.WriteLine(helloTeacher); Console.WriteLine(helloStudents); static string SayHello(string name) { return "Hello, " + name; } public class Greeter { public string Greet(string name) { return "Hello, " + name; } }
Top-level statements must precede namespace and type declarations. [CS9Features]csharp(CS8803)
سطوح دسترسی به کلاسها و متدهای تعریف شدهی در Top level programs
اگر قطعه کد مثال قبل را کامپایل کنیم، نمونهی دیکامپایل شدهی آن به صورت زیر است:
using System; using System.Runtime.CompilerServices; [CompilerGenerated] internal static class <Program>$ { private static void <Main>$(string[] args) { Greeter greeter = new Greeter(); string helloTeacher = greeter.Greet("teacher"); string helloStudents = SayHello("students"); Console.WriteLine(helloTeacher); Console.WriteLine(helloStudents); static string SayHello(string name) { return "Hello, " + name; } } }
نوع متدهای تعریف شدهی در Top level programs
مثال زیر را که یک top level program است، درنظر بگیرید:
using System; Foo(); var x = 3; int result = AddToX(4); Console.WriteLine(result); static void Foo() { Console.WriteLine("Foo"); } int AddToX(int y) { return x + y; }
پاسخ: متدهایی که در top level programs تعریف میشوند در حقیقت از نوع local functions هستند که در ابتدا در C# 7.0 معرفی شدند و سپس در C# 8.0 امکان تعریف نمونههای static آنها نیز میسر شد.
قطعه کد فوق در اصل به صورت زیر کامپایل میشود که متدهای AddToX و Foo در آن داخل متد Main تشکیل شده، به صورت local function تعریف شدهاند:
// <Program>$ using System; using System.Runtime.CompilerServices; [CompilerGenerated] internal static class <Program>$ { private static void <Main>$(string[] args) { Foo(); int x = 3; int result = AddToX(4); Console.WriteLine(result); int AddToX(int y) { return x + y; } static void Foo() { Console.WriteLine("Foo"); } } }
فریم ورک Identity در سال 2013 معرفی شد، که دنباله سیستم ASP.NET Membership بود. سیستم قبلی گرچه طی سالیان استفاده میشد اما مشکلات زیادی هم بهمراه داشت. بعلاوه با توسعه دنیای وب و نرم افزار، قابلیتهای مدرنی مورد نیاز بودند که باید پشتیبانی میشدند. فریم ورک Identity در ابتدا سیستم ساده و کارآمدی برای مدیریت کاربران بوجود آورد و مشکلات پیشین را تا حد زیادی برطرف نمود. بعنوان مثال فریم ورک جدید مبتنی بر EF Code-first است، که سفارشی کردن سیستم عضویت را بسیار آسان میکند و به شما کنترل کامل میدهد. یا مثلا احراز هویت مبتنی بر پروتوکل OAuth پشتیبانی میشود که به شما اجازه استفاده از فراهم کنندگان خارجی مانند گوگل، فیسبوک و غیره را میدهد.
نسخه جدید این فریم ورک ویژگیهای زیر را معرفی میکند (بعلاوه مواردی دیگر):
- مدل حسابهای کاربری توسعه داده شده. مثلا آدرس ایمیل و اطلاعات تماس را هم در بر میگیرد
- احراز هویت دو مرحله ای (Two-Factor Authentication) توسط اطلاع رسانی ایمیلی یا پیامکی. مشابه سیستمی که گوگل، مایکروسافت و دیگران استفاده میکنند
- تایید حسابهای کاربری توسط ایمیل (Account Confirmation)
- مدیریت کاربران و نقشها (Administration of Users & Roles)
- قفل کردن حسابهای کاربری در پاسخ به Invalid log-in attempts
- تامین کننده شناسه امنیتی (Security Token Provider) برای بازتولید شناسهها در پاسخ به تغییرات تنظیمات امنیتی (مثلا هنگام تغییر کلمه عبور)
- بهبود پشتیبانی از Social log-ins
- یکپارچه سازی ساده با Claims-based Authorization
Identity 2.0 تغییرات چشم گیری نسبت به نسخه قبلی بهوجود آورده است. به نسبت ویژگیهای جدید، پیچیدگیهایی نیز معرفی شدهاند. اگر به تازگی (مانند خودم) با نسخه 1 این فریم ورک آشنا شده و کار کرده اید، آماده شوید! گرچه لازم نیست از صفر شروع کنید، اما چیزهای بسیاری برای آموختن وجود دارد.
در این مقاله نگاهی اجمالی به نسخهی جدید این فریم ورک خواهیم داشت. کامپوننتهای جدید و اصلی را خواهیم شناخت و خواهیم دید هر کدام چگونه در این فریم ورک کار میکنند. بررسی عمیق و جزئی این فریم ورک از حوصله این مقاله خارج است، بنابراین به این مقاله تنها بعنوان یک نقطه شروع برای آشنایی با این فریم ورک نگاه کنید.
اگر به دنبال اطلاعات بیشتر و بررسیهای عمیقتر هستید، لینک هایی در انتهای این مقاله نگاشت شده اند. همچنین طی هفتههای آینده چند مقاله تخصصیتر خواهم نوشت تا از دید پیاده سازی بیشتر با این فریم ورک آشنا شوید.
در این مقاله با مقدار قابل توجهی کد مواجه خواهید شد. لازم نیست تمام جزئیات آنها را بررسی کنید، تنها با ساختار کلی این فریم ورک آشنا شوید. کامپوننتها را بشناسید و بدانید که هر کدام در کجا قرار گرفته اند، چطور کار میکنند و اجزای کلی سیستم چگونه پیکربندی میشوند. گرچه، اگر به برنامه نویسی دات نت (#ASP.NET, C) تسلط دارید و با نسخه قبلی Identity هم کار کرده اید، درک کدهای جدید کار ساده ای خواهد بود.
Identity 2.0 با نسخه قبلی سازگار نیست
اپلیکیشن هایی که با نسخه 1.0 این فریم ورک ساخته شده اند نمیتوانند بسادگی به نسخه جدید مهاجرت کنند. قابلیت هایی جدیدی که پیاده سازی شده اند تغییرات چشمگیری در معماری این فریم ورک بوجود آورده اند، همچنین API مورد استفاده در اپلیکیشنها نیز دستخوش تغییراتی شده است. مهاجرت از نسخه 1.0 به 2.0 نیاز به نوشتن کدهای جدید و اعمال تغییرات متعددی دارد که از حوصله این مقاله خارج است. فعلا همین قدر بدانید که این مهاجرت نمیتواند بسادگی در قالب Plug-in and play صورت پذیرد!
شروع به کار : پروژه مثالها را از NuGet دریافت کنید
در حال حاظر (هنگام نوشتن این مقاله) قالب پروژه استانداردی برای اپلیکیشنهای ASP.NET MVC که ا ز Identity 2.0 استفاده کنند وجود ندارد. برای اینکه بتوانید از نسخه جدید این فریم ورک استفاده کنید، باید پروژه مثال را توسط NuGet دریافت کنید. ابتدا پروژه جدیدی از نوع ASP.NET Web Application بسازید و قالب Empty را در دیالوگ تنظیمات انتخاب کنید.
کنسول Package Manager را باز کنید و با اجرای فرمان زیر پروژه مثالها را دانلود کنید.
PM> Install-Package Microsoft.AspNet.Identity.Samples -Pre
پیکربندی Identity : دیگر به سادگی نسخه قبلی نیست
به نظر من یکی از مهمترین نقاط قوت فریم ورک Identity یکی از مهمترین نقاط ضعفش نیز بود. سادگی نسخه 1.0 این فریم ورک کار کردن با آن را بسیار آسان میکرد و به سادگی میتوانستید ساختار کلی و روند کارکردن کامپوننتهای آن را درک کنید. اما همین سادگی به معنای محدود بودن امکانات آن نیز بود. بعنوان مثال میتوان به تایید حسابهای کاربری یا پشتیبانی از احراز هویتهای دو مرحله ای اشاره کرد.
برای شروع نگاهی اجمالی به پیکربندی این فریم ورک و اجرای اولیه اپلیکیشن خواهیم داشت. سپس تغییرات را با نسخه 1.0 مقایسه میکنیم.
در هر دو نسخه، فایلی بنام Startup.cs در مسیر ریشه پروژه خواهید یافت. در این فایل کلاس واحدی بنام Startup تعریف شده است که متد ()ConfigureAuth را فراخوانی میکند. چیزی که در این فایل مشاهده نمیکنیم، خود متد ConfigureAuth است. این بدین دلیل است که مابقی کد کلاس Startup در یک کلاس پاره ای (Partial) تعریف شده که در پوشه App_Start قرار دارد. نام فایل مورد نظر Startup.Auth.cs است که اگر آن را باز کنید تعاریف یک کلاس پاره ای بهمراه متد ()ConfigureAuth را خواهید یافت. در یک پروژه که از نسخه Identity 1.0 استفاده میکند، کد متد ()ConfigureAuth مطابق لیست زیر است.
public partial class Startup { public void ConfigureAuth(IAppBuilder app) { // Enable the application to use a cookie to // store information for the signed in user app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, LoginPath = new PathString("/Account/Login") }); // Use a cookie to temporarily store information about a // user logging in with a third party login provider app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie); // Uncomment the following lines to enable logging // in with third party login providers //app.UseMicrosoftAccountAuthentication( // clientId: "", // clientSecret: ""); //app.UseTwitterAuthentication( // consumerKey: "", // consumerSecret: ""); //app.UseFacebookAuthentication( // appId: "", // appSecret: ""); //app.UseGoogleAuthentication(); } }
public partial class Startup { public void ConfigureAuth(IAppBuilder app) { // Configure the db context, user manager and role // manager to use a single instance per request app.CreatePerOwinContext(ApplicationDbContext.Create); app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create); app.CreatePerOwinContext<ApplicationRoleManager>(ApplicationRoleManager.Create); // Enable the application to use a cookie to store information for the // signed in user and to use a cookie to temporarily store information // about a user logging in with a third party login provider // Configure the sign in cookie app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, LoginPath = new PathString("/Account/Login"), Provider = new CookieAuthenticationProvider { // Enables the application to validate the security stamp when the user // logs in. This is a security feature which is used when you // change a password or add an external login to your account. OnValidateIdentity = SecurityStampValidator .OnValidateIdentity<ApplicationUserManager, ApplicationUser>( validateInterval: TimeSpan.FromMinutes(30), regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager)) } }); app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie); // Enables the application to temporarily store user information when // they are verifying the second factor in the two-factor authentication process. app.UseTwoFactorSignInCookie( DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5)); // Enables the application to remember the second login verification factor such // as phone or email. Once you check this option, your second step of // verification during the login process will be remembered on the device where // you logged in from. This is similar to the RememberMe option when you log in. app.UseTwoFactorRememberBrowserCookie( DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie); // Uncomment the following lines to enable logging in // with third party login providers //app.UseMicrosoftAccountAuthentication( // clientId: "", // clientSecret: ""); //app.UseTwitterAuthentication( // consumerKey: "", // consumerSecret: ""); //app.UseFacebookAuthentication( // appId: "", // appSecret: ""); //app.UseGoogleAuthentication(); } }
مورد بعدی ای که جلب توجه میکند فراخوانیهای دیگری برای پیکربندی احراز هویت دو مرحلهای است. همچنین پیکربندیهای جدیدی برای کوکیها تعریف شده است که در نسخه قبلی وجود نداشتند.
تا اینجا پیکربندیهای اساسی برای اپلیکیشن شما انجام شده است و میتوانید از اپلیکیشن خود استفاده کنید. بکارگیری فراهم کنندگان خارجی در حال حاضر غیرفعال است و بررسی آنها نیز از حوصله این مقاله خارج است. این کلاس پیکربندیهای اساسی Identity را انجام میدهد. کامپوننتهای پیکربندی و کدهای کمکی دیگری نیز وجود دارند که در کلاس IdentityConfig.cs تعریف شده اند.
پیش از آنکه فایل IdentityConfig.cs را بررسی کنیم، بهتر است نگاهی به کلاس ApplicationUser بیاندازیم که در پوشه Models قرار گرفته است.
کلاس جدید ApplicationUser در Identity 2.0
اگر با نسخه 1.0 این فریم ورک اپلیکیشنی ساخته باشید، ممکن است متوجه شده باشید که کلاس پایه IdentityUser محدود و شاید ناکافی باشد. در نسخه قبلی، این فریم ورک پیاده سازی IdentityUser را تا حد امکان ساده نگاه داشته بود تا اطلاعات پروفایل کاربران را معرفی کند.
public class IdentityUser : IUser { public IdentityUser(); public IdentityUser(string userName); public virtual string Id { get; set; } public virtual string UserName { get; set; } public virtual ICollection<IdentityUserRole> Roles { get; } public virtual ICollection<IdentityUserClaim> Claims { get; } public virtual ICollection<IdentityUserLogin> Logins { get; } public virtual string PasswordHash { get; set; } public virtual string SecurityStamp { get; set; } }
اگر از نسخه Identity 1.0 استفاده کرده باشید و مطالعاتی هم در این زمینه داشته باشید، میدانید که توسعه کلاس کاربران بسیار ساده است. مثلا برای افزودن فیلد آدرس ایمیل و اطلاعات دیگر کافی بود کلاس ApplicationUser را ویرایش کنیم و از آنجا که این فریم ورک مبتنی بر EF Code-first است بروز رسانی دیتابیس و مابقی اپلیکیشن کار چندان مشکلی نخواهد بود.
با ظهور نسخه Identity 2.0 نیاز به برخی از این سفارشی سازیها از بین رفته است. گرچه هنوز هم میتوانید بسادگی مانند گذشته کلاس ApplicationUser را توسعه و گسترش دهید، تیم ASP.NET تغییراتی بوجود آورده اند تا نیازهای رایج توسعه دهندگان را پاسخگو باشد.
اگر به کد کلاسهای مربوطه دقت کنید خواهید دید که کلاس ApplicationUser همچنان از کلاس پایه IdentityUser ارث بری میکند، اما این کلاس پایه پیچیدهتر شده است. کلاس ApplicationUser در پوشه Models و در فایلی بنام IdentityModels.cs تعریف شده است. همانطور که میبینید تعاریف خود این کلاس بسیار ساده است.
public class ApplicationUser : IdentityUser { public async Task<ClaimsIdentity> GenerateUserIdentityAsync( UserManager<ApplicationUser> manager) { // Note the authenticationType must match the one // defined in CookieAuthenticationOptions.AuthenticationType var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie); // Add custom user claims here return userIdentity; } }
public class IdentityUser<TKey, TLogin, TRole, TClaim> : IUser<TKey> where TLogin : Microsoft.AspNet.Identity.EntityFramework.IdentityUserLogin<TKey> where TRole : Microsoft.AspNet.Identity.EntityFramework.IdentityUserRole<TKey> where TClaim : Microsoft.AspNet.Identity.EntityFramework.IdentityUserClaim<TKey> { public IdentityUser(); // Used to record failures for the purposes of lockout public virtual int AccessFailedCount { get; set; } // Navigation property for user claims public virtual ICollection<TClaim> Claims { get; } // Email public virtual string Email { get; set; } // True if the email is confirmed, default is false public virtual bool EmailConfirmed { get; set; } // User ID (Primary Key) public virtual TKey Id { get; set; } // Is lockout enabled for this user public virtual bool LockoutEnabled { get; set; } // DateTime in UTC when lockout ends, any // time in the past is considered not locked out. public virtual DateTime? LockoutEndDateUtc { get; set; } // Navigation property for user logins public virtual ICollection<TLogin> Logins { get; } // The salted/hashed form of the user password public virtual string PasswordHash { get; set; } // PhoneNumber for the user public virtual string PhoneNumber { get; set; } // True if the phone number is confirmed, default is false public virtual bool PhoneNumberConfirmed { get; set; } // Navigation property for user roles public virtual ICollection<TRole> Roles { get; } // A random value that should change whenever a users // credentials have changed (password changed, login removed) public virtual string SecurityStamp { get; set; } // Is two factor enabled for the user public virtual bool TwoFactorEnabled { get; set; } // User name public virtual string UserName { get; set; } }
اما از همه چیز مهمتر امضا (Signature)ی خود کلاس است. این آرگومانهای جنریک چه هستند؟ به امضای این کلاس دقت کنید.
public class IdentityUser<TKey, TLogin, TRole, TClaim> : IUser<TKey> where TLogin : Microsoft.AspNet.Identity.EntityFramework.IdentityUserLogin<TKey> where TRole : Microsoft.AspNet.Identity.EntityFramework.IdentityUserRole<TKey> where TClaim : Microsoft.AspNet.Identity.EntityFramework.IdentityUserClaim<TKey>
public virtual TKey Id { get; set; }
public virtual ICollection<TRole> Roles { get; }
در نسخه Identity 1.0 کلاس IdentityUserRole بصورت زیر تعریف شده بود.
public class IdentityUserRole { public IdentityUserRole(); public virtual IdentityRole Role { get; set; } public virtual string RoleId { get; set; } public virtual IdentityUser User { get; set; } public virtual string UserId { get; set; } }
public class IdentityUserRole<TKey> { public IdentityUserRole(); public virtual TKey RoleId { get; set; } public virtual TKey UserId { get; set; } }
بررسی پیاده سازی جدید IdentityUser از حوصله این مقاله خارج است. فعلا همین قدر بدانید که گرچه تعاریف پایه کلاس کاربران پیچیدهتر شده است، اما انعطاف پذیری بسیار خوبی بدست آمده که شایان اهمیت فراوانی است.
از آنجا که کلاس ApplicationUser از IdentityUser ارث بری میکند، تمام خواص و تعاریف این کلاس پایه در ApplicationUser قابل دسترسی هستند.
کامپوننتهای پیکربندی Identity 2.0 و کدهای کمکی
گرچه متد ()ConfigAuth در کلاس Startup، محلی است که پیکربندی Identity در زمان اجرا صورت میپذیرد، اما در واقع کامپوننتهای موجود در فایل IdentityConfig.cs هستند که اکثر قابلیتهای Identity 2.0 را پیکربندی کرده و نحوه رفتار آنها در اپلیکیشن ما را کنترل میکنند.
اگر محتوای فایل IdentityConfig.cs را بررسی کنید خواهید دید که کلاسهای متعددی در این فایل تعریف شده اند. میتوان تک تک این کلاسها را به فایلهای مجزایی منتقل کرد، اما برای مثال جاری کدها را بهمین صورت رها کرده و نگاهی اجمالی به آنها خواهیم داشت. بهرحال در حال حاظر تمام این کلاسها در فضای نام ApplicationName.Models قرار دارند.
Application User Manager و Application Role Manager
اولین چیزی که در این فایل به آنها بر میخوریم دو کلاس ApplicationUserManager و ApplicationRoleManager هستند. آماده باشید، مقدار زیادی کد با انواع داده جنریک در پیش روست!
public class ApplicationUserManager : UserManager<ApplicationUser> { public ApplicationUserManager(IUserStore<ApplicationUser> store) : base(store) { } public static ApplicationUserManager Create( IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context) { var manager = new ApplicationUserManager( new UserStore<ApplicationUser>( context.Get<ApplicationDbContext>())); // Configure validation logic for usernames manager.UserValidator = new UserValidator<ApplicationUser>(manager) { AllowOnlyAlphanumericUserNames = false, RequireUniqueEmail = true }; // Configure validation logic for passwords manager.PasswordValidator = new PasswordValidator { RequiredLength = 6, RequireNonLetterOrDigit = true, RequireDigit = true, RequireLowercase = true, RequireUppercase = true, }; // Configure user lockout defaults manager.UserLockoutEnabledByDefault = true; manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5); manager.MaxFailedAccessAttemptsBeforeLockout = 5; // Register two factor authentication providers. This application uses // Phone and Emails as a step of receiving a code for verifying // the user You can write your own provider and plug in here. manager.RegisterTwoFactorProvider("PhoneCode", new PhoneNumberTokenProvider<ApplicationUser> { MessageFormat = "Your security code is: {0}" }); manager.RegisterTwoFactorProvider("EmailCode", new EmailTokenProvider<ApplicationUser> { Subject = "SecurityCode", BodyFormat = "Your security code is {0}" }); manager.EmailService = new EmailService(); manager.SmsService = new SmsService(); var dataProtectionProvider = options.DataProtectionProvider; if (dataProtectionProvider != null) { manager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>( dataProtectionProvider.Create("ASP.NET Identity")); } return manager; } public virtual async Task<IdentityResult> AddUserToRolesAsync( string userId, IList<string> roles) { var userRoleStore = (IUserRoleStore<ApplicationUser, string>)Store; var user = await FindByIdAsync(userId).ConfigureAwait(false); if (user == null) { throw new InvalidOperationException("Invalid user Id"); } var userRoles = await userRoleStore .GetRolesAsync(user) .ConfigureAwait(false); // Add user to each role using UserRoleStore foreach (var role in roles.Where(role => !userRoles.Contains(role))) { await userRoleStore.AddToRoleAsync(user, role).ConfigureAwait(false); } // Call update once when all roles are added return await UpdateAsync(user).ConfigureAwait(false); } public virtual async Task<IdentityResult> RemoveUserFromRolesAsync( string userId, IList<string> roles) { var userRoleStore = (IUserRoleStore<ApplicationUser, string>) Store; var user = await FindByIdAsync(userId).ConfigureAwait(false); if (user == null) { throw new InvalidOperationException("Invalid user Id"); } var userRoles = await userRoleStore .GetRolesAsync(user) .ConfigureAwait(false); // Remove user to each role using UserRoleStore foreach (var role in roles.Where(userRoles.Contains)) { await userRoleStore .RemoveFromRoleAsync(user, role) .ConfigureAwait(false); } // Call update once when all roles are removed return await UpdateAsync(user).ConfigureAwait(false); } }
مورد حائز اهمیت بعدی در متد ()Create فراخوانی ()<context.Get<ApplicationDBContext است. بیاد بیاورید که پیشتر نگاهی به متد ()ConfigAuth داشتیم که چند فراخوانی CreatePerOwinContext داشت که توسط آنها Callback هایی را رجیستر میکردیم. فراخوانی متد ()<context.Get<ApplicationDBContext این Callbackها را صدا میزند، که در اینجا فراخوانی متد استاتیک ()ApplicationDbContext.Create خواهد بود. در ادامه بیشتر درباره این قسمت خواهید خواهند.
اگر دقت کنید میبینید که احراز هویت، تعیین سطوح دسترسی و تنظیمات مدیریتی و مقادیر پیش فرض آنها در متد ()Create انجام میشوند و سپس وهله ای از نوع خود کلاس ApplicationUserManager بازگشت داده میشود. همچنین سرویسهای احراز هویت دو مرحله ای نیز در همین مرحله پیکربندی میشوند. اکثر پیکربندیها و تنظیمات نیازی به توضیح ندارند و قابل درک هستند. اما احراز هویت دو مرحله ای نیاز به بررسی عمیقتری دارد. در ادامه به این قسمت خواهیم پرداخت. اما پیش از آن نگاهی به کلاس ApplicationRoleManager بیاندازیم.
public class ApplicationRoleManager : RoleManager<IdentityRole> { public ApplicationRoleManager(IRoleStore<IdentityRole,string> roleStore) : base(roleStore) { } public static ApplicationRoleManager Create( IdentityFactoryOptions<ApplicationRoleManager> options, IOwinContext context) { var manager = new ApplicationRoleManager( new RoleStore<IdentityRole>( context.Get<ApplicationDbContext>())); return manager; } }
سرویسهای ایمیل و پیامک برای احراز هویت دو مرحله ای و تایید حسابهای کاربری
دو کلاس دیگری که در فایل IdentityConfig.cs وجود دارند کلاسهای EmailService و SmsService هستند. بصورت پیش فرض این کلاسها تنها یک wrapper هستند که میتوانید با توسعه آنها سرویسهای مورد نیاز برای احراز هویت دو مرحله ای و تایید حسابهای کاربری را بسازید.
public class EmailService : IIdentityMessageService { public Task SendAsync(IdentityMessage message) { // Plug in your email service here to send an email. return Task.FromResult(0); } }
public class SmsService : IIdentityMessageService { public Task SendAsync(IdentityMessage message) { // Plug in your sms service here to send a text message. return Task.FromResult(0); } }
// Register two factor authentication providers. This application uses // Phone and Emails as a step of receiving a code for verifying // the user You can write your own provider and plug in here. manager.RegisterTwoFactorProvider("PhoneCode", new PhoneNumberTokenProvider<ApplicationUser> { MessageFormat = "Your security code is: {0}" }); manager.RegisterTwoFactorProvider("EmailCode", new EmailTokenProvider<ApplicationUser> { Subject = "SecurityCode", BodyFormat = "Your security code is {0}" }); manager.EmailService = new EmailService(); manager.SmsService = new SmsService();
کلاس کمکی SignIn
هنگام توسعه پروژه مثال Identity، تیم توسعه دهندگان کلاسی کمکی برای ما ساختهاند که فرامین عمومی احراز هویت کاربران و ورود آنها به اپلیکیشن را توسط یک API ساده فراهم میسازد. برای آشنایی با نحوه استفاده از این متدها میتوانیم به کنترلر AccountController در پوشه Controllers مراجعه کنیم. اما پیش از آن بگذارید نگاهی به خود کلاس SignInHelper داشته باشیم.
public class SignInHelper { public SignInHelper( ApplicationUserManager userManager, IAuthenticationManager authManager) { UserManager = userManager; AuthenticationManager = authManager; } public ApplicationUserManager UserManager { get; private set; } public IAuthenticationManager AuthenticationManager { get; private set; } public async Task SignInAsync( ApplicationUser user, bool isPersistent, bool rememberBrowser) { // Clear any partial cookies from external or two factor partial sign ins AuthenticationManager.SignOut( DefaultAuthenticationTypes.ExternalCookie, DefaultAuthenticationTypes.TwoFactorCookie); var userIdentity = await user.GenerateUserIdentityAsync(UserManager); if (rememberBrowser) { var rememberBrowserIdentity = AuthenticationManager.CreateTwoFactorRememberBrowserIdentity(user.Id); AuthenticationManager.SignIn( new AuthenticationProperties { IsPersistent = isPersistent }, userIdentity, rememberBrowserIdentity); } else { AuthenticationManager.SignIn( new AuthenticationProperties { IsPersistent = isPersistent }, userIdentity); } } public async Task<bool> SendTwoFactorCode(string provider) { var userId = await GetVerifiedUserIdAsync(); if (userId == null) { return false; } var token = await UserManager.GenerateTwoFactorTokenAsync(userId, provider); // See IdentityConfig.cs to plug in Email/SMS services to actually send the code await UserManager.NotifyTwoFactorTokenAsync(userId, provider, token); return true; } public async Task<string> GetVerifiedUserIdAsync() { var result = await AuthenticationManager.AuthenticateAsync( DefaultAuthenticationTypes.TwoFactorCookie); if (result != null && result.Identity != null && !String.IsNullOrEmpty(result.Identity.GetUserId())) { return result.Identity.GetUserId(); } return null; } public async Task<bool> HasBeenVerified() { return await GetVerifiedUserIdAsync() != null; } public async Task<SignInStatus> TwoFactorSignIn( string provider, string code, bool isPersistent, bool rememberBrowser) { var userId = await GetVerifiedUserIdAsync(); if (userId == null) { return SignInStatus.Failure; } var user = await UserManager.FindByIdAsync(userId); if (user == null) { return SignInStatus.Failure; } if (await UserManager.IsLockedOutAsync(user.Id)) { return SignInStatus.LockedOut; } if (await UserManager.VerifyTwoFactorTokenAsync(user.Id, provider, code)) { // When token is verified correctly, clear the access failed // count used for lockout await UserManager.ResetAccessFailedCountAsync(user.Id); await SignInAsync(user, isPersistent, rememberBrowser); return SignInStatus.Success; } // If the token is incorrect, record the failure which // also may cause the user to be locked out await UserManager.AccessFailedAsync(user.Id); return SignInStatus.Failure; } public async Task<SignInStatus> ExternalSignIn( ExternalLoginInfo loginInfo, bool isPersistent) { var user = await UserManager.FindAsync(loginInfo.Login); if (user == null) { return SignInStatus.Failure; } if (await UserManager.IsLockedOutAsync(user.Id)) { return SignInStatus.LockedOut; } return await SignInOrTwoFactor(user, isPersistent); } private async Task<SignInStatus> SignInOrTwoFactor( ApplicationUser user, bool isPersistent) { if (await UserManager.GetTwoFactorEnabledAsync(user.Id) && !await AuthenticationManager.TwoFactorBrowserRememberedAsync(user.Id)) { var identity = new ClaimsIdentity(DefaultAuthenticationTypes.TwoFactorCookie); identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id)); AuthenticationManager.SignIn(identity); return SignInStatus.RequiresTwoFactorAuthentication; } await SignInAsync(user, isPersistent, false); return SignInStatus.Success; } public async Task<SignInStatus> PasswordSignIn( string userName, string password, bool isPersistent, bool shouldLockout) { var user = await UserManager.FindByNameAsync(userName); if (user == null) { return SignInStatus.Failure; } if (await UserManager.IsLockedOutAsync(user.Id)) { return SignInStatus.LockedOut; } if (await UserManager.CheckPasswordAsync(user, password)) { return await SignInOrTwoFactor(user, isPersistent); } if (shouldLockout) { // If lockout is requested, increment access failed // count which might lock out the user await UserManager.AccessFailedAsync(user.Id); if (await UserManager.IsLockedOutAsync(user.Id)) { return SignInStatus.LockedOut; } } return SignInStatus.Failure; } }
این متدها ویژگیهای جدیدی که در Identity 2.0 عرضه شده اند را در بر میگیرند. متد آشنایی بنام ()SignInAsync را میبینیم، و متدهای دیگری که مربوط به احراز هویت دو مرحله ای و external log-ins میشوند. اگر به متدها دقت کنید خواهید دید که برای ورود کاربران به اپلیکیشن کارهای بیشتری نسبت به نسخه پیشین انجام میشود.
بعنوان مثال متد Login در کنترلر AccountController را باز کنید تا نحوه مدیریت احراز هویت در Identity 2.0 را ببینید.
[HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task<ActionResult> Login(LoginViewModel model, string returnUrl) { if (!ModelState.IsValid) { return View(model); } // This doen't count login failures towards lockout only two factor authentication // To enable password failures to trigger lockout, change to shouldLockout: true var result = await SignInHelper.PasswordSignIn( model.Email, model.Password, model.RememberMe, shouldLockout: false); switch (result) { case SignInStatus.Success: return RedirectToLocal(returnUrl); case SignInStatus.LockedOut: return View("Lockout"); case SignInStatus.RequiresTwoFactorAuthentication: return RedirectToAction("SendCode", new { ReturnUrl = returnUrl }); case SignInStatus.Failure: default: ModelState.AddModelError("", "Invalid login attempt."); return View(model); } }
مقایسه Sign-in با نسخه Identity 1.0
در نسخه 1.0 این فریم ورک، ورود کاربران به اپلیکیشن مانند لیست زیر انجام میشد. اگر متد Login در کنترلر AccountController را باز کنید چنین قطعه کدی را میبینید.
[HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task<ActionResult> Login(LoginViewModel model, string returnUrl) { if (ModelState.IsValid) { var user = await UserManager.FindAsync(model.UserName, model.Password); if (user != null) { await SignInAsync(user, model.RememberMe); return RedirectToLocal(returnUrl); } else { ModelState.AddModelError("", "Invalid username or password."); } } // If we got this far, something failed, redisplay form return View(model); }
private async Task SignInAsync(ApplicationUser user, bool isPersistent) { AuthenticationManager.SignOut( DefaultAuthenticationTypes.ExternalCookie); var identity = await UserManager.CreateIdentityAsync( user, DefaultAuthenticationTypes.ApplicationCookie); AuthenticationManager.SignIn( new AuthenticationProperties() { IsPersistent = isPersistent }, identity); }
ApplicationDbContext
اگر از نسخه پیشین Identity در اپلیکیشنهای ASP.NET MVC استفاده کرده باشید با کلاس ApplicationDbContext آشنا هستید. این کلاس پیاده سازی پیش فرض EF فریم ورک است، که اپلیکیشن شما توسط آن دادههای مربوط به Identity را ذخیره و بازیابی میکند.
در پروژه مثال ها، تیم Identity این کلاس را بطور متفاوتی نسبت به نسخه 1.0 پیکربندی کرده اند. اگر فایل IdentityModels.cs را باز کنید تعاریف کلاس ApplicationDbContext را مانند لیست زیر خواهید یافت.
public class ApplicationDbContext : IdentityDbContext<ApplicationUser> { public ApplicationDbContext() : base("DefaultConnection", throwIfV1Schema: false) { } static ApplicationDbContext() { // Set the database intializer which is run once during application start // This seeds the database with admin user credentials and admin role Database.SetInitializer<ApplicationDbContext>(new ApplicationDbInitializer()); } public static ApplicationDbContext Create() { return new ApplicationDbContext(); } }
public class ApplicationDbInitializer : DropCreateDatabaseIfModelChanges<ApplicationDbContext> { protected override void Seed(ApplicationDbContext context) { InitializeIdentityForEF(context); base.Seed(context); } public static void InitializeIdentityForEF(ApplicationDbContext db) { var userManager = HttpContext .Current.GetOwinContext() .GetUserManager<ApplicationUserManager>(); var roleManager = HttpContext.Current .GetOwinContext() .Get<ApplicationRoleManager>(); const string name = "admin@admin.com"; const string password = "Admin@123456"; const string roleName = "Admin"; //Create Role Admin if it does not exist var role = roleManager.FindByName(roleName); if (role == null) { role = new IdentityRole(roleName); var roleresult = roleManager.Create(role); } var user = userManager.FindByName(name); if (user == null) { user = new ApplicationUser { UserName = name, Email = name }; var result = userManager.Create(user, password); result = userManager.SetLockoutEnabled(user.Id, false); } // Add user admin to Role Admin if not already added var rolesForUser = userManager.GetRoles(user.Id); if (!rolesForUser.Contains(role.Name)) { var result = userManager.AddToRole(user.Id, role.Name); } } }
نکته حائز اهمیت دیگر متد ()InitializeIdentityForEF است. این متد کاری مشابه متد ()Seed انجام میدهد که هنگام استفاده از مهاجرتها (Migrations) از آن استفاده میکنیم. در این متد میتوانید رکوردهای اولیه ای را در دیتابیس ثبت کنید. همانطور که مشاهده میکنید در قطعه کد بالا نقشی مدیریتی بنام Admin ایجاد شده و کاربر جدیدی با اطلاعاتی پیش فرض ساخته میشود که در آخر به این نقش منتسب میگردد. با انجام این مراحل، پس از اجرای اولیه اپلیکیشن کاربری با سطح دسترسی مدیر در اختیار خواهیم داشت که برای تست اپلیکیشن بسیار مفید خواهد بود.
در این مقاله نگاهی اجمالی به Identity 2.0 در پروژههای ASP.NET MVC داشتیم. کامپوننتهای مختلف فریم ورک و نحوه پیکربندی آنها را بررسی کردیم و با تغییرات و قابلیتهای جدید به اختصار آشنا شدیم. در مقالات بعدی بررسی هایی عمیقتر خواهیم داشت و با نحوه استفاده و پیاده سازی قسمتهای مختلف این فریم ورک آشنا خواهیم شد.
مطالعه بیشتر
if(person == null) { }
if(person is null) { }
سربارگذاری عملگرها و مقایسهی وهلههای اشیاء با null
در عمل، تفاوتی بین استفادهی از عملگر == و واژهی کلیدی is برای بررسی نال بودن وهلهای از یک شیء وجود ندارد؛ اما ... با یک شرط! فقط در حالتیکه عملگر == سربارگذاری نشده باشد.
برای نمونه کلاس Person زیر را در نظر بگیرید که عملگرهای == و =! آن بازنویسی شدهاند:
public class Person { public static bool operator ==(Person x, Person y) { return false; } public static bool operator !=(Person x, Person y) { return !(x == y); } public override bool Equals(object obj) { return base.Equals(obj); } }
Person person = null; Console.WriteLine("Is Person null?"); Console.WriteLine($"== says: {person == null}"); Console.WriteLine($"is says: {person is null}");
اگر در کلاس Person سربارگذاری عملگر == صورت نمیگرفت، خروجی زیر را مشاهده میکردیم:
Is Person null? == says: True is says: True
Is Person null? == says: False is says: True
public static bool operator ==(Person x, Person y) { return false; }
یک نکته: null coalescing operator یعنی ?? و null coalescing assignment operator یعنی =?? نیز همانند واژهی کلیدی is عمل میکنند. یعنی از == سربارگذاری شده صرفنظر خواهند کرد.
تعیین شمای جداول بانکی در EF Code First
// DAL Layer public class Configuration : DbMigrationsConfiguration<MyContext> { public Configuration() { AutomaticMigrationsEnabled = true; AutomaticMigrationDataLossAllowed = true; // Global.asax void Application_Start(object sender, EventArgs e) { Database.SetInitializer( new MigrateDatabaseToLatestVersion<MyContext, Configuration>()); // Program.cs class Program { static void Main(string[] args) { Database.SetInitializer( new MigrateDatabaseToLatestVersion<MyContext, Configuration>());