مطالب
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 قرار می‌گیرد.
مطالب
مقدمه‌ای بر صفحات Razor در ASP.NET Core 2.0 - بخش اوّل
Page یا «صفحه» در Razor، یکی از ویژگی‌های جدید در  ASP.NET Core MVC است که تمرکز کدنویسی را بر روی صفحات قرار می‌دهد و این موجب راحتی کدنویسی و بالارفتن راندمان می‌شود. این «صفحات» نیازمند استفاده از نسخۀ ASP.NET Core 2.0.0 و نسخه‌های بعد از آن هستند که در  Visual Studio 2017 Update 3 و نسخه‌های بعدی در دسترس است.
«صفحات» Razor به‌طور پیش‌‎فرض در MVC در دسترس است و کافیست در فایل  Startup.cs، «صفحات» Razor فعال شود:
public class Startup
{
    public void ConfigureServices(IServiceCollections services)
    {
        services.AddMvc(); // موجب فعال شدن «صفحات»  و کنترلرها می‌شود
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseMvc();
    }
}
تمام ویژگی‌های جدید «صفحات» Razor در اسمبلی Microsoft.AspNetCore.Mvc.RazorPages در دسترس است و کافیست بدان ارجاع دهید. همچنین اگر ارجاعی به پکیج  Microsoft.AspNetCore.Mvc  داشته باشید نیز «صفحات» Razor در دسترس خواهند بود.
«صفحۀ» زیر را در نظر بگیرید:
@page

@{
    var message = "Hello, World!";
}

<html>
<body>
    <p>@message</p>
</body>
</html>
این کد، بسیار شبیه «صفحات» ویوی Razor معمولی است؛ اما آنچه آن را متفاوت می‌سازد، استفاده از دایرکتیو page@  است. با استفاده از  page@، درواقع این فایل نقش اکشن متد MVC را نیز انجام می‌دهد و بدین معناست که می‌تواند به‌طورمستقیم، درخواست‌ها را بدون وساطت هیچ کنترلری مدیریت کند. دایرکتیو  page@، باید در ابتدای صفحۀ Razor قرار بگیرد. این دایرکتیو رفتار اصلی سایر Razorها را تحت تأثیر قرار می‌دهد.
ارتباط میان مسیرهای URL با صفحات، از طریق محل قرارگیری «صفحات» در فایل سیستم، اتفاق می‌افتد. جدول زیر نحوۀ ارتباط میان مسیر «صفحات» Razor و URL متناظر آن را نشان می‌دهد: 
 URL متناظر  نام فایل و مسیر آن 
/  یا  /Index
 /Pages/Index.cshtml
/Contact /Pages/Store/Contact.cshtml
/Store/Contact /Pages/Store/Contact.cshtml
برنامه هنگام اجرا، به‌طور پیش‌فرض، برای یافتن فایل «صفحات» Razor در پوشۀ «صفحات» جستجو می‌کند.
ویژگی‌های جدید «صفحات» Razor  بدین منظور طراحی شده‌اند تا الگوهای عمومی به‌کار رفته در پیمایش‌گر صفحات وب را آسان‌تر کنند. صفحۀ «تماس با ما» را در نظر بگیرید که در آن مدل Contact پیاده‌سازی شده است. فایل  MyApp/Contact.cs  بدین شکل است:
using System.ComponentModel.DataAnnotations;

namespace MyApp 
{
    public class Contact
    {
        [Required]
        public string Name { get; set; }

        [Required]
        public string Email { get; set; }
    }
}
و فایل ویوی آن یعنی MyApp/Pages/Contact.cshtml به شکل زیر است:
@page
@using MyApp
@using Microsoft.AspNetCore.Mvc.RazorPages
@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"
@inject ApplicationDbContext Db

@functions {
    [BindProperty]
    public Contact Contact { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (ModelState.IsValid)
        {
            Db.Contacts.Add(Contact);
            await Db.SaveChangesAsync();
            return RedirectToPage();
        }

        return Page();
    }
}

<html>
<body>
    <p>فرم زیر را پر کنید تا در اسرع وقت، کارشناسان ما با شما بگیرند</p> 
    <div asp-validation-summary="All"></div>
    <form method="POST">
      <div>Name: <input asp-for="Contact.Name" /></div>
      <div>Email: <input asp-for="Contact.Email" /></div>
      <input type="submit" />
    </form>
</body>
</html>
این «صفحه»، هندلری با عنوان OnPostAsync  دارد که هنگام ارسال درخواست‌های POST (هنگامی‌که کاربری فرم را ثبت می‌کند) اجرا می‌شود. البته می‌توان برای هر نوع تقاضای HTTP، هندلری را اضافه کرد که معمولاً از  OnGet  برای هرگونه تقاضای نشان دادن یک صفحۀ HTML استفاده می‌شود و از  OnPost  برای ارسال اطلاعات از طریق فرم استفاده می‌شود و همان‌طور که می‌دانیم پسوند Async اختیاری است. اما اغلب به‌طور قراردادی به‌کار برده می‌شود. کد نوشته شده داخل OnPostAsync بسیار شبیه چیزی است که به‌طور معمول در داخل یک کنترلر نوشته می‌شود. این الگویی است برای صفحاتی که در آن‌ها بیشتر اصول اوّلیۀ MVC از قبیل بایند کردن مدل‌ها، اعتبارسنجی و نتایج اکشن‌ها قابل استفاده است.  
روند اصلی  OnPostAsync  به‌شکل زیر است:
کنترل خطاهای اعتبارسنجی.
  1. اگر خطایی نبود، اطلاعات ذخیره شده و به صفحۀ دیگر ریدایرکت می‌شود.
  2. درغیراین‌صورت، صفحه را دوباره به‌همراه خطاهای اعتبار سنجی نمایش می‌دهد.
  3. اگر اطلاعات موفقت‌آمیز وارد شوند، آنگاه متد هندلر OnPostAsync، هلپر RedirctToPage را برای برگرداندن نمونه‌ای از RedirectToPageResult فراخوانی می‌کند. این یک نوع جدید بازگشتی برای اکشن متد است که شبیه RedirectToAction  یا RedirectToRoute  است؛ با این تفاوت که مخصوص صفحات طراحی شده است.
هنگامیکه فرم ثبت شده، خطای اعتبارسنجی داشته باشد، آنگاه متد هندلر OnPostAsync هلپر متد Page را فراخوانی می‌کند. این هلپر یک نمونه از PageResult را برمی‌گرداند. این شبیه کاری است که اکشن‌ها در کنترلر‌ها، یک ویو را برگردانند. PageResult خروجی پیش‌فرض یک هندلر متد است و هندلر متدی که نوع  void  را برگرداند «صفحۀ» جاری را رندر می‌کند.
خصیصۀ  Contact از attribute جدید [BindProperty] برای مقید کردن مدل استفاده می‌کند. «صفحات» به‌طور پیش‌فرض، ویژگی‌هایی را که GET نیستند، مقید می‌کنند. مقیدکردن به خواص، موجب کاهش کدی می‌شود که شما باید در فرم مورد نظر خود بیاورید؛ مثلاً به‌راحتی می‌توان نوشت ( <input asp-for="Contacts.Name" /> ) که با این کار مقیدسازی فیلد و خصیصۀ مورد نظر به‌طور خودکار انجام می‌شود.
به‌جای استفاده از دایرکتیو  model@ در اینجا، از ویژگی جدید «صفحات» استفاده می‌کنیم. به‌طور پیش‌فرض دایرکتیو کلاس Page همان مدل است و ویژگی‌هایی شبیه مقیدکردن مدل‌ها، تگ هلپرها و هلپرهای HTML، همگی فقط با ویژگی‌هایی که درون functions@ نوشته شده‌اند، کار می‌کنند. هنگام استفاده از «صفحات» به‌طور خودکار به ویومدل دسترسی وجود دارد.
دایرکتیو  inject@  قبل از شروع هندلر متد، قرار گرفته و برای تزریق وابستگی در «صفحات» استفاده می‌شود که همانند Razor ویوهای قبل کار می‌کند. متغیر Db در اینجا از نوع  ApplicationDbContext است که به «صفحه» تزریق می‌شود. 
در اینجا نیازی به نوشتن هیچ دستوری برای اعتبارسنجی anti-forgery نیست. تولید و اعتبارسنجی anti-forgery به‌طور خودکار در «صفحات» انجام می‌شود و برای دستیابی به این ویژگی امنیتی نیاز به تنظیمات اضافه‌تری نیست.
می‌توان ویوی MyApp/Pages/Contact.chsml  را از مدل آن به شکل زیر جدا کرد: 
@page
@using MyApp
@using MyApp.Pages
@using Microsoft.AspNetCore.Mvc.RazorPages
@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"
@model ContactModel

<html>
<body>
    <p>فرم زیر را پر کنید تا در اسرع وقت، کارشناسان ما با شما بگیرند </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
      <div>Name: <input asp-for="Contact.Name" /></div>
      <div>Email: <input asp-for="Contact.Email" /></div>
      <input type="submit" />
    </form>
</body>
</html>
و بخش مدل «صفحه» را به شکل زیر:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace MyApp.Pages
{
    public class ContactModel : PageModel
    {
        public ContactModel(ApplicationDbContext db)
        {
            Db = db;
        }

        [BindProperty]
        public Contact Contact { get; set; }

        private ApplicationDbContext Db { get; }

        public async Task<IActionResult> OnPostAsync()
        {
            if (ModelState.IsValid)
            {
                Db.Contacts.Add(Contact);
                await Db.SaveChangesAsync();
                return RedirectToPage();
            }

            return Page();
        }
    }
}
براساس قرارداد، کلاس باید به‌شکل PageModel باشد و در همان فضای نامی باشد که صفحۀ آن قرار دارد و نیازی به استفاده از functions @ نیست و تنها تغییر آن، تزریق وابستگی از طریق کلاس سازنده است. 
با استفاده از PageModel می‌توان از الگوی آزمون واحد بهره برد؛ اما مستلزم نوشتن صریح کلاس و سازندۀ آن است. مزیت «صفحات» بدون فایل PageModel این است که از کامپایل در زمان اجرا پشتیبانی می‌کنند که این قابلیت در هنگام کدنویسی مفید است.
ادامه دارد...
مطالب
Globalization در ASP.NET MVC - قسمت پنجم
در قسمت قبل راجع به مدل پیش‌فرض پرووایدر منابع در ASP.NET بحث نسبتا مفصلی شد. در این قسمت تولید یک پرووایدر سفارشی برای استفاده از دیتابیس به جای فایل‌های resx. به عنوان منبع نگهداری داده‌ها بحث می‌شود.
قبلا هم اشاره شده بود که در پروژه‌های بزرگ ذخیره تمام ورودی‌های منابع درون فایل‌های resx. بازدهی مناسبی نخواهد داشت. هم‌چنین به مرور زمان و با افزایش تعداد این فایل‌ها، کار مدیریت آن‌ها بسیار دشوار و طاقت‌فرسا خواهد شد. درضمن به‌دلیل رفتار سیستم کشینگ این منابع در ASP.NET، که محتویات کل یک فایل را بلافاصله پس از اولین درخواست یکی از ورودی‌های آن در حافظه سرور کش می‌کند، در صورت وجود تعداد زیادی فایل منبع و با ورودی‌های بسیار، با گذشت زمان بازدهی کلی سایت به شدت تحت تاثیر قرار خواهد گرفت.
بنابراین استفاده از یک منبع مثل دیتابیس برای چنین شرایطی و نیز کنترل مدیریت دسترسی به ورودی‌های آن به صورت سفارشی، می‌تواند به بازدهی بهتر برنامه کمک زیادی کند. درضمن فرایند به‌روزرسانی مقادیر این ورودی‌ها در صورت استفاده از یک دیتابیس می‌تواند ساده‌تر از حالت استفاده از فایل‌های resx. انجام شود.
 
تولید یک پرووایدر منابع دیتابیسی - بخش اول
در بخش اول این مطلب با نحوه پیاده‌سازی کلاس‌های اصلی و اولیه موردنیاز آشنا خواهیم شد. مفاهیم پیشرفته‌تر (مثل کش‌کردن ورودی‌ها و عملیات fallback) و نیز ساختار مناسب جدول یا جداول موردنیاز در دیتابیس و نحوه ذخیره ورودی‌ها برای انواع منابع در دیتابیس در مطلب بعدی آورده می‌شود.
با توجه به توضیحاتی که در قسمت قبل داده شد، می‌توان از طرح اولیه‌ای به صورت زیر برای سفارشی‌سازی یک پرووایدر منابع دیتابیسی استفاده کرد:


اگر مطالب قسمت قبل را خوب مطالعه کرده باشید، پیاده سازی اولیه طرح بالا نباید کار سختی باشد. در ادامه یک نمونه از پیاده‌سازی‌های ممکن نشان داده شده است.
برای آغاز کار ابتدا یک پروژه ClassLibrary جدید مثلا با نام DbResourceProvider ایجاد کنید و ریفرنسی از اسمبلی System.Web به این پروژه اضافه کنید. سپس کلاس‌هایی که در ادامه شرح داده شده‌اند را به آن اضافه کنید.

کلاس DbResourceProviderFactory
همه چیز از یک ResourceProviderFactory شروع می‌شود. نسخه سفارشی نشان داده شده در زیر برای منابع محلی و کلی از کلاس‌های پرووایدر سفارشی استفاده می‌کند که در ادامه آورده شده‌اند.
using System.Web.Compilation;
namespace DbResourceProvider
{
  public class DbResourceProviderFactory : ResourceProviderFactory
  {
    #region Overrides of ResourceProviderFactory
    public override IResourceProvider CreateGlobalResourceProvider(string classKey)
    {
      return new GlobalDbResourceProvider(classKey);
    }
    public override IResourceProvider CreateLocalResourceProvider(string virtualPath)
    {
      return new LocalDbResourceProvider(virtualPath);
    }
    #endregion
  }
}
درباره اعضای کلاس ResourceProviderFactory در قسمت قبل توضیحاتی داده شد. در نمونه سفارشی بالا دو متد این کلاس برای برگرداندن پرووایدرهای سفارشی منابع محلی و کلی بازنویسی شده‌اند. سعی شده است تا نمونه‌های سفارشی در اینجا رفتاری همانند نمونه‌های پیش‌فرض در ASP.NET داشته باشند، بنابراین برای پرووایدر منابع کلی (GlobalDbResourceProvider) نام منبع درخواستی (className) و برای پرووایدر منابع محلی (LocalDbResourceProvider) مسیر مجازی درخواستی (virtualPath) به عنوان پارامتر کانستراکتور ارسال می‌شود.
 
نکته: برای استفاده از این کلاس به جای کلاس پیش‌فرض ASP.NET باید یکسری تنظیمات در فایل کانفیگ برنامه مقصد اعمال کرد که در ادامه آورده شده است.

کلاس BaseDbResourceProvider
برای پیاده‌سازی راحت‌تر کلاس‌های موردنظر، بخش‌های مشترک بین دو پرووایدر محلی و کلی در یک کلاس پایه به صورت زیر قرار داده شده است. این طرح دقیقا مشابه نمونه پیش‌فرض ASP.NET است.
using System.Globalization;
using System.Resources;
using System.Web.Compilation;
namespace DbResourceProvider
{
  public abstract class BaseDbResourceProvider : IResourceProvider
  {
    private DbResourceManager _resourceManager;
    protected abstract DbResourceManager CreateResourceManager();
    private void EnsureResourceManager()
    {
      if (_resourceManager != null) return;
      _resourceManager = CreateResourceManager();
    }
    #region Implementation of IResourceProvider
    public object GetObject(string resourceKey, CultureInfo culture)
    {
      EnsureResourceManager();
      if (_resourceManager == null) return null;
      if (culture == null) culture = CultureInfo.CurrentUICulture;
      return _resourceManager.GetObject(resourceKey, culture);
    }
    public virtual IResourceReader ResourceReader { get { return null; } }
    #endregion
  }
}
کلاس بالا چون یک کلاس صرفا پایه است بنابراین به صورت abstract تعریف شده است. در این کلاس، از نمونه سفارشی DbResourceManager برای بازیابی داده‌ها از دیتابیس استفاده شده است که در ادامه شرح داده شده است.
در اینجا، از متد CreateResourceManager برای تولید نمونه مناسب از کلاس DbResourceManager استفاده می‌شود. این متد به صورت abstract و protected تعریف شده است بنابراین پیاده‌سازی آن باید در کلاس‌های مشتق شده که در ادامه آورده شده‌اند انجام شود.
در متد EnsureResourceManager کار بررسی نال نبودن resouceManager_ انجام می‌شود تا درصورت نال بودن آن، بلافاصله نمونه‌ای تولید شود.

نکته: ازآنجاکه نقطه آغازین فرایند یعنی تولید نمونه‌ای از کلاس DbResourceProviderFactory توسط خود ASP.NET انجام خواهد شد، بنابراین مدیریت تمام نمونه‌های ساخته شده از کلاس‌هایی که در این مطلب شرح داده می‌شوند درنهایت عملا برعهده ASP.NET است. در ASP.NET درطول عمر یک برنامه تنها یک نمونه از کلاس Factory تولید خواهد شد، و متدهای موجود در آن در حالت عادی تنها یکبار به ازای هر منبع درخواستی (کلی یا محلی) فراخوانی می‌شوند. درنتیجه به ازای هر منبع درخواستی (کلی یا محلی) هر یک از کلاس‌های پرووایدر منابع تنها یک‌بار نمونه‌سازی خواهد شد. بنابراین بررسی نال نبودن این متغیر و تولید نمونه‌ای جدید تنها در صورت نال بودن آن، کاری منطقی است. این نمونه بعدا توسط ASP.NET به ازای هر منبع یا صفحه درخواستی کش می‌شود تا در درخواست‌های بعدی تنها از این نسخه کش‌شده استفاده شود.

در متد GetObject نیز کار استخراج ورودی منابع انجام می‌شود. ابتدا با استفاده از متد EnsureResourceManager از وجود نمونه‌ای از کلاس DbResourceManager اطمینان حاصل می‌شود. سپس درصورتی‌که مقدار این کلاس همچنان نال باشد مقدار نال برگشت داده می‌شود. این حالت وقتی پیش می‌آید که نتوان با استفاده از داده‌های موجود نمونه‌ای مناسب از کلاس DbResourceManager تولید کرد.
سپس مقدار کالچر ورودی بررسی می‌شود و درصورتی‌که نال باشد مقدار کالچر UI ثرد جاری که در CultureInfo.CurrentUICulture قرار دارد برای آن درنظر گرفته می‌شود. درنهایت با فراخوانی متد GetObject از DbResourceManager تولیدی برای کلید و کالچر مربوطه کار استخراج ورودی درخواستی پایان می‌پذیرد.
پراپرتی ResourceReader در این کلاس به صورت virtual تعریف شده است تا بتوان پیاده‌سازی مناسب آن را در هر یک از کلاس‌های مشتق‌شده اعمال کرد. فعلا برای این کلاس پایه مقدار نال برگشت داده می‌شود.

کلاس GlobalDbResourceProvider
برای پرووایدر منابع کلی از این کلاس استفاده می‌شود. نحوه پیاده‌سازی آن نیز دقیقا همانند طرح نمونه پیش‌فرض ASP.NET است.
using System;
using System.Resources;
namespace DbResourceProvider
{
  public class GlobalDbResourceProvider : BaseDbResourceProvider
  {
    private readonly string _classKey;
    public GlobalDbResourceProvider(string classKey)
    {
      _classKey = classKey;
    }
    #region Implementation of BaseDbResourceProvider
    protected override DbResourceManager CreateResourceManager()
    {
      return new DbResourceManager(_classKey);
    }
    public override IResourceReader ResourceReader
    {
      get { throw new NotSupportedException(); }
    }
    #endregion
  }
}
GlobalDbResourceProvider از کلاس پایه‌ای که در بالا شرح داده شد مشتق شده است. بنابراین تنها بخش‌های موردنیاز یعنی متد CreateResourceManager و پراپرتی ResourceReader در این کلاس پیاده‌سازی شده است.
در اینجا نمونه مخصوص کلاس ResourceManager (همان DbResourceManager) با توجه به نام فایل مربوط به منبع کلی تولید می‌شود. نام فایل در اینجا همان چیزی است که در دیتابیس برای نام منبع مربوطه ذخیره می‌شود. ساختار آن بعدا بحث می‌شود.
همان‌طور که می‌بینید برای پراپرتی ResourceReader خطای عدم پشتیبانی صادر می‌شود. دلیل آن در قسمت قبل و نیز به‌صورت کمی دقیق‌تر در ادامه آورده شده است.

کلاس LocalDbResourceProvider
برای منابع محلی نیز از طرحی مشابه نمونه پیش‌فرض ASP.NET که در قسمت قبل نشان داده شد، استفاده شده است.
using System.Resources;
namespace DbResourceProvider
{
  public class LocalDbResourceProvider : BaseDbResourceProvider
  {
    private readonly string _virtualPath;
    public LocalDbResourceProvider(string virtualPath)
    {
      _virtualPath = virtualPath;
    }
    #region Implementation of BaseDbResourceProvider
    protected override DbResourceManager CreateResourceManager()
    {
      return new DbResourceManager(_virtualPath);
    }
    public override IResourceReader ResourceReader
    {
      get { return new DbResourceReader(_virtualPath); }
    }
    #endregion
  }
}
این کلاس نیز از کلاس پایه‌ای BaseDbResourceProvider مشتق شده و پیاده‌سازی‌های مخصوص منابع محلی برای متد CreateResourceManager و پراپرتی ResourceReader در آن انجام شده است.
در متد CreateResourceManager کار تولید نمونه‌ای از DbResourceManager با استفاده از مسیر مجازی صفحه درخواستی انجام می‌شود. این فرایند شبیه به پیاده‌سازی پیش‌فرض ASP.NET است. در واقع در پیاده‌سازی جاری، نام منابع محلی همنام با مسیر مجازی متناظر آن‌ها در دیتابیس ذخیره می‌شود. درباره ساختار جدول دیتابیس بعدا بحث می‌شود.
در این کلاس کار بازخوانی کلیدهای موجود برای پراپرتی‌های موجود در یک صفحه از طریق نمونه‌ای از کلاس DbResourceReader انجام شده است. شرح این کلاس در ادامه آمده است. 

نکته: همانطور که در قسمت قبل هم اشاره کوتاهی شده بود، از خاصیت ResourceReader در پرووایدر منابع برای تعیین تمام پراپرتی‌های موجود در منبع استفاده می‌شود تا کار جستجوی کلیدهای موردنیاز در عبارات بومی‌سازی ضمنی برای رندر صفحه وب راحت‌تر انجام شود. بنابراین از این پراپرتی تنها در پرووایدر منابع محلی استفاده می‌شود. ازآنجاکه در عبارات بومی‌سازی ضمنی تنها قسمت اول نام کلید ورودی منبع آورده می‌شود، بنابراین قسمت دوم (و یا قسمت‌های بعدی) کلید موردنظر که همان نام پراپرتی کنترل متناظر است از جستجو میان ورودی‌های یافته شده توسط این پراپرتی بدست می‌آید تا ASP.NET بداند که برای رندر صفحه چه پراپرتی‌هایی نیاز به رجوع به پرووایدر منبع محلی مربوطه دارد (برای آشنایی بیشتر با عبارت بومی‌سازی ضمنی رجوع شود به قسمت قبل).

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

کلاس DbResourceManager
کار اصلی مدیریت و بازیابی ورودی‌های منابع از دیتابیس از طریق کلاس DbResourceManager انجام می‌شود. نمونه‌ای بسیار ساده و اولیه از این کلاس را در زیر مشاهده می‌کنید:
using System.Globalization;
using DbResourceProvider.Data;
namespace DbResourceProvider
{
  public class DbResourceManager
  {
    private readonly string _resourceName;
    public DbResourceManager(string resourceName)
    {
      _resourceName = resourceName;
    }
    public object GetObject(string resourceKey, CultureInfo culture)
    {
      var data = new ResourceData();
      return data.GetResource(_resourceName, resourceKey, culture.Name).Value;
    }
  }
}
کار استخراج ورودی‌های منابع با استفاده از نام منبع درخواستی در این کلاس مدیریت خواهد شد. این کلاس با استفاده نام منیع درخواستی به عنوان پارامتر کانستراکتور ساخته می‌شود. با استفاده از متد GetObject که نام کلید ورودی موردنظر و کالچر مربوطه را به عنوان پارامتر ورودی دریافت می‌کند فرایند استخراج انجام می‌شود.
برای کپسوله‌سازی عملیات از کلاس جداگانه‌ای (ResourceData) برای تبادل با دیتابیس استفاده شده است. شرح بیشتر درباره این کلاس و نیز پیاده سازی کامل‌تر کلاس DbResourceManager به همراه مدیریت کش ورودی‌های منابع و نیز عملیات fallback در مطلب بعدی آورده می‌شود.

کلاس DbResourceReader
این کلاس که درواقع پیاده‌سازی اینترفیس IResourceReader است برای یافتن تمام کلیدهای تعریف شده برای یک منبع به‌کار می‌رود، پیاده‌سازی آن نیز به صورت زیر است:
using System.Collections;
using System.Resources;
using System.Security;
using DbResourceProvider.Data;
namespace DbResourceProvider
{
  public class DbResourceReader : IResourceReader
  {
    private readonly string _resourceName;
    private readonly string _culture;
    public DbResourceReader(string resourceName, string culture = "")
    {
      _resourceName = resourceName;
      _culture = culture;
    }
    #region Implementation of IResourceReader
    public void Close() { }
    public IDictionaryEnumerator GetEnumerator()
    {
      return new DbResourceEnumerator(new ResourceData().GetResources(_resourceName, _culture));
    }
    #endregion
    #region Implementation of IEnumerable
    IEnumerator IEnumerable.GetEnumerator()
    {
      return GetEnumerator();
    }
    #endregion
    #region Implementation of IDisposable
    public void Dispose()
    {
      Close();
    }
    #endregion
  }
}
این کلاس تنها با استفاده از نام منبع و عنوان کالچر موردنظر کار بازخوانی ورودی‌های موجود را انجام می‌دهد.
تنها نکته مهم در کد بالا متد GetEnumerator است که نمونه‌ای از اینترفیس IDictionaryEnumerator را برمی‌گرداند. در اینجا از کلاس DbResourceEnumerator که برای کار با دیتابیس طراحی شده، استفاده شده است. همانطور که قبلا هم اشاره شده بود، هر یک از اعضای این enumerator از نوع DictionaryEntry هستند که یک struct است. این کلاس در ادامه شرح داده شده است.
متد Close برای بستن و از بین بردن منابعی است که در تهیه enumerator موردبحث نقش داشته‌اند. مثل منایع شبکه‌ای یا فایلی که باید قبل از اتمام کار با این کلاس به صورت کامل بسته شوند. هرچند در نمونه جاری چنین موردی وجود ندارد و بنابراین این متد بلااستفاده است.
در کلاس فوق نیز برای دریافت اطلاعات از ResourceData استفاده شده است که بعدا به همراه ساختار مناسب جدول دیتابیس شرح داده می‌شود.
 
نکته: دقت کنید که در پیاده‌سازی نشان داده شده برای کلاس LocalDbResourceProvider برای یافتن ورودی‌های موجود از مقدار پیش‌فرض (یعنی رشته خالی) برای کالچر استفاده شده است تا از ورودی‌های پیش‌فرض که در حالت عادی باید شامل تمام موارد تعریف شده موجود هستند استفاده شود (قبلا هم شرح داده شد که منبع اصلی و پیش‌فرض یعنی همانی که برای زبان پیش‌فرض برنامه درنظر گرفته می‌شود و بدون نام کالچر مربوطه است، باید شامل حداکثر ورودی‌های تعریف شده باشد. منابع مربوطه به سایر کالچرها می‌توانند همه این ورودی‌های تعریف‌شده در منبع اصلی و یا قسمتی از آن را شامل شوند. عملیات fallback تضمین می‌دهد که درنهایت نزدیک‌ترین گزینه متناظر با درخواست جاری را برگشت دهد).
 
کلاس DbResourceEnumerator
کلاس دیگری که در اینجا استفاده شده است، DbResourceEnumerator است. این کلاس در واقع پیاده سازی اینترفیس IDictionaryEnumerator است. محتوای این کلاس در زیر آورده شده است:
using System.Collections;
using System.Collections.Generic;
using DbResourceProvider.Models;
namespace DbResourceProvider
{
  public sealed class DbResourceEnumerator : IDictionaryEnumerator
  {
    private readonly List<Resource> _resources;
    private int _dataPosition;
    public DbResourceEnumerator(List<Resource> resources)
    {
      _resources = resources;
      Reset();
    }
    public DictionaryEntry Entry
    {
      get
      {
        var resource = _resources[_dataPosition];
        return new DictionaryEntry(resource.Key, resource.Value);
      }
    }
    public object Key { get { return Entry.Key; } }
    public object Value { get { return Entry.Value; } }
    public object Current { get { return Entry; } }
    public bool MoveNext()
    {
      if (_dataPosition >= _resources.Count - 1) return false;
      ++_dataPosition;
      return true;
    }
    public void Reset()
    {
      _dataPosition = -1;
    }
  }
}
تفاوت این اینترفیس با اینترفیس IEnumerable در سه عضو اضافی است که برای استفاده در سیستم مدیریت منابع ASP.NET نیاز است. همان‌طور که در کد بالا مشاهده می‌کنید این سه عضو عبارتند از پراپرتی‌های Entry و Key و Value. پراپرتی Entry که ورودی جاری در enumerator را مشخص می‌کند از نوع DictionaryEntry است. پراپرتی‌های Key و Value هم که از نوع object تعریف شده‌اند برای کلید و مقدار ورودی جاری استفاده می‌شوند.
این کلاس لیستی از Resource به عنوان پارامتر کانستراکتور برای تولید enumerator دریافت می‌کند. کلاس Resource مدل تولیدی از ساختار جدول دیتابیس برای ذخیره ورودی‌های منابع است که در مطلب بعدی شرح داده می‌شود. بقیه قسمت‌های کد فوق هم پیاده‌سازی معمولی یک enumerator است.

نکته: به جای تعریف کلاس جداگانه‌ای برای enumerator اینترفیس IResourceProvider می‌توان از enumerator کلاس‌هایی که IDictionary را پیاده‌سازی کرده‌اند نیز استفاده کرد، مانند کلاس <Dictionary<object,object یا ListDictionary.
 
تنظیمات فایل کانفیگ
برای اجبار کردن ASP.NET به استفاده از Factory موردنظر باید تنظیمات زیر را در فایل web.config اعمال کرد:
<system.web>
    ...
    <globalization resourceProviderFactoryType=" نام کامل اسمبلی مربوطه ,نام پرووایدر فکتوری به همراه فضای نام آن " />
    ...
</system.web>
روش نشان داده شده در بالا حالت کلی تعریف و تنظیم یک نوع داده در فایل کانفیگ را نشان می‌دهد. درباره نام کامل اسمبلی در اینجا شرح داده شده است.
مثلا برای پیاده‌سازی نشان داده شده در این مطلب خواهیم داشت:
<globalization resourceProviderFactoryType="DbResourceProvider.DbResourceProviderFactory, DbResourceProvider" />

در مطلب بعدی درباره ساختار مناسب جدول یا جداول دیتابیس برای ذخیره ورودهای منابع و نیز پیاده‌سازی کامل‌تر کلاس‌های مورداستفاده بحث خواهد شد.

منابع:
مطالب دوره‌ها
استفاده از AOP Interceptors برای حذف کدهای تکراری کش کردن اطلاعات در لایه سرویس برنامه
اکثر برنامه‌های ما دارای قابلیت‌هایی هستند که با موضوعاتی مانند امنیت، کش کردن اطلاعات، مدیریت استثناها، ثبت وقایع و غیره گره خورده‌اند. به هر یک از این موضوعات یک Aspect یا cross-cutting concern نیز گفته می‌شود.
در این قسمت قصد داریم اطلاعات بازگشتی از لایه سرویس برنامه را کش کنیم؛ اما نمی‌خواهیم مدام کدهای مرتبط با کش کردن اطلاعات را در مکان‌های مختلف لایه سرویس پراکنده کنیم. می‌خواهیم یک ویژگی یا Attribute سفارشی را تهیه کرده (مثلا به نام CacheMethod) و به متد یا متدهایی خاص اعمال کنیم. سپس برنامه، در زمان اجرا، بر اساس این ویژگی‌ها، خروجی‌های متدهای تزئین شده با ویژگی CacheMethod را کش کند.
در اینجا نیز از ترکیب StructureMap و DynamicProxy پروژه Castle، برای رسیدن به این مقصود استفاده خواهیم کرد. به کمک StructureMap می‌توان در زمان وهله سازی کلاس‌ها، آن‌ها را به کمک متدی به نام EnrichWith توسط یک محصور کننده دلخواه، مزین یا غنی سازی کرد. این مزین کننده را جهت دخالت در فراخوانی‌های متدها، یک DynamicProxy درنظر می‌گیریم. با پیاده سازی اینترفیس IInterceptor کتابخانه DynamicProxy مورد استفاده و تحت کنترل قرار دادن نحوه و زمان فراخوانی متدهای لایه سرویس، یکی از کارهایی را که می‌توان انجام داد، کش کردن نتایج است که در ادامه به جزئیات آن خواهیم پرداخت.


پیشنیازها

ابتدا یک برنامه جدید کنسول را آغاز کنید. تنظیمات آن‌را از حالت Client profile به Full تغییر دهید.
سپس همانند قسمت‌های قبل، ارجاعات لازم را به StructureMap و Castle.Core نیز اضافه نمائید:
 PM> Install-Package structuremap
PM> Install-Package Castle.Core
همچنین ارجاعی را به اسمبلی استاندارد System.Web.dll نیز اضافه نمائید.
از این جهت که از HttpRuntime.Cache قصد داریم استفاده کنیم. HttpRuntime.Cache در برنامه‌های کنسول نیز کار می‌کند. در این حالت از حافظه سیستم استفاده خواهد کرد و در پروژه‌های وب از کش IIS بهره می‌برد.


ویژگی CacheMethod مورد استفاده

using System;

namespace AOP02.Core
{
    [AttributeUsage(AttributeTargets.Method)]
    public class CacheMethodAttribute : Attribute
    {
        public CacheMethodAttribute()
        {
            // مقدار پیش فرض
            SecondsToCache = 10;
        }

        public double SecondsToCache { get; set; }
    }
}
همانطور که عنوان شد، قصد داریم متدهای مورد نظر را توسط یک ویژگی سفارشی، مزین سازیم تا تنها این موارد توسط AOP Interceptor مورد استفاده پردازش شوند.
در ویژگی CacheMethod، خاصیت SecondsToCache بیانگر مدت زمان کش شدن نتیجه متد خواهد بود.


ساختار لایه سرویس برنامه

using System;
using System.Threading;
using AOP02.Core;

namespace AOP02.Services
{
    public interface IMyService
    {
        string GetLongRunningResult(string input);
    }

    public class MyService : IMyService
    {
        [CacheMethod(SecondsToCache = 60)]
        public string GetLongRunningResult(string input)
        {
            Thread.Sleep(5000); // simulate a long running process
            return string.Format("Result of '{0}' returned at {1}", input, DateTime.Now);
        }
    }
}
اینترفیس IMyService و پیاده سازی نمونه آن‌را در اینجا مشاهده می‌کنید. از این لایه در برنامه استفاده شده و قصد داریم نتیجه بازگشت داده شده توسط متدی زمانبر را در اینجا توسط AOP Interceptors کش کنیم.


تدارک یک CacheInterceptor

using System;
using System.Web;
using Castle.DynamicProxy;

namespace AOP02.Core
{
    public class CacheInterceptor : IInterceptor
    {
        private static object lockObject = new object();

        public void Intercept(IInvocation invocation)
        {
            cacheMethod(invocation);
        }

        private static void cacheMethod(IInvocation invocation)
        {
            var cacheMethodAttribute = getCacheMethodAttribute(invocation);
            if (cacheMethodAttribute == null)
            {
                // متد جاری توسط ویژگی کش شدن مزین نشده است
                // بنابراین آن‌را اجرا کرده و کار را خاتمه می‌دهیم
                invocation.Proceed();
                return;
            }

            // دراینجا مدت زمان کش شدن متد از ویژگی کش دریافت می‌شود
            var cacheDuration = ((CacheMethodAttribute)cacheMethodAttribute).SecondsToCache;

            // برای ذخیره سازی اطلاعات در کش نیاز است یک کلید منحصربفرد را
            //  بر اساس نام متد و پارامترهای ارسالی به آن تهیه کنیم
            var cacheKey = getCacheKey(invocation);

            var cache = HttpRuntime.Cache;
            var cachedResult = cache.Get(cacheKey);


            if (cachedResult != null)
            {
                // اگر نتیجه بر اساس کلید تشکیل شده در کش موجود بود
                // همان را بازگشت می‌دهیم
                invocation.ReturnValue = cachedResult;
            }
            else
            {
                lock (lockObject)
                {
                    // در غیر اینصورت ابتدا متد را اجرا کرده
                    invocation.Proceed();
                    if (invocation.ReturnValue == null)
                        return;

                    // سپس نتیجه آن‌را کش می‌کنیم
                    cache.Insert(key: cacheKey,
                                 value: invocation.ReturnValue,
                                 dependencies: null,
                                 absoluteExpiration: DateTime.Now.AddSeconds(cacheDuration),
                                 slidingExpiration: TimeSpan.Zero);
                }
            }
        }

        private static Attribute getCacheMethodAttribute(IInvocation invocation)
        {
            var methodInfo = invocation.MethodInvocationTarget;
            if (methodInfo == null)
            {
                methodInfo = invocation.Method;
            }
            return Attribute.GetCustomAttribute(methodInfo, typeof(CacheMethodAttribute), true);
        }

        private static string getCacheKey(IInvocation invocation)
        {
            var cacheKey = invocation.Method.Name;

            foreach (var argument in invocation.Arguments)
            {
                cacheKey += ":" + argument;
            }

            // todo: بهتر است هش این کلید طولانی بازگشت داده شود
            // کار کردن با هش سریعتر خواهد بود
            return cacheKey;
        }
    }
}
کدهای CacheInterceptor مورد استفاده را در بالا مشاهده می‌کنید.
توضیحات ریز قسمت‌های مختلف آن به صورت کامنت، جهت درک بهتر عملیات، ذکر شده‌اند.


اتصال Interceptor به سیستم

خوب! تا اینجای کار صرفا تعاریف اولیه تدارک دیده شده‌اند. در ادامه نیاز است تا DI و DynamicProxy را از وجود آن‌ها مطلع کنیم.
using System;
using AOP02.Core;
using AOP02.Services;
using Castle.DynamicProxy;
using StructureMap;

namespace AOP02
{
    class Program
    {
        static void Main(string[] args)
        {
            ObjectFactory.Initialize(x =>
            {
                var dynamicProxy = new ProxyGenerator();
                x.For<IMyService>()
                 .EnrichAllWith(myTypeInterface =>
                        dynamicProxy.CreateInterfaceProxyWithTarget(myTypeInterface, new CacheInterceptor()))
                 .Use<MyService>();
            });

            var myService = ObjectFactory.GetInstance<IMyService>();
            Console.WriteLine(myService.GetLongRunningResult("Test"));
            Console.WriteLine(myService.GetLongRunningResult("Test"));
        }
    }
}
در قسمت تنظیمات اولیه DI مورد استفاده، هر زمان که شیءایی از نوع IMyService درخواست شود، کلاس MyService وهله سازی شده و سپس توسط CacheInterceptor محصور می‌گردد. اکنون ادامه برنامه با این شیء محصور شده کار می‌کند.
حال اگر برنامه را اجرا کنید یک چنین خروجی قابل مشاهده خواهد بود:
 Result of 'Test' returned at 2013/04/09 07:19:43
Result of 'Test' returned at 2013/04/09 07:19:43
همانطور که ملاحظه می‌کنید هر دو فراخوانی یک زمان را بازگشت داده‌اند که بیانگر کش شدن اطلاعات اولی و خوانده شدن اطلاعات فراخوانی دوم از کش می‌باشد (با توجه به یکی بودن پارامترهای هر دو فراخوانی).

از این پیاده سازی می‌شود به عنوان کش سطح دوم ORMها نیز استفاده کرد (صرفنظر از نوع ORM در حال استفاده).

دریافت مثال کامل این قسمت
AOP02.zip
نظرات مطالب
EF Code First #12
به این ترتیب باید پیاده سازی بشه. یک حالت عمومی است و به کلاس و شیء خاصی گره نخورده:
using System;
using System.ComponentModel.DataAnnotations;
namespace Test
{
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public class CompareAttribute : ValidationAttribute
    {
        public CompareAttribute(string originalProperty, string confirmProperty)
        {
            OriginalProperty = originalProperty;
            ConfirmProperty = confirmProperty;
        }
        public string ConfirmProperty { get; private set; }
        public string OriginalProperty { get; private set; }
        protected override ValidationResult IsValid(object value, ValidationContext ctx)
        {
            if (value == null)
                return new ValidationResult("لطفا فیلدها را تکمیل نمائید");
            var confirmProperty = ctx.ObjectType.GetProperty(ConfirmProperty);
            if (confirmProperty == null)
                throw new InvalidOperationException(string.Format("لطفا فیلد {0} را تعریف نمائید", ConfirmProperty));
            var confirmValue = confirmProperty.GetValue(ctx.ObjectInstance, null) as string;
            if (string.IsNullOrWhiteSpace(confirmValue))
                return new ValidationResult(string.Format("لطفا فیلد {0} را تکمیل نمائید", ConfirmProperty));
            var originalProperty = ctx.ObjectType.GetProperty(OriginalProperty);
            if (originalProperty == null)
                throw new InvalidOperationException(string.Format("لطفا فیلد {0} را تعریف نمائید", OriginalProperty));
            var originalValue = originalProperty.GetValue(ctx.ObjectInstance, null) as string;
            if (string.IsNullOrWhiteSpace(originalValue))
                return new ValidationResult(string.Format("لطفا فیلد {0} را تکمیل نمائید", OriginalProperty));
            return originalValue == confirmValue ? ValidationResult.Success : new ValidationResult("مقادیر وارد شده یکسان نیستند");
        }
    }
}

مطالب
سفارشی سازی ASP.NET Core Identity - قسمت دوم - سرویس‌های پایه
در قسمت قبل کلاس‌های متناظر با جداول پایه‌ی ASP.NET Core Identity را تغییر دادیم. اما هنوز سرویس‌های پایه‌ی این فریم ورک مانند مدیریت و ذخیره‌ی کاربران و مدیریت و ذخیره‌ی نقش‌ها، اطلاعی از وجود آن‌ها ندارند. در ادامه این سرویس‌ها را نیز سفارشی سازی کرده و سپس به ASP.NET Core Identity معرفی می‌کنیم.

سفارشی سازی RoleStore

ASP.NET Core Identity دو سرویس را جهت کار با نقش‌های کاربران پیاده سازی کرده‌است:
- سرویس RoleStore: کار آن دسترسی به ApplicationDbContext ایی است که در قسمت قبل سفارشی سازی کردیم و سپس ارائه‌ی زیر ساختی به سرویس RoleManager جهت استفاده‌ی از آن برای ذخیره سازی و یا تغییر و حذف نقش‌های سیستم.
وجود Storeها از این جهت است که اگر علاقمند بودید، بتوانید از سایر ORMها و یا زیرساخت‌های ذخیره سازی اطلاعات برای کار با بانک‌های اطلاعاتی استفاده کنید. در اینجا از همان پیاده سازی پیش‌فرض آن که مبتنی بر EF Core است استفاده می‌کنیم. به همین جهت است که وابستگی ذیل را در فایل project.json مشاهده می‌کنید:
   "Microsoft.AspNetCore.Identity.EntityFrameworkCore": "1.1.0",
- سرویس RoleManager: این سرویس از سرویس RoleStore و تعدادی سرویس دیگر مانند اعتبارسنج نام نقش‌ها و نرمال ساز نام نقش‌ها، جهت ارائه‌ی متدهایی برای یافتن، افزودن و هر نوع عملیاتی بر روی نقش‌ها استفاده می‌کند.
برای سفارشی سازی سرویس‌های پایه‌ی ASP.NET Core Identity‌، ابتدا باید سورس این مجموعه را جهت یافتن نگارشی از سرویس مدنظر که کلاس‌های موجودیت را به صورت آرگومان‌های جنریک دریافت می‌کند، پیدا کرده و سپس از آن ارث بری کنیم:
public class ApplicationRoleStore :
     RoleStore<Role, ApplicationDbContext, int, UserRole, RoleClaim>,
     IApplicationRoleStore
تا اینجا مرحله‌ی اول تشکیل کلاس ApplicationRoleStore سفارشی به پایان می‌رسد. نگارش جنریک RoleStore پایه را یافته و سپس موجودیت‌های سفارشی سازی شده‌ی خود را به آن معرفی می‌کنیم.
این ارث بری جهت تکمیل، نیاز به بازنویسی سازنده‌ی RoleStore پایه را نیز دارد:
public ApplicationRoleStore(
   IUnitOfWork uow,
   IdentityErrorDescriber describer = null)
   : base((ApplicationDbContext)uow, describer)
در اینجا پارامتر اول آن‌را به IUnitOfWork بجای DbContext متداول تغییر داده‌ایم؛ چون IUnitOfWork دقیقا از همین نوع است و همچنین امکان دسترسی به متدهای ویژه‌ی آن‌را نیز فراهم می‌کند.
در نگارش 1.1 این کتابخانه، بازنویسی متد CreateRoleClaim نیز اجباری است تا در آن مشخص کنیم، نحوه‌ی تشکیل کلید خارجی و اجزای یک RoleClaim به چه نحوی است:
        protected override RoleClaim CreateRoleClaim(Role role, Claim claim)
        {
            return new RoleClaim
            {
                RoleId = role.Id,
                ClaimType = claim.Type,
                ClaimValue = claim.Value
            };
        }
در نگارش 2.0 آن، این new RoleClaim به صورت خودکار توسط کتابخانه صورت خواهد گرفت و سفارشی کردن آن ساده‌تر می‌شود.

در ادامه اگر به انتهای تعریف امضای کلاس دقت کنید، یک اینترفیس IApplicationRoleStore را نیز مشاهده می‌کنید. برای تشکیل آن بر روی نام کلاس سفارشی خود کلیک راست کرده و با استفاده از ابزارهای Refactoring کار Extract interface را انجام می‌دهیم؛ از این جهت که در سایر لایه‌های برنامه نمی‌خواهیم از تزریق مستقیم کلاس ApplicationRoleStore استفاده کنیم. بلکه می‌خواهیم اینترفیس IApplicationRoleStore را در موارد ضروری، به سازنده‌های کلاس‌های تزریق نمائیم.
پس از تشکیل این اینترفیس، مرحله‌ی بعد، معرفی آن‌ها به سیستم تزریق وابستگی‌های ASP.NET Core است. چون تعداد تنظیمات مورد نیاز ما زیاد هستند، یک کلاس ویژه به نام IdentityServicesRegistry تشکیل شده‌است تا به عنوان رجیستری تمام سرویس‌های سفارشی سازی شده‌ی ما عمل کند و تنها با فراخوانی متد AddCustomIdentityServices آن در کلاس آغازین برنامه، کار ثبت یکجای تمام سرویس‌های ASP.NET Core Identity انجام شود و بی‌جهت کلاس آغازین برنامه شلوغ نگردد.
ثبت ApplicationDbContext طبق روش متداول آن در کلاس آغازین برنامه انجام شده‌است. سپس معرفی سرویس IUnitOfWork را که از ApplicationDbContext تامین می‌شود، در کلاس IdentityServicesRegistry مشاهده می‌کنید.
 services.AddScoped<IUnitOfWork, ApplicationDbContext>();
در ادامه RoleStore سفارشی ما نیاز به دو تنظیم جدید را خواهد داشت:
services.AddScoped<IApplicationRoleStore, ApplicationRoleStore>();
services.AddScoped<RoleStore<Role, ApplicationDbContext, int, UserRole, RoleClaim>, ApplicationRoleStore>();
ابتدا مشخص کرده‌ایم که اینترفیس IApplicationRoleStore، از طریق کلاس سفارشی ApplicationRoleStore تامین می‌شود.
سپس RoleStore توکار ASP.NET Core Identity را نیز به ApplicationRoleStore خود هدایت کرده‌ایم. به این ترتیب هر زمانیکه در کدهای داخلی این فریم ورک وهله‌ای از RoleStore جنریک آن درخواست می‌شود، دیگر از همان پیاده سازی پیش‌فرض خود استفاده نخواهد کرد و به پیاده سازی ما هدایت می‌شود.

این روشی است که جهت سفارشی سازی تمام سرویس‌های پایه‌ی ASP.NET Core Identity بکار می‌گیریم:
1) ارث بری از نگارش جنریک سرویس پایه‌ی موجود و معرفی موجودیت‌های سفارشی سازی شده‌ی خود به آن
2) سفارشی سازی سازنده‌ی این کلاس‌ها با سرویس‌هایی که تهیه کرده‌ایم (بجای سرویس‌های پیش فرض).
3) تکمیل متدهایی که باید به اجبار پس از این ارث بری پیاده سازی شوند.
4) استخراج یک اینترفیس از کلاس نهایی تشکیل شده (توسط ابزارهای Refactoring).
5) معرفی اینترفیس و همچنین نمونه‌ی توکار این سرویس به سیستم تزریق وابستگی‌های ASP.NET Core جهت استفاده‌ی از این سرویس جدید سفارشی سازی شده.


سفارشی سازی RoleManager

در اینجا نیز همان 5 مرحله‌ای را که عنوان کردیم باید جهت تشکیل کلاس جدید ApplicationRoleManager پیگیری کنیم.
1) ارث بری از نگارش جنریک سرویس پایه‌ی موجود و معرفی موجودیت‌های سفارشی سازی شده‌ی خود به آن
public class ApplicationRoleManager :
    RoleManager<Role>,
    IApplicationRoleManager
در اینجا نگارشی از RoleManager را انتخاب کرده‌ایم که بتواند Role سفارشی خود را به سیستم معرفی کند.

2) سفارشی سازی سازنده‌ی این کلاس با سرویسی که تهیه کرده‌ایم (بجای سرویس پیش فرض).
public ApplicationRoleManager(
            IApplicationRoleStore store,
            IEnumerable<IRoleValidator<Role>> roleValidators,
            ILookupNormalizer keyNormalizer,
            IdentityErrorDescriber errors,
            ILogger<ApplicationRoleManager> logger,
            IHttpContextAccessor contextAccessor,
            IUnitOfWork uow) :
            base((RoleStore<Role, ApplicationDbContext, int, UserRole, RoleClaim>)store, roleValidators, keyNormalizer, errors, logger, contextAccessor)
در این سفارشی سازی دو مورد را تغییر داده‌ایم:
الف) ذکر IApplicationRoleStore بجای RoleStore آن
ب) ذکر IUnitOfWork بجای ApplicationDbContext

3) تکمیل متدهایی که باید به اجبار پس از این ارث بری پیاده سازی شوند.
RoleManager پایه نیازی به پیاده سازی اجباری متدی ندارد.

4) استخراج یک اینترفیس از کلاس نهایی تشکیل شده (توسط ابزارهای Refactoring).
محتوای این اینترفیس را در IApplicationRoleManager‌ مشاهده می‌کنید.

5) معرفی اینترفیس و همچنین نمونه‌ی توکار این سرویس به سیستم تزریق وابستگی‌های ASP.NET Core جهت استفاده‌ی از این سرویس جدید سفارشی سازی شده.
services.AddScoped<IApplicationRoleManager, ApplicationRoleManager>();
services.AddScoped<RoleManager<Role>, ApplicationRoleManager>();
در کلاس IdentityServicesRegistry، یکبار اینترفیس و یکبار اصل سرویس توکار RoleManager را به سرویس جدید و سفارشی سازی شده‌ی ApplicationRoleManager خود هدایت کرده‌ایم.


سفارشی سازی UserStore

در مورد مدیریت کاربران نیز دو سرویس Store و Manager را مشاهده می‌کنید. کار کلاس Store، کپسوله سازی لایه‌ی دسترسی به داده‌ها است تا کتابخانه‌های ثالث، مانند وابستگی Microsoft.AspNetCore.Identity.EntityFrameworkCore بتوانند آن‌را پیاده سازی کنند و کار کلاس Manager، استفاده‌ی از این Store است جهت کار با بانک اطلاعاتی.

5 مرحله‌ای را که باید جهت تشکیل کلاس جدید ApplicationUserStore پیگیری کنیم، به شرح زیر هستند:
1) ارث بری از نگارش جنریک سرویس پایه‌ی موجود و معرفی موجودیت‌های سفارشی سازی شده‌ی خود به آن
public class ApplicationUserStore :
   UserStore<User, Role, ApplicationDbContext, int, UserClaim, UserRole, UserLogin, UserToken, RoleClaim>,
   IApplicationUserStore
از بین نگارش‌های مختلف UserStore، نگارشی را انتخاب کرده‌ایم که بتوان در آن موجودیت‌های سفارشی سازی شده‌ی خود را معرفی کنیم.

2) سفارشی سازی سازنده‌ی این کلاس با سرویسی که تهیه کرده‌ایم (بجای سرویس پیش فرض).
public ApplicationUserStore(
   IUnitOfWork uow,
   IdentityErrorDescriber describer = null)
   : base((ApplicationDbContext)uow, describer)
در این سفارشی سازی یک مورد را تغییر داده‌ایم:
الف) ذکر IUnitOfWork بجای ApplicationDbContext

3) تکمیل متدهایی که باید به اجبار پس از این ارث بری پیاده سازی شوند.
در اینجا نیز تکمیل 4 متد از کلاس پایه UserStore جنریک انتخاب شده، جهت مشخص سازی نحوه‌ی انتخاب کلیدهای خارجی جداول سفارشی سازی شده‌ی مرتبط با جدول کاربران ضروری است:
        protected override UserClaim CreateUserClaim(User user, Claim claim)
        {
            var userClaim = new UserClaim { UserId = user.Id };
            userClaim.InitializeFromClaim(claim);
            return userClaim;
        }

        protected override UserLogin CreateUserLogin(User user, UserLoginInfo login)
        {
            return new UserLogin
            {
                UserId = user.Id,
                ProviderKey = login.ProviderKey,
                LoginProvider = login.LoginProvider,
                ProviderDisplayName = login.ProviderDisplayName
            };
        }

        protected override UserRole CreateUserRole(User user, Role role)
        {
            return new UserRole
            {
                UserId = user.Id,
                RoleId = role.Id
            };
        }

        protected override UserToken CreateUserToken(User user, string loginProvider, string name, string value)
        {
            return new UserToken
            {
                UserId = user.Id,
                LoginProvider = loginProvider,
                Name = name,
                Value = value
            };
        }
در نگارش 2.0 آن، این new‌ها به صورت خودکار توسط خود فریم ورک صورت خواهد گرفت و سفارشی کردن آن ساده‌تر می‌شود.

4) استخراج یک اینترفیس از کلاس نهایی تشکیل شده (توسط ابزارهای Refactoring).
محتوای این اینترفیس را در IApplicationUserStore‌ مشاهده می‌کنید.

5) معرفی اینترفیس و همچنین نمونه‌ی توکار این سرویس به سیستم تزریق وابستگی‌های ASP.NET Core جهت استفاده‌ی از این سرویس جدید سفارشی سازی شده.
services.AddScoped<IApplicationUserStore, ApplicationUserStore>();
services.AddScoped<UserStore<User, Role, ApplicationDbContext, int, UserClaim, UserRole, UserLogin, UserToken, RoleClaim>, ApplicationUserStore>();
در کلاس IdentityServicesRegistry، یکبار اینترفیس و یکبار اصل سرویس توکار UserStore را به سرویس جدید و سفارشی سازی شده‌ی ApplicationUserStore خود هدایت کرده‌ایم.


سفارشی سازی UserManager

5 مرحله‌ای را که باید جهت تشکیل کلاس جدید ApplicationUserManager پیگیری کنیم، به شرح زیر هستند:
1) ارث بری از نگارش جنریک سرویس پایه‌ی موجود و معرفی موجودیت‌های سفارشی سازی شده‌ی خود به آن
public class ApplicationUserManager :
   UserManager<User>,
   IApplicationUserManager
از بین نگارش‌های مختلف UserManager، نگارشی را انتخاب کرده‌ایم که بتوان در آن موجودیت‌های سفارشی سازی شده‌ی خود را معرفی کنیم.

2) سفارشی سازی سازنده‌ی این کلاس با سرویسی که تهیه کرده‌ایم (بجای سرویس پیش فرض).
public ApplicationUserManager(
            IApplicationUserStore store,
            IOptions<IdentityOptions> optionsAccessor,
            IPasswordHasher<User> passwordHasher,
            IEnumerable<IUserValidator<User>> userValidators,
            IEnumerable<IPasswordValidator<User>> passwordValidators,
            ILookupNormalizer keyNormalizer,
            IdentityErrorDescriber errors,
            IServiceProvider services,
            ILogger<ApplicationUserManager> logger,
            IHttpContextAccessor contextAccessor,
            IUnitOfWork uow,
            IUsedPasswordsService usedPasswordsService)
            : base((UserStore<User, Role, ApplicationDbContext, int, UserClaim, UserRole, UserLogin, UserToken, RoleClaim>)store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
در این سفارشی سازی چند مورد را تغییر داده‌ایم:
الف) ذکر IUnitOfWork بجای ApplicationDbContext (البته این مورد، یک پارامتر اضافی است که بر اساس نیاز این سرویس سفارشی، اضافه شده‌است)
تمام پارامترهای پس از logger به دلیل نیاز این سرویس اضافه شده‌اند و جزو پارامترهای سازنده‌ی کلاس پایه نیستند.
ب) استفاده‌ی از IApplicationUserStore بجای UserStore پیش‌فرض

3) تکمیل متدهایی که باید به اجبار پس از این ارث بری پیاده سازی شوند.
UserManager پایه نیازی به پیاده سازی اجباری متدی ندارد.

4) استخراج یک اینترفیس از کلاس نهایی تشکیل شده (توسط ابزارهای Refactoring).
محتوای این اینترفیس را در IApplicationUserManager‌ مشاهده می‌کنید.

5) معرفی اینترفیس و همچنین نمونه‌ی توکار این سرویس به سیستم تزریق وابستگی‌های ASP.NET Core جهت استفاده‌ی از این سرویس جدید سفارشی سازی شده.
services.AddScoped<IApplicationUserManager, ApplicationUserManager>();
services.AddScoped<UserManager<User>, ApplicationUserManager>();
در کلاس IdentityServicesRegistry، یکبار اینترفیس و یکبار اصل سرویس توکار UserManager را به سرویس جدید و سفارشی سازی شده‌ی ApplicationUserManager خود هدایت کرده‌ایم.


سفارشی سازی SignInManager

سرویس پایه SignInManager از سرویس UserManager جهت فراهم آوردن زیرساخت لاگین کاربران استفاده می‌کند.
5 مرحله‌ای را که باید جهت تشکیل کلاس جدید ApplicationSignInManager پیگیری کنیم، به شرح زیر هستند:
1) ارث بری از نگارش جنریک سرویس پایه‌ی موجود و معرفی موجودیت‌های سفارشی سازی شده‌ی خود به آن
public class ApplicationSignInManager :
    SignInManager<User>,
    IApplicationSignInManager
از بین نگارش‌های مختلف SignInManager، نگارشی را انتخاب کرده‌ایم که بتوان در آن موجودیت‌های سفارشی سازی شده‌ی خود را معرفی کنیم.

2) سفارشی سازی سازنده‌ی این کلاس با سرویسی که تهیه کرده‌ایم (بجای سرویس پیش فرض).
public ApplicationSignInManager(
            IApplicationUserManager userManager,
            IHttpContextAccessor contextAccessor,
            IUserClaimsPrincipalFactory<User> claimsFactory,
            IOptions<IdentityOptions> optionsAccessor,
            ILogger<ApplicationSignInManager> logger)
            : base((UserManager<User>)userManager, contextAccessor, claimsFactory, optionsAccessor, logger)
در این سفارشی سازی یک مورد را تغییر داده‌ایم:
الف) استفاده‌ی از IApplicationUserManager بجای UserManager پیش‌فرض

3) تکمیل متدهایی که باید به اجبار پس از این ارث بری پیاده سازی شوند.
SignInManager پایه نیازی به پیاده سازی اجباری متدی ندارد.

4) استخراج یک اینترفیس از کلاس نهایی تشکیل شده (توسط ابزارهای Refactoring).
محتوای این اینترفیس را در IApplicationSignInManager‌ مشاهده می‌کنید.

5) معرفی اینترفیس و همچنین نمونه‌ی توکار این سرویس به سیستم تزریق وابستگی‌های ASP.NET Core جهت استفاده‌ی از این سرویس جدید سفارشی سازی شده.
services.AddScoped<IApplicationSignInManager, ApplicationSignInManager>();
services.AddScoped<SignInManager<User>, ApplicationSignInManager>();
در کلاس IdentityServicesRegistry، یکبار اینترفیس و یکبار اصل سرویس توکار مدیریت لاگین را به سرویس جدید و سفارشی سازی شده‌ی ApplicationSignInManager خود هدایت کرده‌ایم.


معرفی نهایی سرویس‌های سفارشی سازی شده به ASP.NET Identity Core

تا اینجا سرویس‌های پایه‌ی این فریم ورک را جهت معرفی موجودیت‌های سفارشی سازی شده‌ی خود سفارشی سازی کردیم و همچنین آن‌ها را به سیستم تزریق وابستگی‌های ASP.NET Core نیز معرفی نمودیم. مرحله‌ی آخر، ثبت این سرویس‌ها در رجیستری ASP.NET Core Identity است:
 services.AddIdentity<User, Role>(identityOptions =>
{
}).AddUserStore<ApplicationUserStore>()
  .AddUserManager<ApplicationUserManager>()
  .AddRoleStore<ApplicationRoleStore>()
  .AddRoleManager<ApplicationRoleManager>()
  .AddSignInManager<ApplicationSignInManager>()
  // You **cannot** use .AddEntityFrameworkStores() when you customize everything
  //.AddEntityFrameworkStores<ApplicationDbContext, int>()
  .AddDefaultTokenProviders();
اگر منابع را مطالعه کنید، تمام آن‌ها از AddEntityFrameworkStores و سپس معرفی ApplicationDbContext به آن استفاده می‌کنند. با توجه به اینکه ما همه چیز را در اینجا سفارشی سازی کرده‌ایم، فراخوانی متد افزودن سرویس‌های EF این فریم ورک، تمام آن‌ها را بازنویسی کرده و به حالت اول و پیش فرض آن بر می‌گرداند. بنابراین نباید از آن استفاده شود.
در اینجا متد AddIdentity یک سری  تنظیم‌های پیش فرض‌ها این فریم ورک مانند اندازه‌ی طول کلمه‌ی عبور، نام کوکی و غیره را در اختیار ما قرار می‌دهد به همراه ثبت تعدادی سرویس پایه مانند نرمال ساز نام‌ها و ایمیل‌ها. سپس توسط متدهای AddUserStore، AddUserManager و ... ایی که مشاهده می‌کنید، سبب بازنویسی سرویس‌های پیش فرض این فریم ورک به سرویس‌های سفارشی خود خواهیم شد.

در این مرحله‌است که اگر Migration را اجرا کنید، کار می‌کند و خطای تبدیل نشدن کلاس‌ها به یکدیگر را دریافت نخواهید کرد.


تشکیل مرحله استفاده‌ی از ASP.NET Core Identity و ثبت اولین کاربر در بانک اطلاعاتی به صورت خودکار

روال متداول کار با امکانات کتابخانه‌های نوشته شده‌ی برای ASP.NET Core، ثبت سرویس‌های پایه‌ی آن‌ها توسط متدهای Add است که نمونه‌ی services.AddIdentity فوق دقیقا همین کار را انجام می‌دهد. مرحله‌ی بعد به app.UseIdentity می‌رسیم که کار ثبت میان‌افزارهای این فریم ورک را انجام می‌دهد. متد UseCustomIdentityServices کلاس IdentityServicesRegistry این‌کار را انجام می‌دهد که از آن در کلاس آغازین برنامه استفاده شده‌است.
        public static void UseCustomIdentityServices(this IApplicationBuilder app)
        {
            app.UseIdentity();

            var identityDbInitialize = app.ApplicationServices.GetService<IIdentityDbInitializer>();
            identityDbInitialize.Initialize();
            identityDbInitialize.SeedData();
        }
در اینجا یک مرحله‌ی استفاده‌ی از سرویس IIdentityDbInitializer را نیز مشاهده می‌کنید. کلاس IdentityDbInitializer‌ این اهداف را برآورده می‌کند:
الف) متد Initialize آن، متد context.Database.Migrate را فراخوانی می‌کند. به همین جهت دیگر نیاز به اعمال دستی حاصل Migrations، به بانک اطلاعاتی نخواهد بود. متد Database.Migrate هر مرحله‌ی اعمال نشده‌ای را که باقی مانده باشد، به صورت خودکار اعمال می‌کند.
ب) متد SeedData آن، به نحو صحیحی یک Scope جدید را ایجاد کرده و توسط آن به ApplicationDbContext دسترسی پیدا می‌کند تا نقش Admin و کاربر Admin را به سیستم اضافه کند. همچنین به کاربر Admin، نقش Admin را نیز انتساب می‌دهد. تنظیمات این کاربران را نیز از فایل appsettings.json و مدخل AdminUserSeed آن دریافت می‌کند.


کدهای کامل این سری را در مخزن کد DNT Identity می‌توانید ملاحظه کنید.
نظرات مطالب
کار با کلیدهای اصلی و خارجی در EF Code first
من 3تا جدول زیر رو در بانک ساختم :

و کلاسها به صورت زیر تعریف کردم:
 public class Tb1 
    {
        public Tb1()
        {
            ListTb2 = new List<Tb2>();
        }
        public int Id { get; set; }
        public string NameTb1 { get; set; }

        public virtual ICollection<Tb2> ListTb2 { get; set; }
    }
    public class Tb2 
    {
        public Tb2()
        {
            ListTb1 = new List<Tb1>();
        }
        public int Id { get; set; }
        public string NameTb2 { get; set; }

        public virtual ICollection<Tb1> ListTb1 { get; set; }
    }
و همینطور mapping :
    public class Tb1Map : EntityTypeConfiguration<Tb1>
    {
        public Tb1Map()
        {
            this.HasKey(x => x.Id);

            this.HasMany(x => x.ListTb2)
                .WithMany(xx => xx.ListTb1)
                .Map
                (
                    x =>
                        {
                            x.MapLeftKey("Tb1Id");
                            x.MapRightKey("Tb2Id");
                            x.ToTable("Tb1Tb2");
                        }
                );

        }
    }

    public class Tb2Map : EntityTypeConfiguration<Tb2>
    {
        public Tb2Map()
        {
            this.HasKey(x => x.Id);
        }
    }
موقعی که در برنامه به صورت زیر استفاده می‌کنم:
            var sv1 = new TableService<Tb1>(_uow);
            var sv2 = new TableService<Tb2>(_uow);

            var t1 = new Tb1 { NameTb1 = "T111" };
            sv1.Add(t1);
            //var res1= _uow.SaveChanges();
            
            var t2 = new Tb2 { NameTb2 = "T222" };
            sv2.Add(t2);
            //var res2 = _uow.SaveChanges();

            t1.ListTb2.Add(t2);
            var result = _uow.SaveChanges();
 هنگام SaveChanges این خطا رو می‌ده: 
An error occurred while saving entities that do not expose foreign key properties for their relationships. The EntityEntries property will return null because a single entity cannot be identified as the source of the exception. Handling of exceptions while saving can be made easier by exposing foreign key properties in your entity types. See the InnerException for details.
همراه با innerException زیر:
{"The INSERT statement conflicted with the FOREIGN KEY constraint \"FK_Tb1Tb2_Tb2\". The conflict occurred in database \"dbTest\", table \"dbo.Tb2\", column 'Id'.\r\nThe statement has been terminated."}
در واقع همینطور که مشخصه من می‌خوام اون جدول رابطه رو در codeFirst حذف کنم یجورایی و رابطه رو بین 2 جدول اصلی بیارم. کجای کارم اشتباهه؟ و راهکارش چیه؟
من با پروفایلر هم نگاه کردم همه چی تا آخر داره پیش می‌ره!
(آیا ForeignKey رو باید طور دیگه ای تعریف کنم؟) 
با تشکر
نظرات مطالب
شروع به کار با EF Core 1.0 - قسمت 8 - بررسی رابطه‌ی Many-to-Many
با سلام و احترام
در صورت ارتقاء به EF Core 2.0 دیگر نیازی به پیاده سازی ذکر شده نیست و می‌توان از پیاده سازی اینترفیس جدید IEntityTypeConfiguration به صورت ذیل استفاده نمود.

class CustomerConfiguration : IEntityTypeConfiguration<Customer>
{
  public void Configure(EntityTypeBuilder<Customer> builder)
  {
     builder.HasKey(c => c.AlternateKey);
     builder.Property(c => c.Name).HasMaxLength(200);
   }
}

...
// OnModelCreating
builder.ApplyConfiguration(new CustomerConfiguration());

موفق باشید.
مطالب
Action و Function در OData
استفاده از OData تنها به عملیات CRUD معطوف نمیشود و در عمل شما این قابلیت را دارید که متد‌های سفارشی و کاملا مجزایی را از همدیگر در سرویس‌های خود داشته باشید.
هرچند در بعضی از سناریو‌ها نیازی به استفاده‌ی بیشتر از CRUD مربوط به آن entity وجود ندارد، اما در اکثر موارد نیاز به رفتاری دارید که به راحتی با استفاده از CRUD معمولی قابلیت پیاده سازی را ندارد. در اینگونه موارد Action‌ها و Function‌ها هستند که به راحتی با استفاده از آنها، قابلیت طراحی ماژول‌های سفارشی فراهم شده است.
Actions و Functions در عمل رفتاری شبیه به هم را دارند؛ اما تفاوت‌های آنها در این است که:
1) Action‌ها برای درخواست‌های از نوع post هستند؛ اما به عکس Function‌ها از نوع get میباشند.
2) Action‌ها اثرات جانبی دارند اما Function‌ها خیر.
3) Function‌ها حتما باید خروجی داشته باشند؛ اما این الزام برای Action‌ها وجود ندارد.
4) هر دو امکان داشتن هر تعداد پارامتری را دارند (یا بدون پارامتر).

اضافه کردن Action
فرض کنید مدل زیر را در اختیار داریم
namespace ProductService.Models
{
    public class ProductRating
    {
        public int ID { get; set; }
        public int Rating { get; set; }
        public int ProductID { get; set; }
        public virtual Product Product { get; set; }  
    }
}
حال نیاز داریم که آن را به EDM اضافه نماییم. در کانفیگ OData، نوع Conventional را استفاده کرده و ابتدا Namespaceی را تعریف کرده و سپس Action خود را تعریف مینماییم؛ به صورت زیر:
ODataModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Product>("Products");

builder.Namespace = "ProductService";
builder.EntityType<Product>()
    .Action("Rate")
    .Parameter<int>("Rating");
در اینجا یک متد اکشن را به نام Rate و پارامتری را از نوع integer به نام Rating تعریف مینماییم.

اضافه کردن متد اکشن در کنترلر
[HttpPost]
public async Task<IHttpActionResult> Rate([FromODataUri] int key, ODataActionParameters parameters)
{
    if (!ModelState.IsValid)
    {
        return BadRequest();
    }

    int rating = (int)parameters["Rating"];
    db.Ratings.Add(new ProductRating
    {
        ProductID = key,
        Rating = rating
    });

    await db.SaveChangesAsync();
    
    return StatusCode(HttpStatusCode.NoContent);
}
توجه کنید که نام متد نوشته شده، همنام اکشنی است که قبلا تعریف کرده‌ایم. کار ODataActionParameters گرفتن پارامتر‌های ارسالی از سمت کلاینت میباشد. دقت کنید که نام پارامتر را نیز تعیین کرده بودیم.
ذکر [HttpPost] برای تعیین کردن post بودن این متد است. برای فراخوانی این اکشن از سمت کلاینت، درخواستی را از نوع post، بدین شکل ارسال مینماییم:
POST http://localhost/Products(1)/ProductService.Rate HTTP/1.1
Content-Type: application/json
Content-Length: 12

{"Rating":5}
توجه داشته باشید که ProductService همان Nampespaceی است که در کانفیگ تعیین کرده بودیم.
و البته برای فراخوانی این درخواست توسط یک کلاینت C#ی، اینگونه رفتار میکنیم (در مقاله‌های آتی از C# Odata client برای فراخوانی درخواست‌ها به صورت strongly typed استفاده خواهیم نمود)
HttpClient client = new HttpClient();
var response = client.PostAsync(postUrl, new StringContent(JsonConvert.SerializeObject(new { Rating = 5 }), Encoding.UTF8, "application/json")).Result;

اضافه کردن متد Function
برای اضافه کردن متد فانکشن نیز ابتدا باید آن را در کانفیگ OData معرفی نماییم؛ به صورت زیر:
ODataModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Product>("Products");
builder.EntitySet<Supplier>("Suppliers");

builder.Namespace = "ProductService";
builder.EntityType<Product>().Collection
    .Function("MostExpensive")
    .Returns<double>();
در اینجا یک متد function را به نام MostExpensive، بدون پارامتر و با نوع بازگشتی double، تعریف نموده‌ایم.

نکته:
 برای اینکه نوع بازگشتی از نوع EntitySet باشد:
ReturnsFromEntitySet<Product>("Products")
و یا نوع بازگشتی، لیستی از EntitySet‌ها باشد:
ReturnsCollectionFromEntitySet<Product>("Products");
و یا نوع بازگشتی بطور مثال لیستی از stringها باشد:
ReturnsCollection<string>();

برای فراخوانی این متد میتوان از آدرس زیر استفاده نمود:
GET http://localhost/Products/ProductService.MostExpensive

حال فقط کافیست که متد آن را در کنترلر مربوطه پیاده سازی نماییم:
public class ProductsController : ODataController
{
    [HttpGet]
    public IHttpActionResult MostExpensive()
    {
        var product = db.Products.Max(x => x.Price);
        return Ok(product);
    }

    // Other controller methods not shown.
}
اگر مقاله‌های قبلی را دنبال کرده باشید، این قسمت برای شما آشنا خواهد بود.
نوع response بازگشتی درخواست فوق چیزی شبیه به این خواهد بود:
HTTP/1.1 200 OK
Content-Type: application/json; odata.metadata=minimal; odata.streaming=true
OData-Version: 4.0
Date: Sat, 28 Jun 2016 00:44:07 GMT
Content-Length: 85

{
  "@odata.context":"http://localhost:38479/$metadata#Edm.Decimal","value":50.00
}

اضافه کردن Unbound Function
در مثال قبلی یک function bound نوشتیم که مربوط به یک EntitySet خاص بود. اما اکنون میخواهیم یک متد Unbound تعریف نماییم، به صورت زیر:
ODataModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Product>("Products");

builder.Function("GetSalesTaxRate")
    .Returns<double>()
    .Parameter<int>("PostalCode");
توجه کنید اینجا ما متد را به صورت مستقیم از ODataModelBuilder تهیه نمود‌ه‌ایم؛ به جای Entity type یا collection مربوطه. این تنظیم به model builder میگوید که متدی unbound میباشد.
متد آن را نیز اینگونه پیاده سازی مینماییم:
[HttpGet]
[ODataRoute("GetSalesTaxRate(PostalCode={postalCode})")]
public IHttpActionResult GetSalesTaxRate([FromODataUri] int postalCode)
{
    double rate = 5.6;  // Use a fake number for the sample.
    return Ok(rate);
}
اهمیتی ندارد که این متد را در چه Controllerی پیاده سازی نمایید. ذکر [ODataRoute] نیز برای تعریف URl این function میباشد.

برای فراخوانی آن نیز از درخواست زیر استفاده مینماییم:
GET http://localhost/GetSalesTaxRate(PostalCode=10) HTTP/1.1
یک مثال از نوع response درخواست فوق
HTTP/1.1 200 OK
Content-Type: application/json; odata.metadata=minimal; odata.streaming=true
OData-Version: 4.0
Date: Sat, 28 Jun 2016 01:05:32 GMT
Content-Length: 82

{
  "@odata.context":"http://localhost:38479/$metadata#Edm.Double","value":5.6
}

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