مطالب
Minimal API's در دات نت 6 - قسمت چهارم - تدارک مقدمات معماری بر اساس ویژگی‌ها
در معماری vertical slices با features سر و کار داریم؛ برای مثال برنامه‌ی ما دو ویژگی نویسنده‌ها و بلاگ‌ها را خواهد داشت و هر ویژگی، کاملا متکی به خود است. برای نمونه هر ویژگی می‌تواند به همراه یک ماژول باشد که به صورت مستقل، تمام سرویس‌ها، endpoints و میان‌افزارهای مورد نیاز خودش را ثبت می‌کند. در این معماری، تمام قسمت‌های مورد نیاز جهت کارکرد یک ویژگی، در کنار هم قرار می‌گیرند تا یافتن آن‌ها و درک ارتباطات بین آن‌ها ساده‌تر شود.


تعریف ساختار ماژول‌های ویژگی‌های معماری vertical slices

برای تعریف ساختار ماژولی که کار ثبت تمام نیازمندی‌های یک ویژگی را انجام می‌دهد، مانند ثبت سرویس‌ها، endpoints و میان‌افزارها، ابتدا پوشه‌ای به نام Contracts را به پروژه‌ی Api اضافه می‌کنیم؛ با این اینترفیس:
namespace MinimalBlog.Api.Contracts;

public interface IModule
{
    IEndpointRouteBuilder RegisterEndpoints(IEndpointRouteBuilder endpoints);
}


ثبت خودکار ماژول‌های برنامه در ابتدای اجرای آن

پس از تعریف این قرارداد، اکنون می‌خواهیم هر ماژولی که در برنامه، اینترفیس فوق را پیاده سازی می‌کند، در ابتدای اجرای برنامه به صورت خودکار، یافت شده و اطلاعات آن به سیستم اضافه شود. برای این منظور متدهای الحاقی زیر را تعریف می‌کنیم:
public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddApplicationServices(this IServiceCollection services,
        WebApplicationBuilder builder)
    {
        // ...

        builder.Services.AddAllModules(typeof(Program));

        return services;
    }

    private static void AddAllModules(this IServiceCollection services, params Type[] types)
    {
        // Using the `Scrutor` to add all of the application's modules at once.
        services.Scan(scan =>
            scan.FromAssembliesOf(types)
                .AddClasses(classes => classes.AssignableTo<IModule>())
                .AsImplementedInterfaces()
                .WithSingletonLifetime());
    }
}
این کلاس ساختار ساده‌ای دارد؛ ابتدا در متد AddAllModules، اسمبلی جاری جهت یافتن کلاس‌های پیاده سازی کننده‌ی اینترفیس IModule، اسکن می‌شود؛ با استفاده از کتابخانه‌ی Scrutor.
سپس کلاس‌های ثبت شده که هم اکنون جزئی از سیستم تزریق وابستگی‌های برنامه هستند، یافت شده و متد RegisterEndpoints آن‌ها فراخوانی می‌شوند تا دیگر نیازی نباشد به ازای هر ماژول، یکبار ثبت دستی این موارد در کلاس Program انجام شود.
using MinimalBlog.Api.Contracts;

namespace MinimalBlog.Api.Extensions;

public static class ModuleExtensions
{
    public static WebApplication RegisterEndpoints(this WebApplication app)
    {
        if (app == null)
        {
            throw new ArgumentNullException(nameof(app));
        }

        var modules = app.Services.GetServices<IModule>();
        foreach (var module in modules)
        {
            module.RegisterEndpoints(app);
        }

        return app;
    }
}
بنابراین در ادامه به کلاس Program مراجعه کرد و متد عمومی کلاس فوق را در آن به صورت app.RegisterEndpoints فراخوانی می‌کنیم:
using MinimalBlog.Api.Extensions;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddApplicationServices(builder);

var app = builder.Build();
app.ConfigureApplication();
app.RegisterEndpoints();

app.Run();
این چند سطر، کل محتوای فایل Program.cs برنامه را تشکیل می‌دهند.


ایجاد اولین Feature برنامه؛ ویژگی نویسندگان

برای تعریف اولین ویژگی برنامه که مختص به نویسندگان است، پوشه‌های جدید Features\Authors را در برنامه‌ی Api ایجاد می‌کنیم.
- اولین کاری را که در ادامه انجام خواهیم داد، انتقال فایل AuthorDto.cs که در قسمت قبل ایجاد کردیم، به درون این پوشه‌ی جدید است.
- سپس ماژول نویسندگان را به صورت زیر به آن اضافه می‌کنیم:
namespace MinimalBlog.Api.Features.Authors;

public class AuthorModule : IModule
{
    public IEndpointRouteBuilder RegisterEndpoints(IEndpointRouteBuilder endpoints)
    {
        endpoints.MapGet("/api/authors", async (MinimalBlogDbContext ctx) =>
        {
            var authors = await ctx.Authors.ToListAsync();
            return authors;
        });

        endpoints.MapPost("/api/authors", async (MinimalBlogDbContext ctx, AuthorDto authorDto) =>
        {
            var author = new Author();
            author.FirstName = authorDto.FirstName;
            author.LastName = authorDto.LastName;
            author.Bio = authorDto.Bio;
            author.DateOfBirth = authorDto.DateOfBirth;

            ctx.Authors.Add(author);
            await ctx.SaveChangesAsync();

            return author;
        });

        return endpoints;
    }
}
در اینجا ماژول نویسندگان را که با پیاده سازی قرارداد IModule تشکیل شده‌است، مشاهده می‌کنید. در متد RegisterEndpoints آن، دو endpoints تعریف شده‌ی در کلاس Program برنامه را در قسمت قبل، Cut کرده و به اینجا منتقل کرده‌ایم. بنابراین اکنون کلاس Program، دیگر به همراه تعریف مستقیم هیچ endpoint ای نیست و خلوت شده‌است. هدف از Features هم دقیقا همین است تا هر ویژگی برنامه، متکی به خود بوده و مستقل باشد؛ به همراه تمام تعاریف مورد نیاز جهت کار با آن در یک محل مشخص (مانند انتقال فایل Dto مربوط به آن، به درون همین پوشه). مزیت این روش، درک ساده‌تر اجزای مرتبط و یافتن سریعتر ارتباطات قسمت‌های یک ویژگی خاص است. در آینده اگر مشکلی رخ داد و باگی بروز پیدا کرد، دقیقا می‌دانیم که محدوده‌ای که باید مورد بررسی قرار گیرد، کجاست و این محدوده، کوچک و متکی به خود است و در بین چندین پروژه‌ی مختلف، پراکنده نشده‌است.
کار نمونه سازی و اجرای متدهای این ماژول‌ها نیز توسط متدهای الحاقی کلاس ModuleExtensions، در ابتدای اجرای برنامه به صورت خودکار انجام می‌شود و نیازی به شلوغ کردن کلاس Program برای ثبت دستی آن‌ها نیست.


افزودن AutoMapper و MediatR به پروژه‌ی Api

در ادامه برای ساده سازی کار نگاشت‌های Dtoهای برنامه به مدل‌های دومین آن، از AutoMapper استفاده خواهیم کرد؛ همچنین از MediatR نیز برای پیاده سازی الگوی CQRS که در قسمت بعدی پیگیری خواهد شد. بنابراین در ابتدا بسته‌های نیوگت این دو را به پروژه‌ی Api اضافه می‌کنیم:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <ItemGroup>
    <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />    
    <PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="10.0.1" />  
  </ItemGroup>
</Project>
سپس به کلاس ServiceCollectionExtensions مراجعه کرده و تعاریف ثبت سرویس‌های این دو را نیز اضافه می‌کنیم:
public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddApplicationServices(this IServiceCollection services,
        WebApplicationBuilder builder)
    {
        // ...

        builder.Services.AddMediatR(typeof(Program));
        builder.Services.AddAutoMapper(typeof(Program));

        return services;
    }
}
اکنون می‌توان اولین Profile مربوط به AutoMapper را که کار نگاشت AuthorDto به Author و برعکس را انجام می‌دهد، به صورت زیر تهیه کنیم:
using AutoMapper;
using MinimalBlog.Domain.Model;

namespace MinimalBlog.Api.Features.Authors;

public class AuthorProfile : Profile
{
    public AuthorProfile()
    {
        CreateMap<AuthorDto, Author>().ReverseMap();
    }
}
این فایل نیز درون پوشه‌ی Features\Authors قرار می‌گیرد.
مطالب
پیاده سازی یک سیستم دسترسی Role Based در Web API و AngularJs - بخش سوم (پایانی)
در بخش پیشین  به بررسی جزئی‌تر ایجاد پایگاه داده و همچنین توسعه Custom Filter Attribute پرداختیم که وظیفه تایید صلاحیت کاربر جاری و بررسی دسترسی وی به API Method مورد نظر را بررسی می‌کرد. در این مقاله به این بحث می‌پردازیم که در Filter Attribute توسعه داده شده، قصد داریم یک سرویس Access Control ایجاد نماییم.
این سرویس وظیفه تمامی اعمال مربوط به نقش‌ها و دسترسی‌های کاربر را بر عهده خواهد داشت. این سرویس به صورت زیر تعریف می‌گردد:
public class AccessControlService
{
        private DbContext db;

        public AccessControlService()
        {
            db = new DbContext();
        }

        public IEnumerable<Permission> GetUserPermissions(string userId)
        {
            var userRoles = this.GetUserRoles(userId);
            var userPermissions = new List<Permission>();
            foreach (var userRole in userRoles)
            {
                foreach (var permission in userRole.Permissions)
                {
                    // prevent duplicates
                    if (!userPermissions.Contains(permission))
                        userPermissions.Add(permission);
                }
            }
            return userPermissions;
        }
        public IEnumerable<Role> GetUserRoles(string userId)
        {
            return db.Users.FirstOrDefault(x => x.UserId == userId).Roles.ToList();
        }

        public bool HasPermission(string userId, string area, string control)
        {
            var found = false;
            var userPermissions = this.GetUserPermissions(userId);
            var permission = userPermissions.FirstOrDefault(x => x.Area == area && x.Control == control);
            if (permission != null)
                found = true;
            return found;
        }
{
همانطور که ملاحظه می‌کنید، ما سه متد GetUserPermissions، GetUserRoles و HasPermission را توسعه داده‌ایم. حال اینکه بر حسب نیاز، می‌توانید متدهای بیشتری را نیز به این سرویس اضافه نمایید. متد اول، وظیفه‌ی واکشی تمامی permissionهای کاربر را عهده دار می‌باشد. متد GetUserRoles نیز تمامی نقش‌های کاربر را در سیستم، بازمی‌گرداند و در نهایت متد سوم، همان متدی است که ما در Filterattribute از آن استفاده کرد‌ه‌ایم. این متد با دریافت پارامترها و بازگردانی یک مقدار درست یا نادرست، تعیین می‌کند که کاربر جاری به آن محدوده دسترسی دارد یا خیر.
تمامی حداقل‌هایی که برای نگارش سمت سرور نیاز بود، به پایان رسید. حال به بررسی این موضوع خواهیم پرداخت که چگونه این سطوح دسترسی را با سمت کاربر همگام نماییم. به طوری که به عنوان مثال اگر کاربری حق دسترسی به ویرایش مطالب یک سایت را ندارد، دکمه مورد نظر که او را به آن صفحه هدایت می‌کند نیز نباید به وی نشان داده شود. سناریویی که ما برای این کار در نظر گرفته‌ایم، به این گونه می‌باشد که در هنگام ورود کاربر، لیستی از تمامی دسترسی‌های او به صورت JSON به سمت کلاینت ارسال می‌گردد. حال وظیفه مدیریت نمایش یا عدم نمایش المان‌های صفحه، بر عهده زبان سمت کلاینت، یعنی AngularJs خواهد بود. بنابراین در ابتدایی‌ترین حالت، ما نیاز به یک کنترلر و متد Web API داریم تا لیست دسترسی‌های کاربر را بازگرداند. این کنترلر در حال حاضر شامل یک متد است. اما بر حسب نیاز، می‌توانید متدهای بیشتری را به کنترلر اضافه نمایید.
    [RoutePrefix("َAuth/permissions")]
    public class PermissionsController : ApiController
    {
        private AccessControlService _AccessControlService = null;
        public PermissionsController()
        {
            _AccessControlService = new AccessControlService();
        }

        [Route("GetUserPermissions")]
        public async Task<IHttpActionResult> GetUserPermissions()
        {
            if (!User.Identity.IsAuthenticated)
            {
                return Unauthorized();
            }
            return Ok(_AccessControlService.GetPermissions(User.Identity.GetCurrentUserId()));
        }        
    }
در متد فوق ما از متد سرویس Access Control که لیست تمامی permissionهای کاربر را باز می‌گرداند، کمک گرفتیم. متد GetUserPermissions پس از ورود کاربر توسط کلاینت فراخوانی می‌گردد و لیست تمامی دسترسی‌ها در سمت کلاینت، در rootScope انگیولار ذخیره سازی می‌گردد. حال نوبت به آن رسیده که به بررسی عملیات سمت کلاینت بپردازیم.

توسعه سرویس‌ها و فرآیندهای سمت وب کلاینت AngularJS

در ابتدا در سمت کلاینت نیاز به سرویسی داریم که دسترسی‌های سمت سرور را دریافت نماید. از این رو ما نام این سرویس را permissionService می‌نامیم.
'use strict';
angular.module('App').factory('permissionService', ['$http', '$q', function ($http, $q) {

    var _getUserPermissions = function () {
        return $http.get(serviceBaseUrl + '/api/permissions/GetUserPermissions/');
    }

    var _isAuthorize = function (area, control) {
        return _.some($scope.permissions, { 'area': area, 'control': control });
    }

    return {
        getUserPermissions: _getUserPermissions,
        isAuthorize: _isAuthorize
    };
}]);
اگر بخواهیم مختصری درباره‌ی این سرویس صحبت کنیم، متد اول که یک دستور GET ساده است و لیست دسترسی‌ها را از PermissionController دریافت می‌کند. متد بعدی که در آینده بیشتر با آن آشنا می‌شویم، عملیات تایید صلاحیت کاربر را به ناحیه مورد نظر، انجام می‌دهد (همان مثال دسترسی به دکمه ویرایش مطلب در یک صفحه). در این متد برای جستجوی لیست permissions از کتابخانه‌ای با نام Lodash کمک گرفته‌ایم. این کتابخانه کاری شبیه به دستورات Linq را در کالکشن‌ها و آرایه‌های جاوااسکریپتی، انجام می‌دهد. متد some یک مقدار درست یا نادرست را بازمی‌گرداند. بازگردانی مقدار درست، به این معنی است که کاربر به ناحیه‌ی مورد نظر اجازه‌ی دسترسی را خواهد داشت.
حال باید متد‌های این سرویس را در کنترلر لاگین فراخوانی نماییم. در این مرحله ما از rootScope dependency استفاده می‌کنیم. برای نحوه‌ی عملکرد rootScope میتوانید به مقاله‌ای در این زمینه در وب سایت toddomotto مراجعه کنید. در این مقاله ویژگی‌ها و اختلاف‌های scope و rootScope به تفصیل بیان شده است. مقاله‌ای دیگر در همین زمینه نوشته شده است که در انتهای مقاله به بررسی چند نکته در مورد کدهای مشترک پرداخته شده‌است. تکه کد زیر، متد login را نمایش می‌دهد که در loginController قرار گرفته است و ما در آن از نوشتن کل بلاک loginController چشم پوشی کرده‌ایم. متد savePermissions تنها یک کار را انجام می‌دهد و آن هم این است که در ابتدا، به سرویس permissionService متصل شده و تمامی دسترسی‌ها را واکشی می‌نماید و پس از آن، آنها را درون rootScope قرار می‌دهد تا در تمامی کنترلرها قابل دسترسی باشد.
    $scope.login = function () {
        authService.login($scope.loginData).then(function (response) {
            savePermissions();
            $location.path('/userPanel');
        },
         function (err) {
             $scope.message = err.error_description;
         });
    };

    var savePermissions = function () {
        permissionService.getUserPermissions().then(function (response) {
            $rootScope.permissions = response.data;
        },
        function (err) {
        });
    }
}

   حال تمامی اطلاعات دسترسی، در سمت کلاینت نیز قابل دسترسی می‌باشد. تنها کاری که نیاز است، broadCast کردن متد isAuthorize است که آن هم باید در rootScope قرار بگیرد. ما برای این انتساب یک راهکار را ارائه کرده‌ایم. معماری سیستم کلاینت به این صورت است که تمامی کنترلرها درون یک parentController قرار گرفته‌اند. از این رو می‌توان در parentController این انتساب (ایجاد دسترسی عمومی برایisAuthorize) صورت گیرد. برای این کار در parentController تغییرات زیر صورت می‌گیرد:
App.controller('parentController', ['$rootScope', '$scope', 'authService', 'permissionService', function ($rootScope, $scope, authService, permissionService) {
        $scope.authentication = authService.authentication;

        // isAuthorize Method
        $scope.isAuthorize = permissionService.isAuthorize();

        // rest of codes
}]);
در کد فوق ما isAuthorize را درون scope قرار داده‌ایم. دلیل آن هم این است که هر چه که در scope قرار بگیرد، تمامی کنترلر‌های child نیز به آن دسترسی خواهند داشت. البته ممکن است که این بهترین نوع پیاده سازی برای به اشتراک گذاری یک منبع نباشد.
 در گام بعدی کافیست المان‌های صفحه را بر اساس همین دسترسی‌ها فعال یا غیر فعال کنیم. برای این کار از دستور ng-if میتوان استفاده کرد. برای این کار به مثال زیر توجه کنید:
<div ng-controller="childController">
    <div ng-if="isAuthorize('articles', 'edit')" >
    <!-- the block that we want to not see unauthorize person -->
    </div>
</div>
همانطور که مشاهده می‌کنید، تمامی المان‌ها را می‌توان با دستور ساده ng-if، از دید کاربران بدون صلاحیت، پنهان نمود. البته توجه داشته باشید که شما نمی‌توانید تنها به پنهان کردن این اطلاعات اکتفا کنید. بلکه باید تمامی متدهای کنترلرهای سمت سرور را هم با همین روش (فیلتر کردن با Filter Attribute) بررسی نمایید. به ازای هر درخواست کاربر باید بررسی شود که او به منبع مورد نظر دسترسی دارد یا خیر.
تنها نکته‌ای که باقی می‌ماند این است که طول عمر scope و rootScope چقدر است؟! برای پاسخ به این سوال باید بگوییم هر بار که صفحه refresh می‌شود، تمامی مقادیر scope و rootScope خالی می‌شوند. برای این کار هم یک راهکار خیلی ساده (و شاید کمی ناشیانه) در نظر گرفته شده‌است. میتوان بلاک مربوط به پر کردن rootScope.permissions را که در loginController نوشته شده بود، به درون parentController انتقال داد و آن را با استفاده از emit اجرا کرد و در حالت عادی، در هنگام refresh شدن صفحه نیز چون parentController در اولین لحظه اجرا می‌شود، میتوان تمامی مقادیر rootScope.permissions را دوباره از سمت سرور دریافت کرد.
مطالب دوره‌ها
معرفی #F
یکی از قدیمی‌ترین روش‌های برنامه نویسی روش برنامه نویسی تابع گراست. زبان IPL به عنوان قدیمی‌ترین زبان برنامه نویسی تابع گرا در سال 1955(یک سال قبل از خلق فرترن) است. دومین زبان تابع گرا زبان LISP بوده است که در سال 1958(یک سال قبل از خلق کوبول) متولد شد. هر دو زبان کوبول و فرترن زبان‌های امری و رویه ای بودند. بعد از آن‌ها در سال 1970 شروع عرصه زبان‌های شی گرا بود و تا امروز بیشترین کاربرد را در تولید نرم افزار‌ها داشته اند.
#F یک زبان برنامه نویسی تابع گرا است و گزینه ای بسیار مناسب برای حل مسایل کامپیوتری. اما استفاده از زبان برنامه نویسی تابعی محض برای نوشتن و تولید پروژه‌های نرم افزاری مناسب نمی‌باشد. به همین دلیل نیاز به استفاده  از این زبان‌ها در کنار سایر زبان‌های شی گرا احساس می‌شود. #F یک زبان همه منظوره دات نت است که برای حالت اجرا به صورت همه منظوره استفاده می‌شود. برخی زبان‌های تابع گرا دیگر نظیر Lisp و Haskel و OCaml (که #F بسیار نزدیک به این زبان می‌باشد) با دستورات زبان اجرای سفارشی کار می‌کنند و این مسئله باعث نبود زبان برنامه نویسی چند فعالیته می‌شود. شما می‌توانید از برنامه نویسی توصیفی هم استفاده کنید و توابع را به راحتی با هم ترکیب کنید و یا روش‌های شی گرایی و دستوری را در همان برنامه استفاده کنید.

تاریخچه

#F توسط دکتر دون سیم ابداع شد. در حال حاضر #F وابسته به تیمی کوچک ولی پیشرفته واقع در مرکز تحقیقات شرکت مایکروسافت می‌باشد. #Fمدل خود را از روی زبان برنامه نویسی OCAML انتخاب کرد و سپس با گسترش قابلیت‌های فنی، خود را در دات نت گنجاند. #F در بسیاری از برنامه‌های بزرگ دنیای واقعی استفاده شده است که این خود نمایانگر آکادمیک نبودن محض این زبان است. با توجه به اینکه زبان تابع گرای دیگر به ندرت در دات نت توسعه پیدا کرده است #F به عنوان استاندارد در این مقوله در آمده است. زبان #F از نظر کیفیت و سازگار بودن با دات نت و VisualStudio بسیار وضعیت بهتری نسبت به رقبای خود دارد و این خود دلیلی دیگری است برای انتخاب این زبان.

استفاده در دات نت
#F کاملا از دات نت پشتیبانی می‌کند و این قابلیت را به برنامه نویسان می‌دهد که هر چیزی را که در سایر زبان‌های دات نت استفاده می‌کنند در این زبان نیز قابل استفاده باشد. همچنین می‌تواند برای کد نویسی IL نیز استفاده شود.
#F به راحتی قابل اجرا در محیط لینوکس و مکینتاش نیز است.

استفاده کنندگان #F
#F در شرکت مایکرو سافت به شدت استفاده می‌شود. رالف هربریش که یکی از مدیران دوگانه گروه بازی‌های مایکروسافت و از متخصصین آموزش ماشین است در این باره می‌گوید:
*اولین برنامه کاربردی برای انتقال 110 گیگا بایت از طریق 11000 فایل متنی در بیش از 300 دایرکتوری و وارد کردن آن‌ها در دیتابیس بود. کل برنامه 90 خط بود و در کمتر از 18 ساعت توانست اطلاعات مربوطه را در SQL ذخیره کند. یعنی ده هزار خط برنامه متنی در هر ثانیه مورد پردازش قرار گرفت.همچنین توجه کنید که من برنامه را بهینه نکردم بلکه به صورت کاملا عادی نوشتم. این جواب بسیار قابل توجه بود زیرا من انتظار داشتم حداقل یک هفته زمان ببرد.

دومین برنامه، برنامه پردازش میلیون‌ها Feekback مشتریان بود. ما روابط مدلی زیادی را توسعه دادیم و من این روابط را در #F قرار دادم و داده‌های مربوط به SQL را در آن فراخوانی کردم و نتایج را در فایل داده ای MATLAB قرار دادم و کل پروژه در حد صد خط بود به همراه توضیحات. زمان اجرای پروژه برای دریافت خروجی ده دقیقه بود در حالی که  همین کار را توسط برنامه #C قبلا توسعه داده بودیم که بیش از هزار خط بود و نزدیک به دو روز زمان می‌برد.*


استفاده از #F تنها در مایکروسافت نیست بلکه در سایر شرکت‌های بزرگ و نام دار نیز استفاده می‌شود و همچنان نیز در حال افزایش است. شرکت
Derivative One  که یک شرکت بزرگ در تولید نرم افزار‌های شبیه ساز مالی است مدل‌های مالی نرم افزار‌های خود را در #F پیاده سازی کرده است.

چرا #F ؟
همیشه باید دلیلی برای انتخاب یک زبان باشد. در حال حاضر #F یکی از قدرتمند‌ترین زبان‌های برنامه نویسی است. در ذیل به چند تا از این دلایل اشاره خواهم کرد:
  • #F یک زبان استنباطی است. برای مثال در هنگام تعریف متغیر و شناسه نیاز به ذکر نوع آن نیست. کامپایلر با توجه به مقدار اولیه تصمیم می‌گیرد که متغیر از چه نوعی است.
  • بسیار راحت می‌توان به کتابخانه قدرتمند دات نت دسترسی داشت و از آن‌ها در پروژه‌های خود استفاده کنید.
  • #F از انواع روش‌های برنامه نویسی نظیر تابعی، موازی، شی گرا و دستوری پیشتیبانی می‌کند.
  • برخلاف تصور بعضی افراد، در #F امکان تهیه و توسعه پروژه‌های وب و ویندوز و حتی WPF و Silverlight هم وجود دارد.
  • نوع کدنویسی و syntax زبان #F به برنامه نویسان این اجازه را میدهد که الگوریتم‌های پیچیده مورد نظر خود را بسیار راحت‌تر پیاده سازی کنند. به همین دلیل بعضی برنامه نویسان این زبان را با Paython مقایسه می‌کنند.
  • #F به راحتی با زبان #C و VB تعامل دارد. یعنی می‌تونیم در طی روند تولید پروژه از قدرت‌های هر سه زبان بهره بگیریم. 
  • طبق آمار گرفته شده از برنامه نویسان، #F به دلیل پشتیبانی از نوع داده ای قوی و مبحث Unit Measure، خطا‌ها و Bug‌های نرم افزار را کاهش می‌دهد.
  • به دلیل پشتیبانی VS.Net از زبان #F و وجود ابزار قدرتمند برای توسعه نرم افزار به کمک این زبان (unitTesting و ابزارهای debuging و ..)این زبان تبدیل به قدرت‌های دنیای برنامه نویسی شده است.
  • #F یک زبان بسیار مناسب برای پیاده سازی الگوریتم‌های data-mining است.
  • #F از immutability در تعریف شناسه‌ها پشتیبانی می‌کند.(در فصل‌های مربوطه بحث خواهد شد)
  • و.....

چرا #F نه ؟

#F هم مانند سایر زبان ها، علاوه بر قدرت بی همتای خود دارای معایبی نیز می‌باشد. (مواردی که در پایین ذکر می‌شود صرفا بر اساس تجربه است نه مستندات).

  • نوع کدنویسی و syntax زبان #F برای برنامه نویسان دات بیگانه ( و البته کمی آزار دهنده) است. اما به مرور این مشکل، تبدیل به قدرت برای مانور‌های مختلف در کد می‌شود.
  • درست است که در #F امکان تعریف اینترفیس وجود دارد و یک کلاس می‌تواند اینترفیس مورد نظر را پیاده سازی کند ولی هنگام فراخوانی متد‌های کلاس (اون هایی که مربوط به اینترفیس است) حتما باید instance کلاس مربوطه به اینترفیس cast شود و این کمی آزار دهنده است.(در فصل شی گرایی در این مورد شرح داده شده است).
  • زبان #F در حال حاضر توسط  VS.Net به صورت Visual پشتیبانی نمی‌شود.(امکاناتی نظیر drag drop کنترل‌ها برای ساخت فرم و ....). البته برای حل این مشکل نیز افزونه هایی وجود دارد که در جای مناسب بحث خواهیم کرد.

آیا برای یادگیری #F نیاز به داشتن دانش در برنامه نویسی #C یا VBداریم؟

به طور قطع نه. نوع کد نویسی (نه مفاهیم)در #F کاملا متفاوت در #C است و این دو زبان از نظر کد نویسی شباهتشان در حد صفر است. برای یادگیری #F بیشتر نیاز به داشتن آگاهی اولیه در برنامه نویسی (آشنایی با تابع، حلقه تکرار، متغیر ها) و شی گرایی(مفاهیم کلاس، اینترفیس، خواص، متد‌ها و...) دارید تا آشنایی با #C یا VB.

چگونه شروع کنیم؟

اولین گام برای یادگیری آشنایی با نحوه کد نویسی #F است. بدین منظور در طی فصول آموزش سعی بر این شده است از مثال‌های بسیار زیاد برای درک بهتر مفاهیم استفاده کنم. تا جای ممکن برای اینکه تکرار مکررات نشود و شما خواننده عزیز به خاطر مطالب واضح و روشن خسته نشوید از تشریح مباحث واضح خودداری کردم و بیشتر به پیاده سازی مثال اکتفا نمودم.


مطالب
ASP.NET MVC و Identity 2.0 : مفاهیم پایه
در تاریخ 20 مارچ 2014 تیم ASP.NET نسخه نهایی Identity 2.0 را منتشر کردند . نسخه جدید برخی از ویژگی‌های درخواست شده پیشین را عرضه می‌کند و در کل قابلیت‌های احراز هویت و تعیین سطح دسترسی ارزنده ای را پشتیبانی می‌کند. این فریم ورک در تمام اپلیکیشن‌های ASP.NET می‌تواند بکار گرفته شود.

فریم ورک Identity در سال 2013 معرفی شد، که دنباله سیستم ASP.NET Membership بود. سیستم قبلی گرچه طی سالیان استفاده می‌شد اما مشکلات زیادی هم بهمراه داشت. بعلاوه با توسعه دنیای وب و نرم افزار، قابلیت‌های مدرنی مورد نیاز بودند که باید پشتیبانی می‌شدند. فریم ورک Identity در ابتدا سیستم ساده و کارآمدی برای مدیریت کاربران بوجود آورد و مشکلات پیشین را تا حد زیادی برطرف نمود. بعنوان مثال فریم ورک جدید مبتنی بر EF Code-first است، که سفارشی کردن سیستم عضویت را بسیار آسان می‌کند و به شما کنترل کامل می‌دهد. یا مثلا احراز هویت مبتنی بر پروتوکل OAuth پشتیبانی می‌شود که به شما اجازه استفاده از فراهم کنندگان خارجی مانند گوگل، فیسبوک و غیره را می‌دهد.

نسخه جدید این فریم ورک ویژگی‌های زیر را معرفی می‌کند (بعلاوه مواردی دیگر):

  • مدل حساب‌های کاربری توسعه داده شده. مثلا آدرس ایمیل و اطلاعات تماس را هم در بر می‌گیرد
  • احراز هویت دو مرحله ای (Two-Factor Authentication) توسط اطلاع رسانی ایمیلی یا پیامکی. مشابه سیستمی که گوگل، مایکروسافت و دیگران استفاده می‌کنند
  • تایید حساب‌های کاربری توسط ایمیل (Account Confirmation)
  • مدیریت کاربران و نقش‌ها (Administration of Users & Roles)
  • قفل کردن حساب‌های کاربری در پاسخ به Invalid log-in attempts
  • تامین کننده شناسه امنیتی (Security Token Provider) برای بازتولید شناسه‌ها در پاسخ به تغییرات تنظیمات امنیتی (مثلا هنگام تغییر کلمه عبور)
  • بهبود پشتیبانی از Social log-ins
  • یکپارچه سازی ساده با Claims-based Authorization

Identity 2.0 تغییرات چشم گیری نسبت به نسخه قبلی به‌وجود آورده است. به نسبت ویژگی‌های جدید، پیچیدگی‌هایی نیز معرفی شده‌اند. اگر به تازگی (مانند خودم) با نسخه 1 این فریم ورک آشنا شده و کار کرده اید، آماده شوید! گرچه لازم نیست از صفر شروع کنید، اما چیزهای بسیاری برای آموختن وجود دارد.

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

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

در این مقاله با مقدار قابل توجهی کد مواجه خواهید شد. لازم نیست تمام جزئیات آنها را بررسی کنید، تنها با ساختار کلی این فریم ورک آشنا شوید. کامپوننت‌ها را بشناسید و بدانید که هر کدام در کجا قرار گرفته اند، چطور کار می‌کنند و اجزای کلی سیستم چگونه پیکربندی می‌شوند. گرچه، اگر به برنامه نویسی دات نت (#ASP.NET, C) تسلط دارید و با نسخه قبلی Identity هم کار کرده اید، درک کدهای جدید کار ساده ای خواهد بود.

Identity 2.0 با نسخه قبلی سازگار نیست

اپلیکیشن هایی که با نسخه 1.0 این فریم ورک ساخته شده اند نمی‌توانند بسادگی به نسخه جدید مهاجرت کنند. قابلیت هایی جدیدی که پیاده سازی شده اند تغییرات چشمگیری در معماری این فریم ورک بوجود آورده اند، همچنین API مورد استفاده در اپلیکیشن‌ها نیز دستخوش تغییراتی شده است. مهاجرت از نسخه 1.0 به 2.0 نیاز به نوشتن کدهای جدید و اعمال تغییرات متعددی دارد که از حوصله این مقاله خارج است. فعلا همین قدر بدانید که این مهاجرت نمی‌تواند بسادگی در قالب Plug-in and play صورت پذیرد!

شروع به کار : پروژه مثال‌ها را از NuGet دریافت کنید

در حال حاظر (هنگام نوشتن این مقاله) قالب پروژه استانداردی برای اپلیکیشن‌های ASP.NET MVC که ا ز Identity 2.0 استفاده کنند وجود ندارد. برای اینکه بتوانید از نسخه جدید این فریم ورک استفاده کنید، باید پروژه مثال را توسط NuGet دریافت کنید. ابتدا پروژه جدیدی از نوع ASP.NET Web Application بسازید و قالب Empty را در دیالوگ تنظیمات انتخاب کنید.

کنسول Package Manager را باز کنید و با اجرای فرمان زیر پروژه مثال‌ها را دانلود کنید.

PM> Install-Package Microsoft.AspNet.Identity.Samples -Pre
پس از آنکه NuGet کار خود را به اتمام رساند، ساختار پروژه ای مشابه پروژه‌های استاندارد MVC مشاهده خواهید کرد. پروژه شما شامل قسمت‌های Models, Views, Controllers و کامپوننت‌های دیگری برای شروع به کار است. گرچه در نگاه اول ساختار پروژه بسیار شبیه به پروژه‌های استاندارد ASP.NET MVC به نظر می‌آید، اما با نگاهی دقیق‌تر خواهید دید که تغییرات جدیدی ایجاد شده‌اند و پیچیدگی هایی نیز معرفی شده اند.

پیکربندی Identity : دیگر به سادگی نسخه قبلی نیست
به نظر من یکی از مهم‌ترین نقاط قوت فریم ورک Identity یکی از مهم‌ترین نقاط ضعفش نیز بود. سادگی نسخه 1.0 این فریم ورک کار کردن با آن را بسیار آسان می‌کرد و به سادگی می‌توانستید ساختار کلی و روند کارکردن کامپوننت‌های آن را درک کنید. اما همین سادگی به معنای محدود بودن امکانات آن نیز بود. بعنوان مثال می‌توان به تایید حساب‌های کاربری یا پشتیبانی از احراز هویت‌های دو مرحله ای اشاره کرد.

برای شروع نگاهی اجمالی به پیکربندی این فریم ورک و اجرای اولیه اپلیکیشن خواهیم داشت. سپس تغییرات را با نسخه 1.0 مقایسه می‌کنیم.

در هر دو نسخه، فایلی بنام Startup.cs در مسیر ریشه پروژه خواهید یافت. در این فایل کلاس واحدی بنام Startup تعریف شده است که متد ()ConfigureAuth را فراخوانی می‌کند. چیزی که در این فایل مشاهده نمی‌کنیم، خود متد ConfigureAuth است. این بدین دلیل است که مابقی کد کلاس Startup در یک کلاس پاره ای (Partial) تعریف شده که در پوشه App_Start قرار دارد. نام فایل مورد نظر Startup.Auth.cs است که اگر آن را باز کنید تعاریف یک کلاس پاره ای بهمراه متد ()ConfigureAuth را خواهید یافت. در یک پروژه که از نسخه Identity 1.0 استفاده می‌کند، کد متد ()ConfigureAuth مطابق لیست زیر است.
public partial class Startup
{
    public void ConfigureAuth(IAppBuilder app)
    {
        // Enable the application to use a cookie to 
        // store information for the signed in user
        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
            LoginPath = new PathString("/Account/Login")
        });

        // Use a cookie to temporarily store information about a 
        // user logging in with a third party login provider
        app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

        // Uncomment the following lines to enable logging 
        // in with third party login providers
        //app.UseMicrosoftAccountAuthentication(
        //    clientId: "",
        //    clientSecret: "");

        //app.UseTwitterAuthentication(
        //   consumerKey: "",
        //   consumerSecret: "");
 
        //app.UseFacebookAuthentication(
        //   appId: "",
        //   appSecret: "");

        //app.UseGoogleAuthentication();
    }
}
در قطعه کد بالا پیکربندی لازم برای کوکی‌ها را مشاهده می‌کنید. همچنین کدهایی بصورت توضیحات وجود دارد که به منظور استفاده از تامین کنندگان خارجی مانند گوگل، فیسبوک، توییتر و غیره استفاده می‌شوند. حال اگر به کد این متد در نسخه Identity 2.0 دقت کنید خواهید دید که کد بیشتری نوشته شده است.
public partial class Startup {
    public void ConfigureAuth(IAppBuilder app) {
        // Configure the db context, user manager and role 
        // manager to use a single instance per request
        app.CreatePerOwinContext(ApplicationDbContext.Create);
        app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
        app.CreatePerOwinContext<ApplicationRoleManager>(ApplicationRoleManager.Create);

        // Enable the application to use a cookie to store information for the 
        // signed in user and to use a cookie to temporarily store information 
        // about a user logging in with a third party login provider 
        // Configure the sign in cookie
        app.UseCookieAuthentication(new CookieAuthenticationOptions {
            AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
            LoginPath = new PathString("/Account/Login"),
            Provider = new CookieAuthenticationProvider {
                // Enables the application to validate the security stamp when the user 
                // logs in. This is a security feature which is used when you 
                // change a password or add an external login to your account.  
                OnValidateIdentity = SecurityStampValidator
                    .OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
                    validateInterval: TimeSpan.FromMinutes(30),
                    regenerateIdentity: (manager, user) 
                    => user.GenerateUserIdentityAsync(manager))
            }
        });

        app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

        // Enables the application to temporarily store user information when 
        // they are verifying the second factor in the two-factor authentication process.
        app.UseTwoFactorSignInCookie(
            DefaultAuthenticationTypes.TwoFactorCookie, 
            TimeSpan.FromMinutes(5));

        // Enables the application to remember the second login verification factor such 
        // as phone or email. Once you check this option, your second step of 
        // verification during the login process will be remembered on the device where 
        // you logged in from. This is similar to the RememberMe option when you log in.
        app.UseTwoFactorRememberBrowserCookie(
            DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);

        // Uncomment the following lines to enable logging in 
        // with third party login providers
        //app.UseMicrosoftAccountAuthentication(
        //    clientId: "",
        //    clientSecret: "");

        //app.UseTwitterAuthentication(
        //   consumerKey: "",
        //   consumerSecret: "");

        //app.UseFacebookAuthentication(
        //   appId: "",
        //   appSecret: "");

        //app.UseGoogleAuthentication();
    }
}
اول از همه به چند فراخوانی متد app.CreatePerOwinContext بر می‌خوریم. با این فراخوانی‌ها Callback هایی را رجیستر می‌کنیم که آبجکت‌های مورد نیاز را بر اساس نوع تعریف شده توسط type arguments وهله سازی می‌کنند. این وهله‌ها سپس توسط فراخوانی متد ()context.Get قابل دسترسی خواهند بود. این به ما می‌گوید که حالا Owin بخشی از اپلیکیشن ما است و فریم ورک Identity 2.0 از آن برای ارائه قابلیت هایش استفاده می‌کند.

مورد بعدی ای که جلب توجه می‌کند فراخوانی‌های دیگری برای پیکربندی احراز هویت دو مرحله‌ای است. همچنین پیکربندی‌های جدیدی برای کوکی‌ها تعریف شده است که در نسخه قبلی وجود نداشتند.

تا اینجا پیکربندی‌های اساسی برای اپلیکیشن شما انجام شده است و می‌توانید از اپلیکیشن خود استفاده کنید. بکارگیری فراهم کنندگان خارجی در حال حاضر غیرفعال است و بررسی آنها نیز از حوصله این مقاله خارج است. این کلاس پیکربندی‌های اساسی Identity را انجام می‌دهد. کامپوننت‌های پیکربندی و کدهای کمکی دیگری نیز وجود دارند که در کلاس IdentityConfig.cs تعریف شده اند.

پیش از آنکه فایل IdentityConfig.cs را بررسی کنیم، بهتر است نگاهی به کلاس ApplicationUser بیاندازیم که در پوشه Models قرار گرفته است.


کلاس جدید ApplicationUser در Identity 2.0
اگر با نسخه 1.0 این فریم ورک اپلیکیشنی ساخته باشید، ممکن است متوجه شده باشید که کلاس پایه IdentityUser محدود و شاید ناکافی باشد. در نسخه قبلی، این فریم ورک پیاده سازی IdentityUser را تا حد امکان ساده نگاه داشته بود تا اطلاعات پروفایل کاربران را معرفی کند.
public class IdentityUser : IUser
{
    public IdentityUser();
    public IdentityUser(string userName);

    public virtual string Id { get; set; }
    public virtual string UserName { get; set; }

    public virtual ICollection<IdentityUserRole> Roles { get; }
    public virtual ICollection<IdentityUserClaim> Claims { get; }
    public virtual ICollection<IdentityUserLogin> Logins { get; }

    public virtual string PasswordHash { get; set; }
    public virtual string SecurityStamp { get; set; }
}
بین خواص تعریف شده در این کلاس، تنها Id, UserName و Roles برای ما حائز اهمیت هستند (از دید برنامه نویسی). مابقی خواص عمدتا توسط منطق امنیتی این فریم ورک استفاده می‌شوند و کمک شایانی در مدیریت اطلاعات کاربران به ما نمی‌کنند.

اگر از نسخه Identity 1.0 استفاده کرده باشید و مطالعاتی هم در این زمینه داشته باشید، می‌دانید که توسعه کلاس کاربران بسیار ساده است. مثلا برای افزودن فیلد آدرس ایمیل و اطلاعات دیگر کافی بود کلاس ApplicationUser را ویرایش کنیم و از آنجا که این فریم ورک مبتنی بر EF Code-first است بروز رسانی دیتابیس و مابقی اپلیکیشن کار چندان مشکلی نخواهد بود.

با ظهور نسخه Identity 2.0 نیاز به برخی از این سفارشی سازی‌ها از بین رفته است. گرچه هنوز هم می‌توانید بسادگی مانند گذشته کلاس ApplicationUser را توسعه و گسترش دهید، تیم ASP.NET تغییراتی بوجود آورده اند تا نیازهای رایج توسعه دهندگان را پاسخگو باشد.

اگر به کد کلاس‌های مربوطه دقت کنید خواهید دید که کلاس ApplicationUser همچنان از کلاس پایه IdentityUser ارث بری می‌کند، اما این کلاس پایه پیچیده‌تر شده است. کلاس ApplicationUser در پوشه Models و در فایلی بنام IdentityModels.cs تعریف شده است. همانطور که می‌بینید تعاریف خود این کلاس بسیار ساده است.
public class ApplicationUser : IdentityUser {
    public async Task<ClaimsIdentity> GenerateUserIdentityAsync(
        UserManager<ApplicationUser> manager) {
        // Note the authenticationType must match the one 
        // defined in CookieAuthenticationOptions.AuthenticationType
        var userIdentity = 
            await manager.CreateIdentityAsync(this, 
                DefaultAuthenticationTypes.ApplicationCookie);

        // Add custom user claims here
        return userIdentity;
    }
}
حال اگر تعاریف کلاس IdentityUser را بازیابی کنید (با استفاده از قابلیت Go To Definition در VS) خواهید دید که این کلاس خود از کلاس پایه دیگری ارث بری می‌کند. اگر به این پیاده سازی دقت کنید کاملا واضح است که ساختار این کلاس به کلی نسبت به نسخه قبلی تغییر کرده است.
public class IdentityUser<TKey, TLogin, TRole, TClaim> : IUser<TKey>
    where TLogin : Microsoft.AspNet.Identity.EntityFramework.IdentityUserLogin<TKey>
    where TRole : Microsoft.AspNet.Identity.EntityFramework.IdentityUserRole<TKey>
    where TClaim : Microsoft.AspNet.Identity.EntityFramework.IdentityUserClaim<TKey>
{
    public IdentityUser();

    // Used to record failures for the purposes of lockout
    public virtual int AccessFailedCount { get; set; }
    // Navigation property for user claims
    public virtual ICollection<TClaim> Claims { get; }
    // Email
    public virtual string Email { get; set; }
    // True if the email is confirmed, default is false
    public virtual bool EmailConfirmed { get; set; }
    // User ID (Primary Key)
    public virtual TKey Id { get; set; }
    // Is lockout enabled for this user
    public virtual bool LockoutEnabled { get; set; }
    // DateTime in UTC when lockout ends, any 
    // time in the past is considered not locked out.
    public virtual DateTime? LockoutEndDateUtc { get; set; }

    // Navigation property for user logins
    public virtual ICollection<TLogin> Logins { get; }
    // The salted/hashed form of the user password
    public virtual string PasswordHash { get; set; }
    // PhoneNumber for the user
    public virtual string PhoneNumber { get; set; }
    // True if the phone number is confirmed, default is false
    public virtual bool PhoneNumberConfirmed { get; set; }
    // Navigation property for user roles
    public virtual ICollection<TRole> Roles { get; }

    // A random value that should change whenever a users 
    // credentials have changed (password changed, login removed)
    public virtual string SecurityStamp { get; set; }
    // Is two factor enabled for the user
    public virtual bool TwoFactorEnabled { get; set; }
    // User name
    public virtual string UserName { get; set; }
}
اول از همه آنکه برخی از خواص تعریف شده هنوز توسط منطق امنیتی فریم ورک استفاده می‌شوند و از دید برنامه نویسی مربوط به مدیریت اطلاعات کاربران نیستند. اما به هر حال فیلد‌های Email و PhoneNumber نیاز به ویرایش تعریف پیش فرض موجودیت کاربران را از بین می‌برد.

اما از همه چیز مهم‌تر امضا (Signature)ی خود کلاس است. این آرگومانهای جنریک چه هستند؟ به امضای این کلاس دقت کنید.
public class IdentityUser<TKey, TLogin, TRole, TClaim> : IUser<TKey>
    where TLogin : Microsoft.AspNet.Identity.EntityFramework.IdentityUserLogin<TKey>
    where TRole : Microsoft.AspNet.Identity.EntityFramework.IdentityUserRole<TKey>
    where TClaim : Microsoft.AspNet.Identity.EntityFramework.IdentityUserClaim<TKey>
نسخه جدید IdentityUser انواع آرگومانهای جنریک را پیاده سازی می‌کند که انعطاف پذیری بسیار بیشتری به ما می‌دهند. بعنوان مثال بیاد بیاورید که نوع داده فیلد Id در نسخه 1.0 رشته (string) بود. اما در نسخه جدید استفاده از آرگومانهای جنریک (در اینجا TKey) به ما اجازه می‌دهد که نوع این فیلد را مشخص کنیم. همانطور که مشاهده می‌کنید خاصیت Id در این کلاس نوع داده TKey را باز می‌گرداند.
public virtual TKey Id { get; set; }
یک مورد حائز اهمیت دیگر خاصیت Roles است که بصورت زیر تعریف شده.
public virtual ICollection<TRole> Roles { get; }
همانطور که می‌بینید نوع TRole بصورت جنریک تعریف شده و توسط تعاریف کلاس IdentityUser مشخص می‌شود. اگر به محدودیت‌های پیاده سازی این خاصیت دقت کنید می‌بینید که نوع این فیلد به <IdentityUserRole<TKey محدود (constraint) شده است، که خیلی هم نسبت به نسخه 1.0 تغییری نکرده. چیزی که تفاوت چشمگیری کرده و باعث breaking changes می‌شود تعریف خود IdentityUserRole است.

در نسخه Identity 1.0 کلاس IdentityUserRole بصورت زیر تعریف شده بود.
public class IdentityUserRole 
{
      public IdentityUserRole();
      public virtual IdentityRole Role { get; set; }
      public virtual string RoleId { get; set; }
      public virtual IdentityUser User { get; set; }
      public virtual string UserId { get; set; }
}
این کلاس را با پیاده سازی نسخه Identity 2.0 مقایسه کنید.
public class IdentityUserRole<TKey> 
{
    public IdentityUserRole();
    public virtual TKey RoleId { get; set; }
    public virtual TKey UserId { get; set; }
}
پیاده سازی پیشین ارجاعاتی به آبجکت هایی از نوع IdentityRole و IdentityUser داشت. پیاده سازی نسخه 2.0 تنها شناسه‌ها را ذخیره می‌کند. اگر در اپلیکیشنی که از نسخه 1.0 استفاده می‌کند سفارشی سازی هایی انجام داده باشید (مثلا تعریف کلاس Role را توسعه داده باشید، یا سیستمی مانند Group-based Roles را پیاده سازی کرده باشید) این تغییرات سیستم قبلی شما را خواهد شکست.

بررسی پیاده سازی جدید IdentityUser از حوصله این مقاله خارج است. فعلا همین قدر بدانید که گرچه تعاریف پایه کلاس کاربران پیچیده‌تر شده است، اما انعطاف پذیری بسیار خوبی بدست آمده که شایان اهمیت فراوانی است.

از آنجا که کلاس ApplicationUser از IdentityUser ارث بری می‌کند، تمام خواص و تعاریف این کلاس پایه در ApplicationUser قابل دسترسی هستند.


کامپوننت‌های پیکربندی Identity 2.0 و کدهای کمکی
گرچه متد ()ConfigAuth در کلاس Startup، محلی است که پیکربندی Identity در زمان اجرا صورت می‌پذیرد، اما در واقع کامپوننت‌های موجود در فایل IdentityConfig.cs هستند که اکثر قابلیت‌های Identity 2.0 را پیکربندی کرده و نحوه رفتار آنها در اپلیکیشن ما را کنترل می‌کنند.

اگر محتوای فایل IdentityConfig.cs را بررسی کنید خواهید دید که کلاس‌های متعددی در این فایل تعریف شده اند. می‌توان تک تک این کلاس‌ها را به فایل‌های مجزایی منتقل کرد، اما برای مثال جاری کدها را بهمین صورت رها کرده و نگاهی اجمالی به آنها خواهیم داشت. بهرحال در حال حاظر تمام این کلاس‌ها در فضای نام ApplicationName.Models قرار دارند.


Application User Manager و Application Role Manager
اولین چیزی که در این فایل به آنها بر می‌خوریم دو کلاس ApplicationUserManager و ApplicationRoleManager هستند. آماده باشید، مقدار زیادی کد با انواع داده جنریک در پیش روست!
public class ApplicationUserManager : UserManager<ApplicationUser>
{
    public ApplicationUserManager(IUserStore<ApplicationUser> store)
        : base(store)
    {
    }

    public static ApplicationUserManager Create(
        IdentityFactoryOptions<ApplicationUserManager> options, 
        IOwinContext context)
    {
        var manager = new ApplicationUserManager(
            new UserStore<ApplicationUser>(
                context.Get<ApplicationDbContext>()));

        // Configure validation logic for usernames
        manager.UserValidator = 
            new UserValidator<ApplicationUser>(manager)
        {
            AllowOnlyAlphanumericUserNames = false,
            RequireUniqueEmail = true
        };

        // Configure validation logic for passwords
        manager.PasswordValidator = new PasswordValidator
        {
            RequiredLength = 6, 
            RequireNonLetterOrDigit = true,
            RequireDigit = true,
            RequireLowercase = true,
            RequireUppercase = true,
        };

        // Configure user lockout defaults
        manager.UserLockoutEnabledByDefault = true;
        manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5);
        manager.MaxFailedAccessAttemptsBeforeLockout = 5;

        // Register two factor authentication providers. This application uses 
        // Phone and Emails as a step of receiving a code for verifying 
        // the user You can write your own provider and plug in here.
        manager.RegisterTwoFactorProvider("PhoneCode", 
            new PhoneNumberTokenProvider<ApplicationUser>
        {
            MessageFormat = "Your security code is: {0}"
        });

        manager.RegisterTwoFactorProvider("EmailCode", 
            new EmailTokenProvider<ApplicationUser>
        {
            Subject = "SecurityCode",
            BodyFormat = "Your security code is {0}"
        });

        manager.EmailService = new EmailService();
        manager.SmsService = new SmsService();

        var dataProtectionProvider = options.DataProtectionProvider;

        if (dataProtectionProvider != null)
        {
            manager.UserTokenProvider = 
                new DataProtectorTokenProvider<ApplicationUser>(
                    dataProtectionProvider.Create("ASP.NET Identity"));
        }

        return manager;
    }

    public virtual async Task<IdentityResult> AddUserToRolesAsync(
        string userId, IList<string> roles)
    {
        var userRoleStore = (IUserRoleStore<ApplicationUser, string>)Store;
        var user = await FindByIdAsync(userId).ConfigureAwait(false);

        if (user == null)
        {
            throw new InvalidOperationException("Invalid user Id");
        }

        var userRoles = await userRoleStore
            .GetRolesAsync(user)
            .ConfigureAwait(false);

        // Add user to each role using UserRoleStore
        foreach (var role in roles.Where(role => !userRoles.Contains(role)))
        {
            await userRoleStore.AddToRoleAsync(user, role).ConfigureAwait(false);
        }

        // Call update once when all roles are added
        return await UpdateAsync(user).ConfigureAwait(false);
    }

    public virtual async Task<IdentityResult> RemoveUserFromRolesAsync(
        string userId, IList<string> roles)
    {
        var userRoleStore = (IUserRoleStore<ApplicationUser, string>) Store;
        var user = await FindByIdAsync(userId).ConfigureAwait(false);

        if (user == null)
        {
            throw new InvalidOperationException("Invalid user Id");
        }

        var userRoles = await userRoleStore
            .GetRolesAsync(user)
            .ConfigureAwait(false);

        // Remove user to each role using UserRoleStore
        foreach (var role in roles.Where(userRoles.Contains))
        {
            await userRoleStore
                .RemoveFromRoleAsync(user, role)
                .ConfigureAwait(false);
        }

        // Call update once when all roles are removed
        return await UpdateAsync(user).ConfigureAwait(false);
    }
}
قطعه کد بالا گرچه شاید به نظر طولانی بیاید، اما در کل، کلاس ApplicationUserManager توابع مهمی را برای مدیریت کاربران فراهم می‌کند: ایجاد کاربران جدید، انتساب کاربران به نقش‌ها و حذف کاربران از نقش ها. این کلاس بخودی خود از کلاس <UserManager<ApplicationUser ارث بری می‌کند بنابراین تمام قابلیت‌های UserManager در این کلاس هم در دسترس است. اگر به متد ()Create دقت کنید می‌بینید که وهله ای از نوع ApplicationUserManager باز می‌گرداند. بیشتر تنظیمات پیکربندی و تنظیمات پیش فرض کاربران شما در این متد اتفاق می‌افتد.

مورد حائز اهمیت بعدی در متد ()Create فراخوانی ()<context.Get<ApplicationDBContext است. بیاد بیاورید که پیشتر نگاهی به متد ()ConfigAuth داشتیم که چند فراخوانی CreatePerOwinContext داشت که توسط آنها Callback هایی را رجیستر می‌کردیم. فراخوانی متد ()<context.Get<ApplicationDBContext این Callback‌‌ها را صدا می‌زند، که در اینجا فراخوانی متد استاتیک ()ApplicationDbContext.Create خواهد بود. در ادامه بیشتر درباره این قسمت خواهید خواهند.

اگر دقت کنید می‌بینید که احراز هویت، تعیین سطوح دسترسی و تنظیمات مدیریتی و مقادیر پیش فرض آنها در متد ()Create انجام می‌شوند و سپس وهله ای از نوع خود کلاس ApplicationUserManager بازگشت داده می‌شود. همچنین سرویس‌های احراز هویت دو مرحله ای نیز در همین مرحله پیکربندی می‌شوند. اکثر پیکربندی‌ها و تنظیمات نیازی به توضیح ندارند و قابل درک هستند. اما احراز هویت دو مرحله ای نیاز به بررسی عمیق‌تری دارد. در ادامه به این قسمت خواهیم پرداخت. اما پیش از آن نگاهی به کلاس ApplicationRoleManager بیاندازیم.
public class ApplicationRoleManager : RoleManager<IdentityRole>
{
    public ApplicationRoleManager(IRoleStore<IdentityRole,string> roleStore)
        : base(roleStore)
    {
    }

    public static ApplicationRoleManager Create(
        IdentityFactoryOptions<ApplicationRoleManager> options, 
        IOwinContext context)
    {
        var manager = new ApplicationRoleManager(
            new RoleStore<IdentityRole>(
                context.Get<ApplicationDbContext>()));

        return manager;
    }
}
مانند کلاس ApplicationUserManager مشاهده می‌کنید که کلاس ApplicationRoleManager از <RoleManager<IdentityRole ارث بری می‌کند. بنابراین تمام قابلیت‌های کلاس پایه نیز در این کلاس در دسترس هستند. یکبار دیگر متدی بنام ()Create را مشاهده می‌کنید که وهله ای از نوع خود کلاس بر می‌گرداند.


سرویس‌های ایمیل و پیامک برای احراز هویت دو مرحله ای و تایید حساب‌های کاربری
دو کلاس دیگری که در فایل IdentityConfig.cs وجود دارند کلاس‌های EmailService و SmsService هستند. بصورت پیش فرض این کلاس‌ها تنها یک wrapper هستند که می‌توانید با توسعه آنها سرویس‌های مورد نیاز برای احراز هویت دو مرحله ای و تایید حساب‌های کاربری را بسازید.
public class EmailService : IIdentityMessageService
{
    public Task SendAsync(IdentityMessage message)
    {
        // Plug in your email service here to send an email.
        return Task.FromResult(0);
    }
}

public class SmsService : IIdentityMessageService
{
    public Task SendAsync(IdentityMessage message)
    {
        // Plug in your sms service here to send a text message.
        return Task.FromResult(0);
    }
}
دقت کنید که هر دو این کلاس‌ها قرارداد واحدی را بنام IIdentityMessageService پیاده سازی می‌کنند. همچنین قطعه کد زیر را از متد ()ApplicationUserManager.Create بیاد آورید.
// Register two factor authentication providers. This application uses 
// Phone and Emails as a step of receiving a code for verifying 
// the user You can write your own provider and plug in here.
manager.RegisterTwoFactorProvider("PhoneCode", 
    new PhoneNumberTokenProvider<ApplicationUser>
{
    MessageFormat = "Your security code is: {0}"
});

manager.RegisterTwoFactorProvider("EmailCode", 
    new EmailTokenProvider<ApplicationUser>
{
    Subject = "SecurityCode",
    BodyFormat = "Your security code is {0}"
});

manager.EmailService = new EmailService();
manager.SmsService = new SmsService();
همانطور که می‌بینید در متد ()Create کلاس‌های EmailService و SmsService وهله سازی شده و توسط خواص مرتبط به وهله ApplicationUserManager ارجاع می‌شوند.


کلاس کمکی SignIn
هنگام توسعه پروژه مثال Identity، تیم توسعه دهندگان کلاسی کمکی برای ما ساخته‌اند که فرامین عمومی احراز هویت کاربران و ورود آنها به اپلیکیشن را توسط یک API ساده فراهم می‌سازد. برای آشنایی با نحوه استفاده از این متد‌ها می‌توانیم به کنترلر AccountController در پوشه Controllers مراجعه کنیم. اما پیش از آن بگذارید نگاهی به خود کلاس SignInHelper داشته باشیم.
public class SignInHelper
{
    public SignInHelper(
        ApplicationUserManager userManager, 
        IAuthenticationManager authManager)
    {
        UserManager = userManager;
        AuthenticationManager = authManager;
    }

    public ApplicationUserManager UserManager { get; private set; }
    public IAuthenticationManager AuthenticationManager { get; private set; }

    public async Task SignInAsync(
        ApplicationUser user, 
        bool isPersistent, 
        bool rememberBrowser)
    {
        // Clear any partial cookies from external or two factor partial sign ins
        AuthenticationManager.SignOut(
            DefaultAuthenticationTypes.ExternalCookie, 
            DefaultAuthenticationTypes.TwoFactorCookie);

        var userIdentity = await user.GenerateUserIdentityAsync(UserManager);

        if (rememberBrowser)
        {
            var rememberBrowserIdentity = 
                AuthenticationManager.CreateTwoFactorRememberBrowserIdentity(user.Id);

            AuthenticationManager.SignIn(
                new AuthenticationProperties { IsPersistent = isPersistent }, 
                userIdentity, 
                rememberBrowserIdentity);
        }
        else
        {
            AuthenticationManager.SignIn(
                new AuthenticationProperties { IsPersistent = isPersistent }, 
                userIdentity);
        }
    }

    public async Task<bool> SendTwoFactorCode(string provider)
    {
        var userId = await GetVerifiedUserIdAsync();

        if (userId == null)
        {
            return false;
        }

        var token = await UserManager.GenerateTwoFactorTokenAsync(userId, provider);

        // See IdentityConfig.cs to plug in Email/SMS services to actually send the code
        await UserManager.NotifyTwoFactorTokenAsync(userId, provider, token);

        return true;
    }

    public async Task<string> GetVerifiedUserIdAsync()
    {
        var result = await AuthenticationManager.AuthenticateAsync(
            DefaultAuthenticationTypes.TwoFactorCookie);

        if (result != null && result.Identity != null 
            && !String.IsNullOrEmpty(result.Identity.GetUserId()))
        {
            return result.Identity.GetUserId();
        }

        return null;
    }

    public async Task<bool> HasBeenVerified()
    {
        return await GetVerifiedUserIdAsync() != null;
    }

    public async Task<SignInStatus> TwoFactorSignIn(
        string provider, 
        string code, 
        bool isPersistent, 
        bool rememberBrowser)
    {
        var userId = await GetVerifiedUserIdAsync();

        if (userId == null)
        {
            return SignInStatus.Failure;
        }

        var user = await UserManager.FindByIdAsync(userId);

        if (user == null)
        {
            return SignInStatus.Failure;
        }

        if (await UserManager.IsLockedOutAsync(user.Id))
        {
            return SignInStatus.LockedOut;
        }

        if (await UserManager.VerifyTwoFactorTokenAsync(user.Id, provider, code))
        {
            // When token is verified correctly, clear the access failed 
            // count used for lockout
            await UserManager.ResetAccessFailedCountAsync(user.Id);
            await SignInAsync(user, isPersistent, rememberBrowser);

            return SignInStatus.Success;
        }

        // If the token is incorrect, record the failure which 
        // also may cause the user to be locked out
        await UserManager.AccessFailedAsync(user.Id);

        return SignInStatus.Failure;
    }

    public async Task<SignInStatus> ExternalSignIn(
        ExternalLoginInfo loginInfo, 
        bool isPersistent)
    {
        var user = await UserManager.FindAsync(loginInfo.Login);

        if (user == null)
        {
            return SignInStatus.Failure;
        }

        if (await UserManager.IsLockedOutAsync(user.Id))
        {
            return SignInStatus.LockedOut;
        }

        return await SignInOrTwoFactor(user, isPersistent);
    }

    private async Task<SignInStatus> SignInOrTwoFactor(
        ApplicationUser user, 
        bool isPersistent)
    {
        if (await UserManager.GetTwoFactorEnabledAsync(user.Id) &&
            !await AuthenticationManager.TwoFactorBrowserRememberedAsync(user.Id))
        {
            var identity = new ClaimsIdentity(DefaultAuthenticationTypes.TwoFactorCookie);
            identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id));

            AuthenticationManager.SignIn(identity);
            return SignInStatus.RequiresTwoFactorAuthentication;
        }

        await SignInAsync(user, isPersistent, false);
        return SignInStatus.Success;
    }

    public async Task<SignInStatus> PasswordSignIn(
        string userName, 
        string password, 
        bool isPersistent, 
        bool shouldLockout)
    {
        var user = await UserManager.FindByNameAsync(userName);

        if (user == null)
        {
            return SignInStatus.Failure;
        }

        if (await UserManager.IsLockedOutAsync(user.Id))
        {
            return SignInStatus.LockedOut;
        }

        if (await UserManager.CheckPasswordAsync(user, password))
        {
            return await SignInOrTwoFactor(user, isPersistent);
        }

        if (shouldLockout)
        {
            // If lockout is requested, increment access failed 
            // count which might lock out the user
            await UserManager.AccessFailedAsync(user.Id);

            if (await UserManager.IsLockedOutAsync(user.Id))
            {
                return SignInStatus.LockedOut;
            }
        }

        return SignInStatus.Failure;
    }
}
کد این کلاس نسبتا طولانی است، و بررسی عمیق آنها از حوصله این مقاله خارج است. گرچه اگر به دقت یکبار این کلاس را مطالعه کنید می‌توانید براحتی از نحوه کارکرد آن آگاه شوید. همانطور که می‌بینید اکثر متدهای این کلاس مربوط به ورود کاربران و مسئولیت‌های تعیین سطوح دسترسی است.

این متدها ویژگی‌های جدیدی که در Identity 2.0 عرضه شده اند را در بر می‌گیرند. متد آشنایی بنام ()SignInAsync را می‌بینیم، و متدهای دیگری که مربوط به احراز هویت دو مرحله ای و external log-ins می‌شوند. اگر به متدها دقت کنید خواهید دید که برای ورود کاربران به اپلیکیشن کارهای بیشتری نسبت به نسخه پیشین انجام می‌شود.

بعنوان مثال متد Login در کنترلر AccountController را باز کنید تا نحوه مدیریت احراز هویت در Identity 2.0 را ببینید.
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }

    // This doen't count login failures towards lockout only two factor authentication
    // To enable password failures to trigger lockout, change to shouldLockout: true
    var result = await SignInHelper.PasswordSignIn(
        model.Email, 
        model.Password, 
        model.RememberMe, 
        shouldLockout: false);

    switch (result)
    {
        case SignInStatus.Success:
            return RedirectToLocal(returnUrl);
        case SignInStatus.LockedOut:
            return View("Lockout");
        case SignInStatus.RequiresTwoFactorAuthentication:
            return RedirectToAction("SendCode", new { ReturnUrl = returnUrl });
        case SignInStatus.Failure:
        default:
            ModelState.AddModelError("", "Invalid login attempt.");

            return View(model);
    }
}

مقایسه Sign-in با نسخه Identity 1.0
در نسخه 1.0 این فریم ورک، ورود کاربران به اپلیکیشن مانند لیست زیر انجام می‌شد. اگر متد Login در کنترلر AccountController را باز کنید چنین قطعه کدی را می‌بینید.
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
    if (ModelState.IsValid)
    {
        var user = await UserManager.FindAsync(model.UserName, model.Password);

        if (user != null)
        {
            await SignInAsync(user, model.RememberMe);
            return RedirectToLocal(returnUrl);
        }
        else
        {
            ModelState.AddModelError("", "Invalid username or password.");
        }
    }

    // If we got this far, something failed, redisplay form
    return View(model);
}
در قطعه کد بالا متدی در کلاس UserManager را فراخوانی می‌کنیم که مشابه قطعه کدی است که در کلاس SignInHelper دیدیم. همچنین متد SignInAsync را فراخوانی می‌کنیم که مستقیما روی کنترلر AccountController تعریف شده است.
private async Task SignInAsync(ApplicationUser user, bool isPersistent)
{
    AuthenticationManager.SignOut(
        DefaultAuthenticationTypes.ExternalCookie);

    var identity = await UserManager.CreateIdentityAsync(
        user, DefaultAuthenticationTypes.ApplicationCookie);

    AuthenticationManager.SignIn(
        new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
}
مسلما با عرضه قابلیت‌های جدید در Identity 2.0 و تغییرات معماری این فریم ورک، پیچیدگی هایی معرفی می‌شود که حتی در امور ساده ای مانند ورود کاربران نیز قابل مشاهده است.


ApplicationDbContext
اگر از نسخه پیشین Identity در اپلیکیشن‌های ASP.NET MVC استفاده کرده باشید با کلاس ApplicationDbContext آشنا هستید. این کلاس پیاده سازی پیش فرض EF فریم ورک است، که اپلیکیشن شما توسط آن داده‌های مربوط به Identity را ذخیره و بازیابی می‌کند.

در پروژه مثال ها، تیم Identity این کلاس را بطور متفاوتی نسبت به نسخه 1.0 پیکربندی کرده اند. اگر فایل IdentityModels.cs را باز کنید تعاریف کلاس ApplicationDbContext را مانند لیست زیر خواهید یافت.
public class ApplicationDbContext : IdentityDbContext<ApplicationUser> {
    public ApplicationDbContext()
        : base("DefaultConnection", throwIfV1Schema: false) {
    }

    static ApplicationDbContext() {
        // Set the database intializer which is run once during application start
        // This seeds the database with admin user credentials and admin role
        Database.SetInitializer<ApplicationDbContext>(new ApplicationDbInitializer());
    }

    public static ApplicationDbContext Create() {
        return new ApplicationDbContext();
    }
}
قطعه کد بالا دو متد استاتیک تعریف می‌کند. یکی ()Create و دیگری ()ApplicationDbContext که سازنده دیتابیس (database initializer) را تنظیم می‌کند. این متد هنگام اجرای اپلیکیشن فراخوانی می‌شود و هر پیکربندی ای که در کلاس ApplicationDbInitializer تعریف شده باشد را اجرا می‌کند. اگر به فایل IdentityConfig.cs مراجعه کنیم می‌توانیم تعاریف این کلاس را مانند لیست زیر بیابیم.
public class ApplicationDbInitializer 
    : DropCreateDatabaseIfModelChanges<ApplicationDbContext> 
{
    protected override void Seed(ApplicationDbContext context) 
    {
        InitializeIdentityForEF(context);
        base.Seed(context);
    }

    public static void InitializeIdentityForEF(ApplicationDbContext db) 
    {
        var userManager = HttpContext
            .Current.GetOwinContext()
            .GetUserManager<ApplicationUserManager>();

        var roleManager = HttpContext.Current
            .GetOwinContext()
            .Get<ApplicationRoleManager>();

        const string name = "admin@admin.com";
        const string password = "Admin@123456";
        const string roleName = "Admin";

        //Create Role Admin if it does not exist
        var role = roleManager.FindByName(roleName);

        if (role == null) 
        {
            role = new IdentityRole(roleName);
            var roleresult = roleManager.Create(role);
        }

        var user = userManager.FindByName(name);

        if (user == null) 
        {
            user = new ApplicationUser { UserName = name, Email = name };

            var result = userManager.Create(user, password);
            result = userManager.SetLockoutEnabled(user.Id, false);
        }

        // Add user admin to Role Admin if not already added
        var rolesForUser = userManager.GetRoles(user.Id);

        if (!rolesForUser.Contains(role.Name)) 
        {
            var result = userManager.AddToRole(user.Id, role.Name);
        }
    }
}
پیکربندی جاری در صورتی که مدل موجودیت‌ها تغییر کنند دیتابیس را پاک کرده و مجددا ایجاد می‌کند. در غیر اینصورت از دیتابیس موجود استفاده خواهد شد. اگر بخواهیم با هر بار اجرای اپلیکیشن دیتابیس از نو ساخته شود، می‌توانیم کلاس مربوطه را به <DropCreateDatabaseAlways<ApplicationDbContext تغییر دهیم. بعنوان مثال هنگام توسعه اپلیکیشن و بمنظور تست می‌توانیم از این رویکرد استفاده کنیم تا هر بار با دیتابیسی (تقریبا) خالی شروع کنیم.

نکته حائز اهمیت دیگر متد ()InitializeIdentityForEF است. این متد کاری مشابه متد ()Seed انجام می‌دهد که هنگام استفاده از مهاجرت‌ها (Migrations) از آن استفاده می‌کنیم. در این متد می‌توانید رکوردهای اولیه ای را در دیتابیس ثبت کنید. همانطور که مشاهده می‌کنید در قطعه کد بالا نقشی مدیریتی بنام Admin ایجاد شده و کاربر جدیدی با اطلاعاتی پیش فرض ساخته می‌شود که در آخر به این نقش منتسب می‌گردد. با انجام این مراحل، پس از اجرای اولیه اپلیکیشن کاربری با سطح دسترسی مدیر در اختیار خواهیم داشت که برای تست اپلیکیشن بسیار مفید خواهد بود.

در این مقاله نگاهی اجمالی به Identity 2.0 در پروژه‌های ASP.NET MVC داشتیم. کامپوننت‌های مختلف فریم ورک و نحوه پیکربندی آنها را بررسی کردیم و با تغییرات و قابلیت‌های جدید به اختصار آشنا شدیم. در مقالات بعدی بررسی هایی عمیق‌تر خواهیم داشت و با نحوه استفاده و پیاده سازی قسمت‌های مختلف این فریم ورک آشنا خواهیم شد.


مطالعه بیشتر
مطالب
احراز هویت و اعتبارسنجی کاربران در برنامه‌های Angular - قسمت پنجم - محافظت از مسیرها
در قسمت سوم، کار ورود به سیستم و امکان مشاهده‌ی صفحه‌ی محافظت شده‌ی پس از لاگین را پیاده سازی کردیم. در این قسمت می‌خواهیم امکان دسترسی به مسیر http://localhost:4200/protectedPage را کنترل کنیم. تا اینجا اگر کاربر بدون لاگین کردن نیز این مسیر را درخواست کند، می‌تواند حداقل ساختار ابتدایی آن‌را مشاهده کند که باید مدیریت شود و این مدیریت دسترسی می‌تواند بر اساس وضعیت لاگین کاربر و همچنین نقش‌های او در سیستم باشد:


طراحی بخش‌هایی از این قسمت، از پروژه‌ی «کنترل دسترسی‌ها در Angular با استفاده از Ng2Permission» ایده گرفته شده‌اند.


استخراج اطلاعات کاربر وارد شده‌ی به سیستم از توکن دسترسی او

یکی از روش‌های دسترسی به اطلاعات کاربر در سمت کلاینت، مانند نقش‌های او، تدارک متدی در سمت سرور و بازگشت Claims او به سمت کلاینت است:
public IActionResult Get()
{ 
    var user = this.User.Identity as ClaimsIdentity; 
    var config = new 
    { 
        userName = user.Name, 
        roles = user.Claims.Where(x => x.Type == ClaimTypes.Role).Select(x => x.Value).ToList() 
    }; 
    return Ok(config); 
}
 اما با توجه به اینکه در زمان لاگین، نقش‌های او (و سایر Claims دلخواه) نیز به توکن دسترسی نهایی اضافه می‌شوند، می‌توان این کوئری گرفتن مدام از سرور را تبدیل به روش بسیار سریعتر استخراج آن‌ها از توکنی که هم اکنون در کش مرورگر ذخیره شده‌است، کرد.
همچنین باید دقت داشت چون این توکن دارای امضای دیجیتال است، کوچکترین تغییری در آن در سمت کاربر، سبب برگشت خوردن خودکار درخواست ارسالی به سمت سرور می‌شود (یکی از مراحل اعتبارسنجی کاربر در سمت سرور، اعتبارسنجی توکن دریافتی (قسمت cfg.TokenValidationParameters) و همچنین بررسی خودکار امضای دیجیتال آن است). بنابراین نگرانی از این بابت وجود ندارد.
اگر اطلاعات کاربر در سمت سرور تغییر کنند، با اولین درخواست ارسالی به سمت سرور، رخ‌داد OnTokenValidated وارد عمل شده و درخواست ارسالی را برگشت می‌زند (در مورد پیاده سازی سمت سرور این مورد، در مطلب «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» بیشتر بحث شده‌است). در این حالت کاربر مجبور به لاگین مجدد خواهد شد که این مورد سبب به روز رسانی خودکار اطلاعات توکن‌های ذخیره شده‌ی او در مرورگر نیز می‌شود.

اگر از قسمت دوم این سری به‌خاطر داشته باشید، توکن decode شده‌ی برنامه، چنین شکلی را دارد:
{
  "jti": "d1272eb5-1061-45bd-9209-3ccbc6ddcf0a",
  "iss": "http://localhost/",
  "iat": 1513070340,
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "1",
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "Vahid",
  "DisplayName": "وحید",
  "http://schemas.microsoft.com/ws/2008/06/identity/claims/serialnumber": "709b64868a1d4d108ee58369f5c3c1f3",
  "http://schemas.microsoft.com/ws/2008/06/identity/claims/userdata": "1",
  "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": [
    "Admin",
    "User"
  ],
  "nbf": 1513070340,
  "exp": 1513070460,
  "aud": "Any"
}
به همین جهت متدی را برای تبدیل این اطلاعات به شیء کاربر، ایجاد خواهیم کرد و در سراسر برنامه از این اطلاعات آماده، برای تعیین دسترسی او به قسمت‌های مختلف برنامه‌ی سمت کلاینت، استفاده می‌کنیم.
برای این منظور اینترفیس src\app\core\models\auth-user.ts را به صورت ذیل ایجاد می‌کنیم:
export interface AuthUser {
  userId: string;
  userName: string;
  displayName: string;
  roles: string[];
}
پس از آن متد getAuthUser را جهت استخراج خواص ویژه‌ی توکن دسترسی decode شده‌ی فوق، به صورت ذیل به سرویس Auth اضافه می‌کنیم:
  getAuthUser(): AuthUser {
    if (!this.isLoggedIn()) {
      return null;
    }

    const decodedToken = this.getDecodedAccessToken();
    let roles = decodedToken["http://schemas.microsoft.com/ws/2008/06/identity/claims/role"];
    if (roles) {
      roles = roles.map(role => role.toLowerCase());
    }
    return Object.freeze({
      userId: decodedToken["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"],
      userName: decodedToken["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"],
      displayName: decodedToken["DisplayName"],
      roles: roles
    });
  }
کار با این متد بسیار سریع است و نیازی به رفت و برگشتی به سمت سرور ندارد؛ چون تمام اطلاعات استخراجی توسط آن هم اکنون در کش سریع مرورگر کلاینت موجود هستند. استفاده‌ی از متد Object.freeze هم سبب read-only شدن این خروجی می‌شود.
همچنین در اینجا تمام نقش‌های دریافتی، تبدیل به LowerCase شده‌اند. با اینکار مقایسه‌ی بعدی آن‌ها با نقش‌های درخواستی، حساس به بزرگی و کوچکی حروف نخواهد بود.


تعریف نقش‌های دسترسی به مسیرهای مختلف سمت کلاینت

مرسوم است اطلاعات اضافی مرتبط با هر مسیر را به خاصیت data آن route انتساب می‌دهند. به همین جهت به فایل dashboard-routing.module.ts مراجعه کرده و نقش‌های مجاز به دسترسی به مسیر protectedPage را به خاصیت data آن به صورت ذیل اضافه می‌کنیم:
import { ProtectedPageComponent } from "./protected-page/protected-page.component";
import { AuthGuardPermission } from "../core/models/auth-guard-permission";

const routes: Routes = [
  {
    path: "protectedPage",
    component: ProtectedPageComponent,
    data: {
      permission: {
        permittedRoles: ["Admin"],
        deniedRoles: null
      } as AuthGuardPermission
    }
  }
];
که در اینجا ساختار اینترفیس AuthGuardPermission، در فایل جدید app\core\models\auth-guard-permission.ts به صورت ذیل تعریف شده‌است:
export interface AuthGuardPermission {
  permittedRoles?: string[];
  deniedRoles?: string[];
}
به این ترتیب هر قسمتی از برنامه که نیاز به اطلاعات سطوح دسترسی مسیری را داشت، ابتدا به دنبال route.data["permission"] خواهد گشت (کلیدی به نام permission در خاصیت data یک مسیر) و سپس اطلاعات آن‌را بر اساس ساختار اینترفیس AuthGuardPermission، تحلیل می‌کند.
در اینجا تنها باید یکی از خواص permittedRoles  (نقش‌های مجاز به دسترسی/صدور دسترسی فقط برای این نقش‌های مشخص، منهای مابقی) و یا deniedRoles (نقش‌های غیرمجاز به دسترسی/دسترسی همه‌ی نقش‌های ممکن، منهای این نقش‌های تعیین شده)، مقدار دهی شوند.


افزودن کامپوننت «دسترسی ندارید» به ماژول Authentication

در ادامه می‌خواهیم اگر کاربری به مسیری دسترسی نداشت، به صورت خودکار به صفحه‌ی «دسترسی ندارید» هدایت شود. به همین جهت این کامپوننت را به صورت ذیل به ماژول authentication اضافه می‌کنیم:
>ng g c Authentication/AccessDenied
با این خروجی که سبب درج خودکار آن در قسمت declaration فایل authentication.module نیز می‌شود:
 AccessDenied
  create src/app/Authentication/access-denied/access-denied.component.html (32 bytes)
  create src/app/Authentication/access-denied/access-denied.component.ts (296 bytes)
  create src/app/Authentication/access-denied/access-denied.component.css (0 bytes)
  update src/app/Authentication/authentication.module.ts (550 bytes)
سپس به فایل authentication-routing.module.ts مراجعه کرده و مسیریابی آن‌را نیز اضافه می‌کنیم:
import { LoginComponent } from "./login/login.component";
import { AccessDeniedComponent } from "./access-denied/access-denied.component";

const routes: Routes = [
  { path: "login", component: LoginComponent },
  { path: "accessDenied", component: AccessDeniedComponent }
];
قالب access-denied.component.html را نیز به صورت ذیل تکمیل می‌کنیم:
<h1 class="text-danger">
  <span class="glyphicon glyphicon-ban-circle"></span> Access Denied
</h1>
<p>Sorry! You don't have access to this page.</p>
<button class="btn btn-default" (click)="goBack()">
  <span class="glyphicon glyphicon-arrow-left"></span> Back
</button>

<button *ngIf="!isAuthenticated" class="btn btn-success" [routerLink]="['/login']"
  queryParamsHandling="merge">
  Login
</button>
که دکمه‌ی Back آن به کمک سرویس Location، صورت ذیل پیاده سازی شده‌است:
import { Component, OnInit } from "@angular/core";
import { Location } from "@angular/common";

import { AuthService } from "../../core/services/auth.service";

@Component({
  selector: "app-access-denied",
  templateUrl: "./access-denied.component.html",
  styleUrls: ["./access-denied.component.css"]
})
export class AccessDeniedComponent implements OnInit {

  isAuthenticated = false;

  constructor(
    private location: Location,
    private authService: AuthService
  ) {
  }

  ngOnInit() {
    this.isAuthenticated = this.authService.isLoggedIn();
  }

  goBack() {
    this.location.back(); // <-- go back to previous location on cancel
  }
}


در اینجا اگر کاربر به سیستم وارد نشده باشد، دکمه‌ی لاگین نیز به او نمایش داده می‌شود. همچنین وجود "queryParamsHandling="merge در لینک مراجعه‌ی به صفحه‌ی لاگین، سبب خواهد شد تا query string موجود در صفحه نیز حفظ شود و به صفحه‌ی لاگین انتقال پیدا کند. در صفحه‌ی لاگین نیز جهت پردازش این نوع کوئری استرینگ‌ها، تمهیدات لازم درنظر گرفته شده‌اند.
دکمه‌ی back آن نیز توسط سرویس توکار Location واقع در مسیر angular/common@ پیاده سازی شده‌است.


ایجاد یک محافظ مسیر سمت کلاینت برای بررسی وضعیت کاربر جاری و همچنین نقش‌های او

پس از تعریف متد getAuthUser و استخراج اطلاعات کاربر از توکن دسترسی دریافتی که شامل نقش‌های او نیز می‌شود، اکنون می‌توان متد بررسی این نقش‌ها را نیز به سرویس Auth اضافه کرد:
  isAuthUserInRoles(requiredRoles: string[]): boolean {
    const user = this.getAuthUser();
    if (!user || !user.roles) {
      return false;
    }
    return requiredRoles.some(requiredRole => user.roles.indexOf(requiredRole.toLowerCase()) >= 0);
  }

  isAuthUserInRole(requiredRole: string): boolean {
    return this.isAuthUserInRoles([requiredRole]);
  }
متد some در جاوا اسکریپت شبیه به متد Any در C# LINQ عمل می‌کند. همچنین در مقایسه‌ی صورت گرفته، با توجه به اینکه user.roles را پیشتر به LowerCase تبدیل کرد‌یم، حساسیتی بین نقش Admin و admin و کلا کوچکی و بزرگی نام نقش‌ها وجود ندارد.
اکنون در هر قسمتی از برنامه که نیاز به بررسی امکان دسترسی یک کاربر به نقش یا نقش‌هایی خاص وجود داشته باشد، می‌توان AuthService را به سازنده‌ی ‌آن تزریق و سپس از متد فوق جهت بررسی نهایی، استفاده کرد.

در ادامه یک Route Guard جدید را در مسیر app\core\services\auth.guard.ts ایجاد می‌کنیم. کار آن بررسی خودکار امکان دسترسی به یک مسیر درخواستی است:
import { Injectable } from "@angular/core";
import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from "@angular/router";

import { AuthService } from "./auth.service";
import { AuthGuardPermission } from "../models/auth-guard-permission";

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private authService: AuthService, private router: Router) { }

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {

    if (!this.authService.isLoggedIn()) {
      this.showAccessDenied(state);
      return false;
    }

    const permissionData = route.data["permission"] as AuthGuardPermission;
    if (!permissionData) {
      return true;
    }

    if (Array.isArray(permissionData.deniedRoles) && Array.isArray(permissionData.permittedRoles)) {
      throw new Error("Don't set both 'deniedRoles' and 'permittedRoles' in route data.");
    }

    if (Array.isArray(permissionData.permittedRoles)) {
      const isInRole = this.authService.isAuthUserInRoles(permissionData.permittedRoles);
      if (isInRole) {
        return true;
      }

      this.showAccessDenied(state);
      return false;
    }

    if (Array.isArray(permissionData.deniedRoles)) {
      const isInRole = this.authService.isAuthUserInRoles(permissionData.deniedRoles);
      if (!isInRole) {
        return true;
      }

      this.showAccessDenied(state);
      return false;
    }
  }

  private showAccessDenied(state: RouterStateSnapshot) {
    this.router.navigate(["/accessDenied"], { queryParams: { returnUrl: state.url } });
  }
}
در اینجا در ابتدا وضعیت لاگین کاربر بررسی می‌گردد. این وضعیت نیز از طریق سرویس Auth که به سازنده‌ی کلاس تزریق شده‌است، تامین می‌شود. اگر کاربر هنوز لاگین نکرده باشد، به صفحه‌ی عدم دسترسی هدایت خواهد شد.
سپس خاصیت permission اطلاعات مسیر استخراج می‌شود. اگر چنین مقداری وجود نداشت، همینجا کار با موفقیت خاتمه پیدا می‌کند.
در آخر وضعیت دسترسی به نقش‌های استخراجی deniedRoles و permittedRoles که از اطلاعات مسیر دریافت شدند، توسط متد isAuthUserInRoles سرویس Auth بررسی می‌شوند.

در متد showAccessDenied کار ارسال آدرس درخواستی (state.url) به صورت یک کوئری استرینگ (queryParams) با کلید returnUrl به صفحه‌ی accessDenied صورت می‌گیرد. در این صفحه نیز دکمه‌ی لاگین به همراه "queryParamsHandling="merge است. یعنی کامپوننت لاگین برنامه، کوئری استرینگ returnUrl را دریافت می‌کند:
 this.returnUrl = this.route.snapshot.queryParams["returnUrl"];
 و پس از لاگین موفق، در صورت وجود این کوئری استرینگ، هدایت خودکار کاربر، به مسیر returnUrl پیشین صورت خواهد گرفت:
if (this.returnUrl) {
   this.router.navigate([this.returnUrl]);
} else {
   this.router.navigate(["/protectedPage"]);
}

محل معرفی این AuthGuard جدید که در حقیقت یک سرویس است، در ماژول Core، در قسمت providers آن، به صورت ذیل می‌باشد:
import { AuthGuard } from "./services/auth.guard";

@NgModule({
  providers: [
    AuthGuard
  ]
})
export class CoreModule {}
در آخر برای اعمال این Guard جدید، به فایل dashboard-routing.module.ts مراجعه کرده و خاصیت canActivate را مقدار دهی می‌کنیم:
import { ProtectedPageComponent } from "./protected-page/protected-page.component";
import { AuthGuardPermission } from "../core/models/auth-guard-permission";
import { AuthGuard } from "../core/services/auth.guard";

const routes: Routes = [
  {
    path: "protectedPage",
    component: ProtectedPageComponent,
    data: {
      permission: {
        permittedRoles: ["Admin"],
        deniedRoles: null
      } as AuthGuardPermission
    },
    canActivate: [AuthGuard]
  }
];
به این ترتیب با درخواست این مسیر، پیش از فعالسازی و نمایش آن، توسط AuthGuard معرفی شده‌ی به آن، کار بررسی وضعیت کاربر و نقش‌های او که از خاصیت permission خاصیت data دریافت می‌شوند، صورت گرفته و اگر عملیات تعیین اعتبار اطلاعات با موفقیت به پایان رسید، آنگاه کاربر مجوز دسترسی به این قسمت از برنامه را خواهد یافت.

اگر قصد آزمایش آن‌را داشتید، فقط کافی است بجای نقش Admin، مثلا Admin1 را در permittedRoles مقدار دهی کنید، تا صفحه‌ی access denied را در صورت درخواست مسیر protectedPage، بتوان مشاهده کرد.




کدهای کامل این سری را از اینجا می‌توانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه‌ی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشه‌ی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
مطالب دوره‌ها
اصل معکوس سازی وابستگی‌ها
پیش از شروع این سری نیاز است با تعدادی از واژه‌های بکار رفته در آن به اختصار آشنا شویم؛ از این واژه‌ها به کرات استفاده شده و در طول دوره به بررسی جزئیات آن‌ها خواهیم پرداخت:

1) Dependency inversion principle یا DIP (اصل معکوس سازی وابستگی‌ها)
DIP یکی از اصول طراحی نرم افزار است و D آن همان D معروف SOLID است (اصول پذیرفته شده شیءگرایی).

2) Inversion of Control یا IOC (معکوس سازی کنترل)
الگویی است که نحوه پیاده سازی DIP را بیان می‌کند.

3) Dependency injection یا DI (تزریق وابستگی‌ها)
یکی از روش‌های پیاده سازی IOC است.

4) IOC container
به فریم ورک‌هایی که کار DI را انجام می‌دهند گفته می‌شود.


Dependency inversion principle چیست؟

اصل معکوس سازی وابستگی‌ها به این معنا است که بجای اینکه ماژول‌های سطح پایین سیستم، رابط‌های قابل استفاده‌ای از خود را در اختیار سطوح بالاتر سیستم قرار دهند، ماژول‌های قرار گرفته در سطوحی بالاتر، اینترفیس‌هایی را تعریف می‌کنند که توسط ماژول‌های سطح پایین پیاده سازی خواهند شد.
همانطور که ملاحظه می‌کنید به این ترتیب وابستگی‌های سیستم معکوس خواهند شد. نمونه‌ای از عدم استفاده از این طراحی را در دنیای واقعی به صورت رومزه با آن‌ها سر و کار داریم؛ مانند وسایل الکترونیکی قابل حملی که نیاز به شارژ مجدد دارند. برای مثال تلفن‌های همراه، دوربین‌های عکاسی دیجیتال و امثال آن.


هر کدام از این‌ها، رابط‌های اتصالی متفاوتی دارند. یکی USB2، یکی USB3 دیگری Mini USB و بعضی‌ها هم از پورت‌های دیگری استفاده می‌کنند. چون هر کدام از لایه‌های زیرین سیستم (در اینجا وسایل قابل شارژ) رابط‌های اتصالی مختلفی را ارائه داده‌اند، برای اتصال آن‌ها به منبع قدرت که در سطحی بالاتر قرار دارد، نیاز به تبدیلگرها و درگاه‌های مختلفی خواهد بود.
اگر در این نوع طراحی‌ها، اصل معکوس سازی وابستگی‌ها رعایت می‌شد، درگاه و رابط اتصال به منبع قدرت باید تعیین کننده نحوه طراحی اینترفیس‌های لایه‌های زیرین می‌بود تا با این آشفتگی نیاز به انواع و اقسام تبدیلگرها، روبرو نمی‌شدیم.


اگر وابستگی‌ها معکوس نشوند مطابق تصویر فوق، کلاس سطح بالایی را خواهیم داشت که به اینترفیس کلاس‌های سطح پایین وابسته است. البته در اینجا اینترفیس یک کلمه عمومی است و بیشتر نحوه در معرض دید و استفاده قرار دادن اعضای یک کلاس مد نظر بوده است تا اینکه مثلا الزاما اینترفیس‌های زبان خاصی مدنظر باشند.
مشکلی که در این حالت به زودی بروز خواهد کرد، افزایش کلاس‌های سطح پایین و بیشتر شدن وابستگی کلاس‌های سطح بالا به آن‌ها است. به این ترتیب قابلیت استفاده مجدد خود را از دست خواهند داد.


در تصویر فوق حالتی را مشاهده می‌کنید که وابستگی‌ها معکوس شده‌اند. تغییر مهمی که در اینجا نسبت به حالت قبل رخ داده است، بالا بردن اینترفیس، به بالای خط میانی است که در تصویر مشخص گردیده است. این خط، معرف تعریف لایه‌های مختلف سیستم است. به عبارتی کلاس‌های سطح بالا در لایه دیگری نسبت به کلاس‌های سطح پایین قرار دارند. در اینجا اجازه داده‌ایم تا کلاس لایه بالایی اینترفیس مورد نیاز خود را تعریف کند. این نوع اینترفیس‌ها در زبان سی شارپ می‌توانند یک کلاس Abstract و یا حتی یک Interface متداول باشند.
با معکوس شدن وابستگی‌ها، لایه سطح بالا است که به لایه زیرین عنوان می‌کند: تو باید این امکانات را در اختیار من قرار دهی تا بتوانم کارم را انجام دهم.


اکنون اگر در یک سیستم واقعی تعداد کلاس‌های سطح پایین افزایش پیدا کنند، نیازی نیست تا کلاس سطح بالا تغییری کند. کلاس‌های سطح پایین تنها باید عملکردهای تعیین شده در اینترفیس را پیاده سازی کنند. و این برخلاف حالتی است که وابستگی‌ها معکوس نشده‌اند:



تاریخچه اصل معکوس سازی وابستگی‌ها

اصل معکوس سازی وابستگی‌ها در نشریه C++ Report سال 1996 توسط شخصی به نام Bob Martin (معروف به Uncle Bob!) برای اولین بار مطرح گردید. ایشان همچنین یکی از آغاز کنندگان گروهی بود که مباحث Agile را ارائه کردند. به علاوه ایشان برای اولین بار مباحث SOLID را در دنیای شیءگرایی معرفی کردند (همان مباحث معروف هر کلاس باید تک مسئولیتی باشد، باز باشد برای توسعه، بسته برای تغییر و امثال آن که ما در این سری مباحث قسمت D آن‌را در حالت بررسی هستیم).

مطابق تعاریف Uncle Bob:
الف) ماژول‌های سطح بالا نباید به ماژول‌های سطح پایین وابسته باشند. هر دوی این‌ها باید به Abstraction وابسته باشند.
ب) Abstraction نباید وابسته به جزئیات باشد. جزئیات (پیاده سازی‌ها) باید وابسته به Abstraction باشند.


مثال برنامه کپی

اگر به مقاله Uncle Bob مراجعه کنید، یکی از مواردی را که عنوان کرده‌اند، یک برنامه کپی است که می‌تواند اطلاعات را از صفحه کلید دریافت و در یک چاپگر، چاپ کند.


حال اگر به این مجموعه، ذخیره سازی اطلاعات بر روی دیسک سخت را اضافه کنیم چطور؟ به این ترتیب سیستم با افزایش وابستگی‌ها، پیچیدگی و if و elseهای بیشتری را خواهد یافت؛ از این جهت که سطح بالایی سیستم به صورت مستقیم وابسته خواهد بود به ماژول‌های سطح پایین آن.
روشی را که ایشان برای حل این مشکل ارائه داده‌اند، معکوس کردن وابستگی‌ها است:


در اینجا سطح بالایی سیستم وابسته است به یک سری تعاریف Abstract خواندن و یا نوشتن؛ بجای وابستگی مستقیم به پیاده سازی‌های سطح پایین آن‌ها.
در این حالت اگر تعداد Readers و یا Writers افزایش یابند، باز هم سطح بالایی سیستم نیازی نیست تغییر کند زیرا وابسته است به یک اینترفیس و نه پیاده سازی آن که محول شده است به لایه‌های زیرین سیستم.
این مساله بر روی لایه بندی سیستم نیز تاثیرگذار است. در روش متداول برنامه نویسی، لایه بالایی به صورت مستقیم متدهای لایه‌های زیرین را صدا زده و مورد استفاده قرار می‌دهد. به این ترتیب هر تغییری در لایه‌های مختلف، بر روی سایر لایه‌ها به شدت تاثیرگذار خواهد بود. اما در حالت معکوس سازی وابستگی‌ها، هر کدام از لایه‌های بالاتر، از طریق اینترفیس از لایه زیرین خود استفاده خواهد کرد. در این حالت هرگونه تغییری در لایه‌های زیرین برنامه تا زمانیکه اینترفیس تعریف شده را پیاده سازی کنند، اهمیتی نخواهد داشت.


مثال برنامه دکمه و لامپ

مثال دیگری که در مقاله Uncle Bob ارائه شده، مثال برنامه دکمه و لامپ است. در حالت متداول، یک دکمه داریم که وابسته است به لامپ. برای مثال وهله‌ای از لامپ به دکمه ارسال شده و سپس دکمه آن‌را کنترل خواهد کرد (خاموش یا روشن). مشکلی که در اینجا وجود دارد وابستگی دکمه به نوعی خاص از لامپ است و تعویض یا استفاده مجدد از آن به سادگی میسر نیست.
راه حلی که برای این مساله ارائه شده، ارائه یک اینترفیس بین دکمه و لامپ است که خاموش و روشن کردن در آن تعریف شده‌اند. اکنون هر لامپی (یا هر وسیله الکتریکی دیگری) که بتواند این متدها را ارائه دهد، در سیستم قابل استفاده خواهد بود.

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

  • طراحی دیتابیس یا بانک اطلاعاتی بر پایه چند زبانه بودن و بررسی سناریوهای مختلف.
  • نکاتی که باید در ساخت سایت‌های چند زبانه به آن‌ها دقت کرد.
  • شیوه‌ی تشخیص و تغییر زبان سایت
  • معرفی چند کامپوننت وب، برای مباحث چند زبانه

طراحی مدل دیتابیس

اولین کار برای داشتن یک سایت چند زبانه، این است که یک مدل صحیح و مناسب را برای دیتابیس خود انتخاب کنید. یکی از اولین روش‌هایی که به ذهن هر فردی می‌رسد این است که برای هر ستون متنی که قرار است چند زبانه باشد، به تعداد زبان‌ها برایش یک ستون در نظر بگیریم. یعنی برای جدول مقالات که قرار است در سه زبان فارسی و انگلیسی و عربی باشد، سه ستون برای عنوان مقاله و سه ستون نیز برای متن آن داشته باشیم. تصویر زیر نمونه‌ای از این مدل را نشان می‌دهد.

مزایا:

  1. پیاده سازی آسان

معایب:

  1. در این روش با زیاد شدن هر زبان، تعداد ستون‌ها افزایش می‌یابد که باعث می‌شود طراحی مناسبی نداشته باشد.
  2. در ضمن این مورد باید توسط برنامه نویس مرتبا اضافه گردد یا اینکه برنامه نویس این امکان را در سیستم قرار دهد که مدیر سایت بتواند در پشت صحنه کوئری افزودن ستون را ایجاد کند که باید جدول مرتبا مورد alter گرفتن قرار بگیرد.
  3. ممکن است همیشه برای هر زبانی مطلبی قرار نگیرد و این مورد باعث می‌شود بی جهت فضایی برای آن در نظر گرفته شود.

پی نوشت: با اینکه امروزه بحث فیلدهای sparse Column وجود دارد ولی این فیلد‌ها در هر شرایطی مورد استفاده قرار نمی‌گیرند وبیشتر متعلق به زمانی است که می‌دانیم آن فیلد به شدت کم مورد استفاده قرار می‌گیرد.

پی نوشت دوم : در صورتی که فیلد شما مانند متن مقاله که عموما از نوع داده (varchar(max است استفاده می‌کنید و در صورتی که زبان مورد استفاده قرار نگیرد در خیلی از اوقات بی جهت فیلد‌های Blob ساخته اید که بهینه سازی آن را نیز باید در نظر بگیرید.


در مرحله‌ی بعد برای رفع مشکلات بالا یک جدول از زبان‌ها، مانند جدول زیر را ایجاد می‌کنیم:
 
 ID  کد
 Language  زبان
 ISO  کد دو رقمی آن زبان
 Flag  پرچم آن کشور
بعد از آن هر مقاله برای یک زبان ایجاد خواهد شد؛ چیزی مانند تصویر زیر:

مزایا:

  1. پیاده سازی آسان

معایب:

  1. ایجاد رکوردهای تکراری، هر مقاله برای بعضی از اطلاعاتش که چند زبانه نیستند داده‌های تکراری خواهد داشت.
  2. هر مقاله یک مقاله‌ی جدا شناخته می‌شود و ارتباطی میان آنان نخواهد بود. بدین ترتیب توانایی ایجاد گزارش‌هایی چون هر گروه از مقاله و دسته بندی آن‌ها از بین خواهد رفت. در ضمن مدیر عموما در یک سیستم مدیریتی می‌خواهد تنها یک لینک را به یک مقاله بدهد و سایت بنا به تشخیص در زبان مزبور، یکی از این مقالات را به کاربر نمایش دهد؛ نه اینکه مرتبا مدیر برای هر زبان، لینکی را مهیا کند و در این حالت چنین چیزی ممکن نخواهد بود.
  3. در یک سیستم فروشگاهی همانند تصویر بالا کار هم سخت‌تر می‌شود و هر رکورد، یک محصول جدا شناخته می‌شود و ویرایش‌ها هم برای هر کدام باید جداگانه صورت بگیرد که در عمل این طرح را رد می‌کند.


سومین راه حل این است که سه جدول ایجاد کنیم:

یک. جدول زبان‌ها (که بالاتر ایجاد شده بود)

دو . جدول نام مقاله به همراه اطلاعات پایه و فیلدها بی نیاز به چند زبانه بودن

سه : یک جدول که هر دو ستون آن کدهای کلید دو جدول بالا را دارند و فیلدهای چند زبانه در آن وجود دارند.

جدول پایه

 ID  کد
 Name  نام مقاله
 CreationDate  تاریخ ایجاد
 Writer  نویسنده
 Visibilty  وضعیت نمایش
 جدول مقالات
 LanguageCode کد زبان
 ArticleID  کد مقاله
 CreationDate  تاریخ ایجاد
 Visibility  وضعیت نمایش مقاله
Title
عنوان مقاله
ContentText
متن مقاله

 در جدول پایه یک مقاله ایجاد می‌شود که اطلاعات عمومی همه مقالات را دارد و حتی خصوصیت وضعیت نمایش آن، روی همه‌ی مقالات با هر زبانی تاثیر می‌گذارد. در جدول دو، هر مقاله یک رکورد دارد که کد زبان و کد مقاله برای آن یک کلید ترکیبی به حساب می‌آیند. پس، از هر مقاله یک یا چند زبان خواهیم داشت. همچنین دارای فیلدهایی با وضعیت مخصوص به خود هم هستند؛ مثل فیلد وضعیت نمایش مقاله که فقط برای این مقاله با این زبان کاربرد دارد.

مزایا:

  1. گزارش گیری آسان برای هر دسته مقاله با زبان‌های مختلف و ارتباط و یکپارچگی
  2. آسان در افزودن زبان.

معایب:

  1. ایجاد کوئری‌های پیچیده‌تر و جوین دار که به نسبت روش‌های قبلی کوئری‌ها پیچیده‌تر شده اند.
  2. کدنویسی زیادتر.

استفاده از ساختارهای XML یا JSON برای ذخیره سازی اطلاعات چند زبانه مانند ساختارهای زیر:

XML
<Articles>
<Article>
this is english text
</Article>
<Article>
این یک متن فارسی است
</Article>
</Articles>

یا 
<Articles>
<en-us>
this is english text
</en-us>
<fa-ir>
این یک متن فارسی است
</fa-ir>
</Articles>
JSON
"Articles":["en-us':{"title":"this is english text","content":" english content"},"fa-ir":{"title":"متن فارسی","content":"محتوای فارسی"}]
ازSQL Server 2005 به بعد از نوع داده xml پشتیبانی می‌شود و در نسخه‌ی 2016 آن نیز پشتیبانی از Json اضافه شده است که حتی شامل اندیکس‌های اختصاصی هم برای این دو نوع می‌باشد.
از مزایای این روش ذخیره‌ی همه داده‌ها در یک ستون و یک جدول است و نیازی به ستون‌های اضافه یا جداول اضافه نیست ولی معایب این روش استفاده از کوئری‌های پیچیده‌تر جهت ارتباط و خواندن است.

استفاده از بانک‌های اطلاعاتی NO SQL
در این بانک‌ها دیگر درگیر تعداد ستون‌ها و جنس آن‌ها نیستیم و میتوانیم برای هر مقاله یا محصول، هر تعداد زبان و یا فیلد را که می‌خواهیم، در نظر بگیریم و اضافه کنیم. برای آشنایی بیشتر با این نوع بانک‌ها و انواع آن، مقالات مربوط به nosql را در سایت دنبال کنید.

نکاتی که در یک سایت چند زبانه باید به آن‌ها توجه کرد.

یک . زبان آن صفحه را معرفی کنید: این کار هم به موتورهای جست و جو برای ثبت سایت شما کمک می‌کند و هم برای معلولین که از ابزارهای صفحه خوان استفاده می‌کنند، کمک بزرگی است. در این روش، صفحه خوان‌ها و دستگاه‌های خط بریل که زبان صفحه را تشخیص نمی‌دهند با خواندن کد زبان می‌توانند زبان صفحه را تشخیص دهند. با استفاده از خط زیر میتوانید زبان اصلی صفحه‌ی خود را تنظیم نمایید:
<html lang="en">

اگر از XHTML استفاده می‌کنید خاصیت زیر را فراموش نکنید. دریافت W3C Validation بدون آن امکان پذیر نخواهد بود.
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
با تغییر زبان هر صفحه، باید تنظیم زبان آن تغییر یابد:



دو. چند زبانه بودن صفحه: در بالا یاد گرفتیم که چگونه زبان اصلی صفحه را تنظیم کنیم، ولی گاهی اوقات صفحه به غیر از زبان اصلی، شامل زبان‌های دیگر هم می‌شود؛ مثل نقل قول‌ها یا موارد دیگر. برای این‌کار می‌توانید از خصوصیت lang که در اکثر تگ‌ها پشتیبانی می‌شود، استفاده کنید. مثال پایین یک نقل قول فرانسوی است که ما آن را به خصوصیت lang، جهت تایید زبانش مزین کرده‌ایم:
<blockquote lang=”fr”>

<p>Le plus grand faible des hommes, c'est l'amour qu'ils ont de la vie.</p>

</blockquote>

سه. لینک ها : اگر دارید در صفحه‌ای لینک به جایی می‌دهید که متفاوت از زبان شماست، حتما باید زبان صفحه یا سایت مقصد را مشخص کنید. مثلا لینک زیر برای صفحه‌ای است که از یک زبان غیر فرانسوی به یک صفحه‌ی با زبان فرانسوی هدایت می‌شود:
<a href="" hreflang="fr">French</a>

همچنین اگر متن لینک شما هم به زبان فرانسوی باشد خیلی خوب می‌شود که آن را هم بیان کنید و از خاصیت lang و هم hreflang همزمان استفاده کنید:
<a href="" hreflang="fr">Francais</a>

چهار. جهت صفحه: به طور پیش فرض شما این مورد را به خاطر طراحی ظاهر صفحه رعایت می‌کنید ولی خالی از لطف نیست که بگوییم جهت زبان را هم با خصوصیت dir  که مقدار پیش فرضش راست به چپ هست، ذکر کنید. هر چند که عموما وب سایت‌های چند زبانه شامل یک قالب دیگر به صورت راست چین یا چپ چین هم هستند که بر اساس زبان انتخاب شده، قالب خود را تغییر خواهند داد.

پنج. انکودینگ صفحه را مشخص کنید: برای اینکه نحوه‌ی رمزگذاری و رمزگشایی حروف و نمادها مشخص گردد، باید انکودینگ تنظیم شود و حتی برای بعضی از موتورهای جست و جو که ممکن است با وب سایت شما به مشکل بر بخورند. امروزه بیشتر از صفحات یونیکد استفاده می‌شود که سطح وسیعی از کاراکترها را پشتیبانی می‌کند.
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">

HTML5
<meta charset="UTF-8">

شش. اندازه‌ی فونت:
موقعی که یک سایت چند زبانه را طراحی می‌کنید این نکته خیلی مهم هست که بدانید اندازه فونت‌های زبان پیش فرض، برای باقی زبان‌ها مناسب نیستند. به عنوان مثال ممکن است اندازه فونتی برای زبان‌های انگلیسی، فرانسوی و آلمانی مناسب باشد ولی برای زبان‌های فارسی و عربی و چینی و ... مناسب نباشد و خواندن آن سخت شود. به همین جهت یکی از راه‌های حل این مشکل استفاده از قالب css است که وابسته به خصوصیت lang ای است که شما برای صفحه و هر المان یا تگی که از این خصوصیت استفاده می‌کند، تعیین کرده‌اید.
:lang(en) {

font-size: 85%;

font-family: arial, verdana, sans-serif;

}

:lang(zh) {

font-size: 125%;

font-family: helvetica, verdana, sans-serif;

}

خط زیر تعیین میکند که از استایل اول استفاده شود:
<html lang="en">
و خط زیر تعیین می‌کند که از استایل دوم استفاده شود:
<html lang="zh">
البته این کد بالا در مرورگرهای فایرفاکس، اپرا و IE8 به بالا پاسخ می‌دهد. برای سایر مروگرها چون کروم و نسخه‌های پیشین IE باید از شیوه‌ی زیر بهره ببرید:
<body class="english"> or <body class="chinese">

و استایل:
.english {

font-size: 85%;

font-family: arial, verdana, sans-serif;

}

.chinese {

font-size: 125%;

font-family: helvetica, verdana, sans-serif;

}
در این شیوه برای تگ مربوطه یک کلاس با نام آن زبان ایجاد کرده که محتوای آن تنظیمات قلم آن زبان می‌باشد.

هفت. اندازه‌ی کلمات و خطوط: در این حالت اندازه کلمات بین هر زبان متفاوت است و میزان فضایی که در بر می‌گیرند با زبانی دیگر تفاوت دارد. ممکن است کلمه‌ای در یک زبان شش حرف باشد، ولی در زبانی دیگر دو یا چهار حرف و به خصوص در یک عبارت یا حتی جمله این نکته بیشتر به چشم می‌آید. تصاویر زیر نمونه‌ای از حل این مشکل توسط سایت آمازون می‌باشند که بعضی المان‌ها مانند لیست علاقمندی‌ها یا بعضی از آیتم‌ها را جابجا کرده است. بهترین جواب برای اینکار این است که قالب خود را برای هر زبان بهینه کنید.

هشت : زمان را نیز تغییر دهید: یکی از مواردی که در کمتر سایت چند زبانه‌ای به چشم می‌خورد و به نظر بنده می‌تواند بسیار مهم باشد این است که time zone منطقه‌ی هر زبان را بدانید. به عنوان مثال برای مقاله‌ی خود، تاریخ ایجاد را به صورت UTC ذخیره کنید و سپس نمایش را بر اساس زبان یا حتی بهتر و دقیق‌تر از طریق IP کشور مربوطه به دست آورید. برای کاربران ثبت نام شده این تاریخ می‌تواند دقیق‌تر باشد همانند انجمن‌های وی بولتین.


شیوه‌های تشخیص زبان سایت

یکی از راه‌های تشخیص زبان این است که موقعی که برای اولین بار کاربری به سایت مراجعه می‌کند، زبان مورد نظرش را سوال کنید و این اطلاعات را در یک کوکی بدون تاریخ انقضاء ذخیره کنید تا در دفعات بعدی آن را بررسی نمایید.

دومین راه، استفاده از IP کاربر مراجعه کننده است تا بر اساس آن زبان مورد نظر را انتخاب کنید.

در سومین شیوه که اغلب استفاده می‌شود، زبان سایت به طور پیش فرض بر روی یک زبان خاص که بهتر است انگلیسی باشد تنظیم شده است و سپس کاربر از طریق یک منو یا ابزارهای موجود در سایت، زبان سایت را تغییر دهد.

پی نوشت: فراموش نگردد که امکان تغییر زبان همیشه برای کاربر مهیا باشد و طوری نباشد که کاربر در آینده نتواند زبان سایت را تغییر دهد؛ حتی اگر تشخیص خودکار سایت برای زبان فعال باشد.

پی نوشت: در روش‌های بالا بهتر است همان مرتبه‌ی اول اطلاعات را در یک کوکی ذخیره کنید تا مراحل پیگیری راحت‌تر و آسان‌تر شود.


پلاگین‌ها و ابزارهای مدیریت زبان

اولین ابزاری که به شما برای تغییر زبان سایت معرفی می‌کنیم، توسط کاربر مورد استفاده قرار می‌گیرد و یک پلاگین جی کوئری می‌باشد. این پلاگین شامل حالت‌های مختلفی است که به نظر من بهترین حالت آن است که پرچم کشور مربوطه نیز نمایش یابد تا کاربر ارتباط راحت‌تری با سایت داشته باشد.

دومین ابزار که بیشتر برای انتخاب کشور می‌باشد و من خودم در بخش مدیریتی سیستم‌ها از آن استفاده میکنم، ابزار CountrySelector است. این پلاگین قابلیت جست و جو زبان را همزمان با تایپ کاربر نیر داراست. اسامی کشورها به صورت انگلیسی شروع شده و به زبان آن کشور در داخل پرانتز خاتمه می‌یابند و پرچم هر کشور نیز در کنار آن قرار دارد. کار کردن با آن بسیار راحت بوده و مستنداتش به طور کامل کار با آن را توضیح می‌دهد.

پلاگین بعدی International Telephone Input است که  پیاده سازی پلاگین بالا می‌باشد. برای مواردی مفید است که شما نیاز دارید کد تلفنی کشوری را انتخاب کنید.

در مقاله‌های زیر که در سایت جاری است در مورد Globalization و به خصوص استفاده از ریسورس‌ها مطالب خوبی بیان شده است:

قسمت بیست و دوم آموزش MVC که مبحث Globalization را دنبال می‌کند.

قسمت اول از شش قسمت مباحث Globalization  که دنباله‌ی آن را می‌توانید در مقاله‌ی خودش دنبال کنید.

نظرات مطالب
ASP.NET MVC #12
- ابتدا نیاز است با مفهوم ViewModel آشنا باشید.
- اگر لیستی قرار است در تمام صفحات نمایش داده شود و محل آن هم باید در layout باشد، یعنی باید این لیست در هر بار نمایش و یا تولید هر View (تمام Viewهای سایت)، تولید شود. بنابراین در BaseViewModelایی که عنوان شد، تعریف خاصیت این لیست را قرار دهید و در layout از آن استفاده کنید.
// در پایه مدل‌ها
public abstract class BaseViewModel
{
    public IList<Post> Posts { get; set; } // خاصیت عمومی که قرار است در فایل مستر قابل دسترسی باشد
}

// در اکشن متد
 return View(model: new HomeViewModel { Posts = .... });

// در اینجا ویوومدل ارسالی از پایه مدل‌ها مشتق می‌شود
public class HomeViewModel : BaseViewModel
- و اگر نمی‌خواهید به ازای هر Viewایی که در سایت تولید می‌شود یکبار این لیست را مقدار دهی کنید، بهتر است از روش Html.RenderAction استفاده کنید (بحث «نمایش اطلاعات از کنترلر‌های مختلف در یک صفحه» در مطلب فوق). مباحث Caching را هم برای لیست‌های عمومی فراموش نکنید.
مطالب
فقط به خاطر یک نیم فاصله!
نسخه‌ی 64 بیتی ویندوز 7 را نصب کرده‌ام و اولین مشکل، نبود صفحه کلید مطابق استاندارد 2091 برای نسخه‌های 64 بیتی ویندوز است. پروژه وب فارسی دانشگاه صنعتی شریف، سال‌ها قبل فقط یک نسخه‌ی 32 بیتی از آن را تهیه کرده و نسخه‌های 64 بیتی موجود، مطابق استاندارد 9147 هستند و من به دلایل ذیل حاضر به استفاده از آن نیستم!
  1. در استاندارد 9147 ، جای ژ و پ مطابق صفحه کلیدی که در بازار ایران فروخته می‌شود نیست (و باید به همه پاسخگو باشید که چرا اینطوری است).
  2. ی و ک در آن اصلاح شده و دیگر عربی نیست که چقدر هم خوب، اما من تعداد زیادی برنامه و همچنین تعداد قابل توجهی فایل word دارم که مطابق استاندارد 2091 تهیه شده‌اند. مشکلات ی و ک فارسی و عربی را هم در اینجا ذکر کرده‌ام و به دنبال باگ‌های جدید در برنامه‌ها نمی‌گردم.
  3. با ی و ک فارسی اگر در گوگل جستجو کنید تعداد پاسخ‌های مرتبط یافت شده بسیار بسیار کمتر از حالتی خواهد بود که با ی و ک عربی جستجو کنید.
  4. در یک سازمان باید رویه‌ا‌ی واحد در این مورد برقرار باشد. یا همه باید از 2091 استفاده کنند و یا همه از 9147.

نسخه‌ی کامل استاندارد 9147 را از سایت آقای اخگری می‌توانید دریافت نمائید:

به همین دلایل خصوصا قسمت دوم (هر چند ممکن است با آن موافق نباشید)، نیاز به صفحه کلید مطابق استاندارد 2091 نسخه‌ی 64 بیتی داشتم.

برای تهیه صفحه کلید فارسی از برنامه Microsoft Keyboard Layout Creator استفاده می‌شود که نسخه‌ی 1.4 آن‌را از آدرس زیر می‌توانید دریافت نمائید. این نسخه قابلیت تولید فایل‌های dll مرتبط 64 بیتی را هم دارد:


یکی از قابلیت‌های این برنامه بارگذاری صفحه کلید فارسی جاری سیستم است:



صفحه کلید استاندارد 2091 پروژه وب فارسی را اگر با آن باز کرده و سپس از منوی Project گزینه‌ی Build DLL and setup package را انتخاب نمائید، با خطای زیر متوقف خواهید شد:
ERROR: 'VK_SPACE' in Shift State 'Shift' must be made up of white space character(s), but is defined as '‌' (U+200c) instead.
که دقیقا مربوطه به این سطر در تعاریف صفحه کلید است (صفحه کلید را می‌شود به صورت فایلی با پسوند klc هم save as کرد):
39 SPACE  0 0020 200c 0020 -1  // SPACE, ZERO WIDTH NON-JOINER, SPACE, <none>
مفاهیم این ستون‌ها هم به شرح زیر هستند:
0 //Column 4
1 //Column 5 : Shft
2 //Column 6 :       Ctrl
3 //Column 7 : Shft  Ctrl
یعنی جهت نمایش نیم فاصله حاصل از ترکیب shift space مطابق استاندارد تایپ ایران، بجای 0x0020 یا همان فاصله، از 0x200C استفاده شده است (مطابق استاندارد تایپ ایران باید نوشت "می‌روم" و نه "می روم". به نیم فاصله و فاصله دقت بفرمائید).
اما این برنامه‌ی محترم دقیقا همین مورد را غلط گرفته و فایل dll نهایی را تولید نمی‌کند (مطابق خطایی که ذکر شد).
برنامه‌ی Microsoft Keyboard Layout Creator هم با دات نت فریم ورک نوشته شده است. اگر کمی با reflector به آنالیز آن بپردازیم به کلاس Accept و متد زیر خواهیم رسید:
private bool VerifySpaceBarIntegrity(ShiftState ss, bool fMustBeDefined)
در این متد جایی که خطای ذکر شده صادر می‌شود، مربوط به چند سطر زیر است:
if (!Utilities.IsSpacing(ss.Characters))
{
    this.WriteErrorToLogFile("SpaceKeyNeedsWhiteSpaceCharacters", new string[] { "VK_" + Utilities.VkStringOfIvk(ss.VK), this.m_stStateLabel[ss.State], ss.Characters, Utilities.FromCharacterToUPlusForm(ss.Characters) });
    flag = false; /*اینجا باید اصلاح شود!*/
}
خوب! به نظر شما ایرادی دارد که این flag همیشه true باشد؟! برای همین منظور، فایل MSKLC.exe را پچ کرده‌ام که از آدرس زیر قابل دریافت است:

  دریافت پچ شده نگارش 1.4.6000.2 : MSKLC.Patched.7z  

با استفاده از این نسخه (ابتدا برنامه اصلی را نصب کرده و سپس فایل exe را جایگزین نمائید)، به راحتی می‌توان نسخه‌ی استاندارد و اصلی 2091 را بارگذاری و مجددا کامپایل کرد (بدون توقف کامپایل به خاطر فقط یک نیم فاصله‌ی غیرمعتبر از دیدگاه نویسندگان اصلی برنامه). به این صورت فایل‌های 64 بیتی لازم هم تولید می‌شوند ( ممکن است در حین کار با برنامه‌ی patch شده، نمایش خطای کار با نیم فاصله را مجددا دریافت کنید؛ اما مهم نیست . نمونه‌ی وصله شده، بدون مشکل حاصل را کامپایل می‌کند که در نمونه‌ی اصلی اینطور نیست. یعنی پس از overwrite کردن فایل exe موجود، با نمونه patch شده، برنامه کار کامپایل را کامل می‌کند) که حاصل نهایی را از آدرس زیر می‌توانید دریافت نمائید:


این فایل‌ها دقیقا بر مبنای همان فایل‌های پروژه وب فارسی استاندارد 2091 هستند؛ با این تفاوت که نسخه‌‌های غیر 32 بیتی هم در آن لحاظ شده‌اند.

نصب این dll‌ها هم از ویندوز ویستا به بعد داستان خودش را پیدا کرده؛ ابتدا باید take ownership شود، سپس می‌توان فایل اصلی را به سادگی جایگزین کرد و سپس reboot .
برای این منظور ابتدا به پوشه‌ی system32 ویندوز مراجعه کرده و فایل KBDFA.DLL را پیدا کنید.
در ادامه به پنجره خواص آن (کلیک راست و انتخاب properties) مراجعه و برگه‌ی Security آن‌را انتخاب کنید.
در این قسمت بر روی دکمه‌ی Advanced کلیک نمائید.
در صفحه‌ی باز شده به برگه‌ی Owner مراجعه کنید.
در این قسمت بر روی دکمه‌ی Edit کلیک نموده و کاربر خودتان را اضافه نمائید.
پس از طی این مرحله (یا همان take ownership) به برگه security مراجعه کرده و به کاربر خودتان دسترسی full control بدهید.
اکنون مجوز تغییر این فایل را بدون هیچ مشکلی خواهید یافت.
در پایان reboot را فراموش نکنید.

راه دوم:
یا برنامه نصاب حاصل از برنامه‌ی Microsoft Keyboard Layout Creator، مدخل جدیدی را به صفحه کلیدهای قابل انتخاب ویندوز در کنترل پنل اضافه می‌کند که نیازی به reboot ندارد.