function isWasmSupported() { try { if (typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function") { const module = new WebAssembly.Module(Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00)); if (module instanceof WebAssembly.Module) return new WebAssembly.Instance(module) instanceof WebAssembly.Instance; } } catch (e) { } return false; } if(!isWasmSupported()) { alert("WebAssembly is not available in your browser. Please try using the latest version of Chrome, Firefox, Edge or Safari."); }
تزریق وابستگیهای AutoMapper در لایه سرویس برنامه
تنظیمات IoC Container مختص به AutoMapper
در ذیل یک کلاس Registry مخصوص StructureMap را مشاهده میکنید که جهت کپسوله کردن اطلاعات خاص AutoMapper تهیه شدهاست. میتوان این اطلاعات را در داخل تنظیمات new Container خود قرار داد و یا میتوان آنها را جهت شلوغ نشدن سایر تنظیمات IoC Container، به یک کلاس Registry منتقل کرد:
public class AutomapperRegistry : Registry { public AutomapperRegistry() { var platformSpecificRegistry = PlatformAdapter.Resolve<IPlatformSpecificMapperRegistry>(); platformSpecificRegistry.Initialize(); For<ConfigurationStore>().Singleton().Use<ConfigurationStore>() .Ctor<IEnumerable<IObjectMapper>>().Is(MapperRegistry.Mappers); For<IConfigurationProvider>().Use(ctx => ctx.GetInstance<ConfigurationStore>()); For<IConfiguration>().Use(ctx => ctx.GetInstance<ConfigurationStore>()); For<ITypeMapFactory>().Use<TypeMapFactory>(); For<IMappingEngine>().Singleton().Use<MappingEngine>() .SelectConstructor(() => new MappingEngine(null)); this.Scan(scanner => { scanner.AssembliesFromApplicationBaseDirectory(); scanner.ConnectImplementationsToTypesClosing(typeof(ITypeConverter<,>)) .OnAddedPluginTypes(t => t.HybridHttpOrThreadLocalScoped()); scanner.ConnectImplementationsToTypesClosing(typeof(ValueResolver<,>)) .OnAddedPluginTypes(t => t.HybridHttpOrThreadLocalScoped()); }); } }
public static class SmObjectFactory { private static readonly Lazy<Container> _containerBuilder = new Lazy<Container>(defaultContainer, LazyThreadSafetyMode.ExecutionAndPublication); public static IContainer Container { get { return _containerBuilder.Value; } } private static Container defaultContainer() { var container = new Container(cfg => { cfg.AddRegistry<AutomapperRegistry>(); cfg.Scan(scan => { scan.TheCallingAssembly(); scan.WithDefaultConventions(); scan.AddAllTypesOf<Profile>().NameBy(item => item.FullName); }); }); configureAutoMapper(container); return container; } private static void configureAutoMapper(IContainer container) { var configuration = container.TryGetInstance<IConfiguration>(); if (configuration == null) return; //saying AutoMapper how to resolve services configuration.ConstructServicesUsing(container.GetInstance); foreach (var profile in container.GetAllInstances<Profile>()) { configuration.AddProfile(profile); } } }
تغییرات لایه سرویس برنامه جهت استفاده از IoC Container
اکنون که IoC Container ما با نحوهی یافتن وابستگیهای IMappingEngine آشنا شدهاست، تنها کافی است این اینترفیس را در سازندهی کلاس سرویس خود تزریق کنیم:
public class UsersService : IUsersService { private readonly IMappingEngine _mappingEngine; public UsersService(IMappingEngine mappingEngine) { _mappingEngine = mappingEngine; } public UserViewModel GetName(int id) { var dbUser1 = new User { Id = 1, Name = "Test", RegistrationDate = DateTime.Now.AddDays(-10) }; var uiUser = new UserViewModel(); _mappingEngine.Map(source: dbUser1, destination: uiUser); return uiUser; } }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید:
AM_Sample03.zip
مقدمهای بر RxJS
اگر به پیشنیازهای نصب AngularJS 2.0 در قسمت اول این سری دقت کرده باشید، یکی از موارد آن، RxJS است:
"dependencies": { "rxjs": "5.0.0-beta.2" },
قرار است Observableها به ES 2016 یا نگارش پس از ES 6 اضافه شوند و یکی از پیشنهادات آن هستند. اما هم اکنون AngularJS 2.0 از این امکان، توسط یک کتابخانهی ثالث، به نام reactive extensions یا Rx، استفاده میکند. از RxJS در سرویس HTTP و همچنین مدیریت سیستم رخدادهای AngularJS 2.0 استفاده میشود. Observableها امکانی را فراهم میکنند تا به ازای دریافت هر اطلاعات async از سرور، بتوان توسط رخدادهایی از وقوع آنها مطلع شد.
در نگارش قبلی AngularJS از Promises برای مدیریت اعمال غیرهمزمان استفاده میشد. Observableها تفاوتهای قابل ملاحظهای با Promises دارند:
- یک Promise تنها یک مقدار، یا خطا را بر میگرداند؛ اما یک Observable چندین مقدار را در طول یک بازهی زمانی باز میگرداند.
- برخلاف Promises، میتوان عملیات یک Observable را لغو کرد.
- Observableها از عملگرهایی مانند map، reduce، filter و غیره نیز پشتیبانی میکنند.
البته باید عنوان کرد که هنوز هم میتوان از Promises در صورت تمایل در AngularJS 2.0 نیز استفاده کرد.
تنظیمات اولیهی کار با RxJS در AngularJS 2.0
برای استفاده از RxJS در AngularJS 2.0، مراحلی مانند افزودن مدخل اسکریپت http.dev.js، ثبت پروایدر HTTP و importهای لازم، باید طی شوند که در ادامه آنها را بررسی خواهیم کرد:
الف) سرویس HTTP جزئی از angular2/core نیست. به همین جهت مدخل اسکریپت متناظر با آن، باید به صفحهی اصلی سایت اضافه شود که این مورد، در قسمت اول بررسی پیشنیازهای نصب AngularJS 2.0 صورت گرفتهاست:
<!-- Required for http --> <script src="~/node_modules/angular2/bundles/http.dev.js"></script>
ب) اکنون فایل app.component.ts را گشوده و سرویس HTTP را به آن اضافه میکنیم. با نحوهی ثبت سرویسها در قسمت قبل آشنا شدیم:
import { Component } from 'angular2/core'; import { HTTP_PROVIDERS } from 'angular2/http'; import 'rxjs/Rx'; // Load all features import { ProductListComponent } from './products/product-list.component'; import { ProductService } from './products/product.service'; @Component({ selector: 'pm-app', template:` <div><h1>{{pageTitle}}</h1> <pm-products></pm-products> </div> `, directives: [ProductListComponent], providers: [ ProductService, HTTP_PROVIDERS ] }) export class AppComponent { pageTitle: string = "DNT AngularJS 2.0 APP"; }
ج) پس از آن نیاز است importهای متناظر نیز به ابتدای ماژول فعلی، جهت شناسایی این سرویس و همچنین امکانات rx.js اضافه شوند.
تعریف 'import 'rxjs/Rx به این شکل، به module loader اعلام میکند که این کتابخانه را بارگذاری کن، اما چیزی را import نکن. هنگامیکه این کتابخانه بارگذاری میشود، کدهای جاوا اسکریپتی آن اجرا شده و سبب میشوند که عملگرهای ویژهی Observable آن مانند map و filter نیز در دسترس برنامه قرار گیرند.
ساخت یک سرویس سمت سرور بازگشت لیست محصولات به صورت JSON
چون در ادامه میخواهیم لیست محصولات را از سرور دریافت کنیم، برنامهی ASP.NET MVC فعلی را اندکی تغییر میدهیم تا این لیست را به صورت JSON بازگشت دهد.
بنابراین ابتدا کلاس مدل محصولات را به نحو ذیل به پوشهی Models اضافه کرده:
namespace MVC5Angular2.Models { public class Product { public int ProductId { set; get; } public string ProductName { set; get; } public string ProductCode { set; get; } public string ReleaseDate { set; get; } public decimal Price { set; get; } public string Description { set; get; } public double StarRating { set; get; } public string ImageUrl { set; get; } } }
using System.Collections.Generic; using System.Text; using System.Web.Mvc; using MVC5Angular2.Models; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; namespace MVC5Angular2.Controllers { public class HomeController : Controller { // GET: Home public ActionResult Index() { return View(); } public ActionResult Products() { var products = new List<Product> { new Product { ProductId= 2, ProductName= "Garden Cart", ProductCode= "GDN-0023", ReleaseDate= "March 18, 2016", Description= "15 gallon capacity rolling garden cart", Price= (decimal) 32.99, StarRating= 4.2, ImageUrl= "app/assets/images/garden_cart.png" }, new Product { ProductId= 5, ProductName= "Hammer", ProductCode= "TBX-0048", ReleaseDate= "May 21, 2016", Description= "Curved claw steel hammer", Price= (decimal) 8.9, StarRating= 4.8, ImageUrl= "app/assets/images/rejon_Hammer.png" } }; return new ContentResult { Content = JsonConvert.SerializeObject(products, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }), ContentType = "application/json", ContentEncoding = Encoding.UTF8 }; } } }
برای مطالعهی بیشتر:
«استفاده از JSON.NET در ASP.NET MVC»
«تنظیمات و نکات کاربردی کتابخانهی JSON.NET»
به این ترتیب، آدرس http://localhost:2222/home/products، خروجی JSON سرویس لیست محصولات را در مثال جاری، ارائه میدهد.
ارسال یک درخواست HTTP به سرور توسط AngularJS 2.0
اکنون پس از تنظیمات ثبت و معرفی سرویس HTTP و همچنین برپایی یک سرویس سمت سرور ارائهی لیست محصولات، میخواهیم سرویس ProductService را که در قسمت قبل ایجاد کردیم (فایل product.service.ts)، جهت دریافت لیست محصولات از سمت سرور، تغییر دهیم:
import { Injectable } from 'angular2/core'; import { IProduct } from './product'; import { Http, Response } from 'angular2/http'; import { Observable } from 'rxjs/Observable'; @Injectable() export class ProductService { private _productUrl = '/home/products'; constructor(private _http: Http) { } getProducts(): Observable<IProduct[]> { return this._http.get(this._productUrl) .map((response: Response) => <IProduct[]>response.json()) .do(data => console.log("All: " + JSON.stringify(data))) .catch(this.handleError); } private handleError(error: Response) { console.error(error); return Observable.throw(error.json().error || 'Server error'); } }
در سازندهی کلاس ProductService، کار تزریق وابستگی سرویس Http انجام شدهاست. به این ترتیب با استفاده از متغیر خصوصی http_، میتوان در کلاس جاری به امکانات این سرویس دسترسی یافت (همان «تزریق سرویسها به کامپوننتها» در قسمت قبل).
سپس متد get آن، یک درخواست HTTP از نوع GET را به آدرس مشخص شدهی در متغیر productUrl_ ارسال میکند (یا همان سرویس سمت سرور برنامه).
سرویس Http و همچنین شیء Response آن در ماژولهای Http و Response قرار دارند که در ابتدای صفحه import شدهاند.
متد http get یک Observable را بازگشت میدهد که در نهایت خروجی این متد نیز به همان <[]Observable<IProduct، تنظیم شدهاست. Observable یک شیء جنریک است و در اینجا نوع آن، آرایهای از محصولات درنظر گرفته شدهاست.
اکنون که امضای این متد تغییر یافته است (پیش از این صرفا یک آرایهی ساده از محصولات بود)، استفاده کننده (در کلاس ProductListComponent) باید به تغییرات آن از طریق متد subscribe گوش فرا دهد.
فعلا در کلاس جاری، پس از پایان کار دریافت اطلاعات از سرور، اطلاعات نهایی در متد map در دسترس قرار میگیرد (که یکی از عملگرهای RxJs است). کار متد map، اصطلاحا projection است. این متد، هر عضو دریافتی از خروجی سرور را به فرمتی جدید نگاشت میکند.
هر درخواست HTTP، در اصل یک عملیات async است. یعنی در اینجا توالی که در اختیار Observable ما قرار میگیرد، تنها یک المان دارد که همان شیء HTTP Response است.
بنابراین کار متد map فوق، تبدیل شیء خروجی از سرور، به آرایهای از محصولات است.
در اینجا یک سری کدهای مدیریت استثناءها را نیز در صورت بروز مشکلی میتوان تعریف کرد. برای مثال در اینجا متد catch، کار پردازش خطاهای رخ داده را انجام میدهد.
از متد do جهت لاگ کردن عملیات رخ داده و دادههای دریافتی در کنسول developer tools مرورگرها استفاده شدهاست.
یک نکته:
اگر خروجی JSON از سرور، برای مثال داخل خاصیتی به نام data محصور شده بود، بجای ()response.json میبایستی از response.json().data استفاده میشد.
گوش فرا دادن به Observable دریافتی از سرور
تا اینجا یک درخواست HTTP GET را به سمت سرور ارسال کردیم و خروجی آن به صورت Observable در اختیار ما است. اکنون نیاز است کدهای ProductListComponent را جهت گوش فرا دادن به این Observable تغییر دهیم. برای این منظور فایل product-list.component.ts را گشوده و تغییرات ذیل را به آن اعمال کنید:
errorMessage: string; ngOnInit(): void { //console.log('In OnInit'); this._productService.getProducts() .subscribe( products => this.products = products, error => this.errorMessage = <any>error); }
اولین پارامتر متد subscribe، کار دریافت نتایج حاصل را به عهده دارد. برای مثال اگر حاصل عملیات در طی سه مرحله صورت گیرد، سه بار نتیجهی دریافتی را میتوان در اینجا پردازش کرد. البته همانطور که عنوان شد، یک عملیات غیرهمزمان HTTP، تنها در طی یک مرحله، HTTP Response را دریافت میکند؛ بنابراین، پارامتر اول متد subscribe نیز تنها یکبار اجرا میشود. در اینجا فرصت خواهیم داشت تا آرایهی دریافتی حاصل از متد map قسمت قبل را به خاصیت عمومی products کلاس جاری نسبت دهیم.
پارامتر دوم متد subscribe در صورت شکست عملیات فراخوانی میشود. در اینجا حاصل آن به خاصیت جدید errorMessage نسبت داده شدهاست.
اکنون برنامه را مجددا اجرا کنید، هنوز باید لیست محصولات، مانند قبل نمایش داده شود.
یک نکته
اگر برنامه را اجرا کردید و خروجی مشاهده نشد، به کنسول developer tools مرورگر مراجعه کنید؛ احتمالا خطای ذیل در آن درج شدهاست:
EXCEPTION: No provider for Http!
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MVC5Angular2.part8.zip
خلاصهی بحث
برای کار با سرور و ارسال درخواستهای HTTP، ابتدا نیاز است مدخل تعریف http.dev.js به index.html اضافه شود و سپس HTTP_PROVIDERS را در بالاترین سطح کامپوننتهای تعریف شده، ثبت و معرفی کرد. پس از آن نیاز است RxJs را نیز import کرد. در ادامه، سرویس دریافت لیست محصولات، وابستگی سرویس HTTP را توسط سازندهی خود دریافت کرده و از آن برای صدور یک فرمان HTTP GET استفاده میکند. سپس با استفاده از متد map، کار نگاشت شیء Response دریافتی، به فرمت مناسب مدنظر، صورت میگیرد.
در ادامه هر کلاسی که نیاز دارد با این کلاس سرویس دریافت اطلاعات کار کند، متد subscribe را فراخوانی کرده و نتیجهی عملیات را پردازش میکند.
برای بهبود این وضعیت میتوان مرحلهی دومی را نیز به این فرآیند لاگین افزود؛ پس از اینکه مشخص شد کاربر وارد شدهی به سایت، دارای اکانتی در IDP ما است، کدی را به آدرس ایمیل او ارسال میکنیم. اگر این ایمیل واقعا متعلق به این شخص است، بنابراین قادر به دسترسی به آن، خواندن و ورود آن به برنامهی ما نیز میباشد. این اعتبارسنجی دو مرحلهای را میتوان به عملیات لاگین متداول از طریق ورود نام کاربری و کلمهی عبور در IDP ما نیز اضافه کرد.
تنظیم میانافزار Cookie Authentication
مرحلهی اول ایجاد گردش کاری اعتبارسنجی دو مرحلهای، فعالسازی میانافزار Cookie Authentication در برنامهی IDP است. برای این منظور به کلاس Startup آن مراجعه کرده و AddCookie را اضافه میکنیم:
namespace DNT.IDP { public class Startup { public const string TwoFactorAuthenticationScheme = "idsrv.2FA"; public void ConfigureServices(IServiceCollection services) { // ... services.AddAuthentication() .AddCookie(authenticationScheme: TwoFactorAuthenticationScheme) .AddGoogle(authenticationScheme: "Google", configureOptions: options => { options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; options.ClientId = Configuration["Authentication:Google:ClientId"]; options.ClientSecret = Configuration["Authentication:Google:ClientSecret"]; }); }
اصلاح اکشن متد Login برای هدایت کاربر به صفحهی ورود اطلاعات کد موقتی
تا این مرحله، در اکشن متد Login کنترلر Account، اگر کاربر، اطلاعات هویتی خود را صحیح وارد کند، به سیستم وارد خواهد شد. برای لغو این عملکرد پیشفرض، کدهای HttpContext.SignInAsync آنرا حذف کرده و با Redirect به اکشن متد نمایش صفحهی ورود کد موقتی ارسال شدهی به آدرس ایمیل کاربر، جایگزین میکنیم.
namespace DNT.IDP.Controllers.Account { [SecurityHeaders] [AllowAnonymous] public class AccountController : Controller { [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Login(LoginInputModel model, string button) { // ... if (ModelState.IsValid) { if (await _usersService.AreUserCredentialsValidAsync(model.Username, model.Password)) { var user = await _usersService.GetUserByUsernameAsync(model.Username); var id = new ClaimsIdentity(); id.AddClaim(new Claim(JwtClaimTypes.Subject, user.SubjectId)); await HttpContext.SignInAsync(scheme: Startup.TwoFactorAuthenticationScheme, principal: new ClaimsPrincipal(id)); await _twoFactorAuthenticationService.SendTemporaryCodeAsync(user.SubjectId); var redirectToAdditionalFactorUrl = Url.Action("AdditionalAuthenticationFactor", new { returnUrl = model.ReturnUrl, rememberLogin = model.RememberLogin }); // request for a local page if (Url.IsLocalUrl(model.ReturnUrl)) { return Redirect(redirectToAdditionalFactorUrl); } if (string.IsNullOrEmpty(model.ReturnUrl)) { return Redirect("~/"); } // user might have clicked on a malicious link - should be logged throw new Exception("invalid return URL"); } await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials")); ModelState.AddModelError("", AccountOptions.InvalidCredentialsErrorMessage); } // something went wrong, show form with error var vm = await BuildLoginViewModelAsync(model); return View(vm); }
- سپس بر اساس Id این کاربر، یک ClaimsIdentity تشکیل میشود.
- در ادامه با فراخوانی متد SignInAsync بر روی این ClaimsIdentity، یک کوکی رمزنگاری شده را با scheme تعیین شده که با authenticationScheme تنظیم شدهی در کلاس آغازین برنامه تطابق دارد، ایجاد میکنیم.
await HttpContext.SignInAsync(scheme: Startup.TwoFactorAuthenticationScheme, principal: new ClaimsPrincipal(id));
public interface ITwoFactorAuthenticationService { Task SendTemporaryCodeAsync(string subjectId); Task<bool> IsValidTemporaryCodeAsync(string subjectId, string code); }
- متد IsValidTemporaryCodeAsync، کد دریافت شدهی از کاربر را با نمونهی موجود در بانک اطلاعاتی مقایسه و اعتبار آنرا اعلام میکند.
ایجاد اکشن متد AdditionalAuthenticationFactor و View مرتبط با آن
پس از ارسال کد موقتی به کاربر، کاربر را به صورت خودکار به اکشن متد جدید AdditionalAuthenticationFactor هدایت میکنیم تا این کد موقتی را که به صورت ایمیل (و یا در اینجا با مشاهدهی لاگ برنامه)، دریافت کردهاست، وارد کند. همچنین returnUrl را نیز به این اکشن متد جدید ارسال میکنیم تا بدانیم پس از ورود موفق کد موقتی توسط کاربر، او را باید در ادامهی این گردش کاری به کجا هدایت کنیم. بنابراین قسمت بعدی کار، ایجاد این اکشن متد و تکمیل View آن است.
ViewModel ای که بیانگر ساختار View مرتبط است، چنین تعریفی را دارد:
using System.ComponentModel.DataAnnotations; namespace DNT.IDP.Controllers.Account { public class AdditionalAuthenticationFactorViewModel { [Required] public string Code { get; set; } public string ReturnUrl { get; set; } public bool RememberLogin { get; set; } } }
سپس اکشن متد AdditionalAuthenticationFactor در حالت Get، این View را نمایش میدهد و در حالت Post، اطلاعات آنرا از کاربر دریافت خواهد کرد:
namespace DNT.IDP.Controllers.Account { public class AccountController : Controller { [HttpGet] public IActionResult AdditionalAuthenticationFactor(string returnUrl, bool rememberLogin) { // create VM var vm = new AdditionalAuthenticationFactorViewModel { RememberLogin = rememberLogin, ReturnUrl = returnUrl }; return View(vm); } [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> AdditionalAuthenticationFactor( AdditionalAuthenticationFactorViewModel model) { if (!ModelState.IsValid) { return View(model); } // read identity from the temporary cookie var info = await HttpContext.AuthenticateAsync(Startup.TwoFactorAuthenticationScheme); var tempUser = info?.Principal; if (tempUser == null) { throw new Exception("2FA error"); } // ... check code for user if (!await _twoFactorAuthenticationService.IsValidTemporaryCodeAsync(tempUser.GetSubjectId(), model.Code)) { ModelState.AddModelError("code", "2FA code is invalid."); return View(model); } // login the user AuthenticationProperties props = null; if (AccountOptions.AllowRememberLogin && model.RememberLogin) { props = new AuthenticationProperties { IsPersistent = true, ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration) }; } // issue authentication cookie for user var user = await _usersService.GetUserBySubjectIdAsync(tempUser.GetSubjectId()); await _events.RaiseAsync(new UserLoginSuccessEvent(user.Username, user.SubjectId, user.Username)); await HttpContext.SignInAsync(user.SubjectId, user.Username, props); // delete temporary cookie used for 2FA await HttpContext.SignOutAsync(Startup.TwoFactorAuthenticationScheme); if (_interaction.IsValidReturnUrl(model.ReturnUrl) || Url.IsLocalUrl(model.ReturnUrl)) { return Redirect(model.ReturnUrl); } return Redirect("~/"); }
- فراخوانی HttpContext.SignInAsync با اسکیمای مشخص شده، یک کوکی رمزنگاری شده را در اکشن متد Login ایجاد میکند. اکنون در اینجا با استفاده از متد HttpContext.AuthenticateAsync و ذکر همان اسکیما، میتوانیم به محتوای این کوکی رمزنگاری شده دسترسی داشته باشیم و از طریق آن، Id کاربر را استخراج کنیم.
- اکنون که این Id را داریم و همچنین Code موقتی نیز از طرف کاربر ارسال شدهاست، آنرا به متد IsValidTemporaryCodeAsync که پیشتر در مورد آن توضیح دادیم، ارسال کرده و اعتبارسنجی میکنیم.
- در آخر این کوکی رمزنگاری شده را با فراخوانی متد HttpContext.SignOutAsync، حذف و سپس یک کوکی جدید را بر اساس اطلاعات هویت کاربر، توسط متد HttpContext.SignInAsync ایجاد و ثبت میکنیم تا کاربر بتواند بدون مشکل وارد سیستم شود.
View متناظر با آن نیز در فایل src\IDP\DNT.IDP\Views\Account\AdditionalAuthenticationFactor.cshtml، به صورت زیر تعریف شدهاست تا کد موقتی را به همراه آدرس بازگشت پس از ورود آن، به سمت سرور ارسال کند:
@model AdditionalAuthenticationFactorViewModel <div> <div class="page-header"> <h1>2-Factor Authentication</h1> </div> @Html.Partial("_ValidationSummary") <div class="row"> <div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title">Input your 2FA code</h3> </div> <div class="panel-body"> <form asp-route="Login"> <input type="hidden" asp-for="ReturnUrl" /> <input type="hidden" asp-for="RememberLogin" /> <fieldset> <div class="form-group"> <label asp-for="Code"></label> <input class="form-control" placeholder="Code" asp-for="Code" autofocus> </div> <div class="form-group"> <button class="btn btn-primary">Submit code</button> </div> </fieldset> </form> </div> </div> </div> </div>
آزمایش برنامه جهت بررسی اعتبارسنجی دو مرحلهای
پس از طی این مراحل، اعتبارسنجی دو مرحلهای در برنامه فعال شدهاست. اکنون برای آزمایش آن، برنامهها را اجرا میکنیم. پس از لاگین، صفحهی زیر نمایش داده میشود:
همچنین کد موقتی این مرحله را نیز در لاگهای برنامه مشاهده میکنید:
پس از ورود آن، کار اعتبارسنجی نهایی آن انجام شده و سپس بلافاصله به برنامهی MVC Client هدایت میشویم.
اضافه کردن اعتبارسنجی دو مرحلهای به قسمت ورود از طریق تامین کنندههای هویت خارجی
دقیقا همین مراحل را نیز به اکشن متد Callback کنترلر ExternalController اضافه میکنیم. در این اکشن متد، تا قسمت کدهای مشخص شدن user آن که از اکانت خارجی وارد شدهاست، با قبل یکی است. پس از آن تمام کدهای لاگین شخص به برنامه را از اینجا حذف و به اکشن متد جدید AdditionalAuthenticationFactor در همین کنترلر منتقل میکنیم.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشهی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشهی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آنرا اجرا کنید تا برنامهی IDP راه اندازی شود.
- در آخر به پوشهی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحهی login نام کاربری را User 1 و کلمهی عبور آنرا password وارد کنید.
EF Code First #12
public class MyContext : DbContext, IUnitOfWork { // ... public IList<T> GetRows<T>(string sql, params object[] parameters) where T: class { return this.Database.SqlQuery<T>(sql, parameters).ToList(); } }
using System; using System.Net; namespace Async15 { class Program { static void Main(string[] args) { using (var webClient = new WebClient { }) { webClient.Headers.Add("User-Agent", "AsyncContext 1.0"); var data = await webClient.DownloadStringTaskAsync("https://www.dntips.ir"); Console.WriteLine(data); } } } }
اضافه کردن واژهی کلیدی async به روالهای رخدادگردان void برنامههای دسکتاپ مجاز است؛ با توجه به اینکه متد async پیش از پایان کار به فراخوان بازگشت داده میشوند (ذات متدهای async به این نحو است). در برنامههای دسکتاپ، این بازگشت به UI event loop است؛ بنابراین برنامه بدون مشکل به کار خود ادامه خواهد داد. اما در اینجا، بازگشت متد Main، به معنای بازگشت به OS است و خاتمهی برنامه. به همین جهت کامپایلر از async کردن آن ممانعت میکند.
برای حل این مشکل در برنامههای کنسول و همچنین برنامههای سرویس ویندوز NT که دارای یک async-compatible context نیستند، میتوان از یک کتابخانهی کمکی سورس باز به نام Nito AsyncEx استفاده کرد. برای نصب آن دستور ذیل را در کنسول پاورشل نیوگت وارد کنید:
PM> Install-Package Nito.AsyncEx
using System; using System.Net; using Nito.AsyncEx; namespace Async15 { class Program { static void Main(string[] args) { AsyncContext.Run(async () => { using (var webClient = new WebClient()) { webClient.Headers.Add("User-Agent", "AsyncContext 1.0"); var data = await webClient.DownloadStringTaskAsync("https://www.dntips.ir"); Console.WriteLine(data); } }); } } }
async lambda
در مثال فوق از یک async lambda، برای فراخوانی استفاده شده است که به همراه دات نت 4.5 ارائه شدهاند:
Action, () => { } Func<Task>, async () => { await Task.Yield(); } Func<TResult>, () => { return 13; } Func<Task<TResult>>, async () => { await Task.Yield(); return 13; }
روش دوم استفاده از AsyncContext.Run و مقدار دهی Func of Task، تعریف یک متد مستقل async Task دار، به نحو ذیل است:
class Program { static async Task<int> AsyncMain() { .. } static int Main(string[] args) { return AsyncContext.Run(AsyncMain); } }
رخدادهای مرتبط با طول عمر برنامه را async تعریف نکنید
همانند متد Main که async تعریف کردن آن سبب بازگشت آنی روال کار به OS میشود و برنامه خاتمه مییابد، روالهای رخدادگردانی که با طول عمر یک برنامهی UI سر و کار دارند مانند Application_Launching، Application_Closing، Application_Activated و Application_Deactivated (خصوصا در برنامههای ویندوز 8) نیز نباید async void تعریف شوند (چون مطابق ذات متدهای async، بلافاصله به برنامه اعلام میکنند که کار تمام شد). در این موارد خاص نیز میتوان از متد AsyncContext.Run برای انجام اعمال async استفاده کرد.
در مثال فوق، در متد Execute کار یافتن کلاسها انجام شد. برای مثال روش یافتن کنترلرهای یک برنامهی MVC در اینجا به صورت زیر است:
var controllers = context.Compilation .SyntaxTrees .SelectMany(syntaxTree => syntaxTree.GetRoot().DescendantNodes()) .Where(x => x is ClassDeclarationSyntax) .Cast<ClassDeclarationSyntax>() .Where(c => c.Identifier.ValueText.EndsWith("Controller", StringComparison.OrdinalIgnoreCase)) .ToImmutableList();
public class ControllerFinder : ISyntaxReceiver { public List<ClassDeclarationSyntax> Controllers { get; } = new(); public void OnVisitSyntaxNode(SyntaxNode syntaxNode) { if (syntaxNode is ClassDeclarationSyntax controller) { if (controller.Identifier.ValueText.EndsWith("Controller")) { Controllers.Add(controller); } } } }
public void Initialize(GeneratorInitializationContext context) { context.RegisterForSyntaxNotifications(() => new ControllerFinder()); }
public void Execute(GeneratorExecutionContext context) { var controllers = ((ControllerFinder) context.SyntaxReceiver)?.Controllers; // use controllers to do work... }
عموما این نوع کوئری گرفتنها اندکی مشکل است. پروژهی SourceGeneratorsKit مثالهای بیشتری را در این زمینه در جهت یافتن سادهتر متدهایی که ویژگیهای خاصی را دارند، کلاسهایی که اینترفیس خاصی را پیاده سازی میکنند و امثال آن، ارائه میدهد.
Try .NET is an interactive documentation generator for .NET Core.
Use Try .NET to create executable C# snippets for your websites or, interactive markdown files that users can run on their machine.
Try .NET execute C# code in client side using Blazor and Web Assembly.
var container = new UnityContainer();
container.RegisterType<ISomeService, SomeService>(new PerRequestLifetimeManager()); container.RegisterType<ISomeBusiness, SomeBusiness>(new PerRequestLifetimeManager()); container.RegisterType<ISomeController, SomeController>(new PerRequestLifetimeManager());
DependencyResolver.SetResolver(new UnityDependencyResolver(container));
var someService=(ISomeService)DependencyResolver.Current.GetService(typeof(ISomeService)); var data=someService.GetData();
در لایه سرویس یک کلاس Service Factory داریم که قرار است همه سرویسها، برای برقراری ارتباط با یکدیگر از آن استفاده کنند.این کلاس معمولا در لایه سرویس به اشکال گوناگونی پیاده سازی میشود که کارش وهه سازی از Interfaceهای درخواستی است. اما برای یکپارچه کردن آن با Unity من آنرا به شکل زیر پیاده سازی کرده ام
public class ServiceFactory : MarshalByRefObject { static IUnityContainer uContainer = new UnityContainer(); public static Type DataContextType { get; set; } public static void Initialise(IUnityContainer unityContainer, Type dbContextType) { uContainer = unityContainer; DataContextType = dbContextType; uContainer.RegisterType(typeof(BaseDataContext), DataContextType, new HierarchicalLifetimeManager()); } public static T Create<T>() { return (T)Activator.CreateInstance<T>(); } public static T Create<T>(string fullTypeName) { return (T)System.Reflection.Assembly.GetExecutingAssembly().CreateInstance(fullTypeName); } public static T Create<T>(Type entityType) { return (T)Activator.CreateInstance(entityType); } public static dynamic Create(Type entityType) { return Activator.CreateInstance(entityType); } public static T Get<T>() { return uContainer.Resolve<T>(); } public static object Get(Type type) { return uContainer.Resolve(type); } }
در این کلاس ما بجای ایجاد داینامیک آبجکتها، از Unity استفاده کردهایم. در همان ابتدا که برنامهی وب ما برای اولین بار اجرا میشود و بعد از Register کردن کلاسها، میتوانیم container را به صورت پارامتر سازنده به کلاس Service Factory ارسال کنیم. به این ترتیب برای استفاده از سرویسها در لایه Business از Unity بهره میبریم.
البته استفاده از Unity برای DataContext خیلی منطقی نیست و بهتر است نوع DataContext را در ابتدا بگیریم و هرجا نیاز داشتیم با استفاده از متد Create از آن وهله سازی بکنیم.
نحوه کار با ftp - بخش اول
مواردی هم که اعلام کزدید کاملا صحیح میباشد.فقط بخش تابعی که درون آن از try/catch استفاده شده رو میخواستم ابتدا در تیکه کد دیگری از یک صفحه aspx بنویسم که وقتی برای بررسی مجدد تابع پیدا نکردم وگرنه اونجا try/catch بلا استفاده است.
موارد متذکر شده برای سهولت بیشتر کاربران در کدهای فوق لحاظ و ویرایش گردید.
با تشکر.