مطالب
ارسال چند درخواست به صورت همزمان به ASP.NET Web API 2.x در AngularJS 1.x

 ما در AngularJs آبجکتی را به نام  q$ داریم که برای اجرای توابع به صورت async مفید است و همچنین در استفاده از مقادیر برگشتی از این درخواست‌ها برای پردازش‌های آینده به ما کمک میکند. برای اطلاعات بیشتر در مورد این سرویس به اینجا مراجعه کنید.

در ادامه ما از تابع ()all از q$ برای ترکیب چند شیء promise داخل یک شیء promise، به منظور صدا زدن چند سرویس به صورت یکجا، استفاده میکنیم.


پیاده سازی ASP.NET Web API


قدم اول : Visual Studio  را بازکنید و یک پروژه empty ASP.NET Web API را مطابق شکل زیر ایجاد کنید.


قدم دوم : در داخل پوشه models، کلاسی را با کدهای زیر ایجاد کنید:
using System.Collections.Generic;
 
namespace NG_Combine_Multiple_Promises.Models
{
    public class Courses
    {
        public int CourseId { get; set; }
        public string CourseName { get; set; }
    }
    public class CourseDatabase : List<Courses>
    {
        public CourseDatabase()
        {
            Add(new Courses() { CourseId=1,CourseName="الکترونیک"});
            Add(new Courses() { CourseId = 2, CourseName = "ریاضی 2" });
            Add(new Courses() { CourseId = 3, CourseName = "طراحی نرم افزار" });
        }
    }
 
    public class Student
    {
        public int StudentId { get; set; }
        public string Name { get; set; }
        public string AcadmicYear { get; set; }
    }
 
    public class StudentDatabase : List<Student>
    {
        public StudentDatabase()
        {
            Add(new Student() {StudentId=101,Name="محمد علوی", 
                               AcadmicYear="اول" });
            Add(new Student() { StudentId = 102, Name = "طاهره موسوی", 
                               AcadmicYear = "دوم" });
            Add(new Student() { StudentId = 103, Name = "علی عباسی", 
                               AcadmicYear = "سوم" });
            Add(new Student() { StudentId = 104, Name = "جواد نوری", 
                               AcadmicYear = "اول" });
            Add(new Student() { StudentId = 105, Name = "محسن خدایی", 
                               AcadmicYear = "دوم" });
            Add(new Student() { StudentId = 106, Name = "علی کاظمی", 
                               AcadmicYear = "سوم" });
            Add(new Student() { StudentId = 107, Name = "زهرا مقدم", 
                               AcadmicYear = "اول" });
            Add(new Student() { StudentId = 108, Name = "لاله فکور", 
                               AcadmicYear = "دوم" });
            Add(new Student() { StudentId = 109, Name = "علی نوروزی", 
                               AcadmicYear = "چهارم" });
        }
    }
     
}

کد بالا شامل موجودیت‌های Courses و Student است و کلاس‌های CourseDatabase و StudentDatabase به ترتیب برای ذخیره این موجودیت‌ها است.


قدم سوم :  بسته‌ی نیوگت Microsoft.AspNet.WebAPi.Cors را با استفاده از NuGet Package Manager، به منظور فعال سازی امکان صدا زدن این وب سرویس از دامنه‌های مختلف، به پروژه اضافه کرده و کد زیر را در کلاس WebApiCofig در پوشه App_Start قرار دهید.

config.EnableCors();


قدم چهارم : دو کنترل از نوع Web API 2 Empty را با نام‌های CourseAPIController و StudentAPIController ایجاد کرده و کدهای زیر را در آنها قرار دهید.

CourseAPIController.cs 

[EnableCors("*","*","*")]
public class CourseAPIController : ApiController
{
    [Route("Courses")]
    public IEnumerable<Courses> Get()
    {
        return new CourseDatabase();
    }
}

StudentAPIController.cs 

[EnableCors("*", "*", "*")]
public class StudentAPIController : ApiController
{
    [Route("Students")]
    public IEnumerable<Student> Get()
    {
        return new StudentDatabase();
    }
}


استفاده از Angular $q.all  

قدم اول : پروژه‌ی جدیدی را از نوع Empty ASP.NET در همین solution اضافه کرده و ارجاعات jQuery, Bootstrap و AngularJS را با استفاده از NuGet Package manager  مانند زیر اضافه کنید:

Install-Package jQuery
Install-Package bootstrap
Install-Package angularjs


قدم دوم : پوشه‌ایی را به نام MyScripts ایجاد کرده و درون آن فایل javascript  زیر را با نام logic.js اضافه کنید: 

var app = angular.module('mymodule', []);

//سرویسی برای بازگرداندن لیست دروس
app.service('courseService', function ($http) {
    this.get = function () {
        var response = $http.get("http://localhost:11696/Courses");
        return response;
    };
});

//سرویسی برای بازگرداندن لیست دانشجویان
app.service('studentService', function ($http) {
    this.get = function () {
        var response = $http.get("http://localhost:11696/Students");
        return response;
    };
});

//تعریف کنترلر
app.controller('ctrl', function ($scope, $q, courseService, studentService) {

    $scope.Courses = [];
    $scope.Students = [];
    loadData();

    /**/
    function loadData() {
        var promiseCourse = courseService.get();
        var promiseStudent = studentService.get();

        $scope.combineResult = $q.all([
            promiseCourse,
            promiseStudent
        ]).then(function (resp) {
            $scope.Courses = resp[0].data;
            $scope.Students = resp[1].data;
        });
    }
});
توضیحات: تابعq$.all لیستی از promise هایی را که  توسط courseService.get و studentService.get بدست آمده‌اند، گرفته و وقتی تمام promise‌ها تمام شدند، قسمت ()then آن اجرا میشود. 

قدم سوم:  فایل html ایی را با نام DataPage در پوشه‌ی view با کد زیر ایجاد کنید: 
<!DOCTYPE html>
<html ng-app="mymodule">
<head>
    <title></title>
    <meta charset="utf-8" />
    <link href="../Content/bootstrap.min.css" rel="stylesheet" />
    <script src="../Scripts/jquery-3.2.1.min.js"></script>
    <script src="../Scripts/angular.min.js"></script>
    <script src="../MyScripts/logic.js"></script>
</head>
<body ng-controller="ctrl">
       <div class="container">
        <h1 class="h1">دروس</h1>
        <table class="table table-striped table-bordered table-condensed">
            <thead>
                <tr>
                    <td class="text-center">کد درس</td>
                    <td class="text-center">نام درس</td>
                </tr>
            </thead>
            <tbody>
                <tr ng-repeat="Course in Courses">
                    <td class="text-center">{{Course.CourseId}}</td>
                    <td class="text-center">{{Course.CourseName}}</td>
                </tr>
            </tbody>
        </table>
        <hr />
        <h1 class="h1">دانشجویان</h1>

        <table class="table table-striped table-bordered table-condensed">
            <thead>
                <tr>
                    <td class="text-center">کد دانشجویی</td>
                    <td class="text-center">نام و نام خانوادگی</td>
                    <td class="text-center">سال دانشجویی</td>
                </tr>
            </thead>
            <tbody>
                <tr ng-repeat="Student in Students">
                    <td class="text-center">{{Student.StudentId}}</td>
                    <td class="text-center">{{Student.Name}}</td>
                    <td class="text-center">{{Student.AcadmicYear}}</td>
                </tr>
            </tbody>
        </table>
    </div>
</body>
</html>

خروجی نهایی پروژه به شکل زیر خواهد بود:

نظرات مطالب
EF Code First #2
لطف کنید سؤالی رو که مطرح می‌کنید در حیطه مطلب جاری عنوان شده باشد و خارج از آن نباشد.
سؤال شما هم بحث کلاینت سروری است و نه بحث کلاینت تنها که EF روی آن مشغول به کار است.
- می‌شود در متد Seed ایی که در بالا توضیح دادم در SQL Server تریگر درست کرد. (که مثلا اگر کاربر دیگری به شرط اینکه این کاربر جزو کاربران تعریف شده در خود SQL Server باشد نه در برنامه شما، اتفاق خاصی رخ دهد. برنامه شما هم بدیهی است باید سرور را مدام چک کند تا از این مساله مطلع شود)- SQL Server مبحثی دارد به نام Service Broker : (^). توسط آن می‌توان از طریق سرور به کلاینت اطلاع رسانی کرد. بازهم خارج است از بحث یک ORM. یا تمام ORMهای موجود. - EF مبحثی دارد به نام Concurrency check که اگر شخصی در شبکه بر روی رکوردی که همین الان شما مشغول به کار هستید، تغییری را ایجاد کرد، به شما اطلاع رسانی کند. (در قسمت‌های بعدی بحث خواهد شد). البته این هم خودکار نیست. لازم است یک رفت و برگشت به سرور انجام شود.- entity framework auditing هم میسر است. خودکار نیست. در همان کلاس Context فوق که از DbContext مشتق می‌شود می‌توان متد تحریف شده public override int SaveChanges را تعریف کرد. در اینجا می‌توان به تمام تغییراتی که قرار است اعمال شوند دسترسی داشت. مثلا آن‌ها را در یک جدول مجزا ثبت کرد. بدیهی است برنامه بعدا نیاز خواهد داشت از این جدول گزارشگیری کند.
اشتراک‌ها
وضعیت بازار کار زبان‌های برنامه‌نویسی در ایران
«... در بین برنامه نویسان سی شارپ، فریمورک ASP.NET، با اختلاف، محبوب‌ترین تکنولوژی برای کارفرماهای داخل ایران است. یعنی کاربرد عمده این زبان در ایران، مربوط به برنامه نویسی وب است ...»


وضعیت بازار کار زبان‌های برنامه‌نویسی در ایران
مطالب
کار با کلیدهای اصلی و خارجی در EF Code first
در حین کار با ارتباطات بین اشیاء و جداول، دانستن یک سری از نکات می‌توانند در کم کردن تعداد رفت و برگشت‌های به سرور مؤثر واقع شده و نهایتا سبب بالا رفتن سرعت برنامه شوند. از این دست می‌توان به یک سری نکات ریز همراه با primary-keys و foreign-keys اشاره کرد که در ادامه به آن‌ها پرداخته خواهد شد.
در ابتدا کلا‌س‌های مدل و Context برنامه را به شکل زیر درنظر بگیرید:
using System;
using System.Data.Entity;
using System.Data.Entity.Migrations;

namespace TestKeys
{
    public class Bill
    {
        public int Id { get; set; }
        public decimal Amount { set; get; }
        public virtual Account Account { get; set; }
    }

    public class Account
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

    public class MyContext : DbContext
    {
        public DbSet<Bill> Bills { get; set; }
        public DbSet<Account> Accounts { get; set; }
    }

    public class Configuration : DbMigrationsConfiguration<MyContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = true;
            AutomaticMigrationDataLossAllowed = true;
        }

        protected override void Seed(MyContext context)
        {
            var a1 = new Account { Name = "a1" };
            var a2 = new Account { Name = "a2" };

            var bill1 = new Bill { Amount = 100, Account = a1 };
            var bill2 = new Bill { Amount = 200, Account = a2 };

            context.Bills.Add(bill1);
            context.Bills.Add(bill2);
            base.Seed(context);
        }
    }

    public static class Test
    {
        public static void Start()
        {
            Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, Configuration>());
            using (var ctx = new MyContext())
            {
                var bill1 = ctx.Bills.Find(1);
                Console.WriteLine(bill1.Amount);
            }
        }
    }
}

در اینجا کلاس صورتحساب و حساب مرتبط به آن تعریف شده‌اند. سپس به کمک DbContext این دو کلاس در معرض دید EF Code first قرار گرفته‌اند و در کلاس Configuration نحوه آغاز بانک اطلاعاتی به همراه تعدادی رکورد اولیه مشخص شده است.


نحوه صحیح مقدار دهی کلید خارجی در EF Code first

تا اینجا یک روال متداول را مشاهده کردیم. اکنون سؤال این است که اگر بخواهیم اولین رکورد صورتحساب ثبت شده توسط متد Seed را ویرایش کرده و مثلا حساب دوم را به آن انتساب دهیم، بهینه‌ترین روش چیست؟ بهینه‌ترین در اینجا منظور روشی است که کمترین تعداد رفت و برگشت به بانک اطلاعاتی را داشته باشد. همچنین فرض کنید در صفحه ویرایش، اطلاعات حساب‌ها در یک Drop down list شامل نام و id آ‌ن‌ها نیز وجود دارد.

روش اول:
using (var ctx = new MyContext())
{
     var bill1 = ctx.Bills.Find(1);
     var a2 = new Account { Id = 2, Name = "a2" };
     bill1.Account = a2;
     ctx.SaveChanges();
}
این روش مخصوص تازه واردهای EF Code first است و آنطور که مدنظر آن‌ها است کار نمی‌کند.
به کمک متد Find اولین رکورد یافت شده و سپس بر اساس اطلاعات drop down در دسترس، یک شیء جدید حساب را ایجاد و سپس تغییرات لازم را اعمال می‌کنیم. در نهایت اطلاعات را هم ذخیره خواهیم کرد.
این روش به ظاهر کار می‌کنه اما حاصل آن ذخیره رکورد حساب سومی با id=3 در بانک اطلاعاتی است و سپس انتساب آن به اولین صورتحساب ثبت شده.
نتیجه: Id را دستی مقدار دهی نکنید؛ تاثیری ندارد. زیرا اطلاعات شیء جدید حساب، در سیستم tracking مرتبط با Context جاری وجود ندارد. بنابراین EF آن‌را به عنوان یک شیء کاملا جدید درنظر خواهد گرفت، صرفنظر از اینکه Id را به چه مقداری تنظیم کرده‌اید.

روش دوم:
using (var ctx = new MyContext())
{
    var bill1 = ctx.Bills.Find(1);
    var a2 = ctx.Accounts.Find(2);
    bill1.Account = a2;
    ctx.SaveChanges();
}
اینبار بر اساس Id دریافت شده از Drop down list، شیء حساب دوم را یافته و به صورتحساب اول انتساب می‌دهیم. این روش درست کار می‌کند؛ اما ... بهینه نیست. فرض کنید شیء جاری دارای 5 کلید خارجی است. آیا باید به ازای هر کلید خارجی یکبار از بانک اطلاعاتی کوئری گرفت؟
مگر نه این است که اطلاعات نهایی ذخیره شده در بانک اطلاعاتی متناظر با حساب صورتحساب جاری، فقط یک عدد بیشتر نیست. بنابراین آیا نمی‌شود ما تنها همین عدد متناظر را بجای دریافت کل شیء به صورتحساب نسبت دهیم؟
پاسخ: بله. می‌شود! ادامه آن در روش سوم.

روش سوم:
در اینجا بهترین کار و یکی از best practices طراحی مدل‌های EF این است که طراحی کلاس صورتحساب را به نحو زیر تغییر دهیم:
public class Bill
{
        public int Id { get; set; }
        public decimal Amount { set; get; }

        [ForeignKey("AccountId")]
        public virtual Account Account { get; set; }
        public int AccountId { set; get; }
}
به این ترتیب هم navigation property که سبب تعریف رابطه بین دو شیء و همچنین lazy loading اطلاعات آن می‌شود پابرجا خواهد بود و هم توسط خاصیت جدید AccountId که توسط ویژگی ForeignKey معرفی شده است، ویرایش اطلاعات آن دقیقا همانند کار با یک بانک اطلاعاتی واقعی خواهد شد.
اینبار به کمک خاصیت متناظر با کلید خارجی جدول، مقدار دهی و ویرایش کلید‌های خارجی یک شیء به سادگی زیر خواهد بود؛ خصوصا بدون نیاز به رفت و برگشت اضافی به بانک اطلاعاتی جهت دریافت اطلاعات متناظر با اشیاء تعریف شده به صورت navigation property :

using (var ctx = new MyContext())
{
    var bill1 = ctx.Bills.Find(1);
    bill1.AccountId = 2;
    ctx.SaveChanges();
}


وارد کردن یک شیء به سیستم Tracking

در قسمت قبل عنوان شد که Id را دستی مقدار دهی نکنید، چون تاثیری ندارد. سؤال: آیا می‌شود این شیء ویژه تعریف شده را به سیستم Tracking وارد کرد؟
پاسخ: بلی. به نحو زیر:
using (var ctx = new MyContext())
{
     var a2 = new Account { Id = 2, Name = "a2_a2" };
     ctx.Entry(a2).State = System.Data.EntityState.Modified;
     ctx.SaveChanges();
}
در اینجا شیء حساب دوم را به صورت دستی و بدون واکشی از بانک اطلاعاتی ایجاد کرده‌ایم. بنابراین از دیدگاه Context جاری هیچ ارتباطی به بانک اطلاعاتی نداشته و یک شیء جدید درنظر گرفته می‌شود (صرفنظر از Id آن). اما می‌توان این وضعیت را تغییر داد. فقط کافی است State آن‌را به نحوی که ملاحظه می‌کنید به Modified تغییر دهیم. اکنون اگر اطلاعات این شیء را ذخیره کنیم، دقیقا حساب با id=2 در بانک اطلاعاتی ویرایش خواهد شد و نه اینکه حساب جدیدی ثبت گردد.

 
نظرات مطالب
معرفی JSON Web Token
باتشکر از پاسخ شما
آیا امکان استفاده از asp.net Identity به همراه jwt وجود دارد؟
  مثلا وقتی در پروژه ای که مانند پروژه  Decision کلاس user به صورت( public class User : IdentityUser ) تعریف شده ایا دیگر  امکان استفاده از jwt برای login هست؟
اگر این امکان وجود ندارد استفاده از کدام پیشنهاد میشود؟
مطالب
API Versioning
فرض کنید امروز یک API را برای استفاده عموم ارائه میدهید. آیا با یک breaking change در منابع شما که باعث تغییر در داده‌های ورودی یا خروجی API شود، باید استفاده کنندگان این API در سیستمی که از آن استفاده کرده‌اند، تغییراتی را اعمال کنند یا خیر؟ جواب خیر می‌باشد؛ اصلی‌ترین استفاده از API Versioning دقیقا برای این منظور است که بدون نگرانی از توسعه‌های بعدی، از ورژن‌های قدیمی API بتوانیم استفاده کنیم. 
در این مقاله با روش‌های مختلف ورژن بندی API آشنا خواهید شد.
سه روش اصلی زیر را میتوان برای این منظور در نظر گرفت:
  1.  URI-based versioning 
  2.  Header-based versioning 
  3.  Media type-based versioning 

پیاده سازی URI-based versioning
حداقل به 3 طریق میتوان این مکانیسم را پیاده کرد:
راه حل اول: اگر از Attribute Routing استفاده نمی‌کنید، با تغییر جزئی در تعاریف مسیریابی خود میتوانید به نتیجه مورد نظر برسید. برای ادامه کار دو ویوومدل و دو کنترلر را که هر کدام مربوط به ورژن خاصی از API ما هستند، پیاده سازی میکنیم:
public class ItemViewModel
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Country { get; set; }
}
public class ItemV2ViewModel : ItemViewModel
{
    public double Price { get; set; }
}
ItemViewModel مربوط به ورژن 1 میباشد.
 public class ItemsController : ApiController   
 {
        [ResponseType(typeof(ItemViewModel))]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country = "Japan" };
            return Ok(viewModel);
        }
    }
 public class ItemsV2Controller : ApiController
    {
        [ResponseType(typeof(ItemV2ViewModel))]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemV2ViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 };
            return Ok(viewModel);
        }
    }
ItemsController مربوط به ورژن 1 میباشد.
اگر قرار باشد از مسیرهای متمرکز استفاده کنیم، کافی است که تغییرات زیر را اعمال کنیم:
config.Routes.MapHttpRoute("ItemsV2", "api/v2/items/{id}", new
{
    controller = "ItemsV2",
    id = RouteParameter.Optional
});
config.Routes.MapHttpRoute("Items", "api/items/{id}", new
{
    controller = "Items",
    id = RouteParameter.Optional
});
config.Routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional }
);
در کد بالا به صراحت برای تک تک کنترلرها مسیریابی متناسب با ورژنی که آن را پشتیبانی میکند، معرفی کرده‌ایم.
نکته: این تنظیمات خاص باید قبل از تنظیمات پیش فرض قرار گیرند.

روش بالا به خوبی کار خواهد کرد ولی آنچنان مطلوب نمیباشد. به همین دلیل یک مسیریابی عمومی و کلی را در نظر میگیریم که شامل ورژن مورد نظر، در قالب یک پارامتر میباشد. در روش جایگزین باید به شکل زیر عمل کنید:
config.Routes.MapHttpRoute(
"defaultVersioned",
"api/v{version}/{controller}/{id}",
new { id = RouteParameter.Optional }, new { version = @"\d+" });

config.Routes.MapHttpRoute(
"DefaultApi",
"api/{controller}/{id}",
new { id = RouteParameter.Optional }
);

با این تنظیمات فعلا به مسیریابی ورژن بندی شده‌ای دست نیافته‌ایم. زیرا فعلا به هیچ طریق به Web API اشاره نکرده‌ایم که به چه صورت از این پارامتر version برای پیدا کردن کنترلر ورژن بندی شده استفاده کند و به همین دلیل این دو مسیریابی نوشته شده در عمل نتیجه یکسانی را خواهند داشت. برای رفع مشکل مطرح شده باید فرآیند پیش فرض انتخاب کنترلر را کمی شخصی سازی کنیم.

IHttpControllerSelector مسئول پیدا کردن کنترلر مربوطه با توجه به درخواست رسیده می‌باشد. شکل زیر مربوط است به مراحل ساخت کنترلر بر اساس درخواست رسیده:

 به جای پیاده سازی مستقیم این اینترفیس، از پیاده سازی کننده پیش فرض موجود (DefaultHttpControllerSelector) اسفتاده کرده و HttpControllerSelector جدید ما از آن ارث بری خواهد کرد.

public class VersionFinder
    {
        private static bool NeedsUriVersioning(HttpRequestMessage request, out string version)
        {
            var routeData = request.GetRouteData();
            if (routeData != null)
            {
                object versionFromRoute;
                if (routeData.Values.TryGetValue(nameof(version), out versionFromRoute))
                {
                    version = versionFromRoute as string;
                    if (!string.IsNullOrWhiteSpace(version))
                    {
                        return true;
                    }
                }
            }
            version = null;
            return false;
        }
        private static int VersionToInt(string versionString)
        {
            int version;
            if (string.IsNullOrEmpty(versionString) || !int.TryParse(versionString, out version))
                return 0;
            return version;
        }
        public static int GetVersionFromRequest(HttpRequestMessage request)
        {
            string version;

            return NeedsUriVersioning(request, out version) ? VersionToInt(version) : 0;
        }
    }

کلاس VersionFinder برای یافتن ورژن رسیده در درخواست جاری  مورد استفاده قرار خواهد گرفت. با استفاده از متد NeedsUriVersioning بررسی صورت می‌گیرد که آیا در این درخواست پارامتری به نام version وجود دارد یا خیر که درصورت موجود بودن، مقدار آن واکشی شده و درون پارامتر out قرار می‌گیرد. در متد GetVersionFromRequest بررسی میشود که اگر خروجی متد NeedsUriVersioning برابر با true باشد، با استفاده از متد VersionToInt مقدار به دست آمده را به int تبدیل کند.

 public class VersionAwareControllerSelector : DefaultHttpControllerSelector
    {
        public VersionAwareControllerSelector(HttpConfiguration configuration) : base(configuration) { }
        public override string GetControllerName(HttpRequestMessage request)
        {
            var controllerName = base.GetControllerName(request);
            var version = VersionFinder.GetVersionFromRequest(request);
                
        return version > 0 ? GetVersionedControllerName(request, controllerName, version) : controllerName;
        }
        private string GetVersionedControllerName(HttpRequestMessage request, string baseControllerName,
        int version)
        {
            var versionControllerName = $"{baseControllerName}v{version}";
            HttpControllerDescriptor descriptor;
            if (GetControllerMapping().TryGetValue(versionControllerName, out descriptor))
            {
                return versionControllerName;
            }

            throw new HttpResponseException(request.CreateErrorResponse(
            HttpStatusCode.NotFound,
                $"No HTTP resource was found that matches the URI {request.RequestUri} and version number {version}"));
        }
    }

متد  GetControllerName وظیفه بازگشت دادن نام کنترلر را برعهده دارد. ما نیز با لغو رفتار پیش فرض این متد و تهیه نام ورژن بندی شده کنترلر و معرفی این پیاده سازی از  IHttpControllerSelector به شکل زیر میتوانیم به Web API بگوییم که به چه صورت از پارامتر version موجود در درخواست استفاده کند. 

config.Services.Replace(typeof(IHttpControllerSelector), new VersionAwareControllerSelector(config));

حال با اجرای برنامه به نتیجه زیر خواهیم رسید: 

راه حل دوم: برای زمانیکه Attribute Routing مورد استفاده شما است میتوان به راحتی با تعریف قالب‌های مناسب مسیریابی، API ورژن بندی شده را ارائه دهید.

[RoutePrefix("api/v1/Items")]
    public class ItemsController : ApiController
    {
        [ResponseType(typeof(ItemViewModel))]
        [Route("{id:int}")]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country =        "Japan" };
            return Ok(viewModel);
        }
    }


 [RoutePrefix("api/V2/Items")]
    public class ItemsV2Controller : ApiController
    {
        [ResponseType(typeof(ItemV2ViewModel))]
        [Route("{id:int}")]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemV2ViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 };
            return Ok(viewModel);
        }
    }

اگر توجه کرده باشید در مثال ما، نام‌های کنترلر‌ها متفاوت از هم میباشند. اگر بجای در نظر گرفتن نام‌های مختلف برای یک کنترلر در ورژن‌های مختلف، آن را با یک نام یکسان درون namespace‌های مختلف احاطه کنیم یا حتی آنها را درون Class Library‌های جدا نگهداری کنیم، به مشکل "یافت شدن چندین کنترلر که با درخواست جاری مطابقت دارند" برخواهیم خورد. برای حل این موضوع به راه حل سوم توجه کنید. 

راه حل سوم: استفاده از یک NamespaceControllerSelector که پیاده سازی دیگری از اینترفیس IHttpControllerSelector میباشد. فرض بر این است که قالب پروژه به شکل زیر می‌باشد:

کار با پیاده سازی اینترفیس IHttpRouteConstraint آغاز میشود:

public class VersionConstraint : IHttpRouteConstraint
{
    public VersionConstraint(string allowedVersion)
    {
        AllowedVersion = allowedVersion.ToLowerInvariant();
    }
    public string AllowedVersion { get; private set; }

    public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName,
    IDictionary<string, object> values, HttpRouteDirection routeDirection)
    {
        object value;
        if (values.TryGetValue(parameterName, out value) && value != null)
        {
            return AllowedVersion.Equals(value.ToString().ToLowerInvariant());
        }
        return false;
    }
}

کلاس بالا در واقع برای اعمال محدودیت خاصی که در ادامه توضیح داده میشود، پیاده سازی شده است.

متد Match آن وظیفه چک کردن برابری مقدار کلید parameterName موجود در درخواست را با مقدار allowedVersion ای که API از آن پشتیبانی میکند، برعهده دارد. با استفاده از این Constraint مشخص کرده‌ایم که دقیقا چه زمانی باید Route نوشته شده انتخاب شود.

 به روش استفاده از این Constraint توجه کنید:

namespace UriBasedVersioning.Namespace.Controllers.V1
{
    using Models.V1;

RoutePrefix("api/{version:VersionConstraint(v1)}/items")]
public class ItemsController : ApiController
    {
        [ResponseType(typeof(ItemViewModel))]
        [Route("{id}")]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country = "Japan" };
            return Ok(viewModel);
        }
    }
}
namespace UriBasedVersioning.Namespace.Controllers.V2
{
    using Models.V2;


RoutePrefix("api/{version:VersionConstraint(v2)}/items")]
public class ItemsController : ApiController
    {
        [ResponseType(typeof(ItemViewModel))]
        [Route("{id}")]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 };
            return Ok(viewModel);
        }
    }
}

با توجه به کد بالا می‌توان دلیل استفاده از VersionConstraint را هم درک کرد؛ از آنجایی که ما دو Route شبیه به هم داریم، لذا باید مشخص کنیم که در چه شرایطی و کدام یک از این Route‌ها انتخاب شود. خوب، اگر برنامه را اجرا کرده و یکی از API‌های بالا را تست کنید، با خطا مواجه خواهید شد؛ زیرا فعلا این Constraint به سیستم Web API معرفی نشده است. تنظیمات زیر را انجام دهید:

var constraintsResolver = new DefaultInlineConstraintResolver();
            constraintsResolver.ConstraintMap.Add(nameof(VersionConstraint), typeof
            (VersionConstraint));
config.MapHttpAttributeRoutes(constraintsResolver);

مرحله بعدی کار، پیاده سازی IHttpControllerSelector می‌باشد:

  public class NamespaceControllerSelector : IHttpControllerSelector
    {
        private readonly HttpConfiguration _configuration;
        private readonly Lazy<Dictionary<string, HttpControllerDescriptor>> _controllers;
        public NamespaceControllerSelector(HttpConfiguration config)
        {
            _configuration = config;
            _controllers = new Lazy<Dictionary<string,
                HttpControllerDescriptor>>(InitializeControllerDictionary);
        }
        public HttpControllerDescriptor SelectController(HttpRequestMessage request)
        {
            var routeData = request.GetRouteData();
            if (routeData == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }

            var controllerName = GetControllerName(routeData);
            if (controllerName == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
            var version = GetVersion(routeData);
            if (version == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
            var controllerKey = string.Format(CultureInfo.InvariantCulture, "{0}.{1}",
                version, controllerName);
            HttpControllerDescriptor controllerDescriptor;
            if (_controllers.Value.TryGetValue(controllerKey, out controllerDescriptor))
            {
                return controllerDescriptor;
            }
            throw new HttpResponseException(HttpStatusCode.NotFound);
        }
        public IDictionary<string, HttpControllerDescriptor> GetControllerMapping()
        {
            return _controllers.Value;
        }
        private Dictionary<string, HttpControllerDescriptor> InitializeControllerDictionary()
        {
            var dictionary = new Dictionary<string,
                HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase);
            var assembliesResolver = _configuration.Services.GetAssembliesResolver();
            var controllersResolver = _configuration.Services.GetHttpControllerTypeResolver();
            var controllerTypes = controllersResolver.GetControllerTypes(assembliesResolver);
            foreach (var controllerType in controllerTypes)
            {
                var segments = controllerType.Namespace.Split(Type.Delimiter);
                var controllerName =
                    controllerType.Name.Remove(controllerType.Name.Length -
                                               DefaultHttpControllerSelector.ControllerSuffix.Length);
                var controllerKey = string.Format(CultureInfo.InvariantCulture, "{0}.{1}",
                    segments[segments.Length - 1], controllerName);

                if (!dictionary.Keys.Contains(controllerKey))
                {
                    dictionary[controllerKey] = new HttpControllerDescriptor(_configuration,
                        controllerType.Name,
                        controllerType);
                }
            }
            return dictionary;
        }
        private static T GetRouteVariable<T>(IHttpRouteData routeData, string name)
        {
            object result;
            if (routeData.Values.TryGetValue(name, out result))
            {
                return (T)result;
            }
            return default(T);
        }
        private static string GetControllerName(IHttpRouteData routeData)
        {
            var subroute = routeData.GetSubRoutes().FirstOrDefault();
            var dataTokenValue = subroute?.Route.DataTokens.First().Value;
            var controllerName =
                ((HttpActionDescriptor[])dataTokenValue)?.First().ControllerDescriptor.ControllerName.Replace("Controller", string.Empty);
            return controllerName;
        }
        private static string GetVersion(IHttpRouteData routeData)
        {
            var subRouteData = routeData.GetSubRoutes().FirstOrDefault();
            return subRouteData == null ? null : GetRouteVariable<string>(subRouteData, "version");
        }
    }

سورس اصلی کلاس بالا از این آدرس قابل دریافت است. در تکه کد بالا بخشی که مربوط به چک کردن تکراری بودن آدرس میباشد، برای ساده سازی کار حذف شده است. ولی نکته‌ی مربوط به SubRoutes که برای واکشی مقادیر پارامترهای مرتبط با Attribute Routing می‌باشد، اضافه شده است. روال کار به این صورت است که ابتدا RouteData موجود در درخواست را واکشی کرده و با استفاده از متدهای GetControllerName و GetVersion، پارامتر‌های controller و version را جستجو میکنیم. بعد با استفاده از مقادیر به دست آمده، controllerKey را تشکیل داده و درون کنترلر‌های موجود در برنامه به دنبال کنترلر مورد نظر خواهیم گشت.

کارکرد متد InitializeControllerDictionary :

همانطور که میدانید به صورت پیش‌فرض Web API توجهی به فضای نام مرتبط با کنترلر‌ها ندارد. از طرفی برای پیاده سازی اینترفیس IHttpControllerSelector نیاز است متدی با امضای زیر را داشته باشیم:

public IDictionary<string, HttpControllerDescriptor> GetControllerMapping()

لذا در کلاس پیاده سازی شده، خصوصیتی به نام ‎‎_controllers را که از به صورت Lazy و از نوع بازگشتی متد بالا می‌باشد، تعریف کرده‌ایم. متد InitializeControllerDictionary کار آماده سازی داده‌های مورد نیاز خصوصیت ‎‎_controllers می‌باشد. به این صورت که تمام کنترلر‌های موجود در برنامه را واکشی کرده و این بار کلید‌های مربوط به دیکشنری را با استفاده از نام کنترلر و آخرین سگمنت فضای نام آن، تولید و درون دیکشنری مورد نظر ذخیره کرده‌ایم.

حال تنظیمات زیر را اعمال کنید:

 public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            var constraintsResolver = new DefaultInlineConstraintResolver();
            constraintsResolver.ConstraintMap.Add(nameof(VersionConstraint), typeof
            (VersionConstraint));

            config.MapHttpAttributeRoutes(constraintsResolver);

            config.Services.Replace(typeof(IHttpControllerSelector), new NamespaceControllerSelector(config));

        }
    }

این بار برنامه را اجرا کرده و API‌های مورد نظر را تست کنید؛ بله بدون مشکل کار خواهد کرد.

نکته تکمیلی: سورس مذکور در سایت کدپلکس برای حالتی که از Centralized Routes استفاده میکنید آماده شده است. روش مذکور در این مقاله هم  فقط قسمت Duplicate Routes آن را کم دارد که میتوانید اضافه کنید. پیاده سازی دیگری را از این راه حل هم میتوانید داشته باشید.

پیاده سازی Header-based versioning  

در این روش کافی است هدری را برای دریافت ورژن API درخواستی مشخص کرده باشید. برای مثال هدری به نام api-version، که استفاده کننده از این طریق، ورژن درخواستی خود را اعلام میکند. همانطور که قبلا اشاره کردیم، با استفاده از Constraint‌ها دست شما خیلی باز خواهد بود. برای مثال این بار به جای واکشی پارامتر version از RouteData، کد همان قسمت را برای واکشی آن از هدر درخواستی تغییر خواهیم داد:
public class HeaderVersionConstraint : IHttpRouteConstraint
    {
        private const string VersionHeaderName = "api-version";

        public HeaderVersionConstraint(int allowedVersion)
        {
            AllowedVersion = allowedVersion;
        }

        public int AllowedVersion { get; }

        public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName,
            IDictionary<string, object> values,
            HttpRouteDirection routeDirection)
        {
            if (!request.Headers.Contains(VersionHeaderName)) return false;

            var version = request.Headers.GetValues(VersionHeaderName).FirstOrDefault();

            return VersionToInt(version) == AllowedVersion;
        }
        private static int VersionToInt(string versionString)
        {
            int version;
            if (string.IsNullOrEmpty(versionString) || !int.TryParse(versionString, out version))
                return 0;
            return version;
        }
    }
این بار در متد Match، به دنبال هدر خاصی به نام api-version می‌گردیم و مقدار آن را با AllowdVersion مقایسه میکنیم تا مشخص کنیم که آیا این Route نوشته شده برای ادامه کار واکشی کنترلر مورد نظر استفاده شود یا خیر. در ادامه یک RouteFactoryAttribute شخصی سازی شده را به منظور معرفی این محدودیت تعریف شده، در نظر میگیریم:
public sealed class HeaderVersionedRouteAttribute : RouteFactoryAttribute
    {
        public HeaderVersionedRouteAttribute(string template) : base(template)
        {
            Order = -1;
        }

        public int Version { get; set; }

        public override IDictionary<string, object> Constraints => new HttpRouteValueDictionary
        {
            {"", new HeaderVersionConstraint(Version)}
        };
    }

با استفاده از خصوصیت Constraints، توانستیم محدودیت پیاده سازی شده خود را به سیستم مسیریابی معرفی کنیم. توجه داشته باشید که این بار هیچ پارامتری در Uri تحت عنوان version را نداریم و از این جهت به صراحت کلیدی برای آن مشخص نکرده ایم (‎‎‎‎‏‏‎‎{"", new HeaderVersionConstraint(Version)} ‎‎ ).
کنترلر‌های ما به شکل زیر خواهند بود:
    [RoutePrefix("api/items")]
    public class ItemsController : ApiController
    {
        [ResponseType(typeof(ItemViewModel))]
        [HeaderVersionedRoute("{id}", Version = 1)]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country = "Japan" };
            return Ok(viewModel);
        }
    }
  [RoutePrefix("api/items")]
    public class ItemsV2Controller : ApiController
    {
        [ResponseType(typeof(ItemV2ViewModel))]
        [HeaderVersionedRoute("{id}", Version = 2)]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemV2ViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 };
            return Ok(viewModel);
        }
    }

این بار از VersionedRoute برای اعمال مسیر یابی Attribute-based استفاده کرده ایم و به صراحت ورژن هر کدام را با استفاده از خصوصیت Version آن مشخص کرده‌ایم. نتیجه:


پیاده سازی Media type-based versioning
قرار است ورژن API مورد نظر را که استفاده کننده درخواست میدهد، از دل درخواست‌هایی به فرم زیر واکشی کنیم:
GET /api/Items HTTP 1.1
Accept: application/vnd.mediatype.versioning-v1+json
GET /api/Items HTTP 1.1
Accept: application/vnd.mediatype.versioning-v2+json
در این روش دست ما کمی بازتر است چرا که میتوانیم بر اساس فرمت درخواستی نیز تصمیماتی را برای ارائه ورژن خاصی از API  داشته باشیم. روال کار شبیه به روش قبلی با پیاده سازی یک Constraint و یک RouteFactoryAttribute انجام خواهد گرفت.
 public class MediaTypeVersionConstraint : IHttpRouteConstraint
    {
        private const string VersionMediaType = "vnd.mediatype.versioning";

        public MediaTypeVersionConstraint(int allowedVersion)
        {
            AllowedVersion = allowedVersion;
        }

        public int AllowedVersion { get; }

        public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values,
            HttpRouteDirection routeDirection)
        {
            if (!request.Headers.Accept.Any()) return false;

            var acceptHeaderVersion =
                request.Headers.Accept.FirstOrDefault(x => x.MediaType.Contains(VersionMediaType));

            //Accept: application/vnd.mediatype.versioning-v1+json
            if (acceptHeaderVersion == null || !acceptHeaderVersion.MediaType.Contains("-v") ||
                !acceptHeaderVersion.MediaType.Contains("+"))
                return false;

            var version = acceptHeaderVersion.MediaType.Between("-v", "+");
            return VersionToInt(version)==AllowedVersion;
        }
}
در متد Match کلاس بالا به دنبال مدیا تایپ مشخصی هستیم که با هدر Accept برای ما ارسال میشود. برای آشنایی با فرمت ارسال Media Type‌ها میتوانید به اینجا مراجعه کنید. کاری که در اینجا انجام شده‌است، یافتن ورژن ارسالی توسط استفاده کننده می‌باشد که آن را با فرمت خاصی که نشان داده شد و مابین (v-) و (+) قرار داده، ارسال میشود. ادامه کار به مانند روش Header-based میباشد و از مطرح کردن آن در مقاله خودداری میکنیم.
نتیجه به شکل زیر خواهد بود:


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.


مطالب
جلوگیری از ورود نام Area های یکسان، در هنگام درج اطلاعات در برنامه‌های ASP.NET MVC 5x
در وب‌سایتی مثل آپارات، چنین آدرسی aparat.com/reporting به منزله‌ی آدرس دهی به کانال شخصیِ فردی است. حال اگر وب‌سایت ما نیز چنین سیستم آدرس دهی را داشته باشد و همچنین پیشتر یک Area با نام Reporting را نیز داشته باشیم، توسط چنین آدرسی (درحالت پیش فرض) به آن Area دسترسی خواهیم داشت:
mysite.com/reporting
حال اگر یکی از کاربران هنگام ساخت کانالی جدید (برای سناریوی بالا)، بخواهد آدرس کانالش Reporting باشد، با توجه به اینکه هم مسیر دسترسی به Area گزارشات (Reporting) و هم مسیر دسترسی به کانال این شخص از طریق Url بالا است، قطعا به مشکل خواهیم خورد.
برای رفع این مشکل میتوان یک فایل xml، txt و ... درست کرد و نام تمامی Area‌‌ها را در آن فایل ثبت کرد و بعد، هنگام ثبت کانال جدید (برای سناریوی بالا) توسط کاربر، فایل مذکور را خوانده و در صورتیکه نام آدرس وارد شده معادل یکی از Area‌‌های سایتمان بود و در لیست Area‌‌های از پیش ثبت شده در آن فایل قرار داشت، پیغام لازم را به کاربر نشان می‌دهیم و از ثبت و یا ویرایش اطلاعات، جلوگیری می‌کنیم.
روش فوق به درستی کار می‌کند و مشکلی ندارد، اما ضعف آن این است که به صورت دستی این عملیات باید انجام شود و در صورتیکه یک Area جدید اضافه شود، باید آن فایل ویرایش شود. اما می‌توان با استفاده از یک Attribute، این کار را انجام و تمامی عملیات را به صورت داینامیک انجام داد.
برای شروع، یک مدل برای کانال و یک منبع داده را برای آن در نظر می‌گیریم:
using System.ComponentModel.DataAnnotations;

namespace SampleProject.Models
{
    public class Channel
    {
        public string ChannelTitle { get; set; }
        [Required]
        public string ChannelUrl { get; set; }
    }
}
using System.Collections.Generic;

namespace SampleProject.Models
{
    public static class ChannelDataSource
    {
        static ChannelDataSource() => Channels = new List<Channel>();
        public static List<Channel> Channels { get; private set; }
        public static void Add(Channel channel) => Channels.Add(channel);
    }
}
منبع داده، شامل یک خاصیت است که لیست تمامی کانال‌های از قبل اضافه شده را بر می‌گرداند و یک متد افزودن که به این لیست، یک کانال را اضافه می‌کند.
حال یک کنترلر به نام Channel را اضافه می‌کنیم:
using SampleProject.Models;
using System.Linq;
using System.Web.Mvc;

namespace SampleProject.Controllers
{
    public class ChannelController : Controller
    {
        // GET: Channel
        public ActionResult Index()
        {
            var channels = ChannelDataSource.Channels;
            return View(channels);
        }

        public ActionResult Channel(string channelUrl)
        {
            if (string.IsNullOrWhiteSpace(channelUrl))
            {
                return new HttpNotFoundResult("channel not found!");
            }
            var channel = ChannelDataSource.Channels.SingleOrDefault(ch => ch.ChannelUrl == channelUrl.ToLower());
            if (channel == null)
            {
                return new HttpNotFoundResult("channel not found!");
            }
            return View(channel);
        }

        public ActionResult Create() => View();

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Create(Channel channel)
        {
            if (!ModelState.IsValid)
            {
                ModelState.AddModelError(string.Empty, "Please check your inputs!");
                return View(channel);
            }
            ChannelDataSource.Add(channel);
            TempData["Message"] = "Channel added successfully!";
            return RedirectToAction(nameof(Index));
        }
    }
}
در اکشن Index، لیستی از تمامی کانال‌های موجود را نمایش می‌دهیم. در اکشن Channel، آدرسی را که وارد شده است، در منبع داده به دنبال آن می‌گردیم و یک ویوو با Template جزئیات (Details)، از مدل کانال را به کاربر نمایش می‌دهیم؛ در غیر اینصورت صفحه 404 را نمایش می‌دهیم. در اکشن‌های Create، صفحه افزودن را به کاربر نمایش داده و در آن یکی اکشن، عمل افزودن را در صورتیکه اطلاعات وارد شده صحیح باشند، انجام می‌دهیم.
با توجه به اینکه میخواهیم سیستم مسیر دهی سایت برای کانال‌ها تغییر کند، فایل RouteConfig در پوشه‌ی App_Start را به شکل ذیل تغییر می‌دهیم:
using System.Web.Mvc;
using System.Web.Routing;

namespace SampleProject
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                name: "ChannelUrls",
                url: "{channelurl}",
                defaults: new { controller = "Channel", action = "Channel", id = UrlParameter.Optional }
            );

            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Channel", action = "Index", id = UrlParameter.Optional }
            );
        }
    }
}
در مسیر دهی بالا اگر "نام سایت، اسلش، نام کانال" را وارد کند اولین سیستم مسیریابی فعال می‌شود و او را به اکشن Channel کنترلر Channel، راهنمایی می‌کند.
حال برای اینکه هنگام ساخت کانال جدید، نام تکراری یکی از Area‌ها را وارد نکند، به این ترتیب عمل می‌کنیم:
ابتدا یک متد کمکی را نوشته که لیست Area‌‌های پروژه‌مان را برگشت دهد ( + ):
using System.Collections.Generic;
using System.Linq;
using System.Web.Routing;

namespace SampleProject.Models
{
    public static class Utility
    {
        public static List<string> GetAllAreaNames()
        {
            var areaNames = RouteTable.Routes.OfType<Route>()
                            .Where(d => d.DataTokens != null)
                            .Where(d=> d.DataTokens.ContainsKey("area"))
                            .Select(r => r.DataTokens["area"].ToString().ToLower())
                            .ToList();
            return areaNames;
        }
    }
}
و بعد Attribute مورد نظر را ایجاد میکنیم:
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web.Mvc;
using SampleProject.Models;

namespace SampleProject.CustomValidators
{
    public class CheckForAreaExisting : ValidationAttribute, IClientValidatable
    {
        public List<string> AreaNames
        {
            get
            {
                return Utility.GetAllAreaNames();
            }
        }
        public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
        {
            var rule = new ModelClientValidationRule
            {
                ValidationType = "checkforareaexisting",
                ErrorMessage = FormatErrorMessage(metadata.GetDisplayName())
            };
            rule.ValidationParameters.Add("areanames", string.Join(",", AreaNames));
            yield return rule;
        }

        public override bool IsValid(object value)
        {
            if (value != null)
            {
                return Utility.GetAllAreaNames()
                   .SingleOrDefault(area => area == value.ToString().ToLower()) == null;
            }
            return true;
        }
    }
}
در کلاس بالا توسط متد IsValid بررسی میکنیم که آیا مقدار وارد شده ( Channel Url ) با یکی از نام‌های Area‌‌های پروژه‌مان تطابق دارد یا خیر، که اگر این چنین بود، مقدار false برگشت داده می‌شود.
توسط واسط IClientValidatable و متود GetClientValidationRules کارهای اعتبارسنجی سمت کلاینت را نیز انجام می‌دهیم ( + ). مقدار خاصیت ValidationType نام متدی است که در سمت کلاینت این کار را انجام می‌دهد. مقدار خاصیت ValidationParameters، مقداری است که به سمت کلاینت به عنوان param فرستاده می‌شود تا از آن جهت اینکه آیا مقدار وارد شده توسط کاربر، یکی از Area‌های سایت هست یا خیر، استفاده کرد. در اینجا نام Area‌‌‌ها را با یک رشته و با یک جداکننده، توسط این خاصیت به سمت کلاینت می‌فرستیم. 
حال در سمت کلاینت یک فایل Js را با نام CustomValidation و محتوای زیر ایجاد می‌کنیم:
jQuery.validator.addMethod("checkforareaexisting",
    function (value, element, param) {
        var isValueOneOfTheAreaNames = $.inArray(value.toLowerCase(), param.areaNames) === -1;
        return isValueOneOfTheAreaNames;
    });

$.validator.unobtrusive.adapters.add('checkforareaexisting', ['areanames'], function (options) {
    options.rules['checkforareaexisting'] = { areaNames: options.params.areanames.split(',') };
    options.messages['checkforareaexisting'] = options.message;
});
در بخش اول، نام متد که در بالا (Attribute) به آن اشاره شده است آمده است، و بعد بررسی می‌کنیم که آیا مقدار آمده توسط کاربر، یکی از نام‌های Area‌‌های موجود سایت است یا خیر که اگر این طور باشد، false برگشت داده می‌شود و پیغام خطا به کاربر نمایش داده می‌شود. در بخش Onubtrusive توسط پارامتری که در Attribute برای فرستادن نام Area‌ها نوشته بودیم (areanames)، نام‌های Area‌ها را می‌گیریم و بعد آن را Split و به Rule انتساب می‌دهیم و ErrorMessage ـی را که به خاصیت ChannelUrl مدلمان نسبت می‌دهیم، به عنوان پیغام خطا در نظر می‌گیریم.
فایل‌های Js در Layout باید به این صورت باشند:
<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>_Layout</title>
    <style>

    </style>
</head>
<body>
    <div>
        @RenderBody()
    </div>
    <script src="~/Scripts/Jquery.js"></script>
    <script src="~/Scripts/jquery.validate.min.js"></script>
    <script src="~/Scripts/jquery.validate.unobtrusive.min.js"></script>
    <script src="~/Scripts/CustomValidation.js"></script>
</body>
</html>

حال کافی است به خاصیت ChannelUrl مدلمان این Attribute را نسبت دهیم:
using SampleProject.CustomValidators;
using System.ComponentModel.DataAnnotations;

namespace SampleProject.Models
{
    public class Channel
    {
        public string ChannelTitle { get; set; }
        [Required]
        [CheckForAreaExisting(ErrorMessage = "You can't use this url for your channel!")]
        public string ChannelUrl { get; set; }
    }
}
اکنون نوبت آزمایش برنامه است. کافی است که یک یا چند Area جدید را با نام‌های متفاوت، اضافه کنید و الان اگر به صفحه افزودن کانال مراجعه کنید و نام یکی از Area‌‌های سایت را در قسمت Channel Url وارد کنید، پیغام خطا نمایش داده می‌شود.
نکته: در این حالت اسامی تمامی Area‌‌های سایت به کلاینت ارسال می‌شود. اگر از این بابت احساس رضایت نمی‌کنید، میتوانید از خاصیت Remote توکار MVC بهره ببرید.
برای اینکار این اکشن را به کنترلر Channel اضافه می‌کنیم:
[HttpPost]
public ActionResult CheckForAreaExisting(string channelUrl)
{
    var isValueOneOfTheAreaNames = Utility.GetAllAreaNames()
                                   .SingleOrDefault(area => area == channelUrl.ToLower()) == null;
    return Json(isValueOneOfTheAreaNames);
}  
و بعد مدل نیز به این صورت تغییر می‌کند:
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;

namespace SampleProject.Models
{
    public class Channel
    {
        public string ChannelTitle { get; set; }
        [Required]
        [Remote("CheckForAreaExisting", "Channel",
            ErrorMessage = "You can't use this url for your channel!",
            HttpMethod = "Post")]
        public string ChannelUrl { get; set; }
    }
}
به این ترتیب هر بار درخواستی به سمت سرور ارسال و طی آن بررسی می‌شود که مقدار وارد شده یکی از Area‌‌‌‌های سایت هست یا خیر؟ بدیهی است که در این حالت، دیگر نیازی به واسط IClientValidatable در کلاس CheckForAreaExisting موجود در پوشه CustomValidators وجود ندارد.
مطالب دوره‌ها
آشنایی با AOP Interceptors
در حین استفاده از Interceptors، کار مداخله و تحت نظر قرار دادن قسمت‌های مختلف کدها، توسط کامپوننت‌های خارجی صورت خواهد گرفت. این کامپوننت‌های خارجی، به صورت پویا، تزئین کننده‌هایی را جهت محصور سازی قسمت‌های مختلف کدهای شما تولید می‌کنند. این‌ها، بسته به توانایی‌هایی که دارند، در زمان اجرا و یا حتی در زمان کامپایل نیز قابل تنظیم می‌باشند.


ابزارهایی جهت تولید AOP Interceptors

متداول‌ترین کامپوننت‌های خارجی که جهت تولید AOP Interceptors مورد استفاده قرار می‌گیرند، همان IOC Containers معروف هستند مانند StructureMap، Ninject، MS Unity و غیره.
سایر ابزارهای تولید AOP Interceptors، از روش تولید Dynamic proxies بهره می‌گیرند. به این ترتیب مزین کننده‌هایی پویا، در زمان اجرا، کدهای شما را محصور خواهند کرد. (نمونه‌ای از آن‌را شاید در حین کار با ORMهای مختلف دیده باشید).


نگاهی به فرآیند Interception

زمانیکه از یک IOC Container در کدهای خود استفاده می‌کنید، مراحلی چند رخ خواهند داد:
الف) کد فراخوان، از IOC Container، یک شیء مشخص را درخواست می‌کند. عموما اینکار با درخواست یک اینترفیس صورت می‌گیرد؛ هرچند محدودیتی نیز وجود نداشته و امکان درخواست یک کلاس از نوعی مشخص نیز وجود دارد.
ب) در ادامه IOC Container به لیست اشیاء قابل ارائه توسط خود نگاه کرده و در صورت وجود، وهله سازی شیء درخواست شده را انجام و نهایتا شیء مطلوب را بازگشت خواهد داد.
ج) سپس، کد فراخوان، وهله دریافتی را مورد پردازش قرار داده و شروع به استفاده از متدها و خواص آن خواهد نمود.

اکنون با اضافه کردن Interception به این پروسه، چند مرحله دیگر نیز در این بین به آن اضافه خواهند شد:
الف) در اینجا نیز در ابتدا کد فراخوان، درخواست وهله‌ای را بر اساس اینترفیسی خاص به IOC Container ارائه می‌دهد.
ب) IOC Container نیز سعی در وهله سازی درخواست رسیده بر اساس تنظیمات اولیه خود می‌کند.
ج) اما در این حالت IOC Container تشخیص می‌دهد، نوعی که باید بازگشت دهد، علاوه بر وهله سازی، نیاز به مزین سازی توسط  Aspects و پیاده سازی Interceptors را نیز دارد. بنابراین نوع مورد انتظار را در صورت وجود، به یک Dynamic Proxy، بجای بازگشت مستقیم به فراخوان ارائه می‌دهد.
د) در  ادامه Dynamic Proxy، نوع مورد انتظار را توسط Interceptors محصور کرده و به فراخوان بازگشت می‌دهد.
ه) اکنون فراخوان، در حین استفاده از امکانات شیء وهله سازی شده، به صورت خودکار مراحل مختلف اجرای یک Aspect را که در قسمت قبل بررسی شدند، سبب خواهد شد.


نحوه ایجاد Interceptors

برای ایجاد یک Interceptor دو مرحله باید انجام شود:
الف) پیاده سازی یک اینترفیس
ب) اتصال آن به کدهای اصلی برنامه

در ادامه قصد داریم از یک IOC Container معروف به نام StructureMap در یک برنامه کنسول استفاده کنیم. برای دریافت آن نیاز است دستور پاورشل ذیل را در کنسول نوگت ویژوال استودیو فراخوانی کنید:
 PM> Install-Package structuremap
پس از آن یک برنامه کنسول جدید را ایجاد کنید. (هدف از استفاده از این نوع پروژه خاص، توضیح جزئیات یک فناوری، بدون درگیر شدن با لایه UI است)
البته باید دقت داشت که برای استفاده از StructureMap نیاز است به خواص پروژه مراجعه و سپس حالت Client profile را به Full profile تغییر داد تا برنامه قابل کامپایل باشد.
using System;
using StructureMap;

namespace AOP00
{
    public interface IMyType
    {
        void DoSomething(string data, int i);
    }

    public class MyType : IMyType
    {
        public void DoSomething(string data, int i)
        {
            Console.WriteLine("DoSomething({0}, {1});", data, i);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            ObjectFactory.Initialize(x =>
            {
                x.For<IMyType>().Use<MyType>();
            });

            var myType = ObjectFactory.GetInstance<IMyType>();
            myType.DoSomething("Test", 1);
        }
    }
}
اکنون کدهای این برنامه را به نحو فوق تغییر دهید.
در اینجا یک اینترفیس نمونه و پیاده سازی آن‌را ملاحظه می‌کنید. همچنین نحوه آغاز تنظیمات StructureMap و نحوه دریافت یک وهله متناظر با IMyType نیز بیان شده‌اند.
نکته‌ی مهمی که در اینجا باید به آن دقت داشت، وضعیت شیء myType حین فراخوانی متد myType.DoSomething است. شیء myType در اینجا، دقیقا یک وهله‌ی متداول از کلاس myType است و هیچگونه دخل و تصرفی در نحوه اجرای آن صورت نگرفته است.
خوب! تا اینجای کار را احتمالا پیشتر نیز دیده بودید. در ادامه قصد داریم یک Interceptor را طراحی و مراحل چهارگانه اجرای یک Aspect را در اینجا بررسی کنیم.

در ادامه نیاز خواهیم داشت تا یک Dynamic proxy را نیز مورد استفاده قرار دهیم؛ از این جهت که StructureMap تنها دارای Interceptorهای وهله سازی اطلاعات است و نه Method Interceptor. برای دسترسی به Method Interceptors نیاز به یک Dynamic proxy نیز می‌باشد. در اینجا از Castle.Core استفاده خواهیم کرد:
 PM> Install-Package Castle.Core
برای دریافت آن تنها کافی است دستور پاور شل فوق را در خط فرمان کنسول پاورشل نوگت در VS.NET اجرا کنید.
سپس کلاس ذیل را به پروژه جاری اضافه کنید:
using System;
using Castle.DynamicProxy;

namespace AOP00
{
    public class LoggingInterceptor : IInterceptor
    {
        public void Intercept(IInvocation invocation)
        {
            try
            {
                Console.WriteLine("Logging On Start.");

                invocation.Proceed(); //فراخوانی متد اصلی در اینجا صورت می‌گیرد

                Console.WriteLine("Logging On Success.");
            }
            catch (Exception ex)
            {
                Console.WriteLine("Logging On Error.");
                throw;
            }
            finally
            {
                Console.WriteLine("Logging On Exit.");
            }
        }
    }
}
در کلاس فوق کار Method Interception توسط امکانات Castle.Core انجام شده است. این کلاس باید اینترفیس IInterceptor را پیاده سازی کند. در این متد سطر invocation.Proceed دقیقا معادل فراخوانی متد مورد نظر است. مراحل چهارگانه شروع، پایان، خطا و موفقیت نیز توسط try/catch/finally پیاده سازی شده‌اند.

اکنون برای معرفی این کلاس به برنامه کافی است سطرهای ذیل را اندکی ویرایش کنیم:
        static void Main(string[] args)
        {
            ObjectFactory.Initialize(x =>
            {
                var dynamicProxy = new ProxyGenerator();
                x.For<IMyType>().Use<MyType>();
                x.For<IMyType>().EnrichAllWith(myTypeInterface => dynamicProxy.CreateInterfaceProxyWithTarget(myTypeInterface, new LoggingInterceptor()));
            });

            var myType = ObjectFactory.GetInstance<IMyType>();
            myType.DoSomething("Test", 1);
        }
در اینجا تنها سطر EnrichAllWith آن جدید است. ابتدا یک پروکسی پویا تولید شده است. سپس این پروکسی پویا کار دخالت و تحت نظر قرار دادن اجرای متدهای اینترفیس IMyType را عهده دار خواهد شد.
برای مثال اکنون با فراخوانی متد myType.DoSomething، ابتدا کنترل برنامه به پروکسی پویای تشکیل شده توسط Castle.Core منتقل می‌شود. در اینجا هنوز هم متد DoSomething فراخوانی نشده است. ابتدا وارد بدنه متد public void Intercept خواهیم شد. سپس سطر invocation.Proceed، فراخوانی واقعی متد DoSomething اصلی را انجام می‌دهد. در ادامه باز هم فرصت داریم تا مراحل موفقیت، خطا یا خروج را لاگ کنیم.
تنها زمانیکه کار متد public void Intercept به پایان می‌رسد، سطر پس از فراخوانی متد  myType.DoSomething اجرا خواهد شد.
در این حالت اگر برنامه را اجرا کنیم، چنین خروجی را نمایش می‌دهد:
 Logging On Start.
DoSomething(Test, 1);
Logging On Success.
Logging On Exit.
بنابراین در اینجا نحوه دخالت و تحت نظر قرار دادن اجرای متدهای یک کلاس عمومی خاص را ملاحظه می‌کنید. برای اینکه کنترل کامل را در دست بگیریم، کلاس پروکسی پویا وارد عمل شده و اینجا است که این کلاس پروکسی تصمیم می‌گیرد چه زمانی باید فراخوانی واقعی متد مورد نظر انجام شود.
برای اینکه فراخوانی قسمت On Error را نیز ملاحظه کنید، یک استثنای عمدی را در متد DoSomething قرار داده و مجددا برنامه را اجرا کنید.
مطالب
Blazor 5x - قسمت 31 - احراز هویت و اعتبارسنجی کاربران Blazor WASM - بخش 1 - انجام تنظیمات اولیه
در قسمت قبل، امکان سفارش یک اتاق را به همراه پرداخت آنلاین آن، به برنامه‌ی Blazor WASM این سری اضافه کردیم؛ اما ... هویت کاربری که مشغول انجام اینکار است، هنوز مشخص نیست. بنابراین در این قسمت می‌خواهیم مباحثی مانند ثبت نام و ورود به سیستم را تکمیل کنیم. البته مقدمات سمت سرور این بحث را در مطلب «Blazor 5x - قسمت 25 - تهیه API مخصوص Blazor WASM - بخش 2 - تامین پایه‌ی اعتبارسنجی و احراز هویت»، بررسی کردیم.


ارائه‌ی AuthenticationState به تمام کامپوننت‌های یک برنامه‌ی Blazor WASM

در قسمت 22، با مفاهیم CascadingAuthenticationState و AuthorizeRouteView در برنامه‌های Blazor Server آشنا شدیم؛ این مفاهیم در اینجا نیز یکی هستند:
- کامپوننت CascadingAuthenticationState سبب می‌شود AuthenticationState (لیستی از Claims کاربر)، به تمام کامپوننت‌های یک برنامه‌یBlazor  ارسال شود. در مورد پارامترهای آبشاری، در قسمت نهم این سری بیشتر بحث شد و هدف از آن، ارائه‌ی یکسری اطلاعات، به تمام زیر کامپوننت‌های یک کامپوننت والد است؛ بدون اینکه نیاز باشد مدام این پارامترها را در هر زیر کامپوننتی، تعریف و تنظیم کنیم. همینقدر که آن‌ها را در بالاترین سطح سلسله مراتب کامپوننت‌های تعریف شده تعریف کردیم، در تمام زیر کامپوننت‌های آن نیز در دسترس خواهند بود.
- کامپوننت AuthorizeRouteView امکان محدود کردن دسترسی به صفحات مختلف برنامه‌ی Blazor را بر اساس وضعیت اعتبارسنجی و نقش‌های کاربر جاری، میسر می‌کند.

روش اعمال این دو کامپوننت نیز یکی است و نیاز به ویرایش فایل BlazorWasm.Client\App.razor در اینجا وجود دارد:
<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                <Authorizing>
                    <p>Please wait, we are authorizing the user.</p>
                </Authorizing>
                <NotAuthorized>
                    <p>Not Authorized</p>
                </NotAuthorized>
            </AuthorizeRouteView>
        </Found>
        <NotFound>
                <LayoutView Layout="@typeof(MainLayout)">
                    <p>Sorry, there's nothing at this address.</p>
                </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>
کامپوننت CascadingAuthenticationState، اطلاعات AuthenticationState را در اختیار تمام کامپوننت‌های برنامه قرار می‌دهد و کامپوننت AuthorizeRouteView، امکان نمایش یا عدم نمایش قسمتی از صفحه را بر اساس وضعیت لاگین شخص و یا محدود کردن دسترسی بر اساس نقش‌ها، میسر می‌کند.


مشکل! برخلاف برنامه‌های Blazor Server، برنامه‌های Blazor WASM به صورت پیش‌فرض به همراه تامین کننده‌ی توکار AuthenticationState نیستند.

اگر سری Blazor جاری را از ابتدا دنبال کرده باشید، کاربرد AuthenticationState را در برنامه‌های Blazor Server، در قسمت‌های 21 تا 23، پیشتر مشاهده کرده‌اید. همان مفاهیم، در برنامه‌های Blazor WASM هم قابل استفاده هستند؛ البته در اینجا به علت جدا بودن برنامه‌ی سمت کلاینت WASM Blazor، از برنامه‌ی Web API سمت سرور، نیاز است یک تامین کننده‌ی سمت کلاینت AuthenticationState را بر اساس JSON Web Token دریافتی از سرور، تشکیل دهیم و برخلاف برنامه‌های Blazor Server، این مورد به صورت خودکار مدیریت نمی‌شود و با ASP.NET Core Identity سمت سروری که JWT تولید می‌کند، یکپارچه نیست.
بنابراین در اینجا نیاز است یک AuthenticationStateProvider سفارشی سمت کلاینت را تهیه کنیم که بر اساس JWT دریافتی از Web API کار می‌کند. به همین جهت در ابتدا یک JWT Parser را طراحی می‌کنیم که رشته‌ی JWT دریافتی از سرور را تبدیل به <IEnumerable<Claim می‌کند. سپس این لیست را در اختیار یک AuthenticationStateProvider سفارشی قرار می‌دهیم تا اطلاعات مورد نیاز کامپوننت‌های CascadingAuthenticationState و AuthorizeRouteView تامین شده و قابل استفاده شوند.


نیاز به یک JWT Parser

در قسمت 25، پس از لاگین موفق، یک JWT تولید می‌شود که به همراه قسمتی از مشخصات کاربر است. می‌توان محتوای این توکن را در سایت jwt.io مورد بررسی قرار داد که برای نمونه به این خروجی می‌رسیم و حاوی claims تعریف شده‌است:
{
  "iss": "https://localhost:5001/",
  "iat": 1616396383,
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "vahid@dntips.ir",
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress": "vahid@dntips.ir",
  "Id": "582855fb-e95b-45ab-b349-5e9f7de40c0c",
  "DisplayName": "vahid@dntips.ir",
  "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Admin",
  "nbf": 1616396383,
  "exp": 1616397583,
  "aud": "Any"
}
بنابراین برای استخراج این claims در سمت کلاینت، نیاز به یک JWT Parser داریم که نمونه‌ای از آن می‌تواند به صورت زیر باشد:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text.Json;

namespace BlazorWasm.Client.Utils
{
    /// <summary>
    /// From the Steve Sanderson’s Mission Control project:
    /// https://github.com/SteveSandersonMS/presentation-2019-06-NDCOslo/blob/master/demos/MissionControl/MissionControl.Client/Util/ServiceExtensions.cs
    /// </summary>
    public static class JwtParser
    {
        public static IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
        {
            var claims = new List<Claim>();
            var payload = jwt.Split('.')[1];

            var jsonBytes = ParseBase64WithoutPadding(payload);

            var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);
            claims.AddRange(keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString())));
            return claims;
        }

        private static byte[] ParseBase64WithoutPadding(string base64)
        {
            switch (base64.Length % 4)
            {
                case 2: base64 += "=="; break;
                case 3: base64 += "="; break;
            }
            return Convert.FromBase64String(base64);
        }
    }
}
که آن‌را در فایل BlazorWasm.Client\Utils\JwtParser.cs برنامه‌ی کلاینت ذخیره خواهیم کرد. متد ParseClaimsFromJwt فوق، رشته‌ی JWT تولیدی حاصل از لاگین موفق در سمت Web API را دریافت کرده و تبدیل به لیستی از Claimها می‌کند.


تامین AuthenticationState مبتنی بر JWT مخصوص برنامه‌‌های Blazor WASM

پس از داشتن لیست Claims دریافتی از یک رشته‌ی JWT، اکنون می‌توان آن‌را تبدیل به یک AuthenticationStateProvider کرد. برای اینکار در ابتدا نیاز است بسته‌ی نیوگت Microsoft.AspNetCore.Components.Authorization را به برنامه‌ی کلاینت اضافه کرد:
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="5.0.4" />
  </ItemGroup>
</Project>
سپس سرویس سفارشی AuthStateProvider خود را به پوشه‌ی Services برنامه اضافه می‌کنیم و متد GetAuthenticationStateAsync کلاس پایه‌ی AuthenticationStateProvider استاندارد را به نحو زیر بازنویسی و سفارشی سازی می‌کنیم:
namespace BlazorWasm.Client.Services
{
    public class AuthStateProvider : AuthenticationStateProvider
    {
        private readonly HttpClient _httpClient;
        private readonly ILocalStorageService _localStorage;

        public AuthStateProvider(HttpClient httpClient, ILocalStorageService localStorage)
        {
            _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
            _localStorage = localStorage ?? throw new ArgumentNullException(nameof(localStorage));
        }

        public override async Task<AuthenticationState> GetAuthenticationStateAsync()
        {
            var token = await _localStorage.GetItemAsync<string>(ConstantKeys.LocalToken);
            if (token == null)
            {
                return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
            }

            _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", token);
            return new AuthenticationState(
                        new ClaimsPrincipal(
                            new ClaimsIdentity(JwtParser.ParseClaimsFromJwt(token), "jwtAuthType")
                        )
                    );
        }
    }
}
- اگر با برنامه‌های سمت کلاینت React و یا Angular پیشتر کار کرده باشید، منطق این کلاس بسیار آشنا به نظر می‌رسد. در این برنامه‌ها، مفهومی به نام Interceptor وجود دارد که توسط آن به صورت خودکار، هدر JWT را به تمام درخواست‌های ارسالی به سمت سرور، اضافه می‌کنند تا از تکرار این قطعه کد خاص، جلوگیری شود. علت اینجا است که برای دسترسی به منابع محافظت شده‌ی سمت سرور، نیاز است هدر ویژه‌ای را به نام "Authorization" که با مقدار "bearer jwt" تشکیل می‌شود، به ازای هر درخواست ارسالی به سمت سرور نیز ارسال کرد؛ تا تنظیمات ویژه‌ی AddJwtBearer که در قسمت 25 در کلاس آغازین برنامه‌ی Web API انجام دادیم، این هدر مورد انتظار را دریافت کرده و پردازش کند و در نتیجه‌ی آن، شیء this.User، در اکشن متدهای کنترلرها تشکیل شده و قابل استفاده شود.
در اینجا نیز مقدار دهی خودکار httpClient.DefaultRequestHeaders.Authorization را مشاهده می‌کنید که مقدار token خودش را از Local Storage دریافت می‌کند که کلید متناظر با آن‌را در پروژه‌ی BlazorServer.Common به صورت زیر تعریف کرده‌ایم:
namespace BlazorServer.Common
{
    public static class ConstantKeys
    {
        // ...
        public const string LocalToken = "JWT Token";
    }
}
به این ترتیب دیگر نیازی نخواهد بود در تمام سرویس‌های برنامه‌ی WASM که با HttpClient کار می‌کنند، مدام سطر مقدار دهی httpClient.DefaultRequestHeaders.Authorization را تکرار کنیم.
- همچنین در اینجا به کمک متد JwtParser.ParseClaimsFromJwt که در ابتدای بحث تهیه کردیم، لیست Claims دریافتی از JWT ارسالی از سمت سرور را تبدیل به یک AuthenticationState قابل استفاده‌ی در برنامه‌ی Blazor WASM کرده‌ایم.

پس از تعریف یک AuthenticationStateProvider سفارشی، باید آن‌را به همراه Authorization، به سیستم تزریق وابستگی‌های برنامه در فایل Program.cs اضافه کرد:
namespace BlazorWasm.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            // ...

            builder.Services.AddAuthorizationCore();
            builder.Services.AddScoped<AuthenticationStateProvider, AuthStateProvider>();

            // ...
        }
    }
}
و برای سهولت استفاده‌ی از امکانات اعتبارسنجی فوق در کامپوننت‌های برنامه، فضای نام زیر را به فایل BlazorWasm.Client\_Imports.razor اضافه می‌کنیم:
@using Microsoft.AspNetCore.Components.Authorization


تهیه‌ی سرویسی برای کار با AccountController

اکنون می‌خواهیم در برنامه‌ی سمت کلاینت، از AccountController سمت سرور که آن‌را در قسمت 25 این سری تهیه کردیم، استفاده کنیم. بنابراین نیاز است سرویس زیر را تدارک دید که امکان لاگین، ثبت نام و خروج از سیستم را در سمت کلاینت میسر می‌کند:
namespace BlazorWasm.Client.Services
{
    public interface IClientAuthenticationService
    {
        Task<AuthenticationResponseDTO> LoginAsync(AuthenticationDTO userFromAuthentication);
        Task LogoutAsync();
        Task<RegisterationResponseDTO> RegisterUserAsync(UserRequestDTO userForRegisteration);
    }
}
و به صورت زیر پیاده سازی می‌شود:
namespace BlazorWasm.Client.Services
{
    public class ClientAuthenticationService : IClientAuthenticationService
    {
        private readonly HttpClient _client;
        private readonly ILocalStorageService _localStorage;

        public ClientAuthenticationService(HttpClient client, ILocalStorageService localStorage)
        {
            _client = client;
            _localStorage = localStorage;
        }

        public async Task<AuthenticationResponseDTO> LoginAsync(AuthenticationDTO userFromAuthentication)
        {
            var response = await _client.PostAsJsonAsync("api/account/signin", userFromAuthentication);
            var responseContent = await response.Content.ReadAsStringAsync();
            var result = JsonSerializer.Deserialize<AuthenticationResponseDTO>(responseContent);

            if (response.IsSuccessStatusCode)
            {
                await _localStorage.SetItemAsync(ConstantKeys.LocalToken, result.Token);
                await _localStorage.SetItemAsync(ConstantKeys.LocalUserDetails, result.UserDTO);
                _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", result.Token);
                return new AuthenticationResponseDTO { IsAuthSuccessful = true };
            }
            else
            {
                return result;
            }
        }

        public async Task LogoutAsync()
        {
            await _localStorage.RemoveItemAsync(ConstantKeys.LocalToken);
            await _localStorage.RemoveItemAsync(ConstantKeys.LocalUserDetails);
            _client.DefaultRequestHeaders.Authorization = null;
        }

        public async Task<RegisterationResponseDTO> RegisterUserAsync(UserRequestDTO userForRegisteration)
        {
            var response = await _client.PostAsJsonAsync("api/account/signup", userForRegisteration);
            var responseContent = await response.Content.ReadAsStringAsync();
            var result = JsonSerializer.Deserialize<RegisterationResponseDTO>(responseContent);

            if (response.IsSuccessStatusCode)
            {
                return new RegisterationResponseDTO { IsRegisterationSuccessful = true };
            }
            else
            {
                return result;
            }
        }
    }
}
که به نحو زیر به سیستم تزریق وابستگی‌های برنامه معرفی می‌شود:
namespace BlazorWasm.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            // ...
            builder.Services.AddScoped<IClientAuthenticationService, ClientAuthenticationService>();
            // ...
        }
    }
}
توضیحات:
- متد LoginAsync، مشخصات لاگین کاربر را به سمت اکشن متد api/account/signin ارسال کرده و در صورت موفقیت این عملیات، اصل توکن دریافتی را به همراه مشخصاتی از کاربر، در Local Storage ذخیره سازی می‌کند. این مورد سبب خواهد شد تا بتوان به مشخصات کاربر در صفحات دیگر و سرویس‌های دیگری مانند AuthStateProvider ای که تهیه کردیم، دسترسی پیدا کنیم. به علاوه مزیت دیگر کار با Local Storage، مواجه شدن با حالت‌هایی مانند Refresh کامل صفحه و برنامه، توسط کاربر است. در یک چنین حالتی، برنامه از نو بارگذاری مجدد می‌شود و به این ترتیب می‌توان به مشخصات کاربر لاگین کرده، به سادگی دسترسی یافت و مجددا قسمت‌های مختلف برنامه را به او نشان داد. نمونه‌ی دیگر این سناریو، بازگشت از درگاه پرداخت بانکی است. در این حالت نیز از یک سرویس سمت سرور دیگر، کاربر به سمت برنامه‌ی کلاینت، Redirect کامل خواهد شد که در اصل اتفاقی که رخ می‌دهد، با Refresh کامل صفحه یکی است. در این حالت نیز باید بتوان کاربری را که از درگاه بانکی ثالث، به سمت برنامه‌ی کلاینت از نو بارگذاری شده، هدایت شده، بلافاصله تشخیص داد.

- اگر برنامه، Refresh کامل نشود، نیازی به Local Storage نخواهد بود؛ از این لحاظ که در برنامه‌های سمت کلاینت Blazor، طول عمر تمام سرویس‌ها، صرفنظر از نوع طول عمری که برای آن‌ها مشخص می‌کنیم، همواره Singleton هستند (ماخذ).
Blazor WebAssembly apps don't currently have a concept of DI scopes. Scoped-registered services behave like Singleton services.
بنابراین می‌توان یک سرویس سراسری توکن را تهیه و به سادگی آن‌را در تمام قسمت‌های برنامه تزریق کرد. این روش هرچند کار می‌کند، اما همانطور که عنوان شد، به Refresh کامل صفحه حساس است. اگر برنامه در مرورگر کاربر Refresh نشود، تا زمانیکه باز است، سرویس‌های در اصل Singleton تعریف شده‌ی در آن نیز در تمام قسمت‌های برنامه در دسترس هستند؛ اما با Refresh کامل صفحه، به علت بارگذاری مجدد کل برنامه، سرویس‌های آن نیز از نو، وهله سازی خواهند شد که سبب از دست رفتن حالت قبلی آن‌ها می‌شود. بنابراین نیاز به روشی داریم که بتوانیم حالت قبلی برنامه را در زمان راه اندازی اولیه‌ی آن بازیابی کنیم و یکی از روش‌های استاندارد اینکار، استفاده از Local Storage خود مرورگر است که مستقل از برنامه و توسط مرورگر مدیریت می‌شود.

- در متد LoginAsync، علاوه بر ثبت اطلاعات کاربر در Local Storage، مقدار دهی client.DefaultRequestHeaders.Authorization را نیز ملاحظه می‌کنید. همانطور که عنوان شد، سرویس‌های Blazor WASM در اصل دارای طول عمر Singleton هستند. بنابراین تنظیم این هدر در اینجا، بر روی تمام سرویس‌های HttpClient تزریق شده‌ی به سایر سرویس‌های برنامه نیز بلافاصله تاثیرگذار خواهد بود.

- متد LogoutAsync، اطلاعاتی را که در حین لاگین موفق در Local Storage ذخیره کردیم، حذف کرده و همچنین client.DefaultRequestHeaders.Authorization را نیز نال می‌کند تا دیگر اطلاعات لاگین شخص قابل بازیابی نبوده و مورد استفاده قرار نگیرد. همین مقدار برای شکست پردازش درخواست‌های ارسالی به منابع محافظت شده‌ی سمت سرور کفایت می‌کند.

- متد RegisterUserAsync، مشخصات کاربر در حال ثبت نام را به سمت اکشن متد api/account/signup ارسال می‌کند که سبب افزوده شدن کاربر جدیدی به بانک اطلاعاتی برنامه و سیستم ASP.NET Core Identity خواهد شد.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-31.zip
مطالب
معرفی و راهنمایی جهت انتخاب پلتفرم‌های توسعه‌ی رابط کاربری جدید مایکروسافت
اگر به تکنولوژی‌های شرکت مایکروسافت علاقمند باشید و اخبار آن را دنبال کرده باشید قطعا در جریان هستید که علاوه بر تکنولوژی‌های قدیمی (WPF, UWP, Xamarin) تکنولوژی‌های جدیدی (Project Reunion, Maui, WinUI, Uno, Xaml Island) نیز بصورت همزمان در حال توسعه هستند. اکثر این تکنولوژی‌ها شبیه و نزدیک به هم هستند و برای کسی که تازه کار باشد ممکن است دچار سردرگمی شود و چون بصورت همزمان در حال توسعه می‌باشند ممکن سوالاتی برای شما پیش بیاید. در این مطلب هر کدام از این تکنولوژی‌ها را معرفی کرده و در انتخاب صحیح به شما کمک خواهیم کرد.

ساخت برنامه با WPF
به کمک تکنولوژی WPF میتوانیم نرم افزارهای دسکتاپ را توسعه دهیم. WPF همچنان پشتیبانی میشود و در سال‌های اخیر بصورت متن باز نیز منتشر شده‌است. اگر نیاز دارید که برنامه شما در ویندوز‌های 7 تا ویندوز 11 اجرا شود، میتوانید از WPF استفاده کنید. لازم به ذکر است که برنامه‌های WPF به عنوان Win32 یا Desktop نیز شناخته میشوند.

ساخت برنامه با UWP
UWP بعد از WPF و با انتشار ویندوز 10 معرفی شد. علت انتشار، هماهنگی برنامه‌ها با سیستم عامل ویندوز 10 و امنیت بیشتر بود. به‌طور فنی برنامه‌ای که بصورت UWP ساخته میشود، همان WPF است؛ با این تفاوت که داخل SandBox اجرا میشود و با محیط خارج ارتباطی ندارد. بدلیل مسائل امنیتی، بسیاری از کارهای ساده و مهم در UWP غیرممکن (البته راه حل‌هایی نیز وجود دارد) می‌باشد و نیاز به دسترسی کاربر دارد. به عنوان مثال، API‌های system.Io.File یا Process قابل استفاده نمی‌باشند.
نرم افزارهایی که با UWP ساخته میشوند فقط بر روی ویندوز 10 به بالا قابلیت اجرایی دارند و توزیع آن از طریق استور مایکروسافت امکان پذیر است. در صورت نیاز به توزیع دستی (فایل نصبی)، توسعه دهنده باید فایل نصبی را بصورت دیجیتال، امضاء کند که دردسر‌های خودش را دارد.

ساخت برنامه با Xamarin
اگر نیاز دارید که برای سیستم عامل اندروید و مک برنامه بنویسید، زامارین میتواند به شما کمک کند.

ساخت برنامه با WinUI
بعد از معرفی UWP نیاز به یک فریمورک رابط کاربری قوی جهت جذب کاربران به سمت UWP احساس شد. در نتیجه مایکروسافت فریمورک WinUI را ایجاد کرد. WinUI در 2 نسخه در حال توسعه می‌باشد:

WinUI 2.X
این نسخه از WinUI فقط قابلیت استفاده در برنامه‌های مبتنی بر UWP را دارد. اخیرا نسخه 2.6 آن منتشر شده که شامل تغییرات بصری عظیمی می‌باشد. لازم به ذکر است که ویندوز 11 که اخیرا معرفی شد، بر پایه WinUI 2.6 ایجاد شده است.

WinUI 3.X
این نسخه از WinUI قابلیت استفاده در پلتفرم‌های دیگر را محیا می‌کند و هم اکنون بصورت پیش نمایش است و بر پایه WinUI 2.5 می‌باشد. در 3 ماهه آخر سال 2021 تمامی استایل‌ها بر پایه نسخه 2.6 خواهد بود.
 

پلتفرم Uno
پلتفرم اونو توسط مایکروسافت ایجاد نشده، اما توسط آن پشتیبانی میشود. شما به کمک پلتفرم اونو میتوانید به کمک WinUI 3، برنامه‌های خود را در ویندوز 7 (به کمک موتور رندر Skia ) تا ویندوز 11، لینوکس (به کمک Skia)، مک و حتی موبایل اجرا کنید.

پلتفرم Maui
مائویی در واقع نسل بعدی زامارین می‌باشد و بصورت تک پروژه‌ای ایجاد شده‌است. در زامارین شما برای هر پلتفرم (ویندوز، اندروید، مک و...) یک پروژه جداگانه داشتید، اما در مائویی فقط یک پروژه واحد وجود دارد. پس اگر نیاز دارید که برای گوشی‌های همراه برنامه نویسی کنید، میتوانید از مائویی استفاده کنید. لازم به ذکر است به کمک مائویی میتوانید برای لینوکس و مک هم برنامه ایجاد کنید. اما بدلیل وجود WinUI در سایر پلتفرم‌ها، بهتر است از مائویی فقط برای ایجاد برنامه‌های موبایل استفاده کنید.
 

پلتفرم Project Reunion
اخیرا نام این پروژه به Windows App SDK تغییر یافته‌است. به کمک این پروژه میتوانید از WinUI 3 در برنامه‌های WPF و سایر تکنولوژی‌های Desktop استفاده کنید و کل برنامه خود را مدرن کنید. لازم به ذکر است که برنامه‌های ساخته شده توسط Reunion فقط در ویندوز 10 به بالا اجرا میشوند. در حال حاضر جهت اجرای برنامه نیاز هست که برنامه بصورت MSIX پکیج بشود. در نسخه 1.0 که تا چند ماه آینده منتشر خواهد شد، نیازی به پکیج کردن نخواهد بود.

پلتفرم Xaml Island
این پلتفرم در واقع پلی است که میتوانید از کنترل‌های UWP یا WinUI در برنامه‌های دسکتاپ (WPF) استفاده کنید. تفاوت این پلتفرم با Reunion در این است که شما فقط میتوانید بخشی از برنامه خود را مدرن کنید و قسمت‌های مدرن شده در ویندوز‌های پایین‌تر از ویندوز 10، کار نخواهند کرد. اما در Reunion تمام بخش‌های برنامه شما مدرن خواهد شد.

سخن آخر اینکه اگر نیاز به برنامه‌های موبایل دارید، بهتر است از مائویی استفاده کنید بدلیل اینکه:
  • نسل بعدی زامارین است
  • از دات نت 6 به بالا استفاده می‌کند
  • روان، سریع و انعطاف پذیر است
  • خطاهای بسیار کمتری دارد
  • مخصوص موبایل طراحی شده است
  • تجربه بیشتری نسبت به سایر پلتفرم‌ها دارد

اگر نیاز به اجرای برنامه بصورت کراس پلتفرم دارید (ویندوز/لینوکس/مک) بهتر است از Uno استفاده کنید بدلیل اینکه:
  • مخصوص کراس پلتفرم طراحی شده
  • از WinUI 3 استفاده می‌کند
  • برای ویندوز 10 به بالا از تکنولوژی UWP و برای ویندوز 7 و لینوکس از Skia استفاده می‌کند

اگر نیاز دارید برنامه شما فقط در ویندوز 10 به بالا اجرا شود بهتر است از Project Reunion استفاده کنید بدلیل اینکه:
  • از WinUI 3 استفاده می‌کند
  • تمام ویژگی‌های UWP را دارد
  • محدودیت‌های UWP را ندارد
  • بصورت Full Trust اجرا میشود
  • پیچیدگی‌های UWP را ندارد
  • از پلتفرم WPF برای اجرا استفاده می‌کند

اگر نیاز دارید که برنامه شما در ویندوز 7 به بالا اجرا شود و در ویندوز 10 ظاهر مدرن‌تری به خود بگیرد بهتر است از Xaml Island استفاده کنید بدلیل اینکه:
  • فقط بخشی از برنامه را مدرن می‌کند
  • قسمت‌های مدرن شده در نسخه‌های قبل ویندوز 10 اجرا نمیشود