[_Host.cshtml] <script src="_framework/blazor.server.js"></script> <script> window.Blazor.defaultReconnectionHandler.onConnectionDown = function () { setTimeout(function () { location.reload(); }, 7000); } window.Blazor.defaultReconnectionHandler._reconnectCallback = function (d) { document.location.reload(); } </script>
var store = GetStore(); string postCode = null; if (store != null && store.Address != null && store.Address.PostCode != null) postCode = store.Address.PostCode.ToString();
public static TResult IfNotNull<TResult, TSource>( this TSource source, Func<TSource, TResult> onNotDefault) where TSource : class { if (onNotDefault == null) throw new ArgumentNullException("onNotDefault"); return source == null ? default(TResult) : onNotDefault(source); }
var postCode = GetStore() .IfNotNull(x => x.Address) .IfNotNull(x => x.PostCode) .IfNotNull(x => x.ToString());
- این متد فقط با انواع ارجاعی (reference types) کار میکند و میبایست برای کار با انواع مقداری (value types) اصلاح شود.
- با انواع داده ای مثل string چه باید کرد؟ در مورد این نوع دادهها تنها مطمئن شدن از null نبودن کافی نیست. برای مثال در مورد string ، گاهی اوقات ما میخواهیم از خالی نبودن آن نیز مطمئن شویم. و یا در مورد collectionها تنها null نبودن کافی نیست بلکه زمانی که نیاز به محاسبه مجموع و یا یافتن بزرگترین عضو است، باید از خالی نبودن مجموعه و وجود حداقل یک عضو در آن مطمئن باشیم.
public static TResult IfNotDefault<TResult, TSource>( this TSource source, Func<TSource, TResult> onNotDefault, Predicate<TSource> isNotDefault = null) { if (onNotDefault == null) throw new ArgumentNullException("onNotDefault"); var isDefault = isNotDefault == null ? EqualityComparer<TSource>.Default.Equals(source, default(TSource)) : !isNotDefault(source); return isDefault ? default(TResult) : onNotDefault(source); }
return person . IfNotDefault(x => x.Name) . IfNotDefault(SomeOperation, x => !string.IsNullOrEmpty(x));
var avg = students .Where(IsNotAGraduate) .FirstOrDefault() .IfNotDefault(s => s.Grades) .IfNotDefault(g => g.Average(), g => g != null && g.Length > 0);
برای مطالعه بیشتر
Get rid of deep null checks
Chained null checks and the Maybe monad
Maybe or IfNotNull using lambdas for deep expressions
Dynamically Check Nested Values for IsNull Values
در ادامه بررسی تصویر امنیتی سایت مواردی که باید پیاده سازی شود برای مورد اول میتوان کلاس زیر را در نظر گرفت که متدهایی برای تولید اعداد بصورت تصادفی در بین بازه معرفی شده را بازگشت میدهد:
// RandomGenerator.cs using System; using System.Security.Cryptography; namespace PersianCaptchaHandler { public class RandomGenerator { private static readonly byte[] Randb = new byte[4]; private static readonly RNGCryptoServiceProvider Rand = new RNGCryptoServiceProvider(); public static int Next() { Rand.GetBytes(Randb); var value = BitConverter.ToInt32(Randb, 0); if (value < 0) value = -value; return value; } public static int Next(int max) { Rand.GetBytes(Randb); var value = BitConverter.ToInt32(Randb, 0); value = value%(max + 1); if (value < 0) value = -value; return value; } public static int Next(int min, int max) { var value = Next(max - min) + min; return value; } } }
// NumberToString.cs namespace PersianCaptchaHandler { public class NumberToString { #region Fields private static readonly string[] Yakan = new[] { "صفر", "یک", "دو", "سه", "چهار", "پنج", "شش", "هفت", "هشت", "نه" }; private static readonly string[] Dahgan = new[] { "", "", "بیست", "سی", "چهل", "پنجاه", "شصت", "هفتاد", "هشتاد", "نود" }; private static readonly string[] Dahyek = new [] { "ده", "یازده", "دوازده", "سیزده", "چهارده", "پانزده", "شانزده", "هفده", "هجده", "نوزده" }; private static readonly string[] Sadgan = new [] { "", "یکصد", "دوصد", "سیصد", "چهارصد", "پانصد", "ششصد", "هفتصد", "هشتصد", "نهصد" }; private static readonly string[] Basex = new [] { "", "هزار", "میلیون", "میلیارد", "تریلیون" }; #endregion private static string Getnum3(int num3) { var s = ""; var d12 = num3 % 100; var d3 = num3 / 100; if (d3 != 0) s = Sadgan[d3] + " و "; if ((d12 >= 10) && (d12 <= 19)) { s = s + Dahyek[d12 - 10]; } else { var d2 = d12 / 10; if (d2 != 0) s = s + Dahgan[d2] + " و "; var d1 = d12 % 10; if (d1 != 0) s = s + Yakan[d1] + " و "; s = s.Substring(0, s.Length - 3); } return s; } public static string ConvertIntNumberToFarsiAlphabatic(string snum) { var stotal = ""; if (snum == "0") return Yakan[0]; snum = snum.PadLeft(((snum.Length - 1) / 3 + 1) * 3, '0'); var l = snum.Length / 3 - 1; for (var i = 0; i <= l; i++) { var b = int.Parse(snum.Substring(i * 3, 3)); if (b != 0) stotal = stotal + Getnum3(b) + " " + Basex[l - i] + " و "; } stotal = stotal.Substring(0, stotal.Length - 3); return stotal; } } }
using System; using System.IO; using System.Security.Cryptography; using System.Text; namespace PersianCaptchaHandler { public class Encryptor { #region constraints private static string Password { get { return "Mehdi"; } } private static string Salt { get { return "Payervand"; } } #endregion public static string Encrypt(string clearText) { // Turn text to bytes var clearBytes = Encoding.Unicode.GetBytes(clearText); var pdb = new PasswordDeriveBytes(Password, Encoding.Unicode.GetBytes(Salt)); var ms = new MemoryStream(); var alg = Rijndael.Create(); alg.Key = pdb.GetBytes(32); alg.IV = pdb.GetBytes(16); var cs = new CryptoStream(ms, alg.CreateEncryptor(), CryptoStreamMode.Write); cs.Write(clearBytes, 0, clearBytes.Length); cs.Close(); var encryptedData = ms.ToArray(); return Convert.ToBase64String(encryptedData); } public static string Decrypt(string cipherText) { // Convert text to byte var cipherBytes = Convert.FromBase64String(cipherText); var pdb = new PasswordDeriveBytes(Password, Encoding.Unicode.GetBytes(Salt)); var ms = new MemoryStream(); var alg = Rijndael.Create(); alg.Key = pdb.GetBytes(32); alg.IV = pdb.GetBytes(16); var cs = new CryptoStream(ms, alg.CreateDecryptor(), CryptoStreamMode.Write); cs.Write(cipherBytes, 0, cipherBytes.Length); cs.Close(); var decryptedData = ms.ToArray(); return Encoding.Unicode.GetString(decryptedData); } } }
و نیز برای اعتبار سنجی عدد دریافتی از کاربر میتوان از عبارت با قاعده زیر استفاده کرد:
// Utils.cs using System.Text.RegularExpressions; namespace PersianCaptchaHandler { public class Utils { private static readonly Regex NumberMatch = new Regex(@"^([0-9]*|\d*\.\d{1}?\d*)$", RegexOptions.Compiled); public static bool IsNumber(string number2Match) { return NumberMatch.IsMatch(number2Match); } } }
<add verb="GET" path="/captcha/" type="PersianCaptchaHandler.CaptchaHandler, PersianCaptchaHandler, Version=1.0.0.0, Culture=neutral" />
<!-- ASPX --> <dl> <dt>تصویر امنیتی</dt> <dd> <asp:Image ID="imgCaptchaText" runat="server" AlternateText="CaptchaImage" /> <asp:HiddenField ID="hfCaptchaText" runat="server" /> <asp:ImageButton ID="btnRefreshCaptcha" runat="server" ImageUrl="/img/refresh.png" OnClick="btnRefreshCaptcha_Click" /> <br /> <asp:TextBox ID="txtCaptcha" runat="server" AutoCompleteType="Disabled"></asp:TextBox> </dd> <dd> <asp:Button ID="btnSubmit" runat="server" Text="ثبت" OnClick="btnSubmit_Click" /> </dd> </dl> <asp:Label ID="lblMessage" runat="server"></asp:Label>
protected void Page_Load(object sender, EventArgs e) { if (!Page.IsPostBack) SetCaptcha(); } private void SetCaptcha() { lblMessage.Text = txtCaptcha.Text = string.Empty; var newNumber = RandomGenerator.Next(100, 999) ; var farsiAlphabatic = NumberToString.ConvertIntNumberToFarsiAlphabatic(newNumber.ToString()); hfCaptchaText.Value = HttpUtility .UrlEncode( Encryptor.Encrypt( farsiAlphabatic ) ); txtCaptcha.Text = string.Empty; imgCaptchaText.ImageUrl = "/captcha/?text=" + hfCaptchaText.Value; }
private string GetCaptcha() { var farsiAlphabatic = NumberToString.ConvertIntNumberToFarsiAlphabatic(txtCaptcha.Text); var encryptedString = HttpUtility .UrlEncode( Encryptor.Encrypt( farsiAlphabatic ) ); return encryptedString; } private bool ValidateUserInputForLogin() { if (!Utils.IsNumber(txtCaptcha.Text)) { lblMessage.Text = "تصویر امنیتی را بطور صحیح وارد نکرده اید"; return false; } var strGetCaptcha = GetCaptcha(); var strDecodedVAlue = hfCaptchaText.Value; if (strDecodedVAlue != strGetCaptcha) { lblMessage.Text = "کلمه امنیتی اشتباه است"; SetCaptcha(); return false; } return true; } protected void btnSubmit_Click(object sender, EventArgs e) { if (!ValidateUserInputForLogin()) return; lblMessage.Text = "کلمه امنیتی درست است"; } protected void btnRefreshCaptcha_Click(object sender, ImageClickEventArgs e) { SetCaptcha(); }
این سری شامل بررسی موارد ذیل خواهد بود:
- قسمت اول - معرفی و ایجاد ساختار برنامه
- قسمت دوم - سرویس اعتبارسنجی
- قسمت سوم - ورود به سیستم
- قسمت چهارم - به روز رسانی خودکار توکنها
- قسمت پنجم - محافظت از مسیرها
- قسمت ششم - کار با منابع محافظت شدهی سمت سرور
پیشنیازها
- آشنایی با Angular CLI
- آشنایی با مسیریابیها در Angular
- آشنایی با فرمهای مبتنی بر قالبها
همچنین اگر پیشتر Angular CLI را نصب کردهاید، قسمت «به روز رسانی Angular CLI» ذکر شدهی در مطلب «Angular CLI - قسمت اول - نصب و راه اندازی» را نیز اعمال کنید. در این سری از angular/cli: 1.6.0@ استفاده شدهاست.
ایجاد ساختار اولیه و مسیریابیهای آغازین مثال این سری
در ادامه، یک پروژهی جدید مبتنی بر Angular CLI را به نام ASPNETCore2JwtAuthentication.AngularClient به همراه تنظیمات ابتدایی مسیریابی آن ایجاد میکنیم:
> ng new ASPNETCore2JwtAuthentication.AngularClient --routing
به علاوه، قصد استفادهی از بوت استرپ را نیز داریم. به همین جهت ابتدا به ریشهی پروژه وارد شده و سپس دستور ذیل را صادر کنید، تا بوت استرپ نصب شود و پرچم save آن سبب به روز رسانی فایل package.json نیز گردد:
> npm install bootstrap --save
"apps": [ { "styles": [ "../node_modules/bootstrap/dist/css/bootstrap.min.css", "styles.css" ],
در ادامه برای تکمیل مثال جاری، دو کامپوننت جدید خوشآمدگویی و همچنین یافتن نشدن مسیرها را به برنامه اضافه میکنیم:
>ng g c welcome >ng g c PageNotFound
@NgModule({ declarations: [ AppComponent, WelcomeComponent, PageNotFoundComponent ],
import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; import { WelcomeComponent } from './welcome/welcome.component'; import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; const routes: Routes = [ { path: 'welcome', component: WelcomeComponent }, { path: '', redirectTo: 'welcome', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
همچنین مدیریت مسیریابی آدرسهای ناموجود در سایت نیز با تعریف ** صورت گرفتهاست.
ایجاد ماژول Authentication و تعریف کامپوننت لاگین
کامپوننتهای احراز هویت و اعتبارسنجی کاربران را در ماژولی به نام Authentication قرار خواهیم داد. بنابراین ماژول جدید آنرا به همراه تنظیمات ابتدایی مسیریابی آن ایجاد میکنیم:
>ng g m Authentication -m app.module --routing
create src/app/authentication/authentication-routing.module.ts (257 bytes) create src/app/authentication/authentication.module.ts (311 bytes) update src/app/app.module.ts (696 bytes)
import { EmployeeRoutingModule } from './employee/employee-routing.module'; @NgModule({ imports: [ BrowserModule, AppRoutingModule, AuthenticationModule ]
بنابراین فایل app.module.ts چنین تعاریفی را پیدا میکند:
import { EmployeeModule } from './employee/employee.module'; @NgModule({ imports: [ BrowserModule, AuthenticationModule, AppRoutingModule ]
در ادامه کامپوننت جدید لاگین را به این ماژول اضافه میکنیم:
>ng g c Authentication/Login
create src/app/Authentication/login/login.component.html (24 bytes) create src/app/Authentication/login/login.component.ts (265 bytes) create src/app/Authentication/login/login.component.css (0 bytes) update src/app/Authentication/authentication.module.ts (383 bytes)
import { LoginComponent } from "./login/login.component"; @NgModule({ declarations: [LoginComponent] })
در ادامه میخواهیم قالب این کامپوننت را در منوی اصلی سایت قابل دسترسی کنیم. به همین جهت به فایل src/app/authentication/authentication-routing.module.ts مراجعه کرده و مسیریابی این کامپوننت را تعریف میکنیم:
import { LoginComponent } from "./login/login.component"; const routes: Routes = [ { path: "login", component: LoginComponent } ];
ایجاد ماژولهای Core و Shared
در مطلب «سازماندهی برنامههای Angular توسط ماژولها» در مورد اهمیت ایجاد ماژولهای Core و Shared بحث شد. در اینجا نیز این دو ماژول را ایجاد خواهیم کرد.
فایل src\app\core\core.module.ts، جهت به اشتراک گذاری سرویسهای singleton سراسری برنامه، یک چنین ساختاری را پیدا میکند:
import { NgModule, SkipSelf, Optional, } from "@angular/core"; import { CommonModule } from "@angular/common"; import { RouterModule } from "@angular/router"; // import RxJs needed operators only once import "./services/rxjs-operators"; import { BrowserStorageService } from "./browser-storage.service"; @NgModule({ imports: [CommonModule, RouterModule], exports: [ // components that are used in app.component.ts will be listed here. ], declarations: [ // components that are used in app.component.ts will be listed here. ], providers: [ // global singleton services of the whole app will be listed here. BrowserStorageService ] }) export class CoreModule { constructor( @Optional() @SkipSelf() core: CoreModule) { if (core) { throw new Error("CoreModule should be imported ONLY in AppModule."); } } }
همچنین سطر "import "./services/rxjs-operators نیز از مطلب «روشهایی برای مدیریت بهتر عملگرهای RxJS در برنامههای Angular» کمک میگیرد تا مدام نیاز به import عملگرهای rxjs نباشد.
و ساختار فایل src\app\shared\shared.module.ts جهت به اشتراک گذاری کامپوننتهای مشترک بین تمام ماژولها، به صورت ذیل است:
import { NgModule, ModuleWithProviders } from "@angular/core"; import { CommonModule } from "@angular/common"; @NgModule({ imports: [ CommonModule ], entryComponents: [ // All components about to be loaded "dynamically" need to be declared in the entryComponents section. ], declarations: [ // common and shared components/directives/pipes between more than one module and components will be listed here. ], exports: [ // common and shared components/directives/pipes between more than one module and components will be listed here. CommonModule ] /* No providers here! Since they’ll be already provided in AppModule. */ }) export class SharedModule { static forRoot(): ModuleWithProviders { // Forcing the whole app to use the returned providers from the AppModule only. return { ngModule: SharedModule, providers: [ /* All of your services here. It will hold the services needed by `itself`. */] }; } }
و در آخر تعاریف این دو ماژول جدید به فایل src\app\app.module.ts اضافه خواهند شد:
import { FormsModule } from "@angular/forms"; import { HttpClientModule } from "@angular/common/http"; import { CoreModule } from "./core/core.module"; import { SharedModule } from "./shared/shared.module"; @NgModule({ imports: [ BrowserModule, FormsModule, HttpClientModule, CoreModule, SharedModule.forRoot(), AuthenticationModule, AppRoutingModule ] }) export class AppModule { }
افزودن کامپوننت Header
در ادامه میخواهیم لینکی را به این مسیریابی جدید در نوار راهبری بالای سایت اضافه کنیم. همچنین قصد نداریم فایل app.component.html را با تعاریف آن شلوغ کنیم. به همین جهت یک کامپوننت هدر جدید را برای این منظور اضافه میکنیم:
> ng g c Header
create src/app/header/header.component.html (25 bytes) create src/app/header/header.component.ts (269 bytes) create src/app/header/header.component.css (0 bytes) update src/app/app.module.ts (1069 bytes)
<nav> <div> <div> <a [routerLink]="['/']">{{title}}</a> </div> <ul> <li role="menuitem" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }"> <a [routerLink]="['/welcome']">Home</a> </li> <li role="menuitem" routerLinkActive="active"> <a queryParamsHandling="merge" [routerLink]="['/login']">Login</a> </li> </ul> </div> </nav>
export class HeaderComponent implements OnInit { title = "Angular.Jwt.Core";
در آخر به فایل app.component.html مراجعه کرده و selector این کامپوننت را در آن درج میکنیم:
<app-header></app-header> <div> <router-outlet></router-outlet> </div>
تا اینجا اگر دستور ng serve -o را صادر کنیم (کار build درون حافظهای، جهت محیط توسعه و نمایش خودکار برنامه در مرورگر)، چنین خروجی در مرورگر نمایان خواهد شد (البته میتوان پنجرهی کنسول ng serve را باز نگه داشت تا کار watch را به صورت خودکار انجام دهد؛ این روش سریعتر و به همراه build تدریجی است):
انتقال کامپوننتهایی که در app.component.ts استفاده میشوند به CoreModule
با توجه به مطلب «سازماندهی برنامههای Angular توسط ماژولها»، کامپوننتهایی که در app.component.ts مورد استفاده قرار میگیرند، باید به Core Module منتقل شوند و قسمت declarations فایل app.module.ts از آنها خالی گردد. به همین جهت پوشهی جدید src\app\core\component را ایجاد کرده و سپس پوشهی src\app\header را به آنجا منتقل میکنیم (با تمام فایلهای درون آن).
پس از آن، تعریف HeaderComponent را از قسمت declarations مربوط به AppModule حذف کرده و آنرا به دو قسمت exports و declarations مربوط به CoreModule منتقل میکنیم:
import { HeaderComponent } from "./component/header/header.component"; @NgModule({ exports: [ // components that are used in app.component.ts will be listed here. HeaderComponent ], declarations: [ // components that are used in app.component.ts will be listed here. HeaderComponent ] }) export class CoreModule {
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژهی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشهی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
بر این اساس، کلاسهای مدل دومین مساله به صورت زیر خواهند بود:
public class Project { public int Id { set; get; } public string Name { set; get; } public virtual ICollection<ProjectIssue> ProjectIssues { set; get; } } public class ProjectIssue { public int Id { set; get; } public string Body { set; get; } [ForeignKey("ProjectStatusId")] public virtual ProjectIssueStatus ProjectIssueStatus { set; get; } public int ProjectStatusId { set; get; } [ForeignKey("ProjectId")] public virtual Project Project { set; get; } public int ProjectId { set; get; } } public class ProjectIssueStatus { public int Id { set; get; } public string Name { set; get; } public virtual ICollection<ProjectIssue> ProjectIssues { set; get; } }
اگر EF Code first را وادار به تهیه جداول و روابط معادل کلاسهای فوق کنیم:
public class MyContext : DbContext { public DbSet<ProjectIssueStatus> ProjectStatus { get; set; } public DbSet<ProjectIssue> ProjectIssues { get; set; } public DbSet<Project> Projects { get; set; } } public class Configuration : DbMigrationsConfiguration<MyContext> { public Configuration() { AutomaticMigrationsEnabled = true; AutomaticMigrationDataLossAllowed = true; } protected override void Seed(MyContext context) { var project1 = new Project { Name = "پروژه جدید" }; context.Projects.Add(project1); var stat1 = new ProjectIssueStatus { Name = "درحال انجام" }; var stat2 = new ProjectIssueStatus { Name = "انجام شد" }; context.ProjectStatus.Add(stat1); context.ProjectStatus.Add(stat2); var issue1 = new ProjectIssue { Body = "تغییر قلم گزارش", ProjectIssueStatus = stat1, Project = project1 }; var issue2 = new ProjectIssue { Body = "تغییر لوگوی گزارش", ProjectIssueStatus = stat1, Project = project1 }; context.ProjectIssues.Add(issue1); context.ProjectIssues.Add(issue2); base.Seed(context); } }
سابقا برای یافتن تعداد متناظر با هر IssueStatus خیلی سریع میشد چنین کوئری را نوشت:
اما اکنون معادل آن با EF Code first چیست؟
public static class Test { public static void RunTests() { Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, Configuration>()); using (var ctx = new MyContext()) { var projectId = 1; var list = ctx.ProjectStatus.Select(x => new { Id = x.Id, Name = x.Name, Count = x.ProjectIssues.Count(p => p.ProjectId == projectId) }).ToList(); foreach (var item in list) Console.WriteLine("{0}:{1}",item.Name, item.Count); } } }
SELECT [Project1].[Id] AS [Id], [Project1].[Name] AS [Name], [Project1].[C1] AS [C1] FROM ( SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name], ( SELECT COUNT(1) AS [A1] FROM [dbo].[ProjectIssues] AS [Extent2] WHERE ([Extent1].[Id] = [Extent2].[ProjectStatusId]) AND ([Extent2].[ProjectId] = 1 /*@p__linq__0*/) ) AS [C1] FROM [dbo].[ProjectIssueStatus] AS [Extent1] ) AS [Project1]
مروری بر قابلیت جدید ASP.NET FriendlyUrls
<script src='<%= ResolveUrl("~/Scripts/jquery-1.4.1.js")%>' type="text/javascript" />
Install-Package Microsoft.AspNetCore.Mvc.Versioning
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddApiVersioning(); // ... }
services.AddApiVersioning(opt => { opt.AssumeDefaultVersionWhenUnspecified = true; opt.DefaultApiVersion = new ApiVersion(1, 0); });
- api/foo?api-version=1.0/
opt.DefaultApiVersion = new ApiVersion(new DateTime(2018, 10, 22));
- api/foo?api-version=2018-10-22/
URL Path Segment Versioning
[Route("api/v{version:apiVersion}/[controller]")] public class FooController : ControllerBase { public ActionResult<IEnumerable<string>> Get() { return new[] { "value1", "value2" }; } }
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddApiVersioning(opt => opt.ApiVersionReader = new HeaderApiVersionReader("api-version")); }
Deprecating
[ApiVersion("2")] [ApiVersion("1", Deprecated = true)] [Route("api/v{version:apiVersion}/[controller]")] public class FooController : ControllerBase { [HttpGet] public string Get() => "I'm deprecated, Bye bye :("; [HttpGet, MapToApiVersion("2.0")] public string GetV2() => "Hello world ! :D"; }
services.AddApiVersioning(opt => { opt.DefaultApiVersion = new ApiVersion(1, 0); opt.AssumeDefaultVersionWhenUnspecified = true; opt.ReportApiVersions = true; });
Ignoring Versioning
[ApiVersionNeutral] [Route("api/[controller]")] public class BarController : ControllerBase { public string Get() => HttpContext.GetRequestedApiVersion().ToString(); }
مطلب تکمیلی:
namespace TestVersioning.Controllers.V1 { [ApiVersion("1", Deprecated = true)] [Route("api/v{version:apiVersion}/[controller]")] public class FooController : ControllerBase { public string Get() => "I'm deprecated, Bye bye :("; } } namespace TestVersioning.Controllers.V2 { [ApiVersion("2")] [Route("api/v{version:apiVersion}/[controller]")] public class FooController : ControllerBase { public string GetV2() => "Hello world ! :D"; } }
public class Service { public int ServiceId { get; set; } public string ServiceName { get; set; } }
public interface ICoreService { Service LoadDefaultService(); }
An unhandled exception occurred while processing the request InvalidOperationException: Unable to resolve service for type 'WebApplication1.Models.ICoreService' while attempting to activate 'WebApplication1.Controllers.HomeController' Microsoft.Extensions.Internal.ActivatorUtilities.GetService(IServiceProvider sp, Type type, Type requiredBy, Boolean isDefaultParameterRequired)
در نسخههای قدیمی MVC (منظور نسخههای قبل از 6)، برای تزریق وابستگیها از یک Controller Factory یا Dependency Resolver سفارشی استفاده میشد. اما در نسخه جدید MVC دیگری خبری از روشهای قدیمی نیست. چونکه یک سیستم تزریق وابستگی توکار، همراه با MVC یکپارچه شدهاست که عملیات تزریق وابستگیها را انجام میدهد. سیستم تزریق وابستگی پیش فرض، تنها از 4 حالت عملیاتی پشتیبانی میکند:
تیم Asp.Net برای فراهم آوردن امکان تزریق وابستگیها، تصمیم به انتزاعی کردن ویژگیهای مشترک محبوبترین Ioc Containerها و اجازه دادن به میان افزارها، جهت ارتباط با این اینترفیسها برای دستیابی به تزریق وابستگی بود.
namespace Microsoft.Extensions.DependencyInjection { // // Summary: // Specifies the lifetime of a service in an Microsoft.Extensions.DependencyInjection.IServiceCollection. public enum ServiceLifetime { // // Summary: // Specifies that a single instance of the service will be created. Singleton = 0, // // Summary: // Specifies that a new instance of the service will be created for each scope. // // Remarks: // In ASP.NET Core applications a scope is created around each server request. Scoped = 1, // // Summary: // Specifies that a new instance of the service will be created every time it is // requested. Transient = 2 } }
public void ConfigureServices(IServiceCollection services) { ServiceDescriptor descriptor = new ServiceDescriptor(typeof(ICoreService),typeof(CoreServise),ServiceLifetime.Transient); services.Add(descriptor); services.AddMvc(); }
ساخت یک Service Descriptor و اضافه کردن آن به سرویسها، فلسفه وجودی میان افزارها را زیر سوال میبرد. پس بجای ایجاد یک Service Descriptor، از متدهای الحاقی تدارک دیده شده استفاده میکنیم. مثلا بجای دو خط کد بالا میتوان از کد زیر استفاده نمود:
services.AddTransient<ICoreService,CoreServise>();
حال که یک دید کلی از نحوه کار مکانیزم تزریق وابستگی بدست آوردیم، میخواهیم این مکانیزم را با StructureMap جایگزین کنیم. بدین منظور ابتدا پکیج StructureMap را نصب میکنم.
در مرحله اول باید کلاسهایی را تدارک ببینیم که اینترفیسهای بالا را پیاده سازی نمایند. یعنی کلاسهای ما باید بتوانند همان کاری را انجام دهند که مکانیزم پیش فرض MVC انجام میدهد.
اولین مورد، کلاس StructureMapServiceProvider میباشد.
internal class StructureMapServiceProvider : IServiceProvider { private readonly IContainer _container; public StructureMapServiceProvider(IContainer container, bool scoped = false) { _container = container; } public object GetService(Type type) { try { return _container.GetInstance(type); } catch { return null; } } }
مورد دوم کلاس StructureMapServiceScope میباشد:
internal class StructureMapServiceScope : IServiceScope { private readonly IContainer _container; private readonly IContainer _childContainer; private IServiceProvider _provider; public StructureMapServiceScope(IContainer container) { _container = container; _childContainer = _container.GetNestedContainer(); _provider = new StructureMapServiceProvider(_childContainer, true); } public IServiceProvider ServiceProvider => _provider; public void Dispose() { _provider = null; if (_childContainer != null) _childContainer.Dispose(); } }
مورد سوم StructureMapServiceScopeFactory میباشد:
internal class StructureMapServiceScopeFactory : IServiceScopeFactory { private IContainer _container; public StructureMapServiceScopeFactory(IContainer container) { _container = container; } public IServiceScope CreateScope() { return new StructureMapServiceScope(_container); } }
مورد بعدی کلاس StructureMapPopulator میباشد. وظیفه این کلاس جمع آوری اطلاعات مربوط به سرویسها میباشد.
internal class StructureMapPopulator { private IContainer _container; public StructureMapPopulator(IContainer container) { _container = container; } public void Populate(IEnumerable<ServiceDescriptor> descriptors) { _container.Configure(c => { c.For<IServiceProvider>().Use(new StructureMapServiceProvider(_container)); c.For<IServiceScopeFactory>().Use<StructureMapServiceScopeFactory>(); foreach (var descriptor in descriptors) { switch (descriptor.Lifetime) { case ServiceLifetime.Singleton: Use(c.For(descriptor.ServiceType).Singleton(), descriptor); break; case ServiceLifetime.Transient: Use(c.For(descriptor.ServiceType), descriptor); break; case ServiceLifetime.Scoped: Use(c.For(descriptor.ServiceType), descriptor); break; } } }); } private static void Use(GenericFamilyExpression expression, ServiceDescriptor descriptor) { if (descriptor.ImplementationFactory != null) { expression.Use(Guid.NewGuid().ToString(), context => { return descriptor.ImplementationFactory(context.GetInstance<IServiceProvider>()); }); } else if (descriptor.ImplementationInstance != null) { expression.Use(descriptor.ImplementationInstance); } else if (descriptor.ImplementationType != null) { expression.Use(descriptor.ImplementationType); } else { throw new InvalidOperationException("IServiceDescriptor is invalid"); } } }
و در آخر کلاس StructureMapRegistration میباشد:
public static class StructureMapRegistration { public static void Populate(this IContainer container, IEnumerable<ServiceDescriptor> descriptors) { var populator = new StructureMapPopulator(container); populator.Populate(descriptors); } }
نهایتاً باید متد ConfigurationServices در کلاس StartUp را اندکی تغییر دهیم.
public IServiceProvider ConfigureServices(IServiceCollection services) { services.AddMvc(); var container = new Container(); container.Configure(configure => { configure.For<ICoreService>().Use<CoreServise>(); }); container.Populate(services); return container.GetInstance<IServiceProvider>(); }
در کد بالا، متد ConfigurationServices به جای آنکه Void برگرداند، نمونهای از اینترفیس IServiceProvider را برمیگرداند. حال اگر برنامه را اجرا کنیم، وابستگیها توسط StructureMap تزریق شده و برنامه بدون هیچ مشکلی اجرا میشود.
Service Layer
نقش لایهی سرویس این است که به عنوان یک مدخل ورودی به برنامه کاربردی عمل کند. در برخی مواقع این لایه را به عنوان لایهی Facade نیز میشناسند. این لایه، دادهها را در قالب یک نوع داده ای قوی (Strongly Typed) به نام View Model، برای لایهی Presentation فراهم میکند. کلاس View Model یک Strongly Typed محسوب میشود که نماهای خاصی از دادهها را که متفاوت از دید یا نمای تجاری آن است، بصورت بهینه ارائه مینماید. در مورد الگوی View Model در مباحث بعدی بیشتر صحبت خواهم کرد.
الگوی Facade یک Interface ساده را به منظور کنترل دسترسی به مجموعه ای از Interfaceها و زیر سیستمهای پیچیده ارائه میکند. در مباحث بعدی در مورد آن بیشتر صحبت خواهم کرد.
کلاسی با نام ProductViewModel را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:
public class ProductViewModel { Public int ProductId {get; set;} public string Name { get; set; } public string Rrp { get; set; } public string SellingPrice { get; set; } public string Discount { get; set; } public string Savings { get; set; } }
برای اینکه کلاینت با لایهی سرویس در تعامل باشد باید از الگوی Request/Response Message استفاده کنیم. بخش Request توسط کلاینت تغذیه میشود و پارامترهای مورد نیاز را فراهم میکند. کلاسی با نام ProductListRequest را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:
using SoCPatterns.Layered.Model; namespace SoCPatterns.Layered.Service { public class ProductListRequest { public CustomerType CustomerType { get; set; } } }
در شی Response نیز بررسی میکنیم که درخواست به درستی انجام شده باشد، دادههای مورد نیاز را برای کلاینت فراهم میکنیم و همچنین در صورت عدم اجرای صحیح درخواست، پیام مناسب را به کلاینت ارسال مینماییم. کلاسی با نام ProductListResponse را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:
public class ProductListResponse { public bool Success { get; set; } public string Message { get; set; } public IList<ProductViewModel> Products { get; set; } }
به منظور تبدیل موجودیت Product به ProductViewModel، به دو متد نیاز داریم، یکی برای تبدیل یک Product و دیگری برای تبدیل لیستی از Product. شما میتوانید این دو متد را به کلاس Product موجود در Domain Model اضافه نمایید، اما این متدها نیاز واقعی منطق تجاری نمیباشند. بنابراین بهترین انتخاب، استفاده از Extension Methodها میباشد که باید برای کلاس Product و در لایهی سرویس ایجاد نمایید. کلاسی با نام ProductMapperExtensionMethods را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:
public static class ProductMapperExtensionMethods { public static ProductViewModel ConvertToProductViewModel(this Model.Product product) { ProductViewModel productViewModel = new ProductViewModel(); productViewModel.ProductId = product.Id; productViewModel.Name = product.Name; productViewModel.RRP = String.Format(“{0:C}”, product.Price.RRP); productViewModel.SellingPrice = String.Format(“{0:C}”, product.Price.SellingPrice); if (product.Price.Discount > 0) productViewModel.Discount = String.Format(“{0:C}”, product.Price.Discount); if (product.Price.Savings < 1 && product.Price.Savings > 0) productViewModel.Savings = product.Price.Savings.ToString(“#%”); return productViewModel; } public static IList<ProductViewModel> ConvertToProductListViewModel( this IList<Model.Product> products) { IList<ProductViewModel> productViewModels = new List<ProductViewModel>(); foreach(Model.Product p in products) { productViewModels.Add(p.ConvertToProductViewModel()); } return productViewModels; } }
حال کلاس ProductService را جهت تعامل با کلاس سرویس موجود در Domain Model و به منظور برگرداندن لیستی از محصولات و تبدیل آن به لیستی از ProductViewModel، ایجاد مینماییم. کلاسی با نام ProductService را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:
public class ProductService { private Model.ProductService _productService; public ProductService(Model.ProductService ProductService) { _productService = ProductService; } public ProductListResponse GetAllProductsFor( ProductListRequest productListRequest) { ProductListResponse productListResponse = new ProductListResponse(); try { IList<Model.Product> productEntities = _productService.GetAllProductsFor(productListRequest.CustomerType); productListResponse.Products = productEntities.ConvertToProductListViewModel(); productListResponse.Success = true; } catch (Exception ex) { // Log the exception… productListResponse.Success = false; // Return a friendly error message productListResponse.Message = ex.Message; } return productListResponse; } }
کلاس Service تمامی خطاها را دریافت نموده و پس از مدیریت خطا، پیغامی مناسب را به کلاینت ارسال میکند. همچنین این لایه محل مناسبی برای Log کردن خطاها میباشد. در اینجا کد نویسی لایه سرویس به پایان رسید و در ادامه به کدنویسی Data Layer میپردازیم.
Data Layer
برای ذخیره سازی محصولات، یک بانک اطلاعاتی با نام Shop01 ایجاد کنید که شامل جدولی به نام Product با ساختار زیر باشد:
برای اینکه کدهای بانک اطلاعاتی را سریعتر تولید کنیم از روش Linq to SQL در Data Layer استفاده میکنم. برای این منظور یک Data Context برای Linq to SQL به این لایه اضافه میکنیم. بر روی پروژه SoCPatterns.Layered.Repository کلیک راست نمایید و گزینه Add > New Item را انتخاب کنید. در پنجره ظاهر شده و از سمت چپ گزینه Data و سپس از سمت راست گزینه Linq to SQL Classes را انتخاب نموده و نام آن را Shop.dbml تعیین نمایید.
از طریق پنجره Server Explorer به پایگاه داده مورد نظر متصل شوید و با عمل Drag & Drop جدول Product را به بخش Design کشیده و رها نمایید.
اگر به یاد داشته باشید، در لایه Model برای برقراری ارتباط با پایگاه داده از یک Interface به نام IProductRepository استفاده نمودیم. حال باید این Interface را پیاده سازی نماییم. کلاسی با نام ProductRepository را با کد زیر به پروژه SoCPatterns.Layered.Repository اضافه کنید:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using SoCPatterns.Layered.Model; namespace SoCPatterns.Layered.Repository { public class ProductRepository : IProductRepository { public IList<Model.Product> FindAll() { var products = from p in new ShopDataContext().Products select new Model.Product { Id = p.ProductId, Name = p.ProductName, Price = new Model.Price(p.Rrp, p.SellingPrice) }; return products.ToList(); } } }
در متد FindAll، با استفاده از دستورات Linq to SQL، لیست تمامی محصولات را برگرداندیم. کدنویسی لایهی Data هم به پایان رسید و در ادامه به کدنویسی لایهی Presentation و UI میپردازیم.
Presentation Layer
به منظور جداسازی منطق نمایش (Presentation) از رابط کاربری (User Interface)، از الگوی Model View Presenter یا همان MVP استفاده میکنیم که در مباحث بعدی با جزئیات بیشتری در مورد آن صحبت خواهم کرد. یک Interface با نام IProductListView را با کد زیر به پروژه SoCPatterns.Layered.Presentation اضافه کنید:
using SoCPatterns.Layered.Service; public interface IProductListView { void Display(IList<ProductViewModel> Products); Model.CustomerType CustomerType { get; } string ErrorMessage { set; } }
این Interface توسط Web Formهای ASP.NET و یا Win Formها باید پیاده سازی شوند. کار با Interfaceها موجب میشود تا تست Viewها به راحتی انجام شوند. کلاسی با نام ProductListPresenter را با کد زیر به پروژه SoCPatterns.Layered.Presentation اضافه کنید:
using SoCPatterns.Layered.Service; namespace SoCPatterns.Layered.Presentation { public class ProductListPresenter { private IProductListView _productListView; private Service.ProductService _productService; public ProductListPresenter(IProductListView ProductListView, Service.ProductService ProductService) { _productService = ProductService; _productListView = ProductListView; } public void Display() { ProductListRequest productListRequest = new ProductListRequest(); productListRequest.CustomerType = _productListView.CustomerType; ProductListResponse productResponse = _productService.GetAllProductsFor(productListRequest); if (productResponse.Success) { _productListView.Display(productResponse.Products); } else { _productListView.ErrorMessage = productResponse.Message; } } } }
کلاس Presenter وظیفهی واکشی داده ها، مدیریت رویدادها و بروزرسانی UI را دارد. در اینجا کدنویسی لایهی Presentation به پایان رسیده است. از مزایای وجود لایهی Presentation این است که تست نویسی مربوط به نمایش دادهها و تعامل بین کاربر و سیستم به سهولت انجام میشود بدون آنکه نگران دشواری Unit Test نویسی Web Formها باشید. حال میتوانید کد نویسی مربوط به UI را انجام دهید که در ادامه به کد نویسی در Win Forms و Web Forms خواهیم پرداخت.