نظرات مطالب
وادار کردن خود به کامنت نوشتن
شما در کل نیازی به این SDK‌ ندارید، چون فرمت HTML help 2x چیزی نیست که به درد کاربر نهایی بخورد.
فقط به برنامه HTML help workshop نیاز است جهت تولید CHM به علاوه برنامه sandcastle help file builder جهت اتوماسیون و سهولت کار.
مطالب
شروع به کار با EF Core 1.0 - قسمت 15 - نوشتن آزمون‌های واحد
یکی از مشخصات آزمون‌های واحد، عدم خروج از مرزهای IO سیستم، در حین اجرای آن‌ها است و چون درهنگام کار با بانک‌های اطلاعاتی حتما از مرزهای IO سیستم رد خواهیم شد (کار با شبکه، کار با فایل سیستم، برای به روز رسانی و درج اطلاعات)، نوشتن آزمون‌های واحد واقعی، برای برنامه‌هایی که از ORMها استفاده می‌کنند مشکل است. به همین جهت مباحث mocking، تقلید قسمت‌های مختلف ORMها و جایگزین کردن آن‌ها با نمونه‌های درون حافظه‌ای بسیار مرسوم است. برای رفع این مشکلات، تیم EF Core، یک تامین کننده‌ی بانک اطلاعاتی ویژه‌ی «درون حافظه‌ای» را به نام «Entity Framework Core InMemory provider» ارائه داده‌است. به این ترتیب، این محل ذخیره سازی اطلاعات درون حافظه‌ای، مشکل رد شدن از مرزهای IO سیستم را برطرف کرده و عملا نیاز به کار کردن با فریم ورک‌های mocking را منتفی می‌کند (حداقل برای تقلید قسمت‌های مختلف EF Core).
در این قسمت ابتدا نحوه‌ی فعال سازی فریم ورک آزمون‌های واحد مایکروسافت و سپس نحوه‌ی فعال سازی این تامین کننده‌ی بانک اطلاعاتی درون حافظه‌ای را بررسی خواهیم کرد. به علاوه برای سرویس بلاگ‌های قسمت قبل نیز آزمون واحد خواهیم نوشت.


نحوه‌ی فعالسازی فریم ورک MSTest در یک پروژه‌ی Class library از نوع NET Core.


تنها نکته‌ی مهم فعالسازی MSTest در یک پروژه‌ی Class library جدید که برای نوشتن آزمون‌های واحد مورد استفاده قرار خواهیم داد، تنظیمات فایل project.json آن است که در ذیل آمده است:
{
    "version": "1.0.0-*",
 
    "testRunner": "mstest",
    "dependencies": {
        "Microsoft.NETCore.App": {
            "type": "platform",
            "version": "1.0.0"
        },
        "dotnet-test-mstest": "1.1.1-preview",
        "MSTest.TestFramework": "1.0.1-preview",
        "NETStandard.Library": "1.6.0",
        "Microsoft.EntityFrameworkCore": "1.0.0",
        "Microsoft.EntityFrameworkCore.InMemory": "1.0.0",
        "Core1RtmEmptyTest.DataLayer": "1.0.0-*",
        "Core1RtmEmptyTest.Entities": "1.0.0-*",
        "Core1RtmEmptyTest.Services": "1.0.0-*",
        "Core1RtmEmptyTest.ViewModels": "1.0.0-*"
    },
 
    "frameworks": {
        "netcoreapp1.0": {
            "imports": [
                "dnxcore50",
                "portable-net45+win8"
            ]
        }
    }
}
- در اینجا قید testRunner الزامی است؛ در غیراینصورت آزمون‌های واحد شما شناسایی نخواهند شد. همچنین بسته‌های dotnet-test-mstest و MSTest.TestFramework نیز باید اضافه شوند.
- به علاوه در اینجا ارجاعاتی را به اسمبلی‌های موجودیت‌ها، Services و DataLayer که در قسمت «شروع به کار با EF Core 1.0 - قسمت 14 - لایه بندی و تزریق وابستگی‌ها» بررسی شدند نیز ملاحظه می‌کنید.
- همچنین وابستگی جدید Microsoft.EntityFrameworkCore.InMemory نیز در اینجا قابل ملاحظه است. این وابستگی را تنها به پروژه‌ی آزمون‌های واحد خود اضافه می‌کنیم. از این جهت که تنظیمات آن صرفا در این قسمت جدید قید می‌شوند و نه در سایر قسمت‌های برنامه.

 پس از آن، کار با این فریم ورک، همانند سایر نگارش‌های دات نت خواهد بود:
using Microsoft.VisualStudio.TestTools.UnitTesting;
 
namespace EFCore.MsTests
{
    [TestClass]
    public class CoreTests
    {
        [TestMethod]
        public void Test1()
        {
            Assert.IsTrue(true);
        }
    }
}
ابتدا کلاس مدنظر، با ویژگی TestClass مزین می‌شود. سپس متد آزمون واحد نوشته شده نیز باید به صورت public void و مزین شده‌ی با ویژگی TestMethod، ارائه شود.
پس از نوشتن اولین آزمون واحد، یکبار پروژه را build کرده و سپس از منوی Test، گزینه‌ی Windows را انتخاب کرده و در اینجا گزینه‌ی Test Explorer را انتخاب کنید. اندکی صبر کنید تا آزمون‌های واحد شما شناسایی شوند و سپس گزینه‌ی Run All را انتخاب کنید:



تغییرات Context برنامه جهت استفاده‌ی از تامین کننده‌ی داخل حافظه‌ای

در مورد نحوه‌ی تعریف و افزودن وابستگی‌های EF Core در مطلب «شروع به کار با EF Core 1.0 - قسمت 1 - برپایی تنظیمات اولیه» پیشتر بحث شد و همچنین در مطلب «شروع به کار با EF Core 1.0 - قسمت 3 - انتقال مهاجرت‌ها به یک اسمبلی دیگر»، اطلاعات Context برنامه را به اسمبلی دیگری منتقل کردیم.
اگر از روش بازنویسی متد OnConfiguring برای تنظیم تامین کننده‌ی بانک اطلاعاتی مورد نظر استفاده می‌کنید، متد OnConfiguring کلاس Context برنامه چنین شکلی را پیدا می‌کند:
public class ApplicationDbContext : DbContext, IUnitOfWork
{
    private readonly IConfigurationRoot _configuration;
 
    public ApplicationDbContext(IConfigurationRoot configuration)
    {
        _configuration = configuration;
    }
 
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
    {
    } 
 
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        if (!optionsBuilder.IsConfigured)
        {
            optionsBuilder.UseSqlServer(
                _configuration["ConnectionStrings:ApplicationDbContextConnection"]
                , serverDbContextOptionsBuilder =>
                {
                    var minutes = (int)TimeSpan.FromMinutes(3).TotalSeconds;
                    serverDbContextOptionsBuilder.CommandTimeout(minutes);
                });
        }
    }
در اینجا دو تغییر جدید قابل ملاحظه هستند:
الف) اضافه شدن سازنده‌ی دومی که <DbContextOptions<ApplicationDbContext را دریافت می‌کند. از آن در سمت کدهای آزمون واحد برنامه جهت ثبت ()options.UseInMemoryDatabase استفاده می‌شود.
ب) به متد OnConfiguring، بررسی optionsBuilder.IsConfigured هم اضافه شده‌است. چون در سمت کدهای آزمون واحد، تامین کننده‌ی بانک اطلاعاتی درون حافظه‌ای اضافه می‌شود، مقدار optionsBuilder.IsConfigured به true تنظیم خواهد شد و دیگر از تامین کننده‌ی SQL Server استفاده نمی‌شود.

اگر از متد OnConfiguring به این شکل استفاده نمی‌کنید، تنها ذکر سازنده‌ی دوم ضروری است. از این جهت که در آزمون‌های واحد، از تنظیمات متد ConfigureServices کلاس آغازین برنامه استفاده نخواهد شد.


نوشتن آزمون‌های واحد مخصوص EF Core

پس از برپایی پیشنیازهای نوشتن آزمون‌ها واحد، شامل تنظیمات فریم ورک MSTest و همچنین افزودن وابستگی‌های مرتبط با فایل project.json ایی که در ابتدای بحث عنوان شد و اصلاح سازنده و متد OnConfiguring کلاس Context برنامه جهت آماده سازی آن‌ها برای پذیرش تامین کننده‌های دیگر، اکنون یک نمونه از آزمون‌های واحد درون حافظه‌ای EF Core، چنین شکلی را خواهد داشت:
using System;
using System.Linq;
using Core1RtmEmptyTest.DataLayer;
using Core1RtmEmptyTest.Entities;
using Core1RtmEmptyTest.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
 
namespace Core1RtmEmptyTest.MsTests
{
    [TestClass]
    public class CoreTests
    {
        private readonly IServiceProvider _serviceProvider;
 
        public CoreTests()
        {
            var services = new ServiceCollection();
            services.AddEntityFrameworkInMemoryDatabase()
                        .AddDbContext<ApplicationDbContext>(options => options.UseInMemoryDatabase());
 
            services.AddScoped<IUnitOfWork, ApplicationDbContext>();
            services.AddScoped<IBlogService, BlogService>();
 
            _serviceProvider = services.BuildServiceProvider();
        }
 
        [TestMethod]
        public void Find_searches_url()
        {
            // Insert seed data into the database using one instance of the context
            using (var serviceScope = _serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope())
            {
                using (var context = serviceScope.ServiceProvider.GetRequiredService<IUnitOfWork>())
                {
                    context.Set<Blog>().Add(new Blog { Url = "http://sample.com/cats" });
                    context.Set<Blog>().Add(new Blog { Url = "http://sample.com/catfish" });
                    context.Set<Blog>().Add(new Blog { Url = "http://sample.com/dogs" });
                    context.SaveAllChanges();
                }
            }
 
            // Use a separate instance of the context to verify correct data was saved to database
            using (var serviceScope = _serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope())
            {
                using (var context = serviceScope.ServiceProvider.GetRequiredService<IUnitOfWork>())
                {
                    Assert.AreEqual(3, context.Set<Blog>().Count());
                    Assert.AreEqual("http://sample.com/cats", context.Set<Blog>().First().Url);
                }
            }
 
            // Use a clean instance of the context to run the test
            using (var serviceScope = _serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope())
            {
                var blogService = serviceScope.ServiceProvider.GetRequiredService<IBlogService>();
                var results = blogService.GetPagedBlogsAsNoTracking(pageNumber: 0, recordsPerPage: 10);
                Assert.AreEqual(3, results.Count);
            }
        }
    }
}
توضیحات:
همانطور که در قسمت «تغییرات Context برنامه جهت استفاده‌ی از تامین کننده‌ی داخل حافظه‌ای» فوق عنوان شد، در حین انجام آزمون‌های واحد، دیگر به کلاس آغازین برنامه و تنظیمات آن مراجعه نمی‌شود. بنابراین باید شبیه به عملکرد متد ConfigureServices آن‌را در اینجا پیاده سازی کرد. نمونه‌ای از انجام اینکار را در سازنده‌ی کلاس انجام آزمون‌های واحد مشاهده می‌کنید:
        private readonly IServiceProvider _serviceProvider;
 
        public CoreTests()
        {
            var services = new ServiceCollection();
            services.AddEntityFrameworkInMemoryDatabase()
                        .AddDbContext<ApplicationDbContext>(options => options.UseInMemoryDatabase());
 
            services.AddScoped<IUnitOfWork, ApplicationDbContext>();
            services.AddScoped<IBlogService, BlogService>();
 
            _serviceProvider = services.BuildServiceProvider();
        }
در اینجا است که توسط متد AddEntityFrameworkInMemoryDatabase، کار افزودن تامین کننده‌ی بانک اطلاعاتی درون حافظه‌ای انجام شده و سپس Context برنامه نیز از آن مطلع می‌شود (علت افزودن سازنده‌ی دومی که <DbContextOptions<ApplicationDbContext را دریافت می‌کند).
سپس همانند قبل، باید تمام سرویس‌های مدنظر تنظیم شوند تا بتوان از آن‌ها استفاده کرد.

نکته‌ی مهم دیگری را که باید به آن دقت داشت، ایجاد scope و سپس دسترسی به سرویس‌ها از طریق این Scope است. از این جهت که چون خارج از طول عمر یک درخواست وب قرار داریم، دیگر Scopeها برای ما به صورت خودکار ایجاد و تخریب نمی‌شوند و باید همان‌کاری را که ASP.NET Core در پشت صحنه انجام می‌دهد، به صورت دستی پیاده سازی کنیم:
            using (var serviceScope = _serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope())
            {
                using (var context = serviceScope.ServiceProvider.GetRequiredService<IUnitOfWork>())
                {
اگر اینکار صورت نگیرد، چون Scope ایی ایجاد و تخریب نمی‌شود، کار کردن با متد serviceProvider.GetRequiredService_ اشتباه بوده و همیشه یک وهله از Context را باز می‌گرداند که مدنظر ما نیست. شبیه به این نکته را در قسمت «مقدار دهی اولیه‌ی جداول بانک‌های اطلاعاتی در EF Core» پیشتر ملاحظه کرده‌اید.


یک نکته‌ی تکمیلی

EF Core به همراه تامین کننده‌ی بانک اطلاعاتی SQLite نیز هست. یکی از نکات ویژه‌ی بانک اطلاعاتی SQLite، امکان تنظیم پارامتری است در رشته‌ی اتصالی آن، که آن‌را نیز تبدیل به یک «بانک اطلاعاتی درون حافظه‌ای» می‌کند. این روش سال‌ها است که جهت انجام آزمون‌های واحد ORMها مورد استفاده قرار می‌گیرد. بنابراین می‌توان آن‌را به عنوان جایگزینی برای مطلب جاری نیز درنظر گرفت.
 var connectionStringBuilder = new SqliteConnectionStringBuilder { DataSource = ":memory:" };
var connectionString = connectionStringBuilder.ToString();
var connection = new SqliteConnection(connectionString);
services.AddEntityFrameworkSqlite().AddDbContext<CmsDbContext>(options => options.UseSqlite(connection));
اهمیت آن در اینجا است که تامین کننده‌ی بانک اطلاعاتی درون حافظه‌ای EF، قیود را اعمال نمی‌کند ؛ اما بانک اطلاعاتی درون حافظه‌ای SQLite واقعا همانند یک بانک اطلاعاتی رابطه‌ای کامل عمل می‌کند.
مطالب
دو نکته کوتاه در مورد RSS های فارسی

اولین نکته مربوط به تاریخ هر مدخل (entry) می‌شود. این تاریخ نباید شمسی باشد! این تاریخ باید حتما استاندارد باشد. عموما یکی از دو استاندارد زیر باید مورد استفاده قرار گیرد:


RFC #822
http://www.ietf.org/rfc/rfc0822.txt
Standard for ARPA Internet Text Messages (Date and Time Specification)

RFC #3339
http://www.ietf.org/rfc/rfc3339.txt
Date and Time on the Internet (Timestamps)


برای مثال در دات نت برای تولید این فرمت استاندارد می‌توان به صورت زیر عمل کرد:
DateTime.Now.ToUniversalTime().ToString("r")

متاسفانه بسیاری از برنامه نویس‌های هم وطن این نکته را رعایت نمی‌کنند و برنامه‌های فیدخوان را دچار مشکل می‌کنند. (برای نمونه برنامه معروف feedDemon تاریخ چند سال پیش را ثبت خواهد کرد و در به روز رسانی و دنبال کردن مطالب سایت مورد نظر دچار مشکل خواهد شد)

چند مثال از این دست: (سورس صفحه را در مرورگر مطالعه نمائید)
http://www.faradade.com/Xml/RSS.xml
و یا
http://www.ayande.ir/atom.xml
و یا
http://www.tci-sk.ir/Rss.aspx
(ایشان بهتر است علاوه بر این مورد، از XmlTextWriter استفاده کنند و خروجی را به صورت یک فایل xml و نه html در مرورگر Flush کنند)

و یا بدتر از این بعضی از سایت‌ها آموزش‌های غلطی را هم ارائه می‌دهند:
http://www.faradade.com/Article.aspx?code=a726ae6a-f8e1-4b29-88b4-8e7a04e6d06d
به قسمت pubDate دقت کنید.
مطابق معمول این آموزش الان در 200 سایت کپی و پیست شده! عنوان آموزش را در گوگل جستجو کنید!
این کد آموزش داده شده یک ایراد دیگر هم دارد. آیا الزامی دارد که حتما قسمت con.Close به همین ترتیب نوشته شده اجرا شود؟ اگر این بین خطایی رخ دهد تکلیف این کانکشن باز و سایر موارد چه خواهد شد؟ کلا استفاده از try و finally و یا استفاده از using را برای چه هدفی اختراع کرده‌اند؟

و یا بعضی از سایت‌ها این مورد را رعایت می‌کنند اما به صورت نصفه و نیمه. برای مثال: (تاریخ ارائه شده کامل نیست. بنابراین استاندارد تلقی نخواهد شد)
http://www.srco.ir/Articles/RSSArticles.xml

برای آزمایش میزان استاندارد بودن خروجی فید خود می‌توان از سرویس زیر استفاده کرد:
http://validator.w3.org/feed/

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

اصلاحیه برای RSS فارسی:
<language>fa-IR</language>

و برای Atom فارسی:
<feed xml:lang="fa">

و اگر می‌خواهید خروجی استانداردی داشته باشید، کتابخانه سورس باز زیر توصیه می‌شود:
http://www.codeplex.com/Argotic

با تشکر از همکاری شما!

مطالب
مهارت‌های تزریق وابستگی‌ها در برنامه‌های NET Core. - قسمت هشتم - ساده سازی معرفی سرویس‌ها توسط Scrutor
قابلیت‌های قرار گرفته‌ی در اسمبلی Microsoft.Extensions.DependencyInjection که پایه‌ی تزریق وابستگی‌های برنامه‌های مبتنی بر NET Core. را ارائه می‌دهد، برای پیاده سازی اکثر پروژه‌ها کافی است. اما اگر از نگارش‌های پیشین ASP.NET MVC به ASP.NET Core مهاجرت کرده باشید، حتما با قابلیت‌های ویژه‌ی اسکن اسمبلی‌های موجود در IoC Containers ثالث، جهت ساده سازی معرفی سرویس‌های برنامه به سیستم تزریق وابستگی‌ها، آشنایی دارید. برای مثال StructureMap قابلیت اسکن اسمبلی‌های موجود در برنامه و معرفی اینترفیس‌ها و سرویس‌های موجود در آن‌را به Container خود دارد:
var container = new Container(x =>
            {
                x.Scan(scanner =>
                {
                    scanner.AssemblyContainingType<IOrderHandler>();
                    // connects `IAccounting` to `Accounting` and `ISales` to `Sales` automatically.
                    scanner.WithDefaultConventions();
                });
            });
و یا AutoFac نیز به همین صورت:
builder.RegisterAssemblyTypes(myAssembly)
    .Where(t => t.IsAssignableTo<IMyInterface>())
    .AsImplementedInterfaces();
البته می‌توان مجددا به تمام این قابلیت‌ها رسید؛ به شرطی‌که سیستم تزریق وابستگی‌های پایه‌ی NET Core. را با یکی از IoC Containers ثالث به طور کامل تعویض کنیم. اگر قصد چنین تعویض پایه‌ای را ندارید و هنوز قصد دارید از همان Microsoft.Extensions.DependencyInjection استفاده کنید، اما تعدادی متد الحاقی جدید تعریف شده‌ی بر فراز آن، کار اسکن کردن اسمبلی‌ها را مانند قبل انجام دهند، می‌توان از کتابخانه‌ی کمکی Scrutor استفاده کرد. این کتابخانه، جایگزین سیستم تزریق وابستگی‌های توکار برنامه‌های NET Core. نیست؛ بلکه صرفا مکمل آن است.


دریافت و نصب کتابخانه‌ی کمکی Scrutor

کتابخانه‌ی کمکی Scrutor سورس باز بوده و بسته‌ی NuGet آن توسط یکی از دستورات زیر به پروژه افزوده می‌شود:
> Install-Package Scrutor
> dotnet add package Scrutor
و یا به صورت مدخلی جدید در فایل csproj:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <ItemGroup>
    <PackageReference Include="Scrutor" Version="3.0.2" />
  </ItemGroup>
</Project>


ثبت و معرفی ساده‌تر سرویس‌ها بر اساس قواعد نامگذاری آن‌ها توسط Scrutor

فرض کنید تعدادی سرویس را به صورت زیر تعریف کرده‌اید:
namespace CoreIocServices
{
    public interface IFoo
    {
        void Run();
    }

    public class Foo : IFoo
    {
        public void Run()
        {
            throw new System.NotImplementedException();
        }
    }

    public interface IBar
    {
        void Add();
    }

    public class Bar : IBar
    {
        public void Add()
        {
            throw new System.NotImplementedException();
        }
    }


    public interface IBaz
    {
        void Stop();
    }

    public class Baz : IBaz
    {
        public void Stop()
        {
            throw new System.NotImplementedException();
        }
    }
}
روش متداول معرفی آن‌ها به IoC Container برنامه به صورت زیر است:
services.AddScoped<IFoo, Foo>();
services.AddScoped<IBar, Bar>();
services.AddScoped<IBaz, Baz>();
و هرچقدر تعداد سرویس‌های برنامه بیشتر شود، سطرهای فوق نیز بیشتر خواهند شد.
در اینجا در حین تعریف سرویس‌های فوق این روش نامگذاری رعایت شده‌است: هر اینترفیس، نامش یک I بیشتر از نام کلاس مشتق شده‌ی از آن دارد؛ مانند اینترفیس IFoo و کلاس Foo. کتابخانه‌ی StructureMap که در ابتدای بحث معرفی شد، کار اسکن و اتصال یک چنین سرویس‌هایی را با تعریف scanner.WithDefaultConventions انجام می‌دهد. معادل آن با Scrutor به صورت زیر است:
namespace CoreIocSample02
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.Scan(scan =>
                //scan.FromCallingAssembly()
                scan.FromAssemblyOf<IFoo>()
                    .AddClasses()
                    .AsMatchingInterface()
                    .WithScopedLifetime());
تعریف فوق به این معنا است:
- scan.FromAssemblyOf کار اسکن اسمبلی را انجام می‌دهد که نوع IFoo در آن قرار دارد. اگر از scan.FromCallingAssembly استفاده کنیم، به این معنا است که کار اسکن را دقیقا از همین اسمبلی فراخوان کدهای جاری، شروع کن. اما چون IFoo تعریف شده، در یک پروژه و اسمبلی دیگر قرار دارد، به همین جهت نیاز به ذکر صریح اسمبلی آن نیز هست.
- AddClasses یعنی تمام کلاس‌های public, non-abstract را به لیست services اضافه کن.
- AsMatchingInterface یعنی بر اساس قرارداد نامگذاری IClassName و ClassName، اتصالات سرویس‌ها را انجام بده.
بجای آن می‌توان از AsImplementedInterfaces نیز استفاده کرد. این حالت برای زمانی مناسب است که یک کلاس، چندین اینترفیس را پیاده سازی کند (مثلا کلاس TestService اینترفیس‌های ITestService و IService را پیاده سازی کرده باشد) و علاقمند باشید به ازای هر اینترفیس، یکبار سرویس آن نیز ثبت شود؛ کاری مانند تنظیمات زیر:
services.AddScoped<ITestService, TestService>();
services.AddScoped<IService, TestService>();
یا حتی می‌توان از متد ()<As<T نیز استفاده کرد. در اینجا به Scrutor گفته می‌شود که تمام کلاس‌های یافت شده را بر اساس نوع سرویس T ثبت و معرفی کن. البته اگر کلاسی نتواند نوع اینترفیس T را پیاده سازی کند، در زمان اجرا با استثناء مواجه خواهید شد.
- WithScopedLifetime نیز طول عمر این سرویس‌های اضافه شده را مشخص می‌کند. در اینجا می‌توان WithTransientLifetime و WithSingletonLifetime را نیز ذکر کرد.

بنابراین همانطور که ملاحظه می‌کنید، هنوز هم همان سیستم Microsoft.Extensions.DependencyInjection برقرار است؛ اما با وجود متد الحاقی جدید Scan، کار تعاریف سرویس‌های برنامه به شدت ساده می‌شود.


کار با وهله‌های کلاس‌های سرویس‌ها بجای اینترفیس‌های آن توسط Scrutor

می‌خواهیم مثال سوم قسمت ششم «چگونه بجای اینترفیس‌ها، یک وهله از کلاسی مشخص را از سیستم تزریق وابستگی‌ها درخواست کنیم؟» را توسط Scrutor پیاده سازی کنیم:
namespace CoreIocServices
{
    public interface IService { }
    public class Service1 : IService { }
    public class Service2 : IService { }
    public class Service : IService { }
}
در حالت متداول آن می‌توان از روش زیر نیز استفاده کرد:
services.AddTransient<Service1>();
services.AddTransient<Service2>();
services.AddTransient<Service>();
که با افزایش تعداد کلاس‌های سرویس برنامه به همین نحو نیز افزایش خواهند یافت. معادل این تنظیمات با Scrutor به صورت زیر است:
namespace CoreIocSample02
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.Scan(scan =>
              //scan.FromCallingAssembly()
              scan.FromAssemblyOf<IService>()
                  .AddClasses()
                  .AsSelf()
                  .WithTransientLifetime());
در اینجا اسمبلی حاوی IService اسکن خواهد شد و سپس تمام کلاس‌های public, non-abstract آن AsSelf (ثبت پیاده سازی خود کلاس به عنوان سرویس) با طول عمر Transient به لیست services اضافه می‌شوند و یا اگر صرفا تعدادی سرویس مشخص مد نظر بود می‌توان به صورت زیر عمل کرد:
services.Scan(scan =>
               scan.AddTypes(new[] { typeof(Service1), typeof(Service2) })
                   .AsSelf()
                   .WithTransientLifetime());
متدهایی که در Scrutor، یک پیاده سازی را به عنوان سرویس معرفی می‌کنند، شامل این موارد هستند:
AsSelf: معادل ()<services.AddTransient<TestService است. در این حالت کلاس‌هایی که اینترفیسی را پیاده سازی نمی‌کنند و یا در کل مایل هستید که از طریق تزریق وابستگی‌ها در دسترس باشند، می‌توان توسط متد AsSelf به سیستم معرفی کرد.
AsSelfWithInterfaces: معادل تنظیمات زیر است:
services.AddSingleton<TestService>();
services.AddSingleton<ITestService>(x => x.GetRequiredService<TestService>());
services.AddSingleton<IService>(x => x.GetRequiredService<TestService>());
فرض کنید کلاس TestService اینترفیس‌های ITestService و IService را پیاده سازی کرده باشد. با استفاده از AsSelfWithInterfaces، یکبار پیاده سازی خود سرویس به سیستم معرفی می‌شود، سپس به ازای هر اینترفیس، از همان وهله‌ی TestService برای وهله سازی سرویس‌های ITestService و IService نیز استفاده می‌شود.


روش‌های متفاوت اسکن اسمبلی‌ها در Scrutor

Scrutor به همراه روش‌های متعددی برای تعریف اسمبلی یا اسمبلی‌هایی است که باید اسکن شوند و نمونه‌ای از آن‌را با FromAssemblyOf بررسی کردیم:
services.Scan(scan =>
              //scan.FromCallingAssembly()
              scan.FromAssemblyOf<IService>()
سایر موارد آن به شرح زیر هستند:
الف) FromAssemblyOf<>, FromAssembliesOf : اسمبلی یا اسمبلی‌هایی که نوع یا نوع‌های تعیین شده را به همراه دارند، اسکن می‌کند.
ب) FromCallingAssembly, FromExecutingAssembly, FromEntryAssembly کار اسکن اسمبلی‌های فراخوان، اسمبلی که هم اکنون در حال اجرا است و اسمبلی آغازین برنامه را انجام می‌دهند.
ج) FromAssemblyDependencies: تمام اسمبلی‌هایی را که وابسته‌ی به اسمبلی معرفی شده‌ی به آن هستند، اسکن می‌کند.
د) FromApplicationDependencies, FromDependencyContext: تمام اسمبلی‌هایی را که توسط برنامه، ارجاعی به آن‌ها وجود دارند، اسکن می‌کند.


انتخاب دقیق‌تر کلاس‌ها و سرویس‌های مدنظر توسط Scrutor

شاید عملکرد کلی متد AddClasses مدنظر شما نباشد و نیاز به انتخاب دقیق‌تری از سرویس‌های اسکن شده را داشته باشید؛ برای این مورد نیز Scrutor روش‌های زیر را ارائه می‌دهد. برای مثال خود کلاس AddClasses دارای overloadهای زیر نیز هست:
    public interface IImplementationTypeSelector : IAssemblySelector, IFluentInterface
    {
        IServiceTypeSelector AddClasses();
        IServiceTypeSelector AddClasses(bool publicOnly);
        IServiceTypeSelector AddClasses(Action<IImplementationTypeFilter> action);
        IServiceTypeSelector AddClasses(Action<IImplementationTypeFilter> action, bool publicOnly);
    }
حالت پیش‌فرض آن انتخاب تمام کلاس‌های public, non-abstract است. اگر پارامتر publicOnly را با false مقدار دهی کنید، internal/private nested classes را نیز انتخاب می‌کند. پارامتر action ای که در اینجا درنظر گرفته شده، جهت فیلتر کردن سرویس‌های انتخابی است که تعدادی از مثال‌های آن‌را در زیر بررسی می‌کنیم:
services.Scan(scan => scan
              .FromAssemblyOf<IService>()
                .AddClasses(classes => classes.AssignableTo<IService>())
// .AddClasses(classes => classes.InNamespaces("MyApp")) 
// .AddClasses(classes => classes.Where(type => type.Name.EndsWith("Repository")) 
                    .AsImplementedInterfaces()
                    .WithTransientLifetime());
در اینجا در حالت اول، کلاس‌هایی که صرفا اینترفیس IService را پیاده سازی کرده باشند، انتخاب می‌شوند. حالت دوم آن، انتخاب‌ها را به یک فضای نام محدود می‌کند و حالت سوم اگر نام کلاسی به Repository ختم شود، آن‌را به عنوان سرویس انتخاب خواهد کرد.


مدیریت جایگزینی سرویس‌ها توسط Scrutor

یکی از مزیت‌های طراحی یک برنامه با درنظر گرفتن الگوی تزریق وابستگی‌ها، امکان جایگزین کردن سرویس‌های پیش‌فرض آن با سرویس‌های دیگری است. فرض کنید کتابخانه‌ای ارائه شده و از الگوریتم هش کردن X استفاده کرده‌است؛ اما شما علاقمندید تا از الگوریتم Y بجای آن استفاده کنید. اگر این کتابخانه وهله‌ی الگوریتم هش کردن را از طریق تزریق وابستگی‌ها تامین کرده باشد، فقط کافی است در ابتدای معرفی تنظیمات تزریق وابستگی‌های آن، سرویس الگوریتم هش کردن موجود را با نمونه‌ی خاص خودتان جایگزین کنید.
اکنون فرض کنید پیش از استفاده‌ی از Scrutor، تعدادی سرویس را به روش متداولی ثبت و معرفی کرده‌اید:
services.AddTransient<ITransientService, TransientService>();
services.AddScoped<IScopedService, ScopedService>();
حال که قرار است متد Scan آن، سرویس‌های یک اسمبلی را به لیست موجود اضافه کند، به سرویس‌های زیر می‌رسد:
public class TransientService : IFooService {}
public class AnotherService : IScopedService {}
 رفتار آن با سرویس‌های معادلی که از پیش ثبت شده‌اند چگونه باید باشد؟ برای مدیریت این مساله، متد UsingRegistrationStrategy پیش بینی شده‌است:
services.Scan(scan =>
                scan.FromAssemblyOf<IFoo>()
                    .AddClasses()
                    .UsingRegistrationStrategy(RegistrationStrategy.Skip)
                    .AsMatchingInterface()
                    .WithScopedLifetime());
و پارامتر دریافتی آن یک چنین امضایی را دارد:
namespace Scrutor
{
    public abstract class RegistrationStrategy
    {
        public static readonly RegistrationStrategy Skip;
        public static readonly RegistrationStrategy Append;
        protected RegistrationStrategy();
        public static RegistrationStrategy Replace();
        public static RegistrationStrategy Replace(ReplacementBehavior behavior);
        public abstract void Apply(IServiceCollection services, ServiceDescriptor descriptor);
    }
}
- حالت Append آن که حالت پیش‌فرض نیز هست، تمام سرویس‌های یافت شده را به لیست IServiceCollection اضافه می‌کند؛ صرفنظر از اینکه پیشتر ثبت شده‌است یا خیر.
- حالت Skip آن، سرویسی را تکراری ثبت نمی‌کند. یعنی اگر سرویسی پیشتر در مجموعه‌ی IServiceCollection موجود بود، مجددا آن‌را ثبت نمی‌کند.

سپس نوبت به متدهای Replace می‌رسد که یک چنین پارامتری را قبول می‌کنند:
namespace Scrutor
{
    [Flags]
    public enum ReplacementBehavior
    {
        Default = 0,
        ServiceType = 1,
        ImplementationType = 2,
        All = 3
    }
}
- در حالت استفاده‌ی از Replace(​ReplacementBehavior.​ServiceType)، اگر سرویسی پیشتر در لیست IServiceCollection ثبت شده باشد، آن‌را حذف کرده و سپس نمونه‌ی جدید را ثبت می‌کند (ثبت سرویس بر اساس اینترفیس و پیاده سازی آن).
- در حالت استفاده‌ی از Replace(​ReplacementBehavior.​ImplementationType)، اگر پیاده سازی کلاسی پیشتر در لیست IServiceCollection ثبت شده باشد، آن‌را حذف کرده و سپس نمونه‌ی جدید را ثبت می‌کند (ثبت سرویس صرفا بر اساس نام کلاس آن).
- حالت Replace(​ReplacementBehavior.All) هر دو حالت قبل را با هم شامل می‌شود.


امکان ترکیب چندین استراتژی جستجو با هم توسط Scrutor

در یک برنامه‌ی واقعی غیرممکن است که بخواهید تمام کلاس‌ها را با یک طول عمر، اسکن و ثبت کنید. برای این منظور می‌توان از قابلیت فیلتر کردن کلاس‌ها که در مورد آن بحث شد و همچنین امکان ترکیب زنجیر وار حالت‌های مختلف اسکن، استفاده کرد:
services.Scan(scan => scan 
  .FromAssemblyOf<CombinedService>() 
    .AddClasses(classes => classes.AssignableTo<ICombinedService>()) // Filter classes 
      .AsSelfWithInterfaces() 
      .WithSingletonLifetime() 
 
    .AddClasses(x=> x.AssignableTo(typeof(IOpenGeneric<>))) // Can close generic types 
      .AsMatchingInterface() 
 
    .AddClasses(x=> x.InNamespaceOf<MyClass>()) 
      .UsingRegistrationStrategy(RegistrationStrategy.Replace()) // Defaults to ReplacementBehavior.ServiceType 
      .AsMatchingInterface() 
      .WithScopedLifetime() 
 
  .FromAssemblyOf<DatabaseContext>()   // Can load from multiple assemblies within one Scan() 
    .AddClasses()  
      .AsImplementedInterfaces() 
);
اشتراک‌ها
موجودیت های مرتبط با مدیریت قابلیت ها و نسخه ها در یک سیستم License Management

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

در راه حل ارائه شده علاوه بر اینکه امکان تنظیم یکسری قابلیت برای نسخه‌ها (Edition ها) وجود دارد، می‌توان برای مشتری خاصی قابلیت خاصی را به صورت مستقیم نیز در نظر گرفت.






موجودیت های مرتبط با مدیریت قابلیت ها و نسخه ها در یک سیستم License Management
مطالب
EF Code First #10

حین کار با ORMهای پیشرفته، ویژگی‌های جالب توجهی در اختیار برنامه نویس‌ها قرار می‌گیرد که در زمان استفاده از کلاس‌های متداول SQLHelper از آن‌ها خبری نیست؛ مانند:
الف) Deferred execution
ب) Lazy loading
ج) Eager loading

نحوه بررسی SQL نهایی تولیدی توسط EF

برای توضیح موارد فوق، نیاز به مشاهده خروجی SQL نهایی حاصل از ORM است و همچنین شمارش تعداد بار رفت و برگشت به بانک اطلاعاتی. بهترین ابزاری را که برای این منظور می‌توان پیشنهاد داد، برنامه EF Profiler است. برای دریافت آن می‌توانید به این آدرس مراجعه کنید: (^) و (^)

پس از وارد کردن نام و آدرس ایمیل، یک مجوز یک ماهه آزمایشی، به آدرس ایمیل شما ارسال خواهد شد.
زمانیکه این فایل را در ابتدای اجرای برنامه به آن معرفی می‌کنید، محل ذخیره سازی نهایی آن جهت بازبینی بعدی، مسیر MyUserName\Local Settings\Application Data\EntityFramework Profiler خواهد بود.

استفاده از این برنامه هم بسیار ساده است:
الف) در برنامه خود، ارجاعی را به اسمبلی HibernatingRhinos.Profiler.Appender.dll که در پوشه برنامه EFProf موجود است، اضافه کنید.
ب) در نقطه آغاز برنامه، متد زیر را فراخوانی نمائید:
HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize();

نقطه آغاز برنامه می‌تواند متد Application_Start برنامه‌های وب، در متد Program.Main برنامه‌های ویندوزی کنسول و WinForms و در سازنده کلاس App برنامه‌های WPF باشد.
ج) برنامه EFProf را اجرا کنید.

مزایای استفاده از این برنامه
1) وابسته به بانک اطلاعاتی مورد استفاده نیست. (برخلاف برای مثال برنامه معروف SQL Server Profiler که فقط به همراه SQL Server ارائه می‌شود)
2) خروجی SQL نمایش داده شده را فرمت کرده و به همراه Syntax highlighting نیز هست.
3) کار این برنامه صرفا به لاگ کردن SQL تولیدی خلاصه نمی‌شود. یک سری از Best practices را نیز به شما گوشزد می‌کند. بنابراین اگر نیاز دارید سیستم خود را بر اساس دیدگاه یک متخصص بررسی کنید (یک Code review ارزشمند)، این ابزار می‌تواند بسیار مفید باشد.
4) می‌تواند کوئری‌های سنگین و سبک را به خوبی تشخیص داده و گزارشات آماری جالبی را به شما ارائه دهد.
5) می‌تواند دقیقا مشخص کند، کوئری را که مشاهده می‌کنید از طریق کدام متد در کدام کلاس صادر شده است و دقیقا از چه سطری.
6) امکان گروه بندی خودکار کوئری‌های صادر شده را بر اساس DbContext مورد استفاده به همراه دارد.
و ...

استفاده از این برنامه حین کار با EF «الزامی» است! (البته نسخه‌های NH و سایر ORMهای دیگر آن نیز موجود است و این مباحث در مورد تمام ORMهای پیشرفته صادق است)
مدام باید بررسی کرد که صفحه جاری چه تعداد کوئری را به بانک اطلاعاتی ارسال کرده و به چه نحوی. همچنین آیا می‌توان با اعمال اصلاحاتی، این وضع را بهبود بخشید. بنابراین عدم استفاده از این برنامه حین کار با ORMs، همانند راه رفتن در خواب است! ممکن است تصور کنید برنامه دارد به خوبی کار می‌کند اما ... در پشت صحنه فقط صفحه جاری برنامه، 100 کوئری را به بانک اطلاعاتی ارسال کرده، در حالیکه شما تنها نیاز به یک کوئری داشته‌اید.


کلاس‌های مدل مثال جاری

کلاس‌های مدل مثال جاری از یک دپارتمان که دارای تعدادی کارمند می‌باشد، تشکیل شده است. ضمنا هر کارمند تنها در یک دپارتمان می‌تواند مشغول به کار باشد و رابطه many-to-many نیست :

using System.Collections.Generic;

namespace EF_Sample06.Models
{
public class Department
{
public int DepartmentId { get; set; }
public string Name { get; set; }

//Creates Employee navigation property for Lazy Loading (1:many)
public virtual ICollection<Employee> Employees { get; set; }
}
}

namespace EF_Sample06.Models
{
public class Employee
{
public int EmployeeId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }

//Creates Department navigation property for Lazy Loading
public virtual Department Department { get; set; }
}
}

نگاشت دستی این کلاس‌ها هم ضرورتی ندارد، زیرا قراردادهای توکار EF Code first را رعایت کرده و EF در اینجا به سادگی می‌تواند primary key و روابط one-to-many را بر اساس navigation properties تعریف شده، تشخیص دهد.

در اینجا کلاس Context برنامه به شرح زیر است:

using System.Data.Entity;
using EF_Sample06.Models;

namespace EF_Sample06.DataLayer
{
public class Sample06Context : DbContext
{
public DbSet<Department> Departments { set; get; }
public DbSet<Employee> Employees { set; get; }
}
}


و تنظیمات ابتدایی نحوه به روز رسانی و آغاز بانک اطلاعاتی نیز مطابق کدهای زیر می‌باشد:

using System.Collections.Generic;
using System.Data.Entity.Migrations;
using EF_Sample06.Models;

namespace EF_Sample06.DataLayer
{
public class Configuration : DbMigrationsConfiguration<Sample06Context>
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
AutomaticMigrationDataLossAllowed = true;
}

protected override void Seed(Sample06Context context)
{
var employee1 = new Employee { FirstName = "f name1", LastName = "l name1" };
var employee2 = new Employee { FirstName = "f name2", LastName = "l name2" };
var employee3 = new Employee { FirstName = "f name3", LastName = "l name3" };
var employee4 = new Employee { FirstName = "f name4", LastName = "l name4" };

var dept1 = new Department { Name = "dept 1", Employees = new List<Employee> { employee1, employee2 } };
var dept2 = new Department { Name = "dept 2", Employees = new List<Employee> { employee3 } };
var dept3 = new Department { Name = "dept 3", Employees = new List<Employee> { employee4 } };

context.Departments.Add(dept1);
context.Departments.Add(dept2);
context.Departments.Add(dept3);
base.Seed(context);
}
}
}

نکته: تهیه خروجی XML از نگاشت‌های خودکار تهیه شده

اگر علاقمند باشید که پشت صحنه نگاشت‌های خودکار EF Code first را در یک فایل XML جهت بررسی بیشتر ذخیره کنید، می‌توان از متد کمکی زیر استفاده کرد:

void ExportMappings(DbContext context, string edmxFile)
{
var settings = new XmlWriterSettings { Indent = true };
using (XmlWriter writer = XmlWriter.Create(edmxFile, settings))
{
System.Data.Entity.Infrastructure.EdmxWriter.WriteEdmx(context, writer);
}
}

بهتر است پسوند فایل XML تولیدی را edmx قید کنید تا بتوان آن‌را با دوبار کلیک بر روی فایل، در ویژوال استودیو نیز مشاهده کرد:

using (var db = new Sample06Context())
{
ExportMappings(db, "mappings.edmx");
}



الف) بررسی Deferred execution یا بارگذاری به تاخیر افتاده

برای توضیح مفهوم Deferred loading/execution بهترین مثالی را که می‌توان ارائه داد، صفحات جستجوی ترکیبی در برنامه‌ها است. برای مثال یک صفحه جستجو را طراحی کرده‌اید که حاوی دو تکست باکس دریافت FirstName و LastName کاربر است. کنار هر کدام از این تکست باکس‌ها نیز یک چک‌باکس قرار دارد. به عبارتی کاربر می‌تواند جستجویی ترکیبی را در اینجا انجام دهد. نحوه پیاده سازی صحیح این نوع مثال‌ها در EF Code first به چه نحوی است؟

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using EF_Sample06.DataLayer;
using EF_Sample06.Models;

namespace EF_Sample06
{
class Program
{
static IList<Employee> FindEmployees(string fName, string lName, bool byName, bool byLName)
{
using (var db = new Sample06Context())
{
IQueryable<Employee> query = db.Employees.AsQueryable();

if (byLName)
{
query = query.Where(x => x.LastName == lName);
}

if (byName)
{
query = query.Where(x => x.FirstName == fName);
}

return query.ToList();
}
}

static void Main(string[] args)
{
// note: remove this line if you received : create database is not supported by this provider.
HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize();

Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample06Context, Configuration>());

var list = FindEmployees("f name1", "l name1", true, true);
foreach (var item in list)
{
Console.WriteLine(item.FirstName);
}
}
}
}

نحوه صحیح این نوع پیاده سازی ترکیبی را در متد FindEmployees مشاهده می‌کنید. نکته مهم آن، استفاده از نوع IQueryable و متد AsQueryable است و امکان ترکیب کوئری‌ها با هم.
به نظر شما با فراخوانی متد FindEmployees به نحو زیر که هر دو شرط آن توسط کاربر انتخاب شده است، چه تعداد کوئری به بانک اطلاعاتی ارسال می‌شود؟

var list = FindEmployees("f name1", "l name1", true, true);

شاید پاسخ دهید که سه بار : یکبار در متد db.Employees.AsQueryable و دوبار هم در حین ورود به بدنه شرط‌های یاد شده و اینجا است که کسانی که قبلا با رویه‌های ذخیره شده کار کرده باشند، شروع به فریاد و فغان می‌کنند که ما قبلا این مسایل رو با یک SP در یک رفت و برگشت مدیریت می‌کردیم!
پاسخ صحیح: «فقط یکبار»! آن‌هم تنها در زمان فراخوانی متد ToList و نه قبل از آن.
برای اثبات این مدعا نیاز است به خروجی SQL لاگ شده توسط EF Profiler مراجعه کرد:

SELECT [Extent1].[EmployeeId]              AS [EmployeeId],
[Extent1].[FirstName] AS [FirstName],
[Extent1].[LastName] AS [LastName],
[Extent1].[Department_DepartmentId] AS [Department_DepartmentId]
FROM [dbo].[Employees] AS [Extent1]
WHERE ([Extent1].[LastName] = 'l name1' /* @p__linq__0 */)
AND ([Extent1].[FirstName] = 'f name1' /* @p__linq__1 */)


IQueryable قلب LINQ است و تنها بیانگر یک عبارت (expression) از رکوردهایی می‌باشد که مد نظر شما است و نه بیشتر. برای مثال زمانیکه یک IQueryable را همانند مثال فوق فیلتر می‌کنید، هنوز چیزی از بانک اطلاعاتی یا منبع داده‌ای دریافت نشده است. هنوز هیچ اتفاقی رخ نداده است و هنوز رفت و برگشتی به منبع داده‌ای صورت نگرفته است. به آن باید به شکل یک expression builder نگاه کرد و نه لیستی از اشیاء فیلتر شده‌ی ما. به این مفهوم، deferred execution (اجرای به تاخیر افتاده) نیز گفته می‌شود.
کوئری LINQ شما تنها زمانی بر روی بانک اطلاعاتی اجرا می‌شود که کاری بر روی آن صورت گیرد مانند فراخوانی متد ToList، فراخوانی متد First یا FirstOrDefault و امثال آن. تا پیش از این فقط به شکل یک عبارت در برنامه وجود دارد و نه بیشتر.
اطلاعات بیشتر: «تفاوت بین IQueryable و IEnumerable در حین کار با ORMs»



ب) بررسی Lazy Loading یا واکشی در صورت نیاز

در مطلب جاری اگر به کلاس‌های مدل برنامه دقت کنید، تعدادی از خواص به صورت virtual تعریف شده‌اند. چرا؟
تعریف یک خاصیت به صورت virtual، پایه و اساس lazy loading است و به کمک آن، تا به اطلاعات شیءایی نیاز نباشد، وهله سازی نخواهد شد. به این ترتیب می‌توان به کارآیی بیشتری در حین کار با ORMs رسید. برای مثال در کلاس‌های فوق، اگر تنها نیاز به دریافت نام یک دپارتمان هست، نباید حین وهله سازی از شیء دپارتمان، شیء لیست کارمندان مرتبط با آن نیز وهله سازی شده و از بانک اطلاعاتی دریافت شوند. به این وهله سازی با تاخیر، lazy loading گفته می‌شود.
Lazy loading پیاده سازی ساده‌ای نداشته و مبتنی است بر بکارگیری AOP frameworks یا کتابخانه‌هایی که امکان تشکیل اشیاء Proxy پویا را در پشت صحنه فراهم می‌کنند. علت virtual تعریف کردن خواص رابط نیز به همین مساله بر می‌گردد، تا این نوع کتابخانه‌ها بتوانند در نحوه تعریف اینگونه خواص virtual در زمان اجرا، در پشت صحنه دخل و تصرف کنند. البته حین استفاده از EF یا انواع و اقسام ORMs دیگر با این نوع پیچیدگی‌ها روبرو نخواهیم شد و تشکیل اشیاء Proxy در پشت صحنه انجام می‌شوند.

یک مثال: قصد داریم اولین دپارتمان ثبت شده در حین آغاز برنامه را یافته و سپس لیست کارمندان آن‌را نمایش دهیم:

using (var db = new Sample06Context())
{
var dept1 = db.Departments.Find(1);
if (dept1 != null)
{
Console.WriteLine(dept1.Name);
foreach (var item in dept1.Employees)
{
Console.WriteLine(item.FirstName);
}
}
}



رفتار یک ORM جهت تعیین اینکه آیا نیاز است برای دریافت اطلاعات بین جداول Join صورت گیرد یا خیر، واکشی حریصانه و غیرحریصانه را مشخص می‌سازد.
در حالت واکشی حریصانه به ORM خواهیم گفت که لطفا جهت دریافت اطلاعات فیلدهای جداول مختلف، از همان ابتدای کار در پشت صحنه، Join های لازم را تدارک ببین. در حالت واکشی غیرحریصانه به ORM خواهیم گفت به هیچ عنوان حق نداری Join ایی را تشکیل دهی. هر زمانی که نیاز به اطلاعات فیلدی از جدولی دیگر بود باید به صورت مستقیم به آن مراجعه کرده و آن مقدار را دریافت کنی.
به صورت خلاصه برنامه نویس در حین کار با ORM های پیشرفته نیازی نیست Join بنویسد. تنها باید ORM را طوری تنظیم کند که آیا اینکار را حتما خودش در پشت صحنه انجام دهد (واکشی حریصانه)، یا اینکه خیر، به هیچ عنوان SQL های تولیدی در پشت صحنه نباید حاوی Join باشند (lazy loading).

در مثال فوق به صورت خودکار دو کوئری به بانک اطلاعاتی ارسال می‌گردد:

SELECT [Limit1].[DepartmentId] AS [DepartmentId],
[Limit1].[Name] AS [Name]
FROM (SELECT TOP (2) [Extent1].[DepartmentId] AS [DepartmentId],
[Extent1].[Name] AS [Name]
FROM [dbo].[Departments] AS [Extent1]
WHERE [Extent1].[DepartmentId] = 1 /* @p0 */) AS [Limit1]


SELECT [Extent1].[EmployeeId] AS [EmployeeId],
[Extent1].[FirstName] AS [FirstName],
[Extent1].[LastName] AS [LastName],
[Extent1].[Department_DepartmentId] AS [Department_DepartmentId]
FROM [dbo].[Employees] AS [Extent1]
WHERE ([Extent1].[Department_DepartmentId] IS NOT NULL)
AND ([Extent1].[Department_DepartmentId] = 1 /* @EntityKeyValue1 */)

یکبار زمانیکه قرار است اطلاعات دپارتمان‌ یک (db.Departments.Find) دریافت شود. تا این لحظه خبری از جدول Employees نیست. چون lazy loading فعال است و فقط اطلاعاتی را که نیاز داشته‌ایم فراهم کرده است.
زمانیکه برنامه به حلقه می‌رسد، نیاز است اطلاعات dept1.Employees را دریافت کند. در اینجا است که کوئری دوم، به بانک اطلاعاتی صادر خواهد شد (بارگذاری در صورت نیاز).


ج) بررسی Eager Loading یا واکشی حریصانه

حالت lazy loading بسیار جذاب به نظر می‌رسد؛ برای مثال می‌توان خواص حجیم یک جدول را به جدول مرتبط دیگری منتقل کرد. مثلا فیلد‌های متنی طولانی یا اطلاعات باینری فایل‌های ذخیره شده، تصاویر و امثال آن. به این ترتیب تا زمانیکه نیازی به اینگونه اطلاعات نباشد، lazy loading از بارگذاری آن‌ها جلوگیری کرده و سبب افزایش کارآیی برنامه می‌شود.
اما ... همین lazy loading در صورت استفاده نا آگاهانه می‌تواند سرور بانک اطلاعاتی را در یک برنامه چندکاربره از پا درآورد! نیازی هم نیست تا شخصی به سایت شما حمله کند. مهاجم اصلی همان برنامه نویس کم اطلاع است!
اینبار مثال زیر را درنظر بگیرید که بجای دریافت اطلاعات یک شخص، مثلا قصد داریم، اطلاعات کلیه دپارتمان‌ها را توسط یک Grid نمایش دهیم (فرقی نمی‌کند برنامه وب یا ویندوز باشد؛ اصول یکی است):

using (var db = new Sample06Context())
{
foreach (var dept in db.Departments)
{
Console.WriteLine(dept.Name);
foreach (var item in dept.Employees)
{
Console.WriteLine(item.FirstName);
}
}
}
یک نکته: اگر سعی کنیم کد فوق را اجرا کنیم به خطای زیر برخواهیم خورد:

There is already an open DataReader associated with this Command which must be closed first

برای رفع این مشکل نیاز است گزینه MultipleActiveResultSets=True را به کانکشن استرینگ اضافه کرد:

<connectionStrings>
<clear/>
<add
name="Sample06Context"
connectionString="Data Source=(local);Initial Catalog=testdb2012;Integrated Security = true;MultipleActiveResultSets=True;"
providerName="System.Data.SqlClient"
/>
</connectionStrings>

سؤال: به نظر شما در دو حلقه تو در توی فوق چندبار رفت و برگشت به بانک اطلاعاتی صورت می‌گیرد؟ با توجه به اینکه در متد Seed ذکر شده در ابتدای مطلب، تعداد رکوردها مشخص است.
پاسخ: 7 بار!


و اینجا است که عنوان شد استفاده از EF Profiler در حین توسعه برنامه‌های مبتنی بر ORM «الزامی» است! اگر از این نکته اطلاعی نداشتید، بهتر است یکبار تمام صفحات گزارش‌گیری برنامه‌های خود را که حاوی یک Grid هستند، توسط EF Profiler بررسی کنید. اگر در این برنامه پیغام خطای n+1 select را دریافت کردید، یعنی در حال استفاده ناصحیح از امکانات lazy loading می‌باشید.

آیا می‌توان این وضعیت را بهبود بخشید؟ زمانیکه کار ما گزارشگیری از اطلاعات با تعداد رکوردهای بالا است، استفاده ناصحیح از ویژگی Lazy loading می‌تواند به شدت کارآیی بانک اطلاعاتی را پایین بیاورد. برای حل این مساله در زمان‌های قدیم (!) بین جداول join می‌نوشتند؛ الان چطور؟
در EF متدی به نام Include جهت Eager loading اطلاعات موجودیت‌های مرتبط به هم درنظر گرفته شده است که در پشت صحنه همینکار را انجام می‌دهد:

using (var db = new Sample06Context())
{
foreach (var dept in db.Departments.Include(x => x.Employees))
{
Console.WriteLine(dept.Name);
foreach (var item in dept.Employees)
{
Console.WriteLine(item.FirstName);
}
}
}

همانطور که ملاحظه می‌کنید اینبار به کمک متد Include، نسبت به واکشی حریصانه Employees اقدام کرده‌ایم. اکنون اگر برنامه را اجرا کنیم، فقط یک رفت و برگشت به بانک اطلاعاتی انجام خواهد شد و کار Join نویسی به صورت خودکار توسط EF مدیریت می‌گردد:

SELECT [Project1].[DepartmentId]            AS [DepartmentId],
[Project1].[Name] AS [Name],
[Project1].[C1] AS [C1],
[Project1].[EmployeeId] AS [EmployeeId],
[Project1].[FirstName] AS [FirstName],
[Project1].[LastName] AS [LastName],
[Project1].[Department_DepartmentId] AS [Department_DepartmentId]
FROM (SELECT [Extent1].[DepartmentId] AS [DepartmentId],
[Extent1].[Name] AS [Name],
[Extent2].[EmployeeId] AS [EmployeeId],
[Extent2].[FirstName] AS [FirstName],
[Extent2].[LastName] AS [LastName],
[Extent2].[Department_DepartmentId] AS [Department_DepartmentId],
CASE
WHEN ([Extent2].[EmployeeId] IS NULL) THEN CAST(NULL AS int)
ELSE 1
END AS [C1]
FROM [dbo].[Departments] AS [Extent1]
LEFT OUTER JOIN [dbo].[Employees] AS [Extent2]
ON [Extent1].[DepartmentId] = [Extent2].[Department_DepartmentId]) AS [Project1]
ORDER BY [Project1].[DepartmentId] ASC,
[Project1].[C1] ASC


متد Include در نگارش‌های اخیر EF پیشرفت کرده است و همانند مثال فوق، امکان کار با lambda expressions را جهت تعریف خواص مورد نظر به صورت strongly typed ارائه می‌دهد. در نگارش‌های قبلی این متد، تنها امکان استفاده از رشته‌ها برای معرفی خواص وجود داشت.
همچنین توسط متد Include امکان eager loading چندین سطح با هم نیز وجود دارد؛ مثلا x.Employees.Kids و همانند آن.


چند نکته در مورد نحوه خاموش کردن Lazy loading

امکان خاموش کردن Lazy loading در تمام کلاس‌های برنامه با تنظیم خاصیت Configuration.LazyLoadingEnabled کلاس Context برنامه به نحو زیر میسر است:

public class Sample06Context : DbContext
{
public Sample06Context()
{
this.Configuration.LazyLoadingEnabled = false;
}

یا اگر تنها در مورد یک کلاس نیاز است این خاموش سازی صورت گیرد، کلمه کلیدی virtual را حذف کنید. برای مثال با نوشتن public ICollection<Employee> Employees بجای public virtual ICollection<Employee> Employees در اولین بار وهله سازی کلاس دپارتمان، لیست کارمندان آن به نال تنظیم می‌شود. البته در این حالت null object pattern را نیز فراموش نکنید (وهله سازی پیش فرض Employees در سازنده کلاس):

public class Department
{
public int DepartmentId { get; set; }
public string Name { get; set; }

public ICollection<Employee> Employees { get; set; }
public Department()
{
Employees = new HashSet<Employee>();
}
}

به این ترتیب به خطای null reference object بر نخواهیم خورد. همچنین وهله سازی، با مقدار دهی لیست دریافتی از بانک اطلاعاتی متفاوت است. در اینجا نیز باید از متد Include استفاده کرد.

بنابراین در صورت خاموش کردن lazy loading، حتما نیاز است از متد Include استفاده شود. اگرlazy loading فعال است، جهت تبدیل آن به eager loading از متد Include استفاده کنید (اما اجباری نیست).
نظرات مطالب
حذف هدرهای مربوط به وب سرور از طریق برنامه نویسی
- برای فایل‌های جاوا اسکریپت توصیه من این است:
الف) اگر از Web forms استفاده می‌کنید: استفاده از Script manager (^ و ^)
ب) اگر از MVC استفاده می‌کنید: استفاده از Bundling & minification
در هر دو حالت نحوه ارائه اسکریپت‌ها تحت کنترل برنامه ASP.NET در خواهد آمد و مستقیما و بدون دخالت ASP.NET، توسط IIS توزیع نمی‌شوند.
- برای مپ کردن فایل‌های استاتیک به موتور ASP.NET می‌شود از StaticFileHandler استفاده کرد. اگر کش کردن اطلاعات استاتیک در سمت سرور فعال شود، این مساله بار اضافه‌ای را به سرور تحمیل نخواهد کرد.
<system.web>
   <httpHandlers>
      <add path="*.js" verb="*" type="System.Web.StaticFileHandler" />
   </httpHandlers>
نظرات مطالب
C# 12.0 - Collection Expressions & Spread Operator
اصلاحیه: کارآیی spread operator بیشتر نیست!

در متن در مورد spread operator عنوان شده «که ... نگارش C# 12 آن کارآیی بیشتری دارد». این مورد بدون توجه به #Low-Level C تولیدی، نوشته شد و ... متاسفانه نادرست است!
برای مثال فرض کنید، چنین متدی را دارید که با استفاده از spread operator، کار بازکردن یک آرایه را انجام می‌دهد:
public int[] WithSpread()
{
   int[] data = new int[10_000];
   int[] results = [..data];
   return results;
}
معادل #Low-Level C آن (کد نهایی که کامپایلر برای تبدیل آن به IL تولید می‌کند) به صورت زیر است ( #Low-Level C را در Rider، در منوی #Tools -> IL Viewer -> Select Low-Level C می‌توانید تولید کنید):
public int[] WithSpread()
{
  int[] numArray1 = new int[10000];
  int index1 = 0;
  int[] numArray2 = new int[numArray1.Length];
  int[] numArray3 = numArray1;
  for (int index2 = 0; index2 < numArray3.Length; ++index2)
  {
    int num = numArray3[index2];
    numArray2[index1] = num;
    ++index1;
  }
  return numArray2;
}
همانطور که مشاهده می‌کنید، این قطعه کد در C#12 و دات‌نت 8، به شدت ابتدایی تولید شده و به همراه هیچ نوع بهینه سازی نیست. کارآیی این قطعه کد، نسبت به زمانیکه از متد قدیمی CopyTo آرایه‌ها استفاده می‌شود، به مراتب کمتر است (تا 3 برابر!)؛ چون متد CopyTo به همراه بهینه سازی‌های سخت‌افزاری هم هست. به نظر قرار شده بهینه سازی کارآیی spread operator در نگارش بعدی دات‌نت انجام شود.

برای آزمایش شخصی آن، از کلاس زیر استفاده کنید:
using BenchmarkDotNet.Attributes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SpreadBenchmark
{
    [MemoryDiagnoser]
    public class Tests
    {
        private readonly int[] _myData = new int[10_000];

        [Benchmark(Baseline = true)]
        public int[] WithToArray()
        {
            int[] results = _myData.ToArray();
            return results;
        }

        [Benchmark]
        public int[] WithCopyTo()
        {
            int[] results = new int[_myData.Length];
            _myData.CopyTo(results, 0);
            return results;
        }

        [Benchmark]
        public int[] WithSpread()
        {
            int[] results = [.._myData];
            return results;
        }
    }
}
که در فایل Program.cs به این صورت فراخوانی می‌شود:
using BenchmarkDotNet.Running;
using SpreadBenchmark;

BenchmarkRunner.Run<Tests>();
با این وابستگی:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="BenchmarkDotNet" Version="0.13.10" />
  </ItemGroup>
</Project>
مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت پنجم - پیاده سازی ورود و خروج از سیستم
پس از راه اندازی IdentityServer، نوبت به امن سازی برنامه‌ی Mvc Client توسط آن می‌رسد و اولین قسمت آن، ورود به سیستم و خروج از آن می‌باشد.


بررسی اجزای Hybrid Flow

در قسمت سوم در حین «انتخاب OpenID Connect Flow مناسب برای یک برنامه‌ی کلاینت از نوع ASP.NET Core» به این نتیجه رسیدیم که Flow مناسب یک برنامه‌ی Mvc Client از نوع Hybrid است. در اینجا هر Flow، شروع به ارسال درخواستی به سمت Authorization Endpoint می‌کند؛ با یک چنین قالبی:
https://idpHostAddress/connect/authorize? 
client_id=imagegalleryclient 
&redirect_uri=https://clientapphostaddress/signin-oidcoidc 
&scope=openid profile 
&response_type=code id_token 
&response_mode=form_post
&nonce=63626...n2eNMxA0
- در سطر اول، Authorization Endpoint مشخص شده‌است. این آدرس از discovery endpoint که یک نمونه تصویر محتوای آن‌را در قسمت قبل مشاهده کردید، استخراج می‌شود.
- سپس client_id جهت تعیین برنامه‌ای که درخواست را ارسال می‌کند، ذکر شده‌است؛ از این جهت که یک IDP جهت کار با چندین نوع کلاینت مختلف طراحی شده‌است.
- redirect_uri همان Redirect Endpoint است که در سطح برنامه‌ی کلاینت تنظیم می‌شود.
- در مورد scope در قسمت قبل در حین راه اندازی IdentityServer توضیح دادیم. در اینجا برنامه‌ی کلاینت، درخواست scopeهای openid و profile را داده‌است. به این معنا که نیاز دارد تا Id کاربر وارد شده‌ی به سیستم و همچنین Claims منتسب به او را در اختیار داشته باشد.
- response_type نیز به code id_token تنظیم شده‌است. توسط response_type، نوع Flow مورد استفاده مشخص می‌شود. ذکر code به معنای بکارگیری Authorization code flow است. ذکر id_token و یا id_token token هر دو به معنای استفاده‌ی از implicit flow است. اما برای مشخص سازی Hybrid flow یکی از سه مقدار code id_token و یا code token و یا code id_token token با هم ذکر می‌شوند:


- در اینجا response_mode مشخص می‌کند که اطلاعات بازگشتی از سمت IDP که توسط response_type مشخص شده‌اند، با چه قالبی به سمت کلاینت بازگشت داده شوند که می‌تواند از طریق Form POST و یا URI باشد.


در Hybrid flow با response_type از نوع code id_token، ابتدا کلاینت یک درخواست Authentication را به Authorization Endpoint ارسال می‌کند (با همان قالب URL فوق). سپس در سطح IDP، کاربر برای مثال با ارائه‌ی کلمه‌ی عبور و نام کاربری، تعیین اعتبار می‌شود. همچنین در اینجا IDP ممکن است رضایت کاربر را از دسترسی به اطلاعات پروفایل او نیز سؤال بپرسد (تحت عنوان مفهوم Consent). سپس IDP توسط یک Redirection و یا Form POST، اطلاعات authorization code و identity token را به سمت برنامه‌ی کلاینت ارسال می‌کند. این همان اطلاعات مرتبط با response_type ای است که درخواست کرد‌ه‌ایم. سپس برنامه‌ی کلاینت این اطلاعات را تعیین اعتبار کرده و در صورت موفقیت آمیز بودن این عملیات، اکنون درخواست تولید توکن هویت را به token endpoint ارسال می‌کند. برای این منظور کلاینت سه مشخصه‌ی authorization code ،client-id و client-secret را به سمت token endpoint ارسال می‌کند. در پاسخ یک identity token را دریافت می‌کنیم. در اینجا مجددا این توکن تعیین اعتبار شده و سپس Id کاربر را از آن استخراج می‌کند که در برنامه‌ی کلاینت قابل استفاده خواهد بود. این مراحل را در تصویر زیر می‌توانید ملاحظه کنید.
البته اگر دقت کرده باشید، یک identity token در همان ابتدای کار از Authorization Endpoint دریافت می‌شود. اما چرا از آن استفاده نمی‌کنیم؟ علت اینجا است که token endpoint نیاز به اعتبارسنجی client را نیز دارد. به این ترتیب یک لایه‌ی امنیتی دیگر نیز در اینجا بکار گرفته می‌شود. همچنین access token و refresh token نیز از همین token endpoint قابل دریافت هستند.




تنظیم IdentityServer جهت انجام عملیات ورود به سیستم بر اساس جزئیات Hybrid Flow

برای افزودن قسمت لاگین به برنامه‌ی MVC قسمت دوم، نیاز است تغییراتی را در برنامه‌ی کلاینت و همچنین IDP اعمال کنیم. برای این منظور کلاس Config پروژه‌ی IDP را که در قسمت قبل ایجاد کردیم، به صورت زیر تکمیل می‌کنیم:
namespace DNT.IDP
{
    public static class Config
    {
        public static IEnumerable<Client> GetClients()
        {
            return new List<Client>
            {
                new Client
                {
                    ClientName = "Image Gallery",
                    ClientId = "imagegalleryclient",
                    AllowedGrantTypes = GrantTypes.Hybrid,
                    RedirectUris = new List<string>
                    {
                        "https://localhost:5001/signin-oidc"
                    },
                    PostLogoutRedirectUris = new List<string>
                    {
                        "https://localhost:5001/signout-callback-oidc"
                    },
                    AllowedScopes =
                    {
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile
                    },
                    ClientSecrets =
                    {
                        new Secret("secret".Sha256())
                    }
                }
             };
        }
    }
}
در اینجا بجای بازگشت لیست خالی کلاینت‌ها، یک کلاینت جدید را تعریف و تکمیل کرده‌ایم.
- ابتدا نام کلاینت را مشخص می‌کنیم. این نام و عنوان، در صفحه‌ی لاگین و Consent (رضایت دسترسی به اطلاعات پروفایل کاربر)، ظاهر می‌شود.
- همچنین نیاز است یک Id دلخواه را نیز برای آن مشخص کنیم؛ مانند imagegalleryclient در اینجا.
- AllowedGrantTypes را نیز به Hybrid Flow تنظیم کرده‌ایم. علت آن‌را در قسمت سوم این سری بررسی کردیم.
- با توجه به اینکه Hybrid Flow از Redirectها استفاده می‌کند و اطلاعات نهایی را به کلاینت از طریق Redirection ارسال می‌کند، به همین جهت آدرس RedirectUris را به آدرس برنامه‌ی Mvc Client تنظیم کرده‌ایم (که در اینجا بر روی پورت 5001 کار می‌کند). قسمت signin-oidc آن‌را در ادامه تکمیل خواهیم کرد.
- در قسمت AllowedScopes، لیست scopeهای مجاز قابل دسترسی توسط این کلاینت مشخص شده‌اند که شامل دسترسی به ID کاربر و Claims آن است.
- به ClientSecrets نیز جهت client authenticating نیاز داریم.


تنظیم برنامه‌ی MVC Client جهت انجام عملیات ورود به سیستم بر اساس جزئیات Hybrid Flow

برای افزودن قسمت لاگین به سیستم، کلاس آغازین پروژه‌ی MVC Client را به نحو زیر تکمیل می‌کنیم:
namespace ImageGallery.MvcClient.WebApp
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(options =>
            {
                options.DefaultScheme = "Cookies";
                options.DefaultChallengeScheme = "oidc";
            }).AddCookie("Cookies")
              .AddOpenIdConnect("oidc", options =>
              {
                  options.SignInScheme = "Cookies";
                  options.Authority = "https://localhost:6001";
                  options.ClientId = "imagegalleryclient";
                  options.ResponseType = "code id_token";
                  //options.CallbackPath = new PathString("...")
                  //options.SignedOutCallbackPath = new PathString("...")
                  options.Scope.Add("openid");
                  options.Scope.Add("profile");
                  options.SaveTokens = true;
                  options.ClientSecret = "secret";
                  options.GetClaimsFromUserInfoEndpoint = true;
              });
این قسمت تنظیمات، سمت کلاینت OpenID Connect Flow را مدیریت می‌کند.

- ابتدا با فراخوانی AddAuthentication، کار تنظیمات میان‌افزار استاندارد Authentication برنامه‌های ASP.NET Core انجام می‌شود. در اینجا DefaultScheme آن به Cookies تنظیم شده‌است تا عملیات Sign-in و Sign-out سمت کلاینت را میسر کند. سپس DefaultChallengeScheme به oidc تنظیم شده‌است. این مقدار با Scheme ای که در ادامه آن‌را تنظیم خواهیم کرد، تطابق دارد.

- سپس متد AddCookie فراخوانی شده‌است که authentication-Scheme را به عنوان پارامتر قبول می‌کند. به این ترتیب cookie based authentication در برنامه میسر می‌شود. پس از اعتبارسنجی توکن هویت دریافتی و تبدیل آن به Claims Identity، در یک کوکی رمزنگاری شده برای استفاده‌های بعدی ذخیره می‌شود.

- در آخر تنظیمات پروتکل OpenID Connect را ملاحظه می‌کنید. به این ترتیب مراحل اعتبارسنجی توسط این پروتکل در اینجا که Hybrid flow است، پشتیبانی خواهد شد.  اینجا است که کار درخواست Authorization، دریافت و اعتبارسنجی توکن هویت صورت می‌گیرد. اولین پارامتر آن authentication-Scheme است که به oidc تنظیم شده‌است. به این ترتیب اگر قسمتی از برنامه نیاز به Authentication داشته باشد، OpenID Connect به صورت پیش‌فرض مورد استفاده قرار می‌گیرد. به همین جهت DefaultChallengeScheme را نیز به oidc تنظیم کردیم. در اینجا SignInScheme به Cookies تنظیم شده‌است که با DefaultScheme اعتبارسنجی تطابق دارد. به این ترتیب نتیجه‌ی موفقیت آمیز عملیات اعتبارسنجی در یک کوکی رمزنگاری شده ذخیره خواهد شد. مقدار خاصیت Authority به آدرس IDP تنظیم می‌شود که بر روی پورت 6001 قرار دارد. تنظیم این مسیر سبب خواهد شد تا این میان‌افزار سمت کلاینت، به discovery endpoint دسترسی یافته و بتواند مقادیر سایر endpoints برنامه‌ی IDP را به صورت خودکار دریافت و استفاده کند. سپس ClientId تنظیم شده‌است که باید با مقدار تنظیم شده‌ی آن در سمت IDP یکی باشد و همچنین مقدار ClientSecret در اینجا نیز باید با ClientSecrets سمت IDP یکی باشد. ResponseType تنظیم شده‌ی در اینجا با AllowedGrantTypes سمت IDP تطابق دارد که از نوع Hybrid است. سپس دو scope درخواستی توسط این برنامه‌ی کلاینت که openid و profile هستند در اینجا اضافه شده‌اند. به این ترتیب می‌توان به مقادیر Id کاربر و claims او دسترسی داشت. مقدار CallbackPath در اینجا به RedirectUris سمت IDP اشاره می‌کند که مقدار پیش‌فرض آن همان signin-oidc است. با تنظیم SaveTokens به true امکان استفاده‌ی مجدد از آن‌ها را میسر می‌کند.

پس از تکمیل قسمت ConfigureServices و انجام تنظیمات میان‌افزار اعتبارسنجی، نیاز است این میان‌افزار را نیز به برنامه افزود که توسط متد UseAuthentication انجام می‌شود:
namespace ImageGallery.MvcClient.WebApp
{
    public class Startup
    {
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseAuthentication();

پس از این تنظیمات، با اعمال ویژگی Authorize، دسترسی به کنترلر گالری برنامه‌ی MVC Client را صرفا محدود به کاربران وارد شده‌ی به سیستم می‌کنیم:
namespace ImageGallery.MvcClient.WebApp.Controllers
{
    [Authorize]
    public class GalleryController : Controller
    {
    // .... 
   
        public async Task WriteOutIdentityInformation()
        {
            var identityToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.IdToken);
            Debug.WriteLine($"Identity token: {identityToken}");

            foreach (var claim in User.Claims)
            {
                Debug.WriteLine($"Claim type: {claim.Type} - Claim value: {claim.Value}");
            }
        }
در اینجا علاوه بر اعمال فیلتر Authorize به کل اکشن متدهای این کنترلر، یک اکشن متد جدید دیگر را نیز به انتهای آن اضافه کرده‌ایم تا صرفا جهت دیباگ برنامه، اطلاعات دریافتی از IDP را در Debug Window، برای بررسی بیشتر درج کند. البته این روش با Debug Window مخصوص Visual Studio کار می‌کند. اگر می‌خواهید آن‌را در صفحه‌ی کنسول dotnet run مشاهده کنید، بجای Debug باید از ILogger استفاده کرد.

فراخوانی متد GetTokenAsync با پارامتر IdToken، همان Identity token دریافتی از IDP را بازگشت می‌دهد. این توکن با تنظیم SaveTokens به true در تنظیمات AddOpenIdConnect که پیشتر انجام دادیم، قابل استخراج از کوکی اعتبارسنجی برنامه شده‌است.
این متد را در ابتدای اکشن متد Index فراخوانی می‌کنیم:
        public async Task<IActionResult> Index()
        {
            await WriteOutIdentityInformation();
            // ....


اجرای برنامه جهت آزمایش تنظیمات انجام شده

برای اجرای برنامه:
- ابتدا به پوشه‌ی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشه‌ی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آن‌را اجرا کنید تا برنامه‌ی IDP راه اندازی شود.
- در آخر به پوشه‌ی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا MVC Client راه اندازی شود.

اکنون که هر سه برنامه با هم در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید:


در این حالت چون فیلتر Authorize به کل اکشن متدهای کنترلر گالری اعمال شده، میان‌افزار Authentication که در فایل آغازین برنامه‌ی کلاینت MVC تنظیم شده‌است، وارد عمل شده و کاربر را به صفحه‌ی لاگین سمت IDP هدایت می‌کند (شماره پورت آن 6001 است). لاگ این اعمال را هم در برگه‌ی network مرورگر می‌تواند مشاهده کنید.

در اینجا نام کاربری و کلمه‌ی عبور اولین کاربر تعریف شده‌ی در فایل Config.cs برنامه‌ی IDP را که User 1 و password است، وارد می‌کنیم. پس از آن صفحه‌ی Consent ظاهر می‌شود:


در اینجا از کاربر سؤال می‌پرسد که آیا به برنامه‌ی کلاینت اجازه می‌دهید تا به Id و اطلاعات پروفایل و یا همان Claims شما دسترسی پیدا کند؟
فعلا گزینه‌ی remember my design را انتخاب نکنید تا همواره بتوان این صفحه را در دفعات بعدی نیز مشاهده کرد. سپس بر روی گزینه‌ی Yes, Allow کلیک کنید.
اکنون به صورت خودکار به سمت برنامه‌ی MVC Client هدایت شده و می‌توانیم اطلاعات صفحه‌ی اول سایت را کاملا مشاهده کنیم (چون کاربر اعتبارسنجی شده‌است، از فیلتر Authorize رد خواهد شد).


همچنین در اینجا اطلاعات زیادی نیز جهت دیباگ برنامه لاگ می‌شوند که در آینده جهت عیب یابی آن می‌توانند بسیار مفید باشند:


با دنبال کردن این لاگ می‌توانید مراحل Hybrid Flow را مرحله به مرحله با مشاهده‌ی ریز جزئیات آن بررسی کنید. این مراحل به صورت خودکار توسط میان‌افزار Authentication انجام می‌شوند و در نهایت اطلاعات توکن‌های دریافتی به صورت خودکار در اختیار برنامه برای استفاده قرار می‌گیرند. یعنی هم اکنون کوکی رمزنگاری شده‌ی اطلاعات اعتبارسنجی کاربر در دسترس است و به اطلاعات آن می‌توان توسط شیء this.User، در اکشن متدهای برنامه‌ی MVC، دسترسی داشت.


تنظیم برنامه‌ی MVC Client جهت انجام عملیات خروج از سیستم

ابتدا نیاز است یک لینک خروج از سیستم را به برنامه‌ی کلاینت اضافه کنیم. برای این منظور به فایل Views\Shared\_Layout.cshtml مراجعه کرده و لینک logout را در صورت IsAuthenticated بودن کاربر جاری وارد شده‌ی به سیستم، نمایش می‌دهیم:
<div class="navbar-collapse collapse">
    <ul class="nav navbar-nav">
        <li><a asp-area="" asp-controller="Gallery" asp-action="Index">Home</a></li>
        <li><a asp-area="" asp-controller="Gallery" asp-action="AddImage">Add an image</a></li>
        @if (User.Identity.IsAuthenticated)
        {
            <li><a asp-area="" asp-controller="Gallery" asp-action="Logout">Logout</a></li>
        }
    </ul>
</div>


شیء this.User، هم در اکشن متدها و هم در Viewهای برنامه، جهت دسترسی به اطلاعات کاربر اعتبارسنجی شده، در دسترس است.
این لینک به اکشن متد Logout، در کنترلر گالری اشاره می‌کند که آن‌را به صورت زیر تکمیل خواهیم کرد:
namespace ImageGallery.MvcClient.WebApp.Controllers
{
    [Authorize]
    public class GalleryController : Controller
    {
        public async Task Logout()
        {
            // Clears the  local cookie ("Cookies" must match the name of the scheme)
            await HttpContext.SignOutAsync("Cookies");
            await HttpContext.SignOutAsync("oidc");
        }
در اینجا ابتدا کوکی Authentication حذف می‌شود. نامی که در اینجا انتخاب می‌شود باید با نام scheme انتخابی مرتبط در فایل آغازین برنامه یکی باشد.
سپس نیاز است از برنامه‌ی IDP نیز logout شویم. به همین جهت سطر دوم SignOutAsync با پارامتر oidc را مشاهده می‌کنید. بدون وجود این سطر، کاربر فقط از برنامه‌ی کلاینت logout می‌شود؛ اما اگر به IDP مجددا هدایت شود، مشاهده خواهد کرد که در آن سمت، هنوز نام کاربری او توسط IDP شناسایی می‌شود.


بهبود تجربه‌ی کاربری Logout

پس از logout، بدون انجام یکسری از تنظیمات، کاربر مجددا به برنامه‌ی کلاینت به صورت خودکار هدایت نخواهد شد و در همان سمت IDP متوقف می‌شد. برای بهبود این وضعیت و بازگشت مجدد به برنامه‌ی کلاینت، اینکار را یا توسط مقدار دهی خاصیت SignedOutCallbackPath مربوط به متد AddOpenIdConnect می‌توان انجام داد و یا بهتر است مقدار پیش‌فرض آن‌را به تنظیمات IDP نسبت داد که پیشتر در تنظیمات متد GetClients آن‌را ذکر کرده بودیم:
PostLogoutRedirectUris = new List<string>
{
     "https://localhost:5001/signout-callback-oidc"
},
با وجود این تنظیم، اکنون IDP می‌داند که پس از logout، چه آدرسی را باید به کاربر جهت بازگشت به سیستم قبلی ارائه دهد:


البته هنوز یک مرحله‌ی انتخاب و کلیک بر روی لینک بازگشت وجود دارد. برای حذف آن و خودکار کردن Redirect نهایی آن، می‌توان کدهای IdentityServer4.Quickstart.UI را که در قسمت قبل به برنامه‌ی IDP اضافه کردیم، اندکی تغییر دهیم. برای این منظور فایل src\IDP\DNT.IDP\Quickstart\Account\AccountOptions.cs را گشوده و سپس فیلد AutomaticRedirectAfterSignOut را که false است، به true تغییر دهید.

 
تنظیمات بازگشت Claims کاربر به برنامه‌ی کلاینت

به صورت پیش‌فرض، Identity Server اطلاعات Claims کاربر را ارسال نمی‌کند و Identity token صرفا به همراه اطلاعات Id کاربر است. برای تنظیم آن می‌توان در سمت تنظیمات IDP، در متد GetClients، زمانیکه new Client صورت می‌گیرد، خاصیت AlwaysIncludeUserClaimsInIdToken هر کلاینت را به true تنظیم کرد؛ اما ایده خوبی نیست. Identity token از طریق Authorization endpoint دریافت می‌شود. در اینجا اگر این اطلاعات از طریق URI دریافت شود و Claims به Identity token افزوده شوند، به مشکل بیش از حد طولانی شدن URL نهایی خواهیم رسید و ممکن است از طرف وب سرور یک چنین درخواستی برگشت بخورد. به همین جهت به صورت پیش‌فرض اطلاعات Claims به Identity token اضافه نمی‌شوند.
در اینجا برای دریافت Claims، یک endpoint دیگر در IDP به نام UserInfo endpoint درنظر گرفته شده‌است. در این حالت برنامه‌ی کلاینت، مقدار Access token دریافتی را که به همراه اطلاعات scopes متناظر با Claims است، به سمت UserInfo endpoint ارسال می‌کند. باید دقت داشت زمانیکه Identity token دوم از Token endpoint دریافت می‌شود (تصویر ابتدای بحث)، به همراه آن یک Access token نیز صادر و ارسال می‌گردد. اینجا است که میان‌افزار oidc، این توکن دسترسی را به سمت UserInfo endpoint ارسال می‌کند تا user claims را دریافت کند:


در تنظیمات سمت کلاینت AddOpenIdConnect، درخواست openid و profile، یعنی درخواست Id کاربر و Claims آن وجود دارند:
options.Scope.Add("openid");
options.Scope.Add("profile");
برای بازگشت آن‌ها به سمت کلاینت، درخواست دریافت claims از UserInfo Endpoint را در سمت کلاینت تنظیم می‌کنیم:
options.GetClaimsFromUserInfoEndpoint = true;
همین اندازه تنظیم میان‌افزار oidc، برای انجام خودکار کل گردش کاری یاد شده کافی است.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشه‌ی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشه‌ی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آن‌را اجرا کنید تا برنامه‌ی IDP راه اندازی شود.
- در آخر به پوشه‌ی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه با هم در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحه‌ی login نام کاربری را User 1 و کلمه‌ی عبور آن‌را password وارد کنید.
مطالب
خلاصه اشتراک‌های روز پنج شنبه 19 آبان 1390