مطالب
اندازه گیری کارآیی کدها توسط NBench
این روزها جهت اندازه‌گیری کارآیی قطعات کدهای دات نتی، استفاده از فریم ورک‌های مخصوصی که بسیاری از نکات ریز مرتبط با اینگونه اندازه‌گیری‌ها را مانند warmup یا گرم کردن JIT (جهت عدم اندازه گیری زمان کامپایل پویای کدها، بجای زمان واقعی اجرای آن‌ها)، اندازه‌گیری فشار بر روی Garbage collector و غیره را انجام می‌دهند، بجای استفاده‌ی از Stop Watch، متداول است. یکی از معروفترین‌های این گروه، که تقریبا حالت استانداردی را در جهت اندازه گیری کارآیی کدهای دات نتی پیدا کرده‌است، فریم ورک سورس باز NBench است.


شروع به کار با NBench

برای شروع به کار با NBench، ابتدا نیاز است دو بسته‌ی نیوگت ذیل را نصب کرد:
PM> Install-Package NBench
PM> Install-Package NBench.Runner
عملکرد این فریم ورک، شبیه به عملکرد فریم ورک‌های آزمون‌های واحد است. برای مثال فرض کنید که می‌خواهید فشار حافظه و فشار بر روی GC قطعه کدی را اندازه گیری کنید:
[PerfBenchmark(RunMode = RunMode.Iterations, TestMode = TestMode.Measurement)]
[MemoryMeasurement(MemoryMetric.TotalBytesAllocated)]
public void AddMemoryMeasurement()
{
    const int numberOfAdds = 1000000;
    var dictionary = new Dictionary<int, int>();
    for (var i = 0; i < numberOfAdds; i++)
    {
        dictionary.Add(i, i);
    }
}
 
[PerfBenchmark(RunMode = RunMode.Iterations, TestMode = TestMode.Measurement)]
[GcMeasurement(GcMetric.TotalCollections, GcGeneration.AllGc)]
public void MeasureGarbageCollections()
{
    var dataCache = new List<int[]>();
    for (var i = 0; i < 500; i++)
    {
        for (var j = 0; j < 10000; j++)
        {
            var data = new int[100];
            dataCache.Add(data.ToArray());
        }
 
        dataCache.Clear();
    }
}
همانند نوشتن متدهای آزمون‌های واحد، ابتدا یک یا چند متد public void را در اینجا اضافه می‌کنیم.
سپس هر متد تست به ویژگی PerfBenchmark مزین می‌شود. در اینجا RunMode.Iterations به این معنا است که خودمان قصد داریم در طی یک حلقه، تعداد بار انجام را مشخص کنیم.
ویژگی MemoryMeasurement برای اندازه گیری حافظه‌ی مصرفی یک قطعه کد و GcMeasurement برای اندازه گیری فشار بر روی Garbage collector بکار می‌رود.


اجرای آزمون‌های NBench

پس از تهیه‌ی دو متد فوق، به پوشه‌ی packages\NBench.Runner.0.3.4\lib\net45 مراجعه کنید. یک فایل exe در آن موجود است که کار یافتن و اجرای آزمون‌های NBench را انجام می‌دهد. به عنوان پارامتر آن تنها کافی است مسیر اسمبلی برنامه (فایل exe و یا dll) را به آن ارسال کنیم:
 D:\Prog\NBenchSample\packages\NBench.Runner.0.3.4\lib\net45\NBench.Runner.exe "D:\Prog\NBenchSample\NBenchSample\bin\Release\NBenchSample.exe"
پس از آن، کار اجرای آزمون‌های NBench شروع شده و پس از مدتی ابتدا BEGIN WARMUP و END WARMUP‌ها را می‌توان مشاهده کرد و در آخر یک چنین خروجی ارائه می‌شود:
 --------------- RESULTS: NBenchSample.Program+AddMemoryMeasurement ---------------
TotalBytesAllocated: Max: 47,842,944.00 bytes, Average: 42,002,757.60 bytes, Min: 41,353,848.00 bytes, StdDev: 2,052,032.33 bytes
TotalBytesAllocated: Max / s: 359,074,078.19 bytes, Average / s: 311,474,786.96 bytes, Min / s: 300,926,928.79 bytes, StdDev / s: 16,869,581.62 bytes

--------------- RESULTS: NBenchSample.Program+MeasureGarbageCollections ---------------
TotalCollections [Gen0]: Max: 708.00 collections, Average: 702.80 collections, Min: 697.00 collections, StdDev: 3.65 collections
TotalCollections [Gen0]: Max / s: 111.55 collections, Average / s: 109.87 collections, Min / s: 107.88 collections, StdDev / s: 1.28 collections

TotalCollections [Gen1]: Max: 338.00 collections, Average: 334.60 collections, Min: 330.00 collections, StdDev: 2.41 collections
TotalCollections [Gen1]: Max / s: 53.61 collections, Average / s: 52.31 collections, Min / s: 51.10 collections, StdDev / s: 0.70 collections

TotalCollections [Gen2]: Max: 32.00 collections, Average: 24.80 collections, Min: 18.00 collections, StdDev: 4.73 collections
TotalCollections [Gen2]: Max / s: 4.91 collections, Average / s: 3.87 collections, Min / s: 2.86 collections, StdDev / s: 0.72 collections


نکته‌ای در مورد اندازه گیری فشار حافظه

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

شبیه به همین استراتژی، در پیاده سازی Dictionary نیز بکارگرفته شده‌است:
[PerfBenchmark(RunMode = RunMode.Iterations, TestMode = TestMode.Measurement)]
[MemoryMeasurement(MemoryMetric.TotalBytesAllocated)]
public void AddMemoryMeasurement_With_initial_Size()
{
    const int numberOfAdds = 1000000;
    var dictionary = new Dictionary<int, int>(numberOfAdds);
    for (var i = 0; i < numberOfAdds; i++)
    {
        dictionary.Add(i, i);
    }
}
اگر اینبار این آزمون را انجام دهیم، به نتیجه‌ی ذیل خواهیم رسید:
 --------------- RESULTS: NBenchSample.Program+AddMemoryMeasurement_With_initial_Size ---------------
TotalBytesAllocated: Max: 23,245,912.00 bytes, Average: 23,245,912.00 bytes, Min: 23,245,912.00 bytes, StdDev: 0.00 bytes
TotalBytesAllocated: Max / s: 394,032,435.34 bytes, Average / s: 389,108,363.43 bytes, Min / s: 378,502,981.34 bytes, StdDev / s: 5,575,519.09 bytes
در اینجا زمانیکه شیء دیکشنری ایجاد شده‌است، اندازه‌ی اولیه‌ی آن نیز مشخص گردیده‌است. همین مساله سبب شده‌است تا مصرف حافظه‌ی آن از نزدیک به 41 مگابایت (متد AddMemoryMeasurement ابتدای بحث) به نزدیک 24 مگابایت (متد AddMemoryMeasurement_With_initial_Size فوق) کاهش یابد.
علت اینجا است که دیکشنری در پشت صحنه، از یک متد ReSize استفاده می‌کند که شبیه به سیستم عامل، بیشتر از مقدار مورد نیاز جهت ذخیره‌ی اشیاء، برای کاهش تعداد بار تخصیص‌های حافظه، حافظه به خود اختصاص می‌دهد. به همین جهت زمانیکه اندازه‌ی اولیه را مشخص کرد‌ه‌ایم، کار تخصیص حافظه‌ی بیش از اندازه‌ی این شیء، به شدت کاهش یافته‌است.


بررسی متد MeasureGarbageCollections

در متد MeasureGarbageCollections، مقدار زیادی شیء بر روی heap ایجاد شده و GC را وادار به عکس العمل شدید می‌کند.
حلقه‌ی داخلی ایجاد شده نیز تعداد زیادی شیء را در جهت پاکسازی GC تخصیص می‌دهد. این پاکسازی در مرحله‌ا‌ی به نام generation 0 صورت می‌گیرد.
اشیاء اضافه شده‌ی به لیست، طول عمر بیشتری دارند (تا پایان حلقه). بنابراین از garbage collection at generation 0 جان سالم به در خواهند برد و در garbage collection at generation 1  به عمر آن‌ها پایان داده خواهد شد. هرچند ممکن است تعدادی از آن‌ها پاکسازی نشده و تا پایان full garbage collection (generation 2) باقی بمانند.
در آزمایش انجام شده، با ذکر GcGeneration.AllGc، هر سه مورد Gen0 تا Gen2 اندازه گیری خواهند شد. عموما اندازه گیری Gen0 و Gen1 مهم نیستند و این‌ها خیلی زود به پایان خواهند رسید. اگر تعداد بار رخ‌دادن Gen2 زیاد بود (یا اصلا وجود داشت)، می‌تواند سبب بروز مشکلات کارآیی شدیدی گردد.
بنابراین می‌توان بجای تنظیم GcGeneration.AllGc، صرفا از GcGeneration.Gen2 استفاده کرد.


اندازه‌گیری Throughput یا تعداد بار اجرای یک متد در ثانیه

روش دیگر کار با فریم ورک NBench، ایجاد یک کلاس مخصوص و سپس افزودن متدهای Setup مزین به PerfSetup، متد Cleanup مزین به PerfCleanup و سپس تعدادی متد اندازه گیری کارآیی توسط ویژگی PerfBenchmark است. در اینجا برای اندازه‌گیری سرعت اجرای متدها، از ویژگی CounterThroughputAssertion استفاده خواهد شد که پارامتر اول آن نام یک شمارشگر است. این شمارشگر در متد Setup ایجاد می‌شود (با یک نام دلخواه).
public class DictionaryThroughputTests
{
    private readonly Dictionary<int, int> _dictionary = new Dictionary<int, int>();
 
    private const string AddCounterName = "AddCounter";
    private Counter _addCounter;
    private int _key;
 
    private const int AverageOperationsPerSecond = 20000000;
 
    [PerfSetup]
    public void Setup(BenchmarkContext context)
    {
        _addCounter = context.GetCounter(AddCounterName);
        _key = 0;
    }
 
    [PerfBenchmark(RunMode = RunMode.Throughput, TestMode = TestMode.Test)]
    [CounterThroughputAssertion(AddCounterName, MustBe.GreaterThan, AverageOperationsPerSecond)]
    public void AddThroughput_ThroughputMode(BenchmarkContext context)
    {
        _dictionary.Add(_key++, _key);
        _addCounter.Increment();
    }
 
    [PerfBenchmark(RunMode = RunMode.Iterations, TestMode = TestMode.Test)]
    [CounterThroughputAssertion(AddCounterName, MustBe.GreaterThan, AverageOperationsPerSecond)]
    public void AddThroughput_IterationsMode(BenchmarkContext context)
    {
        for (var i = 0; i < AverageOperationsPerSecond; i++)
        {
            _dictionary.Add(i, i);
            _addCounter.Increment();
        }
    }
 
    [PerfCleanup]
    public void Cleanup(BenchmarkContext context)
    {
        _dictionary.Clear();
    }
}
در این آزمایش‌ها، RunMode.Throughput به معنای اجرای متد آزمایش به تعداد AverageOperationsPerSecond توسط فریم ورک NBench است. در حالت قید RunMode.Iterations، تعداد بار اجرا، توسط حلقه‌ای که ما مشخص کرده‌ایم، تعیین می‌گردد.
 --------------- RESULTS: NBenchSample.DictionaryThroughputTests+AddThroughput_ThroughputMode ---------------
[Counter] AddCounter: Max: 575,654.00 operations, Average: 575,654.00 operations, Min: 575,654.00 operations, StdDev: 0.00 operations
[Counter] AddCounter: Max / s: 7,205,997.59 operations, Average / s: 7,163,894.30 operations, Min / s: 7,075,316.79 operations, StdDev / s: 42,518.20 operations

--------------- RESULTS: NBenchSample.DictionaryThroughputTests+AddThroughput_IterationsMode ---------------
[Counter] AddCounter: Max: 20,000,000.00 operations, Average: 20,000,000.00 operations, Min: 20,000,000.00 operations, StdDev: 0.00 operations
[Counter] AddCounter: Max / s: 7,409,380.61 operations, Average / s: 7,250,991.24 operations, Min / s: 6,880,938.73 operations, StdDev / s: 148,085.19 operations
اگر دقت کنید، کارآیی اندازه گیری شده‌ی در حالت RunMode.Iterations بیشتر است از حالت RunMode.Throughput. چون در حالت RunMode.Throughput، فریم ورک کار اجرای متد را از طریق Reflection انجام می‌دهد. بنابراین بهتر است از حالت RunMode.Iterations، جهت رسیدن به نتایج دقیق‌تری استفاده کرد.
در اینجا برای گزارش دادن، عددهای Average و  Average / s باید مورد استفاده قرار گیرند.
مطالب
آشنایی با OWIN و بررسی نقش آن در ASP.NET Core
در این مطلب می‌خواهیم نگاهی به قسمت‌های کلیدی OWIN و همچنین پروژه‌ی Katana بیندازیم و در نهایت نیز نقش OWIN را در ASP.NET Core بررسی خواهیم کرد.



OWIN چیست؟

همانطور که می‌دانید OWIN یک specification است که استانداری را بین وب‌سرور و وب‌اپلیکیشن‌ها تعریف کرده است. در واقع OWIN یکسری لایه‌ی انتزاعی را جهت ایجاد اپلیکیشن‌هایی که نحوه‌ی میزبانی آنها اهمیتی ندارد، تعریف خواهد کرد. به صورت خلاصه توسط این لایه‌ی انتزاعی می‌توانیم وابستگی بین وب‌سرور و وب‌اپلیکیشن را حذف کنیم. در این specification منظور از وب‌سرور یک delegate و همچنین یک دیکشنری است. در واقع هدف این است که وقتی درخواستی به این وب‌سرور ارسال شد، درخواست به قسمت‌های کوچکی تقسیم‌بندی شده و درون این دیکشنری قرار خواهند گرفت (این دیکشنری حاوی کلیدهای از پیش‌تعریف شده‌ای است که توسط OWIN تعریف شده‌اند). سپس این دیکشنری از طریق یک application function به درون pipeline ارسال خواهد شد و از یکسری middleware عبور خواهد کرد. در اینحالت می‌توانیم کنترلی را بر روی درخواست‌های وارده و صادره داشته باشیم. ایده‌ی middleware خیلی شبیه به HTTP moduleها در IIS است؛ اما تفاوت آن این است که middlewareها وابستگی‌ایی به IIS ندارند و همچنین مبتنی بر رویداد نیستند. هر middleware بعد از انجام تغییرات بر روی درخواست، تا زمان رسیدن دیکشنری به آخرین middleware، آن را به middleware بعدی ارسال خواهد کرد. در این حین می‌توانیم به response streams اطلاعاتی را append کنیم. وقتی دیکشنری از تمامی middlewareها عبور کرد، سرور مطلع خواهد شد و نتیجه را به کلاینت ارسال می‌کند.


استاندارد OWIN تعدادی کلید را درون یک دیکشنری تعریف کرده است که بعد از ورود به هر middleware مقداردهی خواهند شد. این کلیدها را می‌توانیم در دو دسته‌ی Request و Response بررسی کنیم.

کلیدهای مربوط به Request

ضروری؟

نام کلید

مقدار

بله

"owin.RequestBody"

یک Stream همراه با request body. اگر body برای request وجود نداشته باشد، Stream.Null به عنوان placeholder قابل استفاده است.

بله

"owin.RequestHeaders"

یک دیکشنری به صورت IDictionary<string, string[]> از هدرهای درخواست.

بله

"owin.RequestMethod"

رشته‌ایی حاوی نوع فعل متد HTTP مربوط به درخواست (مانند GET and POST )

بله

"owin.RequestPath"

path درخواست شده به صورت string

بله

"owin.RequestPathBase"

قسمتی از path درخواست به صورت string

بله

"owin.RequestProtocol"

نام و نسخه‌ی پروتکل (مانند HTTP/1.0 or HTTP/1.1 )

بله

"owin.RequestQueryString"

رشته‌ای حاوی query string ؛ بدون علامت ? (مانند foo=bar&baz=quux )

بله

"owin.RequestScheme"

رشته‌ایی حاوی URL scheme استفاده شده در درخواست (مانند HTTP or HTTPS )



کلیدهای مربوط به Response

ضروری؟

نام کلید

مقدار

بله

"owin.ResponseBody"

یک Stream جهت نوشتن response body در خروجی

بله

"owin.ResponseHeaders"

یک دیکشنری به صورت IDictionary<string, string[]> از هدرهای response

خیر

"owin.ResponseStatusCode"

یک عدد صحیح حاوی کد وضعیت HTTP response ؛ حالت پیش‌فرض 200 است.

خیر

"owin.ResponseReasonPhrase"

یک رشته حاوی reason phrase مربوط به status code ؛ اگر خالی باشد در نتیجه سرور بهتر است آن را مقداردهی کند.

خیر

"owin.ResponseProtocol"

یک رشته حاوی نام و نسخه‌ی پروتکل (مانند HTTP/1.0 or HTTP/1.1 )؛ اگر خالی باشد؛ “owin.RequestProtocol” به عنوان مقدار پیش‌فرض در نظر گرفته خواهد شد.


Katana
پروژه‌ی Katana یک پیاده‌سازی از استاندارد OWIN است که توسط مایکروسافت ایجاد شده است. مایکروسافت علاوه بر پیاده‌سازی OWIN، یکسری قابلیت دیگر را نیز به آن اضافه کرده است. برای شروع کار با Katana یک پروژه خالی از نوع ASP.NET Web Application را ایجاد کنید. در ادامه لازم است پکیج Microsoft.Owin.Host.SystemWeb را نیز نصب کنیم. همراه با نصب این پکیج، دو وابستگی دیگر نیز نصب خواهند شد؛ زیرا پیاده‌سازی OWIN درون پکیج Microsoft.Owin قرار دارد:
<package id="Microsoft.Owin" version="3.0.1" targetFramework="net461" />
<package id="Microsoft.Owin.Host.SystemWeb" version="3.0.1" targetFramework="net461" />
<package id="Owin" version="1.0" targetFramework="net461" />
در ادامه نیاز به یک نقطه‌ی شروع برای اپلیکیشن‌مان داریم. طبق convention باید یک فایل را با نام Startup.cs با محتویات زیر ایجاد کنیم:
using Owin;
namespace SimpleOwinWebApp
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {

        } 
    }
}
توسط IAppBuilder می‌توانیم middlewareها را به pipeline اضافه کنیم:
using Owin;
namespace SimpleOwinWebApp
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.Use(async (ctx, next) =>
            {
                await ctx.Response.WriteAsync("Hello");
            });
        } 
    }
توسط متد Use، یک middleware را به صورت inline تعریف کرده‌ایم. متد Use یک delegate را از ورودی دریافت خواهد کرد و امضای آن به اینصورت است:
Func<IOwinContext, Func<Task>, Task> handler

IOwinContext در واقع یک wrapper برای environment dictionaryایی است که در ابتدا به آن اشاره کردیم. در مثال قبل، از پراپرتی Response، جهت ارسال خروجی به کلاینت استفاده شده است. این پراپرتی در واقع معادل کلید owin.ResponseBody درون دیکشنری است. اما در اینجا به صورت strongly-typed و ساده به آن دسترسی داریم؛ هر چند که امکان کار با دیکشنری خام نیز وجود دارد. به عنوان مثال معادل مثال قبل بدون استفاده از پراپرتی Response، اینچنین خواهد بود:
app.Use(async (ctx, next) =>
{
   var response = ctx.Environment["owin.ResponseBody"] as Stream;
   using (var writer = new StreamWriter(response))
   {
      await writer.WriteAsync("Hello");
   }
});
اکنون اگر پروژه را اجرا کنید، با وارد کردن هر آدرسی، پیام Hello درون مرورگر برایتان نمایش داده خواهد شد:


به هر تعداد middleware که خواستید می‌توانید به pipeline اضافه کنید؛ اما باید دقت داشته باشید که ترتیب قرار دادن آنها اهمیت دارد.

Self-hosting OWIN
در مثال قبلی، اپلیکیشن توسط IIS Express اجرا می‌شد. برای میزبانی درون یک کنسول اپلیکیشن، ابتدا یک پروژه‌ی Console Application را ایجاد کرده و پکیج Microsoft.Owin.SelfHost را نصب کنید. سپس کلاس Startup موردنظرتان را ایجاد کرده و در نهایت درون متد Main، کار راه‌اندازی سرور را انجام خواهیم داد:
using System;
using Microsoft.Owin.Hosting;

namespace SimpleOwinConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            using (WebApp.Start<Startup>("http://localhost:12345"))
            {
                Console.WriteLine("Listening to port 12345");
                Console.WriteLine("Press Enter to end...");
                Console.ReadLine();
            }
        }
    }
}

OWIN در ASP.NET Core
ASP.NET Core دارای مفهومی با عنوان pipeline است. این pipeline خیلی شبیه به OWIN است اما OWIN نیست؛ بلکه عملکرد آن شبیه به OWIN است. به عنوان مثال اینبار به جای دیکشنری، شیء HttpContext را داریم. در ادامه یک پروژه‌ی ASP.NET Core Web Application از نوع Empty را شروع خواهیم کرد. اگر دقت کنید اینبار برای کلاس Startup باید دو متد را پیاده‌سازی کنیم:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace SimpleOwinCoreApp
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.Run(async (context) =>
            {
                await context.Response.WriteAsync("Hello World!");
            });
        }
    }
}

متد Configure: همانطور که در ابتدای مطلب مشاهده کردید این متد قبلاً در پروژه‌های مبتنی بر کاتانا Configuration نام داشت؛ همچنین به جای IAppBuilder اینبار IApplicationBuilder را داریم. مزیت ASP.NET Core این است که در هر جایی از اپلیکیشن می‌توانیم از سیستم DI توکار آن استفاده کنیم؛ در نتیجه علاوه بر IApplicationBuilder وابستگی‌های دیگری مانند IHostingEnvironment و ILoggerFactory را نیز می‌توانیم تزریق کنیم.
متد ConfigureServices: در اینجا می‌توانیم سرویس‌های موردنیاز درون اپلیکیشن را برای IoC ریجستر کنیم.
در کد فوق استفاده از متد Use به معنای آخرین نقطه در pipeline است. یعنی جایی که response برگردانده خواهد شد و چیزی بعد از آن اجرا نخواهد شد؛ در واقع ارجاعی به middleware بعدی وجود ندارد.

ایجاد یک Middleware جدید
تا اینجا تمامی کدها را به صورت inline نوشتیم. اما اگر بخواهیم middlewareمان قابلیت استفاده‌ی مجدد داشته باشد می‌توانیم تعاریف آن را به یک کلاس با ساختار زیر منتقل نمائیم:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;

namespace SimpleOwinCoreApp.Middlewares
{
    public class SimpleMiddleware
    {
        private readonly RequestDelegate _next;

        public SimpleMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task Invoke(HttpContext ctx)
        {
            // قبل از فراخوانی میان‌افزار بعدی

            await ctx.Response.WriteAsync("Hello DNT!");

            await _next(ctx);

            // بعد از فراخوانی میان‌افزار بعدی
        }
    }
}

درون متد Invoke بعد از پردازش درخواست باید متد middleware بعدی را همراه با context جاری فراخوانی کنیم. در نتیجه قبل و بعد از فراخوانی middleware بعدی فرصت این را خواهیم داشت تا درخواست را پردازش کنیم. در نهایت برای استفاده از middleware فوق می‌توانیم از متد الحاقی UseMiddleware استفاده کنیم:
app.UseMiddleware<SimpleMiddleware>();

استفاده از middlewareهای مبتنی بر Katana در ASP.NET Core
middlewareهایی را که برای Katana نوشته‌اید، درون یک اپلیکیشن ASP.NET Core نیز قابل استفاده هستند. برای اینکار با مراجعه به فایل project.json می‌توانید پکیج زیر را نصب کنید:
"Microsoft.AspNetCore.Owin": "1.0.0"
سپس درون متد Configure می‌توانید Owin را به pipeline اضافه کرده و middleware خود را ریجستر کنید:
app.UseOwin(pipeline =>
{
pipeline(next => new MyKatanaBasedMiddleware(next).Invoke)
});

مثال تکمیلی:
در ادامه می‌خواهیم ماژول مطرح شده در این مطلب  را به صورت یک middleware با قابلیت پذیرفتن تنظیمات، نوشته و سپس درون pipeline استفاده کنیم. برای شروع یک کلاس با نام IpBlockerMiddleware با محتویات زیر ایجاد خواهیم کرد:
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;

namespace SimpleOwinAspNetCore.Middleware
{
    public class IpBlockerMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly IpBlockerOptions _options;

        public IpBlockerMiddleware(RequestDelegate next, IpBlockerOptions options)
        {
            _next = next;
            _options = options;
        }

        public async Task Invoke(HttpContext context)
        {
            var ipAddress = context.Request.Host.Host;
            if (IsBlockedIpAddress(ipAddress))
            {
                context.Response.StatusCode = 403;
                await context.Response.WriteAsync("Forbidden : The server understood the request, but It is refusing to fulfill it.");
                return;
            }
            await _next.Invoke(context);
        }

        private bool IsBlockedIpAddress(string ipAddress)
        {
            return _options.Ips.Any(ip => ip == ipAddress);
        }
    }
}
در کدهای فوق لیست Ipها از پراپرتی Ips درون کلاس IpBlockerOptions دریافت خواهد شد:
using System.Collections.Generic;

namespace SimpleOwinAspNetCore.Middleware
{
    public class IpBlockerOptions
    {
        public IpBlockerOptions()
        {
            Ips = new[] { "192.168.1.1" };
        }
        public IList<string> Ips { get; set; }
    }
}
همچنین برای استفاده راحت‌تر از middleware، یک متد الحاقی را برای آن ایجاد کرده‌ایم و سپس پراپرتی Ips را توسط اینترفیس IConfigurationRoot دریافت کرده‌ایم:
using System.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;

namespace SimpleOwinAspNetCore.Middleware
{
    public static class IpBlockerExtensions
    {
        public static IApplicationBuilder UseIpBlocker(this IApplicationBuilder builder, IConfigurationRoot configuration, IpBlockerOptions options = null)
        {
            return builder.UseMiddleware<IpBlockerMiddleware>(options ?? new IpBlockerOptions
            {
                Ips = configuration.GetSection("block_list").GetChildren().Select(p => p.Value).ToArray()
            });
        }
    }
}
قبلاً در رابطه با فایل‌های کانفیگ مطلبی را مطالعه کرده‌اید؛ در نتیجه نیازی به توضیح اضافه‌تری ندارد. تنها کاری که در اینجا انجام شده است، دریافت محتویات کلید block_list از فایل کانفیگ است. 
محتویات فایل blockedIps.json:
{
  "block_list": [
    "192.168.1.1",
    "localhost",
    "127.0.0.1",
    "172.16.132.151"
  ]
}

برای خواندن فایل فوق در برنامه نیز خواهیم داشت:
public IConfigurationRoot Configuration { set; get; }

public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("blockedIps.json");
Configuration = builder.Build();
}
در نهایت برای استفاده از middleware فوق خواهیم داشت:
app.UseIpBlocker(Configuration);
اکنون هر درخواستی که با آدرس‌های تعیین شده درون فایل blockedIps.json وارد pipeline شود، امکان استفاده‌ی از سایت را نخواهد داشت.

کدهای این مطلب را می‌توانید از اینجا دریافت کنید.
مطالب
مبانی TypeScript؛ کلاس‌ها
تا قبل از ES 6 در جاوا اسکریپت از توابع جهت ایجاد کامپوننت‌هایی با قابلیت استفاده مجدد استفاده می‌شد. این امر برای برنامه‌نویسانی که با زبان‌های OOP آشنایی دارند، شاید چندان خوشایند نباشد. در TypeScript نیز همانند ES 6 امکان استفاده از کلاس‌ها مهیا است.
در حالت کلی یک کلاس قالبی برای ایجاد اشیاء است. تمامی اشیاء ایجاد شده از این الگو دارای یکسری پراپرتی و متد می‌باشند. از پراپرتی‌ها جهت تعریف وضعیت‌ها و از متدها جهت تعریف رفتارها استفاده خواهد شد. همچنین مزیت اصلی یک کلاس، کپسوله‌سازی قابلیت‌های یک موجودیت خاص است. همانند دیگر زبان‌های شیءگرا، در TypeScript نیز یک کلاس می‌تواند ویژگی‌های زیر را داشته باشد:
  • سازنده (constructor)
  • پراپرتی، متد
  • Access Modifiers
  • ارث‌بری
  • کلاس‌های Abstract
در ادامه هر کدام از موارد فوق را بررسی خواهیم کرد.

سازنده (Constructor)
از سازنده‌ها جهت مقداردهی وهله‌های یک کلاس استفاده می‌شود. در ادامه یک کلاس جدید را با استفاده از کلمه‌ی کلیدی class ایجاد کرده‌ایم. این کلاس دارای یک سازنده است:
class ReferenceItem {
    constructor(title: string, publisher?: string) {
        // perform initialization here
    }
}
همانطور که مشاهده می‌کنید یک سازنده شبیه به یک متد است؛ با این تفاوت که برای نام آن از کلمه کلیدی constructor استفاده می‌شود. در TypeScript برای یک کلاس تنها یک سازنده را می‌توانیم داشته باشیم. البته در دیگر زبان‌های برنامه‌نویسی امکان تعریف چندین سازنده را با پارامترهای مختلف برای یک کلاس می‌توانید داشته باشید. برای رسیدن به این هدف در TypeScript می‌توان از Optional Parameters استفاده کرد. برای ایجاد یک وهله از کلاس فوق می‌توانیم به این صورت عمل کنیم:
let encyclopedia = new ReferenceItem('WorldPedia', 'WorldPub');
در کد فوق با استفاده از کلمه‌ی کلیدی new یک وهله از کلاس ReferenceItem را ایجاد کرده‌ایم و در نهایت آن را به متغیری با نام encyclopedia انتساب داده‌ایم. یعنی در واقع با استفاده از new توانسته‌ایم سازنده‌ی کلاس را فراخوانی کرده و سپس وهله‌ایی از آن را به متغیر ذکر شده انتساب دهیم.

پراپرتی، متد 
همانند اینترفیس‌ها، کلاس‌ها نیز می‌توانند پراپرتی و متد داشته باشند. با این تفاوت که در کلاس‌ها جزئیات پیاده‌سازی نیز ذکر خواهد شد. در یک کلاس به دو روش متفاوت می‌توانیم پراپرتی را تعریف کنیم. روش اول همانند تعریف یک متغیر است. به عنوان مثال در کلاس زیر یک پراپرتی با نام numberOfPages را از نوع عددی تعریف کرده‌ایم:
class ReferenceItem {
    numberOfPages: number;
}
برای دسترسی به این پراپرتی می‌توانیم از سینتکس نقطه (.) استفاده کنیم. روش دوم برای تعریف یک پراپرتی، ایجاد accessor‌های سفارشی است. accessors در واقع توابع getter و setter هستند که به شما در نحوه‌ی get و set کردن یک پراپرتی کمک خواهند کرد:
class ReferenceItem {
    numberOfPages: number;
    
    get editor(): string {
        // custom getter logic goes here, should return a value
    }
    
    set editor(newEditor: string) {
        // custom setter logic goes here
    }
}
همانطور که مشاهده می‌کنید، accessorهایی را برای پراپرتی editor با استفاده از کلمات کلیدی get و set ایجاد کرده‌ایم. این accessorها در واقع توابعی همنام هستند. تابع get همیشه فاقد پارامتر است. می‌توانیم برای تابع get نوع برگشتی را نیز تعیین کنیم (به عنوان مثال در کد فوق نوع برگشتی string است). setter نیز باید تنها یک پارامتر از ورودی دریافت کند. همچنین نمی‌توانیم برای آن نوع برگشتی را تعیین کنیم. درون بدنه‌ی این accessorها می‌توانیم هر نوع کنترلی را بر روی پراپرتی داشته باشیم. برای دسترسی این accessorها نیز باید از سینتکس نقطه (.) استفاده کنیم.
متدها نیز توابعی هستند که درون یک کلاس تعریف می‌شوند. برای نمونه در کد زیر یک تابع با نام printChapterTitle را تعریف کرده‌ایم که یک پارامتر را از ورودی دریافت کرده و هیچ مقداری را در خروجی بر نمی‌گرداند:
class ReferenceItem {
    numberOfPages: number;
    
    get editor(): string {
        // custom getter logic goes here, should return a value
    }
    
    set editor(newEditor: string) {
        // custom setter logic goes here
    }
    
    printChapterTitle(chapterNum: number): void {
        // print title here
    }
}

Parameter properties
در حالت عادی برای مقداردهی اولیه‌ی پراپرتی‌ها یک شیء می‌توانیم یکسری پارامتر را برای سازنده کلاس تعریف کرده و درون سازنده، پراپرتی‌های موردنیازمان را مقداردهی کنیم:
class Author {
    name: string;
    
    constructor(authorName: string) {
        name = authorName;
    }
}
با کمک Parameter properties می‌توانیم به صورت خلاصه‌تری اینکار را انجام دهیم:
class Author {
    constructor(public name: string){}
}
همانطور که مشاهده می‌کنید اینکار را با افزودن کلمه‌ی کلیدی public به ابتدای پارامتر name انجام داده‌ایم. در این‌حالت دیگر نیازی به تعریف یک پراپرتی اضافی درون کلاس نخواهیم داشت. کامپایلر TypeScript خودش یک پراپرتی را با همین نام ایجاد کرده و مقدار دریافتی از سازنده را برای آن ست خواهد کرد.

Static Properties
تاکنون درباره‌ی اعضای مربوط به هر وهله از کلاس‌ها صحبت کردیم؛ یعنی اعضایی که در زمان وهله‌سازی در دسترس خواهند بود. در واقع می‌توانیم اعضای استاتیک را نیز برای کلاس‌ها داشته باشیم. منظور از استاتیک این است که مقادیر یک عضوء استاتیک در وهله‌های مختلف یک شیء، متفاوت نیست. بلکه یک مقدار آن برای تمامی وهله‌ها به اشتراک گذاشته خواهد شد:
class Library {
    constructor(public name: string) {}
    
    static description: string = 'A source of knowledge';
}

let lib = new Library('New York Public Library');
console.log(lib.name); // available on instances of the class

console.log(Library.description);

Access Modifiers
با استفاده از Access Modifier می‌توانیم میدان دید یک پراپرتی و یا یک متد را برای مصرف کننده‌ی کلاس کنترل کنیم. TypeScript دارای سه Access Modifier است:
public: در حالت پیش‌فرض تمامی اعضای یک کلاس عمومی (public) هستند. در نتیجه لزومی به ذکر آن برای پراپرتی‌ها و متدها نیست. یک حالت استثناء، استفاده از Parameter properties است. در این حالت باید کلمه‌ی کلیدی public حتماً ذکر شود. 
private: برای محدود کردن دسترسی اعضای یک کلاس می‌توانید از کلمه‌ی کلیدی private استفاده کنید. در این‌حالت مصرف کننده‌ی کلاس به اعضای خصوصی (private) دسترسی نخواهد داشت. 
protected: این modifier نیز شبیه به private عمل می‌کند، با این تفاوت که توسط subclassهای مربوط به کلاس تعریف شده در آن نیز قابل دسترس است.


Inheritance
منظور از Inheritance یا ارث‌بری، اشتراک‌گذاری تعاریف یک کلاس برای یک یا چند sub-class است. فرض کنید یک کلاس با نام ReferenceItem با یکسری اعضای تعریف شده درون آن داریم و می‌خواهیم دو کلاس مشتق شده را از این کلاس تهیه کنیم. در این‌حالت کلاس ReferenceItem کلاس پایه (base class) و کلاس‌های مشتق شده از آن sub-class نامیده می‌شوند. بنابراین وهله‌های ایجاد شده از کلاس‌های مشتق شده دارای پراپرتی‌های کلاس پایه نیز خواهند بود. برای داشتن قابلیت ارث‌بری در TypeScript می‌توانیم به اینصورت عمل کنیم:
class ReferenceItem {
    title: string;
    printItem(): void { 
        // print something here 
    }
}

class Journal extends ReferenceItem {
    constructor() {
        super();
    }
    
    contributors: string[];
}
همانطور که مشاهده می‌کنید با استفاده از کلمه‌ی کلیدی extends توانسته‌ایم یک sub-class ایجاد کنیم. بنابراین وهله‌های کلاس Journal علاوه بر پراپرتی‌های خود (در اینجا contributors ) دارای پراپرتی title و همچنین متد printItem نیز هستند. نکته‌ایی که در اینجا وجود دارد این است که تمامی sub-classها یا کلاس‌های مشتق شده باید درون سازنده‌ی خود، تابع super را فراخوانی کنند؛ با اینکار سازنده‌ی کلاس پایه فراخوانی خواهد شد.
لازم به ذکر است که می‌توان متدهای کلاس پایه را درون کلاس‌های مشتق شده، override کرد. برای اینکار کافی است متد موردنظر در کلاس پایه را درون کلاس مشتق شده مجدداً تعریف کرده و منطق موردنظر را درون آن نوشت:
class Journal extends ReferenceItem {
    constructor() {
        super();
    }
    
    printItem(): void { 
        super.printItem();
        console.log('message from Journal');
    }
    
    contributors: string[];
}
با استفاده از super.printItem به کامپایلر TypeScript گفته‌ایم که تمامی کدهای درون متد printItem در کلاس پایه نیز اجرا شوند. اگر مایل بودید می‌توانید از آن صرفنظر کنید.

Abstract Classes 
کلاس‌های Abstract یک نوع خاص از کلاس‌ها هستند که نمی‌توان آنها را وهله‌سازی کرد. یعنی تنها برای تعریف کلاس‌های پایه از آنها استفاده خواهد شد. این نوع کلاس‌ها شبیه به اینترفیس‌ها هستند؛ اما ممکن است دارای پیاده‌سازی نیز باشند. در ادامه یک نمونه از abstract class را مشاهده می‌کنید:
abstract class ReferenceItem {
    private _publisher: string;
    static departement: string = 'Research';
    
    constructor(public title: string, protected year: number) {
        
    }
    
    printItem(): void {
        console.log('message from abstract class');
    } 
    
    get publisher(): string {
        return this._publisher.toUpperCase();
    }
    
    set publisher(newPublisher: string) {
        this._publisher = newPublisher;
    }
    
    abstract printCitation(): void;
}

class Encyclopedia extends ReferenceItem {
    
    constructor(newTitle: string, newYear, public edition: number) {
        super(newTitle, newYear);
    }
    
    printCitation(): void {
        console.log('message');
    }
}

let test = new Encyclopedia('WorldPerdia', 1900, 10);
test.printItem();
همانطور که مشاهده می‌کنید درون یک کلاس abstract می‌توانیم متدهای abstract را نیز داشته باشیم؛ یعنی تنها امضای متد را تعیین کرده و پیاده‌سازی آن را به کلاس‌های مشتق شده واگذار کنیم. 
مطالب
Roslyn #5
بررسی Semantic Models

همانطور که از قسمت قبل به‌خاطر دارید، برای دسترسی به اطلاعات semantics، نیاز به یک context مناسب که همان Compilation API است، می‌باشد. این context دارای اطلاعاتی مانند دسترسی به تمام نوع‌های تعریف شده‌ی توسط کاربر و متادیتاهای ارجاعی، مانند کلاس‌های پایه‌ی دات نت فریم‌ورک است. بنابراین پس از ایجاد وهله‌ای از Compilation API، کار با فراخوانی متد GetSemanticModel آن ادامه می‌یابد. در ادامه با مثال‌هایی، کاربرد این متد را بررسی خواهیم کرد.


ساختار جدید Optional

خروجی‌های تعدادی از متدهای Roslyn با ساختار جدیدی به نام Optional ارائه می‌شوند:
    public struct Optional<T>
    {
        public bool HasValue { get; }
        public T Value { get; }
    }
این ساختار که بسیار شبیه است به ساختار قدیمی <Nullable<T، منحصر به Value types نیست و Reference types را نیز شامل می‌شود و بیانگر این است که آیا یک Reference type، واقعا مقدار دهی شده‌است یا خیر؟


دریافت مقادیر ثابت Literals

فرض کنید می‌خواهیم مقدار ثابت ; int x = 42 را دریافت کنیم. برای اینکار ابتدا باید syntax tree آن تشکیل شود و سپس نیاز به یک سری حلقه و if و else و همچنین بررسی نال بودن بسیاری از موارد است تا به نود مقدار ثابت 42 برسیم. سپس متد GetConstantValue مربوط به GetSemanticModel را بر روی آن فراخوانی می‌کنیم تا به مقدار واقعی آن که ممکن است در اثر محاسبات جاری تغییر کرده باشد، برسیم.
اما روش بهتر و توصیه شده، استفاده از CSharpSyntaxWalker است که در انتهای قسمت سوم معرفی شد:
class ConsoleWriteLineWalker : CSharpSyntaxWalker
{
    public ConsoleWriteLineWalker()
    {
        Arguments = new List<ExpressionSyntax>();
    }
 
    public List<ExpressionSyntax> Arguments { get; }
 
    public override void VisitInvocationExpression(InvocationExpressionSyntax node)
    {
        var member = node.Expression as MemberAccessExpressionSyntax;
        var type = member?.Expression as IdentifierNameSyntax;
        if (type != null && type.Identifier.Text == "Console" && member.Name.Identifier.Text == "WriteLine")
        {
            if (node.ArgumentList.Arguments.Count == 1)
            {
                var arg = node.ArgumentList.Arguments.Single().Expression;
                Arguments.Add(arg);
                return;
            }
        }
 
        base.VisitInvocationExpression(node);
    }
}
اگر به کدهای ادامه‌ی بحث دقت کنید، قصد داریم مقادیر ثابت آرگومان‌های Console.WriteLine را استخراج کنیم. به همین جهت در این SyntaxWalker، نوع Console و متد WriteLine آن مورد بررسی قرار گرفته‌اند. اگر این نود دارای یک تک آرگومان بود، آین آرگومان استخراج شده و به لیست آرگومان‌های خروجی این کلاس اضافه می‌شود.
در ادامه نحوه‌ی استفاده‌ی از این SyntaxWalker را ملاحظه می‌کنید. در اینجا ابتدا سورس کدی حاوی یک سری Console.WriteLine که دارای تک آرگومان‌های ثابتی هستند، تبدیل به syntax tree می‌شود. سپس از روی آن CSharpCompilation تولید می‌گردد تا بتوان به اطلاعات semantics دسترسی یافت:
static void getConstantValue()
{
    // Get the syntax tree.
    var code = @"
                using System;
 
                class Foo
                {
                    void Bar(int x)
                    {
                        Console.WriteLine(3.14);
                        Console.WriteLine(""qux"");
                        Console.WriteLine('c');
                        Console.WriteLine(null);
                        Console.WriteLine(x * 2 + 1);
                    }
                }
                ";
 
    var tree = CSharpSyntaxTree.ParseText(code);
    var root = tree.GetRoot();
 
    // Get the semantic model from the compilation.
    var mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
    var comp = CSharpCompilation.Create("Demo").AddSyntaxTrees(tree).AddReferences(mscorlib);
    var model = comp.GetSemanticModel(tree);
 
    // Traverse the tree.
    var walker = new ConsoleWriteLineWalker();
    walker.Visit(root);
 
 
    // Analyze the constant argument (if any).
    foreach (var arg in walker.Arguments)
    {
        var val = model.GetConstantValue(arg);
        if (val.HasValue)
        {
            Console.WriteLine(arg + " has constant value " + (val.Value ?? "null") + " of type " + (val.Value?.GetType() ?? typeof(object)));
        }
        else
        {
            Console.WriteLine(arg + " has no constant value");
        }
    }
}
در ادامه با استفاده از CSharpCompilation و متد GetSemanticModel آن به SemanticModel جاری دسترسی خواهیم یافت. اکنون SyntaxWalker را وارد به حرکت بر روی ریشه‌ی syntax tree سورس کد آنالیز شده می‌کنیم. به این ترتیب لیست آرگومان‌های متدهای Console.WriteLine بدست می‌آیند. سپس با فراخوانی متد model.GetConstantValue بر روی هر آرگومان دریافتی، مقادیر آن‌ها با فرمت <Optional<T استخراج می‌شوند.
خروجی نمایش داده شده‌ی توسط برنامه به صورت ذیل است:
 3.14 has constant value 3.14 of type System.Double
"qux" has constant value qux of type System.String
'c' has constant value c of type System.Char
null has constant value null of type System.Object
x * 2 + 1 has no constant value


درک مفهوم Symbols

اینترفیس ISymbol در Roslyn، ریشه‌ی تمام Symbolهای مختلف مدل سازی شده‌ی در آن است که تعدادی از آن‌ها را در تصویر ذیل مشاهده می‌کنید:


API کار با Symbols بسیار شبیه به API کار با Reflection است با این تفاوت که در زمان آنالیز کدها رخ می‌دهد و نه در زمان اجرای برنامه. همچنین در Symbols API امکان دسترسی به اطلاعاتی مانند locals, labels و امثال آن نیز وجود دارد که با استفاده از Reflection زمان اجرای برنامه قابل دسترسی نیستند. برای مثال فضاهای نام در Reflection صرفا به صورت رشته‌ای، با دات جدا شده از نوع‌های آنالیز شده‌ی توسط آن است؛ اما در اینجا مطابق تصویر فوق، یک اینترفیس مجزای خاص خود را دارد. جهت سهولت کار کردن با Symbols، الگوی Visitor با معرفی کلاس پایه‌ی SymbolVisitor نیز پیش بینی شده‌است.
static void workingWithSymbols()
{
    // Get the syntax tree.
    var code = @"
                using System;
 
                class Foo
                {
                    void Bar(int x)
                    {
                        // #insideBar
                    }
                }
 
                class Qux
                {
                    protected int Baz { get; set; }
                }
                ";
 
    var tree = CSharpSyntaxTree.ParseText(code);
    var root = tree.GetRoot();
 
    // Get the semantic model from the compilation.
    var mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
    var comp = CSharpCompilation.Create("Demo").AddSyntaxTrees(tree).AddReferences(mscorlib);
    var model = comp.GetSemanticModel(tree);
 
    // Traverse enclosing symbol hierarchy.
    var cursor = code.IndexOf("#insideBar");
    var barSymbol = model.GetEnclosingSymbol(cursor);
    for (var symbol = barSymbol; symbol != null; symbol = symbol.ContainingSymbol)
    {
        Console.WriteLine(symbol);
    }
 
    // Analyze accessibility of Baz inside Bar.
    var bazProp = ((CompilationUnitSyntax)root)
        .Members.OfType<ClassDeclarationSyntax>()
        .Single(m => m.Identifier.Text == "Qux")
        .Members.OfType<PropertyDeclarationSyntax>()
        .Single();
    var bazSymbol = model.GetDeclaredSymbol(bazProp);
    var canAccess = model.IsAccessible(cursor, bazSymbol);
}
یکی از کاربردهای مهم Symbols API دریافت اطلاعات Symbols نقطه‌ای خاص از کدها می‌باشد. برای مثال در محل اشاره‌گر ادیتور، چه Symbols ایی تعریف شده‌اند و از آن‌ها در مباحث ساخت افزونه‌های آنالیز کدها زیاد استفاده می‌شود. نمونه‌ای از آن‌را در قطعه کد فوق ملاحظه می‌کنید. در اینجا با استفاده از متد GetEnclosingSymbol، سعی در یافتن Symbols قرار گرفته‌ی در ناحیه‌ی insideBar# کدهای فوق داریم؛ با خروجی ذیل که نام demo.exe آن از نام CSharpCompilation آن گرفته شده‌است:
 Foo.Bar(int)
Foo
<global namespace>
Demo.exe
Demo, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null


همچنین در ادامه‌ی کد، توسط متد IsAccessible قصد داریم بررسی کنیم آیا Symbol قرار گرفته در محل کرسر، دسترسی به خاصیت protected کلاس Qux را دارد یا خیر؟ که پاسخ آن خیر است.


آشنایی با Binding symbols

یکی از مراحل کامپایل کد، binding نام دارد و در این مرحله است که اطلاعات Symbolic هر نود از Syntax tree دریافت می‌شود. برای مثال در اینجا مشخص می‌شود که این x، آیا یک متغیر محلی است، یا یک فیلد و یا یک خاصیت؟
مثال ذیل بسیار شبیه است به مثال getConstantValue ابتدای بحث، با این تفاوت که در حلقه‌ی آخر کار از متد GetSymbolInfo استفاده شده‌است:
static void bindingSymbols()
{
    // Get the syntax tree.
    var code = @"
                using System;
 
                class Foo
                {
                    private int y;
 
                    void Bar(int x)
                    {
                        Console.WriteLine(x);
                        Console.WriteLine(y);
 
                        int z = 42;
                        Console.WriteLine(z);
 
                        Console.WriteLine(a);
                    }
                }";
 
    var tree = CSharpSyntaxTree.ParseText(code);
    var root = tree.GetRoot();
 
    // Get the semantic model from the compilation.
    var mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
    var comp = CSharpCompilation.Create("Demo").AddSyntaxTrees(tree).AddReferences(mscorlib);
    var model = comp.GetSemanticModel(tree);
 
    // Traverse the tree.
    var walker = new ConsoleWriteLineWalker();
    walker.Visit(root);
 
    // Bind the arguments.
    foreach (var arg in walker.Arguments)
    {
        var symbol = model.GetSymbolInfo(arg);
        if (symbol.Symbol != null)
        {
            Console.WriteLine(arg + " is bound to " + symbol.Symbol + " of type " + symbol.Symbol.Kind);
        }
        else
        {
            Console.WriteLine(arg + " could not be bound");
        }
    }
}
با این خروجی:
 x is bound to int of type Parameter
y is bound to Foo.y of type Field
z is bound to z of type Local
a could not be bound
در مثال فوق، با استفاده از Syntax Walker طراحی شده در ابتدای بحث که کار استخراج آرگومان‌های متدهای Console.WriteLine را انجام می‌دهد، قصد داریم بررسی کنیم، هر آرگومان به چه Symbol ایی بایند شده‌است و نوعش چیست؟ برای مثال Console.WriteLine اول که از پارامتر x استفاده می‌کند، نوع x مورد استفاده‌اش چیست؟ آیا فیلد است، متغیر محلی است یا یک پارامتر؟ این اطلاعات را با استفاده از متد model.GetSymbolInfo می‌توان استخراج کرد.
مطالب
OpenCVSharp #12
قطعه بندی (segmentation) تصویر با استفاده از الگوریتم watershed

در تصویر ذیل، تصویر یک راه‌رو را مشاهده می‌کنید که توسط ماوس قطعه بندی شده‌است (تصویر اصلی یا سمت چپ). تصویر سمت راست، نسخه‌ی قطعه بندی شده‌ی این تصویر به کمک الگوریتم watershed است.

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


انتخاب نواحی مختلف به کمک ماوس

در اینجا کدهای آغازین مثال بحث جاری را ملاحظه می‌کنید:
var src = new Mat(@"..\..\Images\corridor.jpg", LoadMode.AnyDepth | LoadMode.AnyColor);
var srcCopy = new Mat();
src.CopyTo(srcCopy);
 
var markerMask = new Mat();
Cv2.CvtColor(srcCopy, markerMask, ColorConversion.BgrToGray);
 
var imgGray = new Mat();
Cv2.CvtColor(markerMask, imgGray, ColorConversion.GrayToBgr);
markerMask = new Mat(markerMask.Size(), markerMask.Type(), s: Scalar.All(0));
 
var sourceWindow = new Window("Source (Select areas by mouse and then press space)")
{
    Image = srcCopy
};
 
var previousPoint = new Point(-1, -1);
sourceWindow.OnMouseCallback += (@event, x, y, flags) =>
{
    if (x < 0 || x >= srcCopy.Cols || y < 0 || y >= srcCopy.Rows)
    {
        return;
    }
 
    if (@event == MouseEvent.LButtonUp || !flags.HasFlag(MouseEvent.FlagLButton))
    {
        previousPoint = new Point(-1, -1);
    }
    else if (@event == MouseEvent.LButtonDown)
    {
        previousPoint = new Point(x, y);
    }
    else if (@event == MouseEvent.MouseMove && flags.HasFlag(MouseEvent.FlagLButton))
    {
        var pt = new Point(x, y);
        if (previousPoint.X < 0)
        {
            previousPoint = pt;
        }
 
        Cv2.Line(img: markerMask, pt1: previousPoint, pt2: pt, color: Scalar.All(255), thickness: 5);
        Cv2.Line(img: srcCopy, pt1: previousPoint, pt2: pt, color: Scalar.All(255), thickness: 5);
        previousPoint = pt;
        sourceWindow.Image = srcCopy;
    }
};
ابتدا تصویر راه‌رو بارگذاری شده‌است. سپس یک نسخه‌ی سیاه و سفید تک کاناله به نام markerMask از آن استخراج می‌شود. از آن برای ترسیم خطوط انتخاب نواحی مختلف تصویر به کمک ماوس استفاده می‌شود. به علاوه متد FindContours که در ادامه معرفی خواهد شد، نیاز به یک تصویر 8 بیتی تک کاناله دارد (به هر یک از اجزای RGB یک کانال گفته می‌شود).
همچنین این نسخه‌ی سیاه و سفید تک کاناله به یک تصویر سه کاناله برای نمایش رنگ‌های قسمت‌های مختلف قطعه بندی شده، تبدیل می‌شود.
سپس پنجره‌ی نمایش تصویر اصلی برنامه ایجاد شده و در اینجا روال رخدادگردان OnMouseCallback آن به صورت inline مقدار دهی شده‌است. در این روال می‌توان مدیریت ماوس را به عهده گرفت و کار نمایش خطوط مختلف را با فشرده شدن و سپس رها شدن کلیک سمت چپ ماوس انجام داد.
خط ترسیم شده بر روی دو تصویر از نوع Mat نمایش داده می‌شود. تصویر srcCopy، همان تصویر نمایش داده شده‌ی در پنجره‌ی اصلی است و تصویر markerMask، بیشتر جنبه‌ی محاسباتی دارد و در متدهای بعدی OpenCV استفاده خواهد شد.


تشخیص کانتورها (Contours) در تصویر

پس از ترسیم نواحی مورد نظر توسط ماوس، یک سری خطوط به هم پیوسته در شکل قابل مشاهده هستند. می‌خواهیم این خطوط را تشخیص داده و سپس از آن‌ها جهت محاسبات قطعه بندی تصویر استفاده کنیم. تشخیص این خطوط متصل، توسط متدی به نام FindContours انجام می‌شود. کانتورها، قسمت‌های خارجی اجزای متصل به هم هستند.
Point[][] contours; //vector<vector<Point>> contours;
HiearchyIndex[] hierarchyIndexes; //vector<Vec4i> hierarchy;
Cv2.FindContours(
    markerMask,
    out contours,
    out hierarchyIndexes,
    mode: ContourRetrieval.CComp,
    method: ContourChain.ApproxSimple);
متد FindContours همان تصویر markerMask را که توسط ماوس، قسمت‌های مختلف تصویر را علامتگذاری کرده‌است، دریافت می‌کند. سپس کانتورهای آن را استخراج خواهد کرد. کانتورها در مثال‌های اصلی OpenCV با verctor مشخص شده‌اند. در اینجا (در کتابخانه‌ی OpenCVSharp) آن‌ها را توسط یک آرایه‌ی دو بعدی از نوع Point مشاهده می‌کنید یا شبیه به لیستی از آرایه‌ی نقاط کانتورهای مختلف تشخیص داده شده (هر کانتور، آرایه‌ی از نقاط است). از hierarchyIndexes جهت یافتن و ترسیم این کانتورها در متد DrawContours استفاده می‌شود.
متد FindContours یک تصویر 8 بیتی تک کاناله را دریافت می‌کند. اگر mode آن CCOMP یا FLOODFILL تعریف شود، امکان دریافت یک تصویر 32 بیتی را نیز خواهد داشت.
پارامتر hierarchy آن یک پارامتر اختیاری است که بیانگر اطلاعات topology تصویر است.
توسط پارامتر Mode، نحوه‌ی استخراج کانتور مشخص می‌شود. اگر به external تنظیم شود، تنها کانتورهای خارجی‌ترین قسمت‌ها را تشخیص می‌دهد. اگر مساوی list قرار گیرد، تمام کانتورها را بدون ارتباطی با یکدیگر و بدون تشکیل hierarchy استخراج می‌کند. حالت ccomp تمام کانتورها را استخراج کرده و یک درخت دو سطحی از آن‌ها را تشکیل می‌دهد. در سطح بالایی مرزهای خارجی اجزاء وجود دارند و در سطح دوم مرزهای حفره‌ها مشخص شده‌اند. حالت و مقدار tree به معنای تشکیل یک درخت کامل از کانتورهای یافت شده‌است.
پارامتر method اگر به none تنظیم شود، تمام نقاط کانتور ذخیره خواهند شد و اگر به simple تنظیم شود، قطعه‌های افقی، عمودی و قطری، فشرده شده و تنها نقاط نهایی آن‌ها ذخیره می‌شوند. برای مثال در این حالت یک کانتور مستطیلی، تنها با 4 نقطه ذخیره می‌شود.


ترسیم کانتورهای تشخیص داده شده بر روی تصویر


می‌توان به کمک متد DrawContours، مرزهای کانتورهای یافت شده را ترسیم کرد:
var markers = new Mat(markerMask.Size(), MatType.CV_32S, s: Scalar.All(0));
 
var componentCount = 0;
var contourIndex = 0;
while ((contourIndex >= 0))
{
    Cv2.DrawContours(
        markers,
        contours,
        contourIndex,
        color: Scalar.All(componentCount + 1),
        thickness: -1,
        lineType: LineType.Link8,
        hierarchy: hierarchyIndexes,
        maxLevel: int.MaxValue);
 
    componentCount++;
    contourIndex = hierarchyIndexes[contourIndex].Next;
}
پارامتر اول آن تصویری است که قرار است ترسیمات بر روی آن انجام شوند. پارامتر کانتور، آرایه‌ای است از کانتورهای یافت شده‌ی در قسمت قبل. پارامتر ایندکس مشخص می‌کند که اکنون کدام کانتور باید رسم شود. برای یافتن کانتور بعدی باید از hierarchyIndexes یافت شده‌ی توسط متد FindContours استفاده کرد. خاصیت Next آن، بیانگر ایندکس کانتور بعدی است و اگر مساوی منهای یک شد، کار متوقف می‌شود. مقدار maxLevel مشخص می‌کند که بر اساس پارامتر hierarchyIndexes، چند سطح از کانتورهای به هم مرتبط باید ترسیم شوند. در اینجا چون به حداکثر مقدار Int32 تنظیم شده‌است، تمام این سطوح ترسیم خواهند شد. اگر پارامتر ضخامت به یک عدد منفی تنظیم شود، سطوح داخلی کانتور ترسیم و پر می‌شوند.



اعمال الگوریتم watershed

در مرحله‌ی آخر، تصویر کانتورهای ترسیم شده را به متد Watershed ارسال می‌کنیم. پارامتر اول آن تصویر اصلی است و پارامتر دوم، یک پارامتر ورودی و خروجی محسوب می‌شود و کار قطعه بندی تصویر بر روی آن انجام خواهد شد.
کار الگوریتم watershed، ایزوله سازی اشیاء موجود در تصویر از پس زمینه‌ی آن‌ها است. این الگوریتم، یک تصویر سیاه و سفید را دریافت می‌کند؛ به همراه یک تصویر ویژه به نام marker. تصویر marker کارش مشخص سازی اشیاء، از پس زمینه‌ی آن‌ها است که در اینجا توسط ماوس ترسیم و سپس به کمک یافتن کانتورها و ترسیم آ‌ن‌ها بهینه سازی شده‌است.
var rnd = new Random();
var colorTable = new List<Vec3b>();
for (var i = 0; i < componentCount; i++)
{
    var b = rnd.Next(0, 255); //Cv2.TheRNG().Uniform(0, 255);
    var g = rnd.Next(0, 255); //Cv2.TheRNG().Uniform(0, 255);
    var r = rnd.Next(0, 255); //Cv2.TheRNG().Uniform(0, 255);
 
    colorTable.Add(new Vec3b((byte)b, (byte)g, (byte)r));
}
 
Cv2.Watershed(src, markers);
 
var watershedImage = new Mat(markers.Size(), MatType.CV_8UC3);
 
// paint the watershed image
for (var i = 0; i < markers.Rows; i++)
{
    for (var j = 0; j < markers.Cols; j++)
    {
        var idx = markers.At<int>(i, j);
        if (idx == -1)
        {
            watershedImage.Set(i, j, new Vec3b(255, 255, 255));
        }
        else if (idx <= 0 || idx > componentCount)
        {
            watershedImage.Set(i, j, new Vec3b(0, 0, 0));
        }
        else
        {
            watershedImage.Set(i, j, colorTable[idx - 1]);
        }
    }
}
 
watershedImage = watershedImage * 0.5 + imgGray * 0.5;
Cv2.ImShow("Watershed Transform", watershedImage);
Cv2.WaitKey(1); //do events
متد Cv2.TheRNG یک تولید کننده‌ی اعداد تصادفی توسط OpenCV است و متد Uniform آن شبیه به متد Next کلاس Random دات نت عمل می‌کند. به نظر این کلاس تولید اعداد تصادفی، آنچنان هم تصادفی عمل نمی‌کند. به همین جهت از کلاس Random دات نت استفاده شد. در اینجا به ازای تعداد کانتورهای ترسیم شده، یک رنگ تصادفی تولید شده‌است.
پس از اعمال متد Watershed، هر نقطه‌ی تصویر marker مشخص می‌کند که متعلق به کدام قطعه‌ی تشخیص داده شده‌است. سپس به این نقطه، رنگ آن قطعه را نسبت داده و آن‌را در تصویر جدیدی ترسیم می‌کنیم.
در آخر، پس زمینه، با نواحی تشخیص داده ترکیب شده‌اند (watershedImage * 0.5 + imgGray * 0.5) تا تصویر ابتدای بحث حاصل شود. اگر این ترکیب صورت نگیرد، چنین تصویری حاصل خواهد شد:




کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید.
مطالب
پردازش‌های Async در Entity framework 6
اجرای Async اعمال نسبتا طولانی، در برنامه‌های مبتنی بر داده، عموما این مزایا را به همراه دارد:

الف) مقیاس پذیری سمت سرور

در اعمال سمت سرور متداول، تردهای متعددی جهت پردازش درخواست‌های کلاینت‌ها تدارک دیده می‌شوند. هر زمانیکه یکی از این تردها، یک عملیات blocking را انجام می‌دهد (مانند دسترسی به شبکه یا اعمال I/O)، ترد مرتبط با آن تا پایان کار این عملیات معطل خواهد شد. با بالا رفتن تعداد کاربران یک برنامه و در نتیجه بیشتر شدن تعداد درخواست‌هایی که سرور باید پردازش کند، تعداد تردهای معطل مانده نیز به همین ترتیب بیشتر خواهند شد. مشکل اصلی اینجا است که نمونه سازی تردها بسیار هزینه بر است (با اختصاص 1MB of virtual memory space) و منابع سرور محدود. با زیاد شدن تعداد تردهای معطل اعمال I/O یا شبکه، سرور مجبور خواهد شد بجای استفاده مجدد از تردهای موجود، تردهای جدیدی را ایجاد کند. همین مساله سبب بالا رفتن بیش از حد مصرف منابع و حافظه برنامه می‌گردد. یکی از روش‌های رفع این مشکل بدون نیاز به بهبودهای سخت افزاری، تبدیل اعمال blocking نامبرده شده به نمونه‌های non-blocking است. به این ترتیب ترد پردازش کننده‌ی این اعمال Async بلافاصله آزاد شده و سرور می‌تواند از آن جهت پردازش درخواست دیگری استفاده کند؛ بجای اینکه ترد جدیدی را وهله سازی نماید.

ب) بالا بردن پاسخ دهی کلاینت‌ها

کلاینت‌ها نیز اگر مدام درخواست‌های blocking را به سرور جهت دریافت پاسخی ارسال کنند، به زودی به یک رابط کاربری غیرپاسخگو خواهند رسید. برای رفع این مشکل نیز می‌توان از توانمندی‌های Async دات نت 4.5 جهت آزاد سازی ترد اصلی برنامه یا همان ترد UI استفاده کرد.

و ... تمام این‌ها یک شرط را دارند. نیاز است یک چنین API خاصی که اعمال Async واقعی را پشتیبانی می‌کنند، فراهم شده باشد. بنابراین صرفا وجود متد Task.Run، به معنای اجرای واقعی Async یک متد خاص نیست. برای این منظور ADO.NET 4.5 به همراه متدهای Async ویژه کار با بانک‌های اطلاعاتی است و پس از آن Entity framework 6 از این زیر ساخت استفاده کرده‌است که در ادامه جزئیات آن‌را بررسی خواهیم کرد.


پیشنیازها

برای کار با امکانات جدید Async موجود در EF 6 نیاز است از VS 2012 به بعد که به همراه کامپایلری است که واژه‌های کلیدی async و await را پشتیبانی می‌کند و همچنین دات نت 4.5 استفاده کرد. چون ADO.NET 4.5 اعمال async واقعی را پشتیبانی می‌کند، دات نت 4 در اینجا قابل استفاده نخواهد بود.


متدهای الحاقی جدید Async در EF 6.x

جهت متدهای الحاقی متداول EF مانند ToList، Max، Min و غیره، نمونه‌های Async آن‌ها نیز اضافه شده‌اند:
 QueryableExtensions:
AllAsync
AnyAsync
AverageAsync
ContainsAsync
CountAsync
FirstAsync
FirstOrDefaultAsync
ForEachAsync
LoadAsync
LongCountAsync
MaxAsync
MinAsync
SingleAsync
SingleOrDefaultAsync
SumAsync
ToArrayAsync
ToDictionaryAsync
ToListAsync

DbSet:
FindAsync

DbContext:
SaveChangesAsync

Database:
ExecuteSqlCommandAsync
بنابراین اولین قدم تبدیل کدهای قدیمی به Async، استفاده از متدهای الحاقی فوق است.


چند مثال


فرض کنید، مدل‌های برنامه، رابطه‌ی one-to-many ذیل را بین یک کاربر و مقالات او دارند:
    public class User
    {
        public int Id { get; set; }
        public string Name { get; set; }

        public virtual ICollection<BlogPost> BlogPosts { get; set; }
    }

    public class BlogPost
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }

        [ForeignKey("UserId")]
        public virtual User User { get; set; }
        public int UserId { get; set; }
    }
همچنین Context برنامه نیز جهت در معرض دید قرار دادن این کلاس‌ها، به نحو ذیل تشکیل شده‌است:
    public class MyContext : DbContext
    {
        public DbSet<User> Users { get; set; }
        public DbSet<BlogPost> BlogPosts { get; set; }

        public MyContext()
            : base("Connection1")
        {
            this.Database.Log = sql => Console.Write(sql);
        }
    }
بر این اساس مثالی که دو رکورد را در جداول کاربران و مقالات به صورت async ثبت می‌کند، به نحو ذیل خواهد بود:
        private async Task<User> addUserAsync(CancellationToken cancellationToken = default(CancellationToken))
        {
            using (var context = new MyContext())
            {
                var user = context.Users.Add(new User
                {
                    Name = "Vahid"
                });
                context.BlogPosts.Add(new BlogPost
                {
                    Content = "Test",
                    Title = "Test",
                    User = user
                });
                await context.SaveChangesAsync(cancellationToken);
                return user;
            }
        }

چند نکته جهت یادآوری مباحث Async

- به امضای متد واژه‌ی کلیدی async اضافه شده‌است، زیرا در بدنه‌ی آن از کلمه‌ی کلیدی await استفاده کرده‌ایم (لازم و ملزوم هستند).
- به انتهای نام متد، کلمه‌ی Async اضافه شده‌است. این مورد ضروری نیست؛ اما به یک استاندارد و قرارداد تبدیل شده‌است.
- مدل Async دات نت 4.5 مبتنی بر Taskها است. به همین جهت اینبار خروجی‌های توابع نیاز است از نوع Task باشند و آرگومان جنریک آن‌ها، بیانگر نوع مقداری که باز می‌گردانند.
- تمام متدهای الحاقی جدیدی که نامبرده شدند، دارای پارامتر اختیاری لغو عملیات نیز هستند. این مورد را با مقدار دهی cancellationToken در کدهای فوق ملاحظه می‌کنید.
نمونه‌ای از نحوه‌ی مقدار دهی این پارامتر در ASP.NET MVC به صورت زیر می‌تواند باشد:
 [AsyncTimeout(8000)]
public async Task<ActionResult> Index(CancellationToken cancellationToken)
در اینجا به امضای اکشن متد جاری، async اضافه شده‌است و خروجی آن نیز به نوع Task تغییر یافته است. همچنین یک پارامتر cancellationToken نیز تعریف شده‌است. این پارامتر به صورت خودکار توسط ASP.NET MVC پس از زمانیکه توسط ویژگی AsyncTimeout تعیین شده‌است، تنظیم خواهد شد. به این ترتیب، اعمال async در حال اجرا به صورت خودکار لغو می‌شوند.
- برای اجرا و دریافت نتیجه‌ی متدهای Async دار EF، نیاز است از واژه‌ی کلیدی await استفاده گردد.

استفاده کننده نیز می‌تواند متد addUserAsync را به صورت زیر فراخوانی کند:
 var user = await addUserAsync();
Console.WriteLine("user id: {0}", user.Id);

شبیه به همین اعمال را نیز جهت به روز رسانی و یا حذف اطلاعات خواهیم داشت:
        private async Task<User> updateAsync(CancellationToken cancellationToken = default(CancellationToken))
        {
            using (var context = new MyContext())
            {
                var user1 = await context.Users.FindAsync(cancellationToken, 1);
                if (user1 != null)
                    user1.Name = "Vahid N.";

                await context.SaveChangesAsync(cancellationToken);
                return user1;
            }
        }

        private async Task<int> deleteAsync(CancellationToken cancellationToken = default(CancellationToken))
        {
            using (var context = new MyContext())
            {
                var user1 = await context.Users.FindAsync(cancellationToken, 1);
                if (user1 != null)
                    context.Users.Remove(user1);

                return await context.SaveChangesAsync(cancellationToken);
            }
        }

کدهای Async تقلبی!

به قطعه کد ذیل دقت کنید:
         public async Task<List<TEntity>> GetAllAsync()
   {
    return await Task.Run(() => _tEntities.ToList());
   }
این متد از یکی از Generic repositoryهای فله‌ای رها شده در اینترنت انتخاب شده‌است.
به این نوع متدها که از Task.Run برای فراخوانی متدهای همزمان قدیمی مانند ToList جهت Async جلوه دادن آن‌ها استفاده می‌شود، کدهای Async تقلبی می‌گویند! این عملیات هر چند در یک ترد دیگر انجام می‌شود اما هم سربار ایجاد یک ترد جدید را به همراه دارد و هم عملیات ToList آن کاملا blocking است.
معادل صحیح Async واقعی این عملیات را در ذیل مشاهده می‌کنید:
        private async Task<List<User>> getUsersAsync(CancellationToken cancellationToken = default(CancellationToken))
        {
            using (var context = new MyContext())
            {
                return await context.Users.ToListAsync(cancellationToken);
            }
        }
متد ToListAsync یک متد Async واقعی است و نه شبیه سازی شده توسط Task.Run. متدهای Async واقعی کار با شبکه و اعمال I/O، از ترد استفاده نمی‌کنند و توسط سیستم عامل به نحو بسیار بهینه‌ای اجرا می‌گردند.
برای مثال پشت صحنه‌ی متد الحاقی SaveChangesAsync به یک چنین متدی ختم می‌شود:
 internal override async Task<long> ExecuteAsync(
//...
rowsAffected = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
//...
متد ExecuteNonQueryAsync جزو متدهای ADO.NET 4.5 است و برای اجرا نیاز به هیچ ترد جدیدی ندارد.
و یا برای شبیه سازی ToListAsync با ADO.NET 4.5 و استفاده از متدهای Async واقعی آن، به یک چنین کدهایی نیاز است: 
    var connectionString = "........";
    var sql = @"......"";
    var users = new List<User>();
 
    using (var cnx = new SqlConnection(connectionString))
    {
      using (var cmd = new SqlCommand(sql, cnx))
      {
       await cnx.OpenAsync(); 
       using (var reader = await cmd.ExecuteReaderAsync(CommandBehavior.CloseConnection))
       {
        while (await reader.ReadAsync())
        {
          var user = new User
          {
           Id = reader.GetInt32(0), 
           Name = reader.GetString(1), 
          };
         users.Add(user);
        }
       }
      }
    }


محدودیت پردازش موازی اعمال در EF

در متد ذیل، دو Task غیرهمزمان تعریف شده‌اند و سپس با await Task.WhenAll درخواست اجرای همزمان و موازی آن‌ها را کرده‌ایم:
        // multiple operations
        private static async Task loadAllAsync(CancellationToken cancellationToken = default(CancellationToken))
        {
            using (var context = new MyContext())
            {
                var task1 = context.Users.ToListAsync(cancellationToken);
                var task2 = context.BlogPosts.ToListAsync(cancellationToken);

                await Task.WhenAll(task1, task2);
                // use task1.Result
            }
        }
این متد ممکن است اجرا شود؛ یا در بعضی از مواقع با استثنای ذیل خاتمه یابد:
  An unhandled exception of type 'System.NotSupportedException' occurred in mscorlib.dll
 Additional information: A second operation started on this context before a previous asynchronous operation completed.
Use 'await' to ensure that any asynchronous operations have completed before calling another method on this context.
Any instance members are not guaranteed to be thread safe.
متن استثنای ارائه شده بسیار مفید است و توضیحات کامل را به همراه دارد. در EF در طی یک Context اگر عملیات Async شروع شده‌ای خاتمه نیافته باشد، مجاز به شروع یک عملیات Async دیگر، به موازت آن نخواهیم بود. برای رفع این مشکل یا باید از چندین Context استفاده کرد و یا await Task.WhenAll را حذف کرده و بجای آن واژه‌ی کلیدی await را همانند معمول، جهت صبر کردن برای دریافت نتیجه‌ی یک عملیات غیرهمزمان استفاده کنیم.
مطالب
Cookie - قسمت دوم

کوکی در جاوا اسکریپت 

همانطور که در قسمت قبل اشاره کوتاهی شد، مدیریت کوکی‌های در دسترس در وضعیت جاری، در جاوا اسکریپت ازطریق پراپرتی cookie از شی document امکان‌پذیر است. این پراپرتی کاری همانند هدرهای Set-Cookie و Cookie (که در قسمت قبل درباره آن‌ها بحث شد) انجام می‌دهد. این پراپرتی یک مورد کاملا استثنایی و نسبتا عجیب در زبان جاوا اسکریپت است. در نگاه اول ظاهرا document.cookie از نوع رشته است، اما قضیه کاملا فرق می‌کند. برای روشن شدن مطلب به ادامه بحث توجه کنید.

افزودن کوکی
- برای افزودن یا ویرایش یک کوکی باید از ساختاری مانند ساختار هدر Set-Cookie که چیزی شبیه به عبارت زیر است، پیروی کرد:
document.cookie = "name=value; expires=date; domain=theDomain; path=thePath; secure";
 
نکته: با توجه به توضیحاتی که در قسمت قبل ارائه شد، بدیهی است که امکان ثبت یک کوکی با فلگ HttpOnly در جاوا اسکریپت وجود ندارد!
 
اجرای دستوری شبیه با ساختار نشان داده شده در بالا، موجب حذف کوکی‌های قبلی نمی‌شود. از این دستور برای ایجاد یک کوکی و یا ویرایش یک کوکی موجود استفاده می‌شود. کوکی‌های ایجادشده با این روش تفاوتی با کوکی‌های ایجادشده توسط هدر Set-Cookie ندارند و همانند آنها در درخواست‌های بعدی با توجه به خواص تنظیم شده، به سمت سرور ارسال خواهند شد.
همانطور که مشاهده می‌کنید خاصیت‌های کوکی به صورت جفت‌های نام-مقدار درون یک رشته به document.cookie نسبت داده می‌شوند. این خاصیت‌ها توسط یک کاراکتر ; از یکدیگر جدا می‌شوند. شرح ساختار فوق در  زیر آورده شده است:
1. همیشه اولین جفتِ نام-مقدار همانند مثال بالا باید «عنوان و مقدار» کوکی را مشخص سازد. این قسمت تنها عضو اجباری ساختار فوق است.
2. سپس یک سمی‌کالن و یک فاصله
3. تاریخ انقضا (expires) یا حداکثر طول عمر کوکی (max-age)
4. سپس یک سمی‌کالن و یک فاصله
5. دمین و یا مسیر مربوط به کوکی
6. سپس یک سمی‌کالن و یک فاصله
7. سایر خواص چون Secure
نکته: این ساختار عجیب معرفی شده را عینا رعایت کنید. بقیه کار توسط مرورگر انجام خواهد شد.
نکته: قسمت‌های مختلف این ساختار case-sensitive نیست، البته به‌جز نام کوکی که کاملا case-sensitive است.
مثلا برای ثبت یک کوکی با عنوان myCookie و مقدار myValue و دمین d.com و مسیر test و طول عمر 5 روزه باید از دستور زیر استفاده کرد:
document.cookie = 'myCookie=myValue; max-age=432000; domain=d.com; path=/test';
 
خواندن کوکی
- برای خواندن کوکی‌ها تنها کافی است مقدار پراپرتی document.cookie بررسی شود. با اینکه از دستور نشان داده شده در بالا اینگونه برمی آید که پراپرتی document.cookie به رشته معرفی شده مقداردهی شده است، اما به محض خواندن این پراپرتی چیزی شبیه به عبارت زیر برگردانده میشود:
myCookie=myValue 
از بقیه خواص اثری نیست! این رفتار به دلیل حفط امنیت کوکی‌ها در تمام مرورگرها رعایت می‌شود.
- برای ثبت کوکی دیگری در وضعیت جاری کافی است یکبار دیگر دستور بالا را برای کوکی جدید به کار ببریم. مثلا به صورت زیر:
document.cookie = 'mySecondCookie=mySecondValue; path=/'
اینار یک کوکی سشنی بدون دمین و با مقدار / برای مسیر کوکی ثبت می‌شود! در این حالت کوکی قبلی دوباره نویسی و یا حذف نمی‌شود و تنها یک کوکی جدید به لیست کوکیهای مرورگر اضافه می‌شود! این رفتار عجیب از ویژگی‌های جالب document.cookie است.
- اگر مقدار document.cookie در این حالت خوانده شود مقدار زیر برگشت داده می‌شود:
myCookie=myValue; mySecondCookie=mySecondValue
باز هم خبری از سایر خاصیت‌ها نیست. ولی همانطور که می‌بینید کوکی دوم به لیست کوکی‌های مرورگر اضافه شده است.

نکته: عبارت برگشت داده شده از پراپرتی document.cookie همانند مقداری است که در هدر Cookie هر درخواست توسط مرورگر گنجانده می‌شود، یعنی جفت نام-مقدار کوکی‌ها به همراه یک ; و یک فاصله بین مقادیر هر کوکی. بنابراین برای بدست آوردن مقدار یک کوکی یکسری عملیات جهت Parse کردن داده‌های آن نیاز است!

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

نکته: متدهای زیر از ترکیب چندین ریفرنس مختلف بدست آمده است. هرچند برای موارد خاص‌تر می‌توانند بیشتر سفارشی شوند.

افزودن و یا ویرایش کوکی
function setCookie(data, value) {
  if (typeof data === "string") {
    data = { name: data, value: value };
  };
  if (!data.name) throw "Cookie's name can not be null.";

  var cookie = escape(data.name) + "=" + escape(data.value);

  var expDate = null;
  if (data.expDays) {
    expDate = new Date();
    expDate.setDate(expDate.getDate() + data.expDays);
  }
  else if (data.expYear && data.expMonth && data.expDay) {
    expDate = new Date(data.expYear, data.expMonth, data.expDay);
  }
  else if (data.expires) {
    expDate = data.expires;
  }
  else if (data.maxAge) {
    expDate = new Date();
    expDate.setSeconds(expDate.getSeconds() + data.maxAge);
  }
  if (expDate != null) cookie += "; expires=" + expDate.toGMTString();

  if (data.domain)
    cookie += "; domain=" + escape(data.domain);

  if (data.path)
    cookie += "; path=" + escape(data.path);

  if (data.secure)
    cookie += "; secure";

  document.cookie = cookie;
  return document.cookie;
}
در کد فوق برای انکد کردن رشته‌های مورد استفاده از متد escape استفاده شده است. برای آشنایی با این متد به اینجا مراجعه کنید.
هم‌چنین کار کردن با نوع داده تاریخ در جاوا اسکریپت کمی متفاوت است. بنابراین برای آشنایی بیشتر با این نوع داده به اینجا رجوع کنید.
 
نکته: در متد بالا بدلیل عدم پشتیبانی از خاصیت max-age در نسخه‌های قدیمی اینترنت اکسپلورر (نسخه 8 و قبل از آن) تنها از خاصیت expires استفاده شده است.
 
نحوه استفاده از متد بالا به صورت زیر است:
setCookie('cookie1', 'Value1');
setCookie({name:'cookie1', value:'Value1'});
setCookie({name:'cookie2', value:'Value2', expDays:10});
setCookie({name:'cookie3', value:'Value3', expires:new Date()});
setCookie({name:'cookie4', value:'Value4', expYear:2013, expMonth:0, expDay:13});
setCookie({name:'cookie3', value:'Value3', maxAge:365*24*60*60});
setCookie({name:'cookie5', value:'Value5', domain:'d.net', path:'/'});
setCookie({name:'cookie6', value:'Value6', secure:true});
setCookie({name:'cookie7', value:'Value7', expDays:100, domain:'dd.com', path:'/employee', secure:true});
 
حذف کوکی
همانطور که در قسمت قبل هم اشاره شد، برای حذف یک کوکی، کافی است تا تاریخ انقضای آن به تاریخی در گذشته مقداردهی شود. بنابراین برای اینکار می‌توان از متد زیر استفاده کرد:
function delCookie(data) {
  if (typeof data === "string") {
    data = { name: data };
  };
  data.expDays = -1;
  return setCookie(data);
}
در متد فوق از متد setCookie که در بالا معرفی شد، استفاده شده است. نحوه استفاده از این متد هم به صورت زیر است:
delCookie('myCookie');
delCookie({ name: 'myCookie', domain: 'd.com', path: '/test' });
 
خواندن کوکی
برای خواندن مقدار یک کوکی می‌توان از متد زیر استفاده کرد:
function getCookie(name) {
  var cookies = document.cookie.split(";");
  for (var i = 0; i < cookies.length; i++) {
    var cookie = cookies[i].split("=");
    if (cookie[0].trim() == escape(name)) {
      return unescape(cookie[1].trim());
    }
  }
  return null;
}
برای آشنایی با متد unescape که در بالا از آن استفاده شده است به اینجا مراجعه کنید. در متد فوق از متد trim زیر استفاده شده است:
String.prototype.trim = function () {
  return this.replace(/^\s+|\s+$/g, "");
};
String.prototype.trimStart = function () {
  return this.replace(/^\s+/, "");
};
String.prototype.trimEnd = function () {
  return this.replace(/\s+$/, "");
};
این متدها از اینجا گرفته شده است.
روش استفاده شده برای خواندن مقادیر کوکی‌ها در متد بالا بسیار ساده و ابتدایی است و صرفا برای آشنایی با نحوه Parse کردن رشته برگشت داده شده توسط document.cookie ارائه شده است. روش‌های مناسب‌تر و مطمئن‌تر با یک جستجوی ساده در دسترس هستند. البته همانطور که قبلا هم اشاره شد، استفاه از کتابخانه‌های موجود راه‌حل بهتری است.
هم‌چنین ازآنجاکه مقدار یک کوکی می‌تواند شامل کاراکتر = نیز باشد، بنابراین قسمت return متد فوق را می‌توان به صورت زیر تغییر داد:
cookie.shift(1);
return unescape(cookie.join('=').trim());
 
نکته: با توجه به مطالب ارائه شده در قسمت قبل  بدست آوردن مقادیر کوکی‌ها کمی پیچیده‌تر از دیگر عملیات‌هاست. ازآنجاکه راه مستقیمی با استفاده از جاوا اسکریپت برای یافتن سایر خواص کوکی وجود ندارد، بنابراین بدست آوردن مقدار دقیق کوکی موردنظر ممکن است غیرممکن باشد! (با توجه به اینکه کوکی‌های متفاوت می‌توانند نام‌های یکسانی داشته باشند).
 
با توجه به نکته بالا، حال اگر با یک نام بخصوص، چندین کوکی ثبت شده باشد (با خواص متفاوت)، یکی از راه‌حل‌ها این است که آرایه‌ای از مقادیر این کوکی‌های همنام برگشت داده شود. بنابراین متد فوق را می‌توان به صورت زیر تکمیل کرد:
function getCookie(name) {
  var foundCookies = [];
  var cookies = document.cookie.split(";");
  for (var i = 0; i < cookies.length; i++) {
    var cookie = cookies[i].split("=");
    if (cookie[0].trim() == escape(name) && cookie.length >= 2) {
      cookie.shift(1);
      foundCookies.push(unescape(cookie.join('=').trim()));
    }
  }
  return foundCookies.length > 1 
            ? foundCookies
            : foundCookies.length == 1
                ? foundCookies[0]
                : null;
}

خلاصه‌ای از نحوه استفاده از متدهای بالا در IE8 (برای نمایش اجرای درست در مرورگری قدیمی!) در تصویر زیر  نشان داده شده است:

 
نکته: کار توسعه این متدها را میتوان برای پشتیبانی از SubCookieها نیز ادامه داد، اما به دلیل دورشدن از مبحث اصلی، این موضوع در این مطلب ارائه نمیشود (درباره این نوع از کوکی‌ها در قسمت قبل شرح کوتاهی داده شده است). اگر علاقه‌مند و نیازمند به این نوع کوکی‌ها هستید، کتابخانه YUI پشتیبانی کاملی از آنها ارائه میکند.
 
در قسمت بعدی به نکات کار با کوکی در ASP.NET میپردازیم.

منابع:
مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت دهم- ذخیره سازی اطلاعات کاربران IDP در بانک اطلاعاتی
تا اینجا تمام قسمت‌های این سری، برای اساس اطلاعات یک کلاس Config استاتیک تشکیل شده‌ی در حافظه ارائه شدند. این روش برای دمو و توضیح مفاهیم پایه‌ی IdentityServer بسیار مفید است؛ اما برای دنیای واقعی خیر. بنابراین در ادامه می‌خواهیم این قسمت را با اطلاعات ذخیره شده‌ی در بانک اطلاعاتی تعویض کنیم. یک روش مدیریت آن، نصب ASP.NET Core Identity دقیقا داخل همان پروژه‌ی IDP است. در این حالت کدهای ASP.NET Core Identity مایکروسافت، کار مدیریت کاربران IDP را انجام می‌دهند. روش دیگر اینکار را که در اینجا بررسی خواهیم کرد، تغییر کدهای Quick Start UI اضافه شده‌ی در «قسمت چهارم - نصب و راه اندازی IdentityServer»، جهت پذیرفتن مدیریت کاربران مبتنی بر بانک اطلاعاتی تهیه شده‌ی توسط خودمان است. مزیت آن آشنا شدن بیشتر با کدهای Quick Start UI و درک زیرساخت آن است.


تکمیل ساختار پروژه‌ی IDP

تا اینجا برای IDP، یک پروژه‌ی خالی وب را ایجاد و به مرور، آن‌را تکمیل کردیم. اما اکنون نیاز است پشتیبانی از بانک اطلاعاتی را نیز به آن اضافه کنیم. برای این منظور چهار پروژه‌ی Class library کمکی را نیز به Solution آن اضافه می‌کنیم:


- DNT.IDP.DomainClasses
در این پروژه، کلاس‌های متناظر با موجودیت‌های جداول مرتبط با اطلاعات کاربران قرار می‌گیرند.
- DNT.IDP.DataLayer
این پروژه Context برنامه و Migrations آن‌را تشکیل می‌دهد. همچنین به همراه تنظیمات و Seed اولیه‌ی اطلاعات بانک اطلاعاتی نیز می‌باشد.
رشته‌ی اتصالی آن نیز در فایل DNT.IDP\appsettings.json ذخیره شده‌است.
- DNT.IDP.Common
الگوریتم هش کردن اطلاعات، در این پروژه‌ی مشترک بین چند پروژه‌ی دیگر قرار گرفته‌است. از آن جهت هش کردن کلمات عبور، در دو پروژه‌ی DataLayer و همچنین Services استفاده می‌کنیم.
- DNT.IDP.Services
کلاس سرویس کاربران که با استفاده از DataLayer با بانک اطلاعاتی ارتباط برقرار می‌کند، در این پروژه قرار گرفته‌است.


ساختار بانک اطلاعاتی کاربران IdentityServer

در اینجا ساختار بانک اطلاعاتی کاربران IdentityServer، بر اساس جداول کاربران و Claims آن‌ها تشکیل می‌شود:
namespace DNT.IDP.DomainClasses
{
    public class User
    {
        [Key]
        [MaxLength(50)]       
        public string SubjectId { get; set; }
    
        [MaxLength(100)]
        [Required]
        public string Username { get; set; }

        [MaxLength(100)]
        public string Password { get; set; }

        [Required]
        public bool IsActive { get; set; }

        public ICollection<UserClaim> UserClaims { get; set; }

        public ICollection<UserLogin> UserLogins { get; set; }
    }
}
در اینجا SubjectId همان Id کاربر، در سطح IDP است. این خاصیت به صورت یک کلید خارجی در جداول UserClaims و UserLogins نیز بکار می‌رود.
ساختار Claims او نیز به صورت زیر تعریف می‌شود که با تعریف یک Claim استاندارد، سازگاری دارد:
namespace DNT.IDP.DomainClasses
{
    public class UserClaim
    {         
        public int Id { get; set; }

        [MaxLength(50)]
        [Required]
        public string SubjectId { get; set; }
        
        public User User { get; set; }

        [Required]
        [MaxLength(250)]
        public string ClaimType { get; set; }

        [Required]
        [MaxLength(250)]
        public string ClaimValue { get; set; }
    }
}
همچنین کاربر می‌توان تعدادی لاگین نیز داشته باشد:
namespace DNT.IDP.DomainClasses
{
    public class UserLogin
    {
        public int Id { get; set; }

        [MaxLength(50)]
        [Required]
        public string SubjectId { get; set; }
        
        public User User { get; set; }

        [Required]
        [MaxLength(250)]
        public string LoginProvider { get; set; }

        [Required]
        [MaxLength(250)]
        public string ProviderKey { get; set; }
    }
}
هدف از آن، یکپارچه سازی سیستم، با IDPهای ثالث مانند گوگل، توئیتر و امثال آن‌ها است.

در پروژه‌ی DNT.IDP.DataLayer در پوشه‌ی Configurations آن، کلاس‌های UserConfiguration و UserClaimConfiguration را مشاهده می‌کنید که حاوی اطلاعات اولیه‌ای برای تشکیل User 1 و User 2 به همراه Claims آن‌ها هستند. این اطلاعات را دقیقا از فایل استاتیک ‍Config که در قسمت‌های قبل تکمیل کردیم، به این دو کلاس جدید IEntityTypeConfiguration منتقل کرده‌ایم تا به این ترتیب متد GetUsers فایل استاتیک Config را با نمونه‌ی دیتابیسی آن جایگزین کنیم.
سرویسی که از طریق Context برنامه با بانک اطلاعاتی ارتباط برقرار می‌کند، چنین ساختاری را دارد:
    public interface IUsersService
    {
        Task<bool> AreUserCredentialsValidAsync(string username, string password);
        Task<User> GetUserByEmailAsync(string email);
        Task<User> GetUserByProviderAsync(string loginProvider, string providerKey);
        Task<User> GetUserBySubjectIdAsync(string subjectId);
        Task<User> GetUserByUsernameAsync(string username);
        Task<IEnumerable<UserClaim>> GetUserClaimsBySubjectIdAsync(string subjectId);
        Task<IEnumerable<UserLogin>> GetUserLoginsBySubjectIdAsync(string subjectId);
        Task<bool> IsUserActiveAsync(string subjectId);
        Task AddUserAsync(User user);
        Task AddUserLoginAsync(string subjectId, string loginProvider, string providerKey);
        Task AddUserClaimAsync(string subjectId, string claimType, string claimValue);
    }
که توسط آن امکان دسترسی به یک کاربر، اطلاعات Claims او و افزودن رکوردهایی جدید وجود دارد.
تنظیمات نهایی این سرویس‌ها و Context برنامه نیز در فایل DNT.IDP\Startup.cs جهت معرفی به سیستم تزریق وابستگی‌ها، صورت گرفته‌اند. همچنین در اینجا متد initializeDb را نیز مشاهده می‌کنید که با فراخوانی متد context.Database.Migrate، تمام کلاس‌های Migrations پروژه‌ی DataLayer را به صورت خودکار به بانک اطلاعاتی اعمال می‌کند.


غیرفعال کردن صفحه‌ی Consent در Quick Start UI

در «قسمت چهارم - نصب و راه اندازی IdentityServer» فایل‌های Quick Start UI را به پروژه‌ی IDP اضافه کردیم. در ادامه می‌خواهیم قدم به قدم این پروژه را تغییر دهیم.
در صفحه‌ی Consent در Quick Start UI، لیست scopes درخواستی برنامه‌ی کلاینت ذکر شده و سپس کاربر انتخاب می‌کند که کدامیک از آن‌ها، باید به برنامه‌ی کلاینت ارائه شوند. این صفحه، برای سناریوی ما که تمام برنامه‌های کلاینت توسط ما توسعه یافته‌اند، بی‌معنا است و صرفا برای کلاینت‌های ثالثی که قرار است از IDP ما استفاده کنند، معنا پیدا می‌کند. برای غیرفعال کردن آن کافی است به فایل استاتیک Config مراجعه کرده و خاصیت RequireConsent کلاینت مدنظر را به false تنظیم کرد.


تغییر نام پوشه‌ی Quickstart و سپس اصلاح فضای نام پیش‌فرض کنترلرهای آن

در حال حاضر کدهای کنترلرهای Quick Start UI داخل پوشه‌ی Quickstart برنامه‌ی IDP قرار گرفته‌اند. با توجه به اینکه قصد داریم این کدها را تغییر دهیم و همچنین این پوشه در اساس، همان پوشه‌ی استاندارد Controllers است، ابتدا نام این پوشه را به Controllers تغییر داده و سپس در تمام کنترلرهای ذیل آن، فضای نام پیش‌فرض IdentityServer4.Quickstart.UI را نیز به فضای نام متناسبی با پوشه بندی پروژه‌ی جاری تغییر می‌دهیم. برای مثال کنترلر Account واقع در پوشه‌ی Account، اینبار دارای فضای نام DNT.IDP.Controllers.Account خواهد شد و به همین ترتیب برای مابقی کنترل‌ها عمل می‌کنیم.
پس از این تغییرات، عبارات using موجود در Viewها را نیز باید تغییر دهید تا برنامه در زمان اجرا به مشکلی برنخورد. البته ASP.NET Core 2.1 در زمان کامپایل برنامه، تمام Viewهای آن‌را نیز کامپایل می‌کند و اگر خطایی در آن‌ها وجود داشته باشد، امکان بررسی و رفع آن‌ها پیش از اجرای برنامه، میسر است.
و یا می‌توان جهت سهولت کار، فایل DNT.IDP\Views\_ViewImports.cshtml را جهت معرفی این فضاهای نام جدید ویرایش کرد تا نیازی به تغییر Viewها نباشد:
@using DNT.IDP.Controllers.Account;
@using DNT.IDP.Controllers.Consent;
@using DNT.IDP.Controllers.Grants;
@using DNT.IDP.Controllers.Home;
@using DNT.IDP.Controllers.Diagnostics;
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers


تعامل با IdentityServer از طریق کدهای سفارشی

پس از تشکیل «ساختار بانک اطلاعاتی کاربران IdentityServer» و همچنین تهیه سرویس‌های متناظری جهت کار با آن، اکنون نیاز است مطمئن شویم IdentityServer از این بانک اطلاعاتی برای دریافت اطلاعات کاربران خود استفاده می‌کند.
در حال حاضر، با استفاده از متد الحاقی AddTestUsers معرفی شده‌ی در فایل DNT.IDP\Startup.cs، اطلاعات کاربران درون حافظه‌ای برنامه را از متد ()Config.GetUsers دریافت می‌کنیم.
بنابراین اولین قدم، بررسی ساختار متد AddTestUsers است. برای این منظور به مخزن کد IdentityServer4 مراجعه کرده و کدهای متد الحاقی AddTestUsers را بررسی می‌کنیم:
 public static class IdentityServerBuilderExtensions
 {
        public static IIdentityServerBuilder AddTestUsers(this IIdentityServerBuilder builder, List<TestUser> users)
        {
            builder.Services.AddSingleton(new TestUserStore(users));
            builder.AddProfileService<TestUserProfileService>();
            builder.AddResourceOwnerValidator<TestUserResourceOwnerPasswordValidator>();

            return builder;
        }
}
- ابتدا یک TestUserStore را به صورت Singleton ثبت کرده‌است.
- سپس سرویس پروفایل کاربران را اضافه کرده‌است. این سرویس با پیاده سازی اینترفیس IProfileService تهیه می‌شود. کار آن اتصال یک User Store سفارشی به سرویس کاربران و دریافت اطلاعات پروفایل آن‌ها مانند Claims است.
- در آخر TestUserResourceOwnerPasswordValidator، کار اعتبارسنجی کلمه‌ی عبور و نام کاربری را در صورت استفاده‌ی از Flow ویژه‌ای به نام ResourceOwner که استفاده‌ی از آن توصیه نمی‌شود (ROBC Flow)، انجام می‌دهد.

برای جایگزین کردن AddTestUsers، کلاس جدید IdentityServerBuilderExtensions را در ریشه‌ی پروژه‌ی IDP با محتوای ذیل اضافه می‌کنیم:
using DNT.IDP.Services;
using Microsoft.Extensions.DependencyInjection;

namespace DNT.IDP
{
    public static class IdentityServerBuilderExtensions
    {
        public static IIdentityServerBuilder AddCustomUserStore(this IIdentityServerBuilder builder)
        {
            // builder.Services.AddScoped<IUsersService, UsersService>();
            builder.AddProfileService<CustomUserProfileService>();
            return builder;
        }
    }
}
در اینجا ابتدا IUsersService سفارشی برنامه معرفی شده‌است که User Store سفارشی برنامه است. البته چون UsersService ما با بانک اطلاعاتی کار می‌کند، نباید به صورت Singleton ثبت شود و باید در پایان هر درخواست به صورت خودکار Dispose گردد. به همین جهت طول عمر آن Scoped تعریف شده‌است. در کل ضرورتی به ذکر این سطر نیست؛ چون پیشتر کار ثبت IUsersService در کلاس Startup برنامه انجام شده‌است.
سپس یک ProfileService سفارشی را ثبت کرده‌ایم. این سرویس، با پیاده سازی IProfileService به صورت زیر پیاده سازی می‌شود:
namespace DNT.IDP.Services
{
    public class CustomUserProfileService : IProfileService
    {
        private readonly IUsersService _usersService;

        public CustomUserProfileService(IUsersService usersService)
        {
            _usersService = usersService;
        }

        public async Task GetProfileDataAsync(ProfileDataRequestContext context)
        {
            var subjectId = context.Subject.GetSubjectId();
            var claimsForUser = await _usersService.GetUserClaimsBySubjectIdAsync(subjectId);
            context.IssuedClaims = claimsForUser.Select(c => new Claim(c.ClaimType, c.ClaimValue)).ToList();
        }

        public async Task IsActiveAsync(IsActiveContext context)
        {
            var subjectId = context.Subject.GetSubjectId();
            context.IsActive = await _usersService.IsUserActiveAsync(subjectId);
        }
    }
}
سرویس پروفایل، توسط سرویس کاربران برنامه که در ابتدای مطلب آن‌را تهیه کردیم، امکان دسترسی به اطلاعات پروفایل کاربران را مانند Claims او، پیدا می‌کند.
در متدهای آن، ابتدا subjectId و یا همان Id منحصربفرد کاربر جاری سیستم، دریافت شده و سپس بر اساس آن می‌توان از usersService، جهت دریافت اطلاعات مختلف کاربر، کوئری گرفت و نتیجه را در خواص context جاری، برای استفاده‌های بعدی، ذخیره کرد.

اکنون به کلاس src\IDP\DNT.IDP\Startup.cs مراجعه کرده و متد AddTestUsers را با AddCustomUserStore جایگزین می‌کنیم:
namespace DNT.IDP
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddIdentityServer()
             .AddDeveloperSigningCredential()
             .AddCustomUserStore()
             .AddInMemoryIdentityResources(Config.GetIdentityResources())
             .AddInMemoryApiResources(Config.GetApiResources())
             .AddInMemoryClients(Config.GetClients());
تا اینجا فقط این سرویس‌های جدید را ثبت کرده‌ایم، اما هنوز کار خاصی را انجام نمی‌دهند و باید از آن‌ها در برنامه استفاده کرد.


اتصال IdentityServer به User Store سفارشی

در ادامه، سازنده‌ی کنترلر DNT.IDP\Quickstart\Account\AccountController.cs را بررسی می‌کنیم:
        public AccountController(
            IIdentityServerInteractionService interaction,
            IClientStore clientStore,
            IAuthenticationSchemeProvider schemeProvider,
            IEventService events,
            TestUserStore users = null)
        {
            _users = users ?? new TestUserStore(TestUsers.Users);

            _interaction = interaction;
            _clientStore = clientStore;
            _schemeProvider = schemeProvider;
            _events = events;
        }
- سرویس توکار IIdentityServerInteractionService، کار تعامل برنامه با IdentityServer4‌  را انجام می‌دهد.
- IClientStore پیاده سازی محل ذخیره سازی اطلاعات کلاینت‌ها را ارائه می‌دهد که در حال حاضر توسط متد استاتیک Config در اختیار آن قرار می‌گیرد.
- IEventService رخ‌دادهایی مانند لاگین موفقیت آمیز یک کاربر را گزارش می‌دهد.
- در آخر، TestUserStore تزریق شده‌است که می‌خواهیم آن‌را با User Store سفارشی خودمان جایگزین کنیم.  بنابراین در ابتدا TestUserStore را با UserStore سفارشی خودمان جایگزین می‌کنیم:
        private readonly TestUserStore _users;
        private readonly IUsersService _usersService;
        public AccountController(
    // ...
            IUsersService usersService)
        {
            _usersService = usersService;
    // ...
        }
فعلا فیلد TestUserStore را نیز سطح کلاس جاری باقی نگه می‌داریم. از این جهت که قسمت‌های لاگین خارجی سیستم (استفاده از گوگل، توئیتر و ...) هنوز از آن استفاده می‌کنند و آن‌را در قسمتی دیگر تغییر خواهیم داد.
پس از معرفی فیلد usersService_، اکنون در قسمت زیر از آن استفاده می‌کنیم:
در اکشن متد لاگین، جهت بررسی صحت نام کاربری و کلمه‌ی عبور و همچنین یافتن کاربر متناظر با آن:
        public async Task<IActionResult> Login(LoginInputModel model, string button)
        {
    //...
            if (ModelState.IsValid)
            {
                if (await _usersService.AreUserCredentialsValidAsync(model.Username, model.Password))
                {
                    var user = await _usersService.GetUserByUsernameAsync(model.Username);
تا همینجا برنامه را کامپایل کرده و اجرا کنید. پس از لاگین در آدرس https://localhost:5001/Gallery/IdentityInformation، هنوز اطلاعات User Claims کاربر وارد شده‌ی به سیستم نمایش داده می‌شوند که بیانگر صحت عملکرد CustomUserProfileService است.


افزودن امکان ثبت کاربران جدید به برنامه‌ی IDP

پس از اتصال قسمت login برنامه‌ی IDP به بانک اطلاعاتی، اکنون می‌خواهیم امکان ثبت کاربران را نیز به آن اضافه کنیم.
این قسمت شامل تغییرات ذیل است:
الف) اضافه شدن RegisterUserViewModel
این ViewModel که فیلدهای فرم ثبت‌نام را تشکیل می‌دهد، ابتدا با نام کاربری و کلمه‌ی عبور شروع می‌شود:
    public class RegisterUserViewModel
    {
        // credentials       
        [MaxLength(100)]
        public string Username { get; set; }

        [MaxLength(100)]
        public string Password { get; set; }
سپس سایر خواصی که در اینجا اضافه می‌شوند:
    public class RegisterUserViewModel
    {
   // ...

        // claims 
        [Required]
        [MaxLength(100)]
        public string Firstname { get; set; }

        [Required]
        [MaxLength(100)]
        public string Lastname { get; set; }

        [Required]
        [MaxLength(150)]
        public string Email { get; set; }

        [Required]
        [MaxLength(200)]
        public string Address { get; set; }

        [Required]
        [MaxLength(2)]
        public string Country { get; set; }
در کنترلر UserRegistrationController، تبدیل به UserClaims شده و در جدول مخصوص آن ذخیره خواهند شد.
ب) افزودن UserRegistrationController
این کنترلر، RegisterUserViewModel را دریافت کرده و سپس بر اساس آن، شیء User ابتدای بحث را تشکیل می‌دهد. ابتدا نام کاربری و کلمه‌ی عبور را در جدول کاربران ثبت می‌کند و سپس سایر خواص این ViewModel را در جدول UserClaims:
varuserToCreate=newUser
{
  Password=model.Password.GetSha256Hash(),
  Username=model.Username,
  IsActive=true
};
userToCreate.UserClaims.Add(newUserClaim("country",model.Country));
userToCreate.UserClaims.Add(newUserClaim("address",model.Address));
userToCreate.UserClaims.Add(newUserClaim("given_name",model.Firstname));
userToCreate.UserClaims.Add(newUserClaim("family_name",model.Lastname));
userToCreate.UserClaims.Add(newUserClaim("email",model.Email));
userToCreate.UserClaims.Add(newUserClaim("subscriptionlevel","FreeUser"));
ج) افزودن RegisterUser.cshtml
این فایل، view متناظر با ViewModel فوق را ارائه می‌دهد که توسط آن، کاربری می‌تواند اطلاعات خود را ثبت کرده و وارد سیستم شود.
د) اصلاح فایل ViewImports.cshtml_ جهت تعریف فضای نام UserRegistration
در RegisterUser.cshtml از RegisterUserViewModel استفاده می‌شود. به همین جهت بهتر است فضای نام آن‌را به ViewImports اضافه کرد.
ه) افزودن لینک ثبت نام به صفحه‌ی لاگین در Login.cshtml
این لینک دقیقا در ذیل چک‌باکس Remember My Login اضافه شده‌است.


اکنون اگر برنامه را اجرا کنیم، ابتدا مشاهده می‌کنیم که صفحه‌ی لاگین به همراه لینک ثبت نام ظاهر می‌شود:


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


برای آزمایش، کاربری را ثبت کنید. پس از ثبت اطلاعات، بلافاصله وارد سیستم خواهید شد. البته چون در اینجا subscriptionlevel به FreeUser تنظیم شده‌است، این کاربر یکسری از لینک‌های برنامه‌ی MVC Client را به علت نداشتن دسترسی، مشاهده نخواهد کرد.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشه‌ی 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 وارد کنید.
مطالب
آغاز به کار با Twitter Bootstrap در ASP.NET MVC
Twitter Bootstrap یک فریم ورک CSS بسیار محبوب سورس باز تولید برنامه‌های وب به کمک HTML، CSS و جاوا اسکریپت است. این فریم ورک حاوی بسیاری از المان‌های مورد نیاز جهت تولید وب سایت‌هایی زیبا، مانند دکمه‌ها، عناصر فرم‌ها، منوها، ویجت‌ها و غیره است. تمام این‌ها نیز همانطور که عنوان شد برمبنای HTML، CSS و جاوا اسکریپت تهیه شده‌اند؛ بنابراین در هر نوع فناوری سمت سروری مانند ASP.NET، PHP، روبی و امثال آن قابل استفاده است.


دریافت Twitter Bootstrap

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


و یا حتی از منابع سایت http://rbootstrap.ir نیز می‌توان استفاده کرد.
برای نمونه دستور زیر را در کنسول پاورشل ویژوال استودیو وارد نمائید تا اسکریپت‌ها و فایل‌های CSS مورد نیاز به پروژه جاری اضافه شوند:
 PM> Install-Package Twitter.BootstrapRTL
پس از نصب، شاهد افزوده شدن چنین فایل‌هایی به یک پروژه ASP.NET MVC خواهیم بود:


در اینجا فایل‌های min، نگارش‌های فشرده شده فایل‌های js یا css هستند که با توجه به امکانات اضافه شده به ASP.NET MVC4، از آن‌ها استفاده نخواهیم کرد و برای افزودن و تعریف آن‌ها از امکانات Bundling and minification توکار فریم ورک ASP.NET MVC به نحوی که در ادامه توضیح داده خواهد شد، استفاده می‌کنیم.
فایل‌های png اضافه شده، آیکون‌های مخصوص Twitter Bootstrap هستند که اصطلاحا به آن‌ها sprite images نیز گفته می‌شود. در این نوع تصاویر، تعداد زیادی آیکون در کنار هم، برای بهینه سازی تعداد بار رفت و برگشت به سرور جهت دریافت تصاویر، طراحی شده و قرار گرفته‌اند.
فایل‌های js این مجموعه اختیاری بوده و برای استفاده از ویجت‌های Twitter Bootstrap مانند آکاردئون کاربرد دارند. این فایل‌ها برای اجرا، نیاز به jQuery خواهند داشت.


افزودن تعاریف اولیه Twitter Bootstrap به یک پروژه ASP.NET MVC

امکانات Bundling and minification در نوع پروژه‌های نسبتا جامع‌تر ASP.NET MVC به صورت پیش فرض لحاظ شده است. اما اگر یک پروژه خالی را شروع کرده‌اید، نیاز است بسته نیوگت آن‌را نیز نصب کنید:
 PM> Install-Package Microsoft.AspNet.Web.Optimization
پس از آن، کلاس کمکی BundleConfig ذیل را به پروژه جاری اضافه نمائید. به کمک آن قصد داریم امکانات Bundling and minification را فعال کرده و همچنین به آن بگوییم اسکریپت‌ها و فایل‌های CSS تعریف شده را به همان نحوی که معرفی می‌کنیم، پردازش کن (توسط کلاس سفارشی AsIsBundleOrderer).

using System.Collections.Generic;
using System.IO;
using System.Web;
using System.Web.Optimization;

namespace Mvc4TwitterBootStrapTest.Helper
{
    /// <summary>
    /// A custom bundle orderer (IBundleOrderer) that will ensure bundles are 
    /// included in the order you register them.
    /// </summary>
    public class AsIsBundleOrderer : IBundleOrderer
    {
        public IEnumerable<FileInfo> OrderFiles(BundleContext context, IEnumerable<FileInfo> files)
        {
            return files;
        }
    }

    public static class BundleConfig
    {
        private static void addBundle(string virtualPath, bool isCss, params string[] files)
        {
            BundleTable.EnableOptimizations = true;

            var existing = BundleTable.Bundles.GetBundleFor(virtualPath);
            if (existing != null)
                return;

            Bundle newBundle;
            if (HttpContext.Current.IsDebuggingEnabled)
            {
                newBundle = new Bundle(virtualPath);
            }
            else
            {
                newBundle = isCss ? new Bundle(virtualPath, new CssMinify()) : new Bundle(virtualPath, new JsMinify());
            }
            newBundle.Orderer = new AsIsBundleOrderer();

            foreach (var file in files)
                newBundle.Include(file);

            BundleTable.Bundles.Add(newBundle);
        }

        public static IHtmlString AddScripts(string virtualPath, params string[] files)
        {
            addBundle(virtualPath, false, files);
            return Scripts.Render(virtualPath);
        }

        public static IHtmlString AddStyles(string virtualPath, params string[] files)
        {
            addBundle(virtualPath, true, files);
            return Styles.Render(virtualPath);
        }

        public static IHtmlString AddScriptUrl(string virtualPath, params string[] files)
        {
            addBundle(virtualPath, false, files);
            return Scripts.Url(virtualPath);
        }

        public static IHtmlString AddStyleUrl(string virtualPath, params string[] files)
        {
            addBundle(virtualPath, true, files);
            return Styles.Url(virtualPath);
        }
    }
}
نکته دیگری که در این کلاس سفارشی درنظر گرفته شده، عدم فعال سازی مباحث Bundling and minification در حالت Debug است. اگر در وب کانفیگ، تنظیمات پروژه را بر روی Release قرار دهید فعال خواهد شد (مناسب برای توزیع) و در حالت توسعه برنامه (حالت دیباگ)، این اسکریپت‌ها و فایل‌های CSS، قابلیت دیباگ و بررسی را نیز خواهند داشت.

پس از افزودن کلاس‌های کمکی فوق، به فایل layout پروژه مراجعه کرده و تعاریف ذیل را به ابتدای فایل اضافه نمائید:
@using Mvc4TwitterBootStrapTest.Helper
<!DOCTYPE html>
<html>
<head>
    <title>@ViewBag.Title</title>
    @BundleConfig.AddStyles("~/Content/css",
                            "~/Content/bootstrap.css",
                            "~/Content/bootstrap-responsive.css",
                            "~/Content/Site.css"
                            )
    @BundleConfig.AddScripts("~/Scripts/js",
                            "~/Scripts/jquery-1.9.1.min.js",
                            "~/Scripts/jquery.validate.min.js",
                            "~/Scripts/jquery.unobtrusive-ajax.min.js",
                            "~/Scripts/jquery.validate.unobtrusive.min.js",
                            "~/Scripts/bootstrap.min.js"
                            )
    @RenderSection("JavaScript", required: false)
</head>
و اگر برنامه را اجرا کنید، بجای حداقل 8 مدخل اشاره کننده به فایل‌های اسکریپت و CSS مورد نیاز یک برنامه ASP.NET MVC، فقط دو مدخل ذیل را مشاهده خواهید نمود:
<!DOCTYPE html>
<html>
<head>
    <title>Index</title>
    <link href="/Content/css?v=vsUQD0OJg4AJ-RZH8jSRRCu_rjl2U1nZrmSsaUyxoAc1" rel="stylesheet"/>
    <script src="/Scripts/js?v=GezdoTDiWY3acc3mI2Ujm_7nKKzh6Lu1Wr8TGyyLpW41"></script>
</head>
به این ترتیب تعداد رفت و برگشت‌های مرورگر به سرور، برای دریافت فایل‌های مورد نیاز جهت رندر صحیح یک صفحه، به شدت کاهش خواهد یافت. همچنین این فایل‌ها در حالت release فشرده نیز خواهند شد؛ به همراه تنظیم خودکار هدر کش شدن آن‌ها برای مدتی مشخص، جهت کاهش بار سرور و کاهش پهنای باند مصرفی آن.


مفاهیم پایه‌ای Twitter Bootstrap

الف) Semantic class names
به عبارتی کلاس‌های Twitter Bootstrap دارای نام‌هایی معنا دار و مفهومی می‌باشند؛ مانند کلاس‌های CSS‌ایی، به نام‌های Succes، Error، Info و امثال آن. این نام‌ها مفهومی را می‌رسانند؛ اما در مورد نحوه پیاده سازی آن‌ها جزئیاتی را بیان نمی‌کنند.
برای نمونه می‌توان کلاسی را به نام redText ایجاد کرد. هر چند این نام، توضیحاتی را در مورد علت وجودی‌اش بیان می‌کند، اما بسیار ویژه بوده و در مورد جزئیات پیاده سازی آن نیز اطلاعاتی را ارائه می‌دهد. در این حالت redText معنایی ندارد. چرا یک Text باید قرمز باشد؟ برای مثال این متن قرمز است چون مثلا شخصی، به آن رنگ ویژه علاقه دارد، یا اینکه قرمز است بخاطر نمایش خطایی در صفحه؟ به همین جهت در Twitter Bootstrap از نام‌های مفهومی یاده شده، مانند Error استفاده می‌شود. نام‌هایی معنا دار اما بدون دقیق شدن در مورد ریز جزئیات پیاده سازی آن‌ها. در این حالت می‌توان قالب جدیدی را تدارک دید و با ارائه تعاریف جدیدی برای کلاس Error و نحوه نمایش دلخواهی را به آن اعمال نمود.
یا برای نمونه نام rightside را برای نمایش ستونی در صفحه، درنظر بگیرید. این نام بسیار ویژه است؛ اما Sematic name آن می‌تواند sidebar باشد تا بدون دقیق شدن در جزئیات پیاده سازی آن، در چپ یا راست صفحه قابل اعمال باشد.
Semantic class names کلیدهایی هستند جهت استفاده مجدد از قابلیت‌های یک فریم ورک CSS.

ب) Compositional classes
اکثر کلاس‌های Twitter Bootstrap دارای محدوده کاری کوچکی هستند و به سادگی قابل ترکیب با یکدیگر جهت رسیدن به نمایی خاص می‌باشند. برای مثال به سادگی می‌توان به یک table سه ویژگی color، hover و width برگرفته شده از Twitter Bootstrap را انتساب داد و نهایتا به نتیجه دلخواه رسید؛ بدون اینکه نگران باشیم افزودن کلاس جدیدی در اینجا بر روی سایر کلاس‌های انتساب داده شده، تاثیر منفی دارد.

ج) Conventions
برای استفاده از اکثر قابلیت‌های این فریم ورک CSS یک سری قراردادهای پیش فرضی وجود دارند. برای مثال اگر از کلاس توکار pagination به همراه یک سری ul و li استفاده کنید، به صورت خودکار یک pager شکیل ظاهر خواهد شد. یا برای مثال اگر به یک html table کلاس‌های table table-striped table-hover را انتساب دهیم، به صورت خودکار قراردادهای پیش فرض table مجموعه Twitter Bootstrap به آن اعمال شده، به همراه رنگی ساختن یک درمیان زمینه ردیف‌ها و همچنین فعال سازی تغییر رنگ ردیف‌ها با حرکت ماوس از روی آن‌ها.


طرحبندی صفحات یک سایت به کمک Twitter Bootstrap

بررسی Grid layouts

Layout به معنای طرحبندی و چیدمان محتوا در یک صفحه است. یکی از متداولترین روش‌های طرحبندی صفحات چه در حالت چاپی و چه در صفحات وب، چیدمان مبتنی بر جداول و گریدها است. از این جهت که نحوه سیلان و نمایش محتوا از چپ به راست و یا راست به چپ را به سادگی میسر می‌سازند؛ به همراه اعمال حاشیه‌های مناسب جهت قسمت‌های متفاوت محتوای ارائه شده. Grid در طرحبندی، نمایش بصری نخواهد داشت اما در ساختار صفحه وجود داشته و مباحثی مانند جهت، موقعیت و یکپارچگی و یکدستی طراحی را سبب می‌شود.
به علاوه مرورگرها و مفهوم Grid نیز به خوبی با یکدیگر سازگار هستند. در دنیای HTML و ،CSS طراحی‌ها بر اساس مفهوم ساختار مستطیلی اشیاء صورت می‌گیرد:


برای نمونه در اینجا تصویر CSS Box Model را مشاهده می‌کنید. به این ترتیب، هر المان دارای محدوده‌ای مستطیلی با طول و عرض مشخص، به همراه ویژگی‌هایی مانند Margin، Border و Padding است.
در سال‌های اولیه طراحی وب، عموما کارهای طراحی صفحات به کمک HTML Tables انجام می‌شد. اما با پخته‌تر شدن CSS، استفاده از Tables برای طراحی صفحات کمتر و کمتر گشت تا اینکه نهایتا فریم ورک‌های CSS ایی پدید آمدند تا طراحی‌های مبتنی بر CSS را با ارائه گرید‌ها، ساده‌تر کنند. مانند Blue print، 960 GS و ... Twitter Bootstrap که طراحی مبتنی بر گرید‌های CSS ایی را به مجموعه قابلیت‌های دیگر خود افزوده است.


بررسی Fixed Grids

در اینجا در صفحه layout برنامه، یک Div دربرگیرنده دو Div دیگر را مشاهده می‌کنید:
<body>
    <div>
        <div>
            <h1>
                Title Title
            </h1>
            Text Text Text Text Text Text Text Text Text Text Text Text Text Text Text Text
        </div>
        <div>
            @RenderBody()
        </div>
    </div>
</body>
اگر در این حالت صفحه را در مرورگر بررسی کنیم، تمام قسمت‌های نمایش داده شده به همین نحو از بالا به پایین صفحه قرار خواهند گرفت. اما قصد داریم این محتوا را، دو ستونی نمایش دهیم. Div به همراه Title در یک طرف صفحه و Div دربرگیرنده محتوای صفحات، در قسمتی دیگر.
برای اینکار در Twitter Bootstrap از کلاسی به نام row استفاده می‌شود که بیانگر یک ردیف است. این کلاس را به خارجی‌ترین Div موجود اعمال خواهیم کرد. در یک صفحه، هر تعداد row ایی را که نیاز باشد، می‌توان تعریف کرد. داخل این ردیف‌ها، امکان تعریف ستون‌های مختلف و حتی تعریف ردیف‌های تو در تو نیز وجود دارد. هر ردیف Twitter Bootstrap از 12 ستون تشکیل می‌شود و برای تعریف آن‌ها از کلاس span استفاده می‌گردد. در این حالت جمع اعداد ذکر شده پس از span باید 12 را تشکیل دهند.
<body>
    <div class="row">
        <div class="span7">
            <h1>
                Title Title
            </h1>
            Text Text Text Text Text Text Text Text Text Text Text Text Text Text Text Text
        </div>
        <div  class="span5">
            @RenderBody()
        </div>
    </div>
</body>
برای نمونه در اینجا کلاس row به خارجی‌ترین Div موجود اعمال شده است. دو Div داخلی هر کدام دارای کلاس‌های span ایی جهت تشکیل ستون‌های داخل این ردیف هستند. جمع اعداد بکارگرفته شده پس از آن‌ها، عدد 12 را تشکیل می‌دهد. به این ترتیب به یک طراحی دو ستونی خواهیم رسید.


در این تصویر، قسمت RenderBody کار رندر اکشن متد Index کنترلر Home برنامه را با Viewایی معادل کدهای ذیل، انجام داده است:
@{
    ViewBag.Title = "Index";
}
<h2>
    Index</h2>
<div class="hero-unit">
    <h2>@ViewBag.Message</h2>
    <p>
        This is a template to demonstrate the way to beautify the default MVC template
        using Twitter Bootstrap website. It is pretty simple and easy.</p>
    <p>
        <a href="http://asp.net/mvc" class="btn btn-primary btn-large" style="color: White;">
            To learn more about ASP.NET MVC visit &raquo;</a></p>
</div>

درک نحوه عملکرد Grid در Twitter Bootstrap

در مثال ذیل 5 ردیف را مشاهده می‌کنید:
    <div class="row">
        <div class="span1">1</div>
        <div class="span1">1</div>
        <div class="span1">1</div>
        <div class="span1">1</div>
        <div class="span1">1</div>
        <div class="span1">1</div>
        <div class="span1">1</div>
        <div class="span1">1</div>
        <div class="span1">1</div>
        <div class="span1">1</div>
        <div class="span1">1</div>
        <div class="span1">1</div>
    </div>
    <div class="row">
        <div class="span3">3</div>
        <div class="span4">4</div>
        <div class="span5">5</div>
    </div>
    <div class="row">
        <div class="span5">5</div>
        <div class="span7">7</div>
    </div>
    <div class="row">
        <div class="span3">3</div>
        <div class="span7 offset2">7 offset 2</div>
    </div>
    <div class="row">
        <div class="span12">12</div>
    </div>
در هر ردیف، امکان استفاده از حداکثر 12 ستون وجود دارد که یا می‌توان مانند ردیف اول، 12 ستون را ایجاد کرد و یا مانند ردیف دوم، سه ستون با عرض‌های متفاوت (و یا هر ترکیب دیگری که به جمع 12 برسد).
در ستون چهارم، از کلاس offset نیز استفاده شده است. این مورد سبب می‌شود ستون جاری به تعدادی که مشخص شده است به سمت چپ (با توجه به استفاده از حالت RTL در اینجا) رانده شود و سپس ترسیم گردد.
یا اینکه می‌توان مانند ردیف آخر، یک ستون را به عرض 12 که در حقیقت 940 پیکسل است، ترسیم نمود.
برای اینکه بتوانیم این گرید تشکیل شده و همچنین ستون‌ها را بهتر مشاهده کنیم، به فایل style.css سایت، تنظیم زیر را اضافه کنید:
[class*="span"]
{
background-color: lightblue;
text-align: center;
margin-top: 15px;
}


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


یک نکته
اگر نمی‌خواهید که چنین رفتار واکنش‌گرایی بروز کند، نیاز است کلیه ردیف‌ها را در div ایی با کلاسی به نام container محصور کنید.
به این ترتیب ابتدا گرید نمایش داده شده، در میانه صفحه ظاهر خواهد شد (پیشتر از سمت راست شروع شده بود). همچنین دیگر با کوچک و بزرگ شدن اندازه مرورگر، ستون‌ها به شکل یک پشته بر روی هم قرار نخواهند گرفت. (اگر پس از این تنظیم، چنین قابلیتی را مشاهده نکردید و هنوز هم طراحی، واکنشگرا بود، تعریف bootstrap-responsive.css را نیاز است برای آزمایش، از هدر صفحه حذف کنید)


بررسی Fluid Grids

به گرید قسمت قبل از این جهت Fixed Grid گفته می‌شود که عرض هر span آن با یک عدد مشخص تعیین گشته است. اما در حالت Fluid Grid، عرض هر span برحسب درصد تعیین می‌شود. بکارگیری درصد در اینجا به معنای امکان تغییر عرض یک ستون بر اساس عرض جاری Container آن است. در اینجا span12 دارای عرض 100 درصد خواهد بود.
در مثال قبل، برای استفاده از Fluid grids، تنها کافی است هرجایی کلاسی مساوی row وجود دارد، به row-fluid تغییر کند. همچنین کلاس container را به container-fluid تغییر دهید.
برای آزمایش آن، اندازه و عرض نمایشی مرورگر خود را تغییر دهید. اینبار مشاهده خواهید کرد که برخلاف حالت Fixed Grid، عرض ستون‌ها به صورت خودکار کم و زیاد می‌شوند. این مورد بر روی محتوای قرار گرفته در این ستون‌ها نیز تاثیر گذار است. برای مثال اگر یک تصویر را در حالت Fluid grid در ستونی قرار دهید، با تغییر عرض مرورگر، اندازه این تصویر نیز تغییر خواهند کرد؛ اما در حالت Fixed Grid خیر.
حالت Fluid، شیوه متداول استفاده از bootstrap در اکثر سایت‌های مهمی است که تابحال از این فریم ورک CSS استفاده کرده‌اند.



مروری بر طراحی واکنشگرا یا Responsive

این روزها تعدادی از کاربران، با استفاده از ابزارهای موبایل و تبلت‌ها از وب سایت‌ها بازدید می‌کنند. هر کدام از این‌ها نیز دارای اندازه نمایشی متفاوتی می‌باشند. بنابراین نیاز خواهد بود تا حالت بهینه‌ای را جهت اینگونه وسایل نیز طراحی نمود. حالت بهینه در اینجا به معنای قابل خواندن بودن متون، امکان استفاده از لینک‌های ورود به صفحات مختلف و همچنین حذف اسکرول و مباحث زوم جهت مشاهده صفحات است.
یکی از ویژگی‌های CSS به نام media چنین قابلیتی را فراهم می‌سازد. برای نمونه قسمتی از فایل bootstrap-responsive.css دارای چنین تعاریفی است:
@media (min-width: 768px) and (max-width: 979px) {
    .hidden-desktop {
        display: inherit !important;
    }
توسط این تنظیمات، شیوه نامه تعیین شده، تنها به صفحاتی با اندازه‌هایی بین 768 تا 979 پیکسل اعمال می‌شود (تعدادی از تبلت‌ها برای نمونه).
Bootstrap برای مدیریت اندازه‌های مختلف وسایل موبایل، شیوه‌نامه‌های خاصی را تدارک دیده است که از اندازه px480 و یا کمتر، تا px1200 و یا بیشتر را پوشش می‌دهد.
به این ترتیب با اندازه px940 که پیشتر در مورد آن بحث شد، اندازه span12 به صورت خودکار به اندازه‌های متناسب با صفحات نمایشی کوچکتر تنظیم می‌گردد. همچنین برای اندازه‌های صفحات کوچکتر از 768px به صورت خودکار از Fluid columns استفاده می‌گردد.
تنها کاری که برای اعمال این قابلیت باید صورت گیرد، افزودن تعاریف فایل bootstrap-responsive.css به هدر صفحه است که در قسمت قبل انجام گردید. این فایل باید پس از فایل اصلی bootstrap.css اضافه شود.


کلاس‌های کمکی طراحی واکنشگرا

Bootstrap شامل تعدادی کلاس کمکی در فایل bootstrap-responsive.css خود می‌باشد شامل visible-phone، visible-tablet و visible-desktop به علاوه hidden-phone، hidden-tablet و hidden-desktop. به این ترتیب می‌توان محتوای خاصی را جهت وسایل ویژه‌ای نمایان یا مخفی ساخت.
برای مثال محتوای مشخص شده با کلاس hidden-desktop، در اندازه وسایل دسکتاپ نمایش داده نخواهد شد.
برای آزمایش آن، شش div را با کلاس‌های یاد شده و محتوایی دلخواه ایجاد کرده و سپس اندازه عرض مرورگر را تغییر دهید تا بهتر بتوان مخفی یا نمایان ساختن محتوا را بر اساس اندازه صفحه جاری درک کرد.
یکی از کاربردهای این قابلیت، قرار دادن تبلیغاتی با اندازه‌های تصاویری مشخص برای وسایل مختلف است؛ بجای اینکه منتظر شویم تا Fluid layout اندازه تصاویر را به صورت خودکار کوچک یا بزرگ کند، که الزاما بهترین کیفیت را حاصل نخواهد ساخت.
    <div class="container-fluid">
        <div class="row-fluid">
            <div class="span4">
                <div class="visible-phone">
                    visible-phone</div>
            </div>
            <div class="span4">
                <div class="visible-tablet">
                    visible-tablet</div>
            </div>
            <div class="span4">
                <div class="visible-desktop">
                    visible-desktop</div>
            </div>
        </div>
    </div>
    <div class="container-fluid">
        <div class="row-fluid">
            <div class="span4">
                <div class="hidden-phone">
                    hidden-phone</div>
            </div>
            <div class="span4">
                <div class="hidden-tablet">
                    hidden-tablet</div>
            </div>
            <div class="span4">
                <div class="hidden-desktop">
                    hidden-desktop</div>
            </div>
        </div>
    </div>

مطالب
بررسی تغییرات Blazor 8x - قسمت چهارم - معرفی فرم‌های جدید تعاملی
در قسمت قبل مشاهده کردیم که چگونه می‌توان کل برنامه را به صورت سراسری، تعاملی کرد تا بتوان توسط آن، Blazor Server سنتی را شبیه سازی نمود؛ اما ... آیا واقعا نیاز است چنین کاری را انجام دهیم؟! چون در این صورت از قابلیت‌‌های جدید SSR به همراه Blazor 8x محروم می‌شویم. اگر کل قابلیت‌های تعاملی مورد نیاز ما در حد یک فرم و ارسال اطلاعات آن به سمت سرور است، می‌توان در Blazor 8x هنوز هم در همان حالت SSR قرار گرفت و از فرم‌های جدید تعاملی آن استفاده کرد تا برای پردازش چنین مواردی، نیازی به برقراری اتصال دائم SignalR نباشد. جزئیات نحوه‌ی کار با اینگونه فرم‌ها را در ادامه بررسی می‌کنیم.


امکان تعریف HTML Forms استاندارد در Blazor 8x

فرم‌های استاندارد HTML، پیش از ظهور جاوااسکریپت و SPAها وجود داشتند (دقیقا همان زمانیکه که فقط مفهوم SSR وجود خارجی داشت) و هنوز هم جزء مهمی از اغلب برنامه‌های وب را تشکیل می‌دهند. با ارائه‌ی دات نت 8 و قابلیت server side rendering آن، کامپوننت‌های برنامه، فقط یکبار در سمت سرور رندر شده و HTML ساده‌ی آن‌ها به سمت مرورگر کاربر بازگشت داده می‌شود. در این حالت، فرم‌های استاندارد HTML، امکان دریافت ورودی‌های کاربر و ارسال داده‌های آن‌ها را به سمت سرور میسر می‌کنند (چون دیگر خبری از اتصال دائم SignalR نیست و باید اطلاعات را به همان نحو استاندارد پروتکل HTTP، به سمت سرور Post کرد). در دات نت 8، دو راه‌حل برای کار با فرم‌ها در برنامه‌های Blazor وجود دارد: استفاده از EditForm خود Blazor و یا استفاده از HTML forms استاندارد و ساده، به همان نحوی که بوده و هست.


روش کار با EditForm در برنامه‌های Blazor SSR

البته ما قصد استفاده از فرم‌های ساده‌ی HTML را در اینجا نداریم و ترجیح می‌دهیم که از همان EditForm استفاده کنیم. EditForms در Blazor بسیار مفید بوده و امکان بایند خواص یک مدل را به اجزای مختلف ورودی‌های تعریف شده‌ی در آن میسر می‌کند و همچنین قابلیت‌هایی مانند اعتبارسنجی و امثال آن‌را نیز به همراه دارد (اطلاعات بیشتر). اما چگونه می‌توان از این امکان در برنا‌مه‌های Blazor SSR نیز استفاده کرد؟
برای این منظور، ابتدا مثالی را به صورت زیر تکمیل می‌کنیم (که بر اساس قالب dotnet new blazor --interactivity Server تهیه شده) و سپس توضیحات آن ارائه خواهد شد:

الف) تهیه یک مدل برای تعریف محل‌های مرتبط با یک سفارش در فایل Models/OrderPlace.cs

using System.ComponentModel.DataAnnotations;

namespace Models;

public record OrderPlace
{
    public Address BillingAddress { get; set; } = new();
    public Address ShippingAddress { get; set; } = new();
}

public class Address
{
    [Required] public string Name { get; set; } = default!;
    public string? AddressLine1 { get; set; }
    public string? AddressLine2 { get; set; }
    public string? City { get; set; }
    [Required] public string PostCode { get; set; } = default!;
}

ب) تهیه‌ی یک کامپوننت Editor برای دریافت اطلاعات آدرس فوق در فایل Components\Pages\Chekout\AddressEntry.razor

@inherits Editor<Models.Address>

<div>
    <label>Name</label>
    <InputText @bind-Value="Value.Name"/>
</div>
<div>
    <label>Address 1</label>
    <InputText @bind-Value="Value.AddressLine1"/>
</div>
<div>
    <label>Address 2</label>
    <InputText @bind-Value="Value.AddressLine2"/>
</div>
<div>
    <label>City</label>
    <InputText @bind-Value="Value.City"/>
</div>
<div>
    <label>Post Code</label>
    <InputText @bind-Value="Value.PostCode"/>
</div>

ج) استفاده از مدل و ادیتور فوق در یک EditForm تغییر یافته برای کار با برنامه‌های Blazor SSR در فایل Components\Pages\Chekout\Checkout.razor

@page "/checkout"

@using Models
@if (!_submitted && PlaceModel != null)
{
    <EditForm Model="PlaceModel" method="post" OnValidSubmit="SubmitOrder" FormName="checkout">
        <DataAnnotationsValidator/>

        <h4>Bill To:</h4>
        <AddressEntry @bind-Value="PlaceModel.BillingAddress"/>

        <h4>Ship To:</h4>
        <AddressEntry @bind-Value="PlaceModel.ShippingAddress"/>

        <button type="submit">Submit</button>
        <ValidationSummary/>
    </EditForm>
}

@if (_submitted && PlaceModel != null)
{
    <div>
        <h2>Order Summary</h2>

        <h3>Shipping To:</h3>
        <dl>
            <dt>Name</dt>
            <dd>@PlaceModel.BillingAddress.Name</dd>
            <dt>Address 1</dt>
            <dd>@PlaceModel.BillingAddress.AddressLine1</dd>
            <dt>Address 2</dt>
            <dd>@PlaceModel.BillingAddress.AddressLine2</dd>
            <dt>City</dt>
            <dd>@PlaceModel.BillingAddress.City</dd>
            <dt>Post Code</dt>
            <dd>@PlaceModel.BillingAddress.PostCode</dd>
        </dl>
    </div>
}

@code {
    bool _submitted;

    [SupplyParameterFromForm]
    public OrderPlace? PlaceModel { get; set; }

    protected override void OnInitialized()
    {
        PlaceModel ??= GetOrderPlace();
    }

    private void SubmitOrder()
    {
        _submitted = true;
    }

    private static OrderPlace GetOrderPlace() =>
        new()
        {
            BillingAddress = new Address
                             {
                                 PostCode = "12345",
                                 Name = "Test 1",
                             },
            ShippingAddress = new Address
                              {
                                  PostCode = "67890",
                                  Name = "Test 2",
                              },
        };

}
توضیحات:
باید بخاطر داشت که این فرم بر اساس حالت Server Side Rendering در اختیار مرورگر کاربر قرار می‌گیرد. یعنی برای بار اول، یک HTML خالص، در سمت سرور بر اساس اطلاعات آن تهیه شده و بازگشت داده می‌شود و زمانیکه به کاربر نمایش داده شد، دیگر برخلاف Blazor Server پیشین، اتصال SignalR ای وجود ندارد تا قابلیت‌های تعاملی آن‌را مدیریت کند. در این حالت اگر به view source صفحه‌ی جاری رجوع کنیم، چنین خروجی قابل مشاهده‌است:
<form method="post">
   <input type="hidden" name="_handler" value="checkout" />
   <input type="hidden" name="__RequestVerificationToken" value="CfDxxx" />
.
.
.
   <button type="submit">Submit</button>
</form>
یعنی زمانیکه این فرم به سمت سرور ارسال می‌شود، همان HTTP POST استاندارد رخ می‌دهد و برای اینکار، نیازی به اتصال وب‌سوکت SignalR ندارد.
این EditForm تعریف شده، دو قسمت اضافه‌تر را نسبت به EditFormهای نگارش‌های قبلی Blazor دارد:
<EditForm Model="PlaceModel" method="post" OnValidSubmit="SubmitOrder" FormName="checkout">
در اینجا نوع HTTP Method ارسال فرم، مشخص شده و همچنین یک FormName نیز تعریف شده‌است. علت اینجا است که Blazor باید بتواند اطلاعات POST شده و دریافتی در سمت سرور را به کامپوننت متناظری نگاشت کند؛ به همین جهت این نامگذاری، ضروری است.
همانطور که در نحوه‌ی تعریف فرم HTML ای فوق مشخص است، فیلد مخفی handler_، کار متمایز ساختن این فرم را به عهده داشته و از مقدار آن در سمت سرور جهت یافتن کامپوننت متناظر، استفاده خواهد شد.

همچنین برای دریافت و پردازش این اطلاعات در سمت سرور، تنها کافی است خاصیت مرتبط با آن‌را با ویژگی SupplyParameterFromForm مزین کنیم:
[SupplyParameterFromForm]
public OrderPlace? PlaceModel { get; set; }

جریان کاری این فرم به صورت خلاصه به نحو زیر است (که در آن متد OnInitialized دوبار فراخوانی می‌شود و باید به آن دقت داشت):
- در بار اول نمایش این صفحه (با فراخوانی مسیر /checkout در مرورگر)، متد OnInitialized فراخوانی شده و در آن، مقدار شیء PlaceModel نال است.
- بنابراین به متد GetOrderPlace مراجعه کرده و اطلاعاتی را دریافت می‌کند؛ برای مثال، این اطلاعات را از سرویسی می‌خواند.
- پس از پایان هر روال رخدادگردانی در Blazor، در پشت صحنه به صورت خودکار، متد تغییر حالت جاری کامپوننت (متد StateHasChanged) هم فراخوانی می‌شود. این فراخوانی خودکار، باعث رندر مجدد UI آن بر اساس اطلاعات جدید خواهد شد. یعنی قسمت‌های نمایش فرم و نمایش اطلاعات ارسالی، یکبار ارزیابی شده و در صورت برقراری شرط‌ها، نمایش داده می‌شوند.
- در ادامه، کاربر فرم را پر کرده و به سمت سرور POST می‌کند.
- پیش از هر رخ‌دادی، خواص شیء PlaceModel به علت مزین بودن به ویژگی SupplyParameterFromForm، بر اساس اطلاعات ارسالی به سرور، مقدار دهی می‌شوند.
- سپس متد OnInitialized فراخوانی شده و چون اینبار مقدار PlaceModel نال نیست، به متد GetOrderPlace جهت دریافت مقادیر ابتدایی خود مراجعه نمی‌کند. سطر تعریف شده‌ی در متد OnInitialized فقط زمانی سبب مقدار دهی شیء PlaceModel می‌شود که مقدار این شیء، نال باشد (یعنی فقط در اولین بار نمایش صفحه)؛ اما اگر این مقدار توسط پارامتر مزین شده‌ی به SupplyParameterFromForm به علت ارسال داده‌های فرم به سرور، مقدار دهی شده باشد، دیگر به منبع داده‌ی ابتدایی رجوع نمی‌کند.
- چون متد رخ‌دادگردان OnInitialized فراخوانی شده، پس از پایان آن (و فراخوانی خودکار متد StateHasChanged در انتهای آن)، یکبار دیگر کار رندر UI فرم جاری بر اساس اطلاعات جدید، انجام خواهد شد.
- اکنون است که پس از طی این رخ‌دادها، متد رویدادگردان SubmitOrder فراخوانی می‌شود. یعنی زمانیکه این متد فراخوانی می‌شود، شیء PlaceModel بر اساس اطلاعات رسیده‌ی از طرف کاربر، مقدار دهی شده و آماده‌ی استفاده است (برای مثال آماده‌ی ذخیره سازی در بانک اطلاعاتی؛ با فراخوانی سرویسی در اینجا).
- پس از پایان فراخوانی متد رویدادگردان SubmitOrder، به علت تغییر حالت کامپوننت (و فراخوانی خودکار متد StateHasChanged در انتهای آن)، یکبار دیگر نیز کار رندر UI فرم جاری بر اساس اطلاعات جدید انجام خواهد شد. یعنی اینبار قسمت Order Summary نمایش داده می‌شود.


مدیریت تداخل نام‌های HTML Forms در Blazor 8x SSR

تمام فرم‌هایی که به این صورت در برنامه‌های Blazor SSR مدیریت می‌شوند، باید دارای نام منحصربفردی که توسط خاصیت FormName مشخص می‌شود، باشند. برای جلوگیری از این تداخل نام‌ها، کامپوننت جدیدی به نام FormMappingScope معرفی شده‌است که نمونه‌ای از آن‌را در فایل فرضی Components\Pages\Chekout\CheckoutForm.razor تعریف شده‌ی به صورت زیر مشاهده می‌کنید:

@page "/checkout"

<FormMappingScope Name="store-checkout">
    <CheckoutForm />
</FormMappingScope>
در اینجا ابتدا ویژگی page@ کامپوننت CheckoutForm را حذف کرده و آن‌را تبدیل به یک کامپوننت معمولی بدون قابلیت مسیریابی کرده‌ایم. سپس آن‌را توسط کامپوننت FormMappingScope در صفحه‌ای دیگر معرفی و محصور می‌کنیم.
اکنون اگر برنامه را اجرا کرده و خروجی HTML آن‌را بررسی کنیم، به فرم زیر خواهیم رسید:
<form method="post">
   <input type="hidden" name="_handler" value="[store-checkout]checkout" />
   <input type="hidden" name="__RequestVerificationToken" value="CfDxxxxx" />
.
.
.
   <button type="submit">Submit</button>
</form>
همانطور که ملاحظه می‌کنید، اینبار مقدار فیلد مخفی handler_ که کار متمایز ساختن این فرم را به عهده دارد و از آن در سمت سرور جهت یافتن کامپوننت متناظری استفاده می‌شود، با حالتی‌که از کامپوننت FormMappingScope استفاده نشده بود، متفاوت است و نام FormMappingScope را در ابتدای خود به همراه دارد تا به این نحو، از تداخل احتمالی نام‌های فرم‌ها جلوگیری شود.

یک نکته: اگر به تگ‌های فرم HTML ای فوق دقت کنید، به همراه یک anti-forgery token نیز هست که کار تولید و مدیریت آن، به صورت خودکار صورت می‌گیرد و میان‌افزاری نیز برای آن طراحی شده که در فایل Program.cs برنامه، به صورت app.UseAntiforgery بکارگرفته شده‌است.


یک نکته: در Blazor 8x SSR می‌توان بجای EditForm، از همان HTML form متداول هم استفاده کرد

اگر بخواهیم بجای استفاده از EditForm، از فرم‌های استاندارد HTML هم در حالت SSR استفاده کنیم، این کار میسر بوده و روش کار به صورت زیر است:
<form method="post" @onsubmit="SaveData" @formname="MyFormName">
    <AntiforgeryToken />

    <InputText @bind-Value="Name" />

    <button>Submit</button>
</form>
در اینجا ذکر دایرکتیوهای onsubmit@ و formname@ را (شبیه به خواص و رویدادگردان‌های مشابهی در EditForm) به همراه ذکر صریح کامپوننت AntiforgeryToken، مشاهده می‌کنید. در حین استفاده از EditForm، نیازی به درج این کامپوننت نیست و به صورت خودکار اضافه می‌شود.


پردازش فرم‌های GET در Blazor 8x

در حالتی‌که از فرم‌های استاندارد HTML ای استفاده می‌شود، ممکن است method فرم، بجای post، حالت get باشد که نتایج آن به صورت کوئری استرینگ در نوار آدرس مرورگر ظاهر می‌شوند؛ مانند جستجوی گوگل که اشخاص می‌توانند کوئری استرینگ و لینک نهایی را به اشتراک بگذارند. روش پردازش یک چنین فرم‌هایی به صورت زیر است:
@page "/"

<form method="GET">
    <input type="text" name="q"/>
    <button type="submit">Search</button>
</form>


@code {
    [SupplyParameterFromQuery(Name="q")]
    public string SearchTerm { get; set; }
    
    protected override async Task OnInitializedAsync()
    {
       // do something with the search term
    }
}
در اینجا از ویژگی SupplyParameterFromQuery برای دریافت کوئری استرینگ استفاده شده و چون نام پارامتر تعریف شده با نام input فرم یکی نیست، این نام به صورت صریحی توسط خاصیت Name آن مشخص شده‌است.


یک ابتکار! تعاملی کردن قسمتی از صفحه بدون فعالسازی کامل Blazor Server و یا Blazor WASM کامل

این دکمه‌ی قرار گرفته‌ی در یک صفحه‌ی SSR را ملاحظه کنید:
<button class="nav-link border-0" @onclick="BeginSignOut">Log out</button>
در اینجا می‌خواهیم، اگر کاربری بر روی آن کلیک کرد، روال رویدادگردان منتسب به onclick اجرا شود. اما ... اگر در این حالت برنامه را اجرا کرده و بر روی دکمه‌ی Log out کلیک کنیم، هیچ اتفاقی رخ نمی‌دهد! یعنی روال رویدادگران BeginSignOut اصلا اجرا نمی‌شود. علت اینجا است که صفحات SSR، در نهایت یک static HTML بیشتر نیستند و فاقد قابلیت‌های تعاملی، مانند واکنش نشان دادن به کلیک بر روی یک دکمه هستند. برای رفع این مشکل یا می‌توان این قسمت از صفحه را کاملا تعاملی کرد که روش انجام آن‌را در قسمت‌های بعدی با جزئیات کاملی بررسی می‌کنیم و یا ... می‌توان این دکمه را داخل یک فرم جدید تعاملی به صورت زیر محصور کرد:
<EditForm Context="ctx" FormName="LogoutForm" method="post" Model="@Foo" OnValidSubmit="BeginSignOut">
     <button type="submit" class="nav-link border-0">Log out</button>
</EditForm>

@code{
    [SupplyParameterFromForm(Name = "LogoutForm")]
    public string? Foo {  get; set; }

    protected override void OnInitialized() => Foo = "";

    async Task BeginSignOut()
    {
        // TODO: SignOutAsync();
        // TODO: NavigateTo("/authentication/logout");
    }
}
در این حالت چون این فرم، از نوع فرم‌های جدید تعاملی است، برای پردازش آن نیازی به اتصال دائم SignalR و یا فعالسازی یک وب‌اسمبلی نیست. پردازش آن بر اساس استاندارد HTTP Post و فرم‌های آن، صورت گرفته و به این ترتیب می‌توان عملکرد onclick@ کاملا تعاملی را با یک فرم تعاملی جدید، شبیه سازی کرد.


یک نکته: می‌توان حالت post-back مانند فرم‌های تعاملی Blazor 8x را تغییر داد.

به همراه ویژگی‌های جدید مرتبط با صفحات SSR، ویژگی هدایت بهبودیافته هم وجود دارد که جزئیات بیشتر آن‌را در قسمت‌های بعدی این سری بررسی می‌کنیم. برای نمونه اگر مثال این قسمت را اجرا کنید، فرم آن به همراه یک post-back مانند به سمت سرور است که کاملا قابل احساس است؛ این رفتار هرچند استاندارد است، اما بی‌شباهت به برنامه‌های MVC ، Razor pages و یا وب‌فرم‌ها نیست و با فرم‌های بی‌صدا و سریع نگارش‌های قبلی Blazor متفاوت است. در Blazor8x می‌توان این نوع ارسال اطلاعات را Ajax ای هم کرد که به آن enhanced navigation می‌گویند. برای اینکار فقط کافی است ویژگی Enhance را به تگ EditForm اضافه کرد و یا ویژگی جدید data-enhance را به تگ‌های فرم‌های استاندارد HTML ای افزود. پس از آن اگر برنامه را اجرا کنیم، دیگر یک post-back استاندارد وب‌فرم‌ها مشاهده نمی‌شود و رفتار این صفحه بسیار سریع، نرم و روان خواهد بود.
<EditForm Model="PlaceModel" method="post" OnValidSubmit="SubmitOrder" FormName="checkout" Enhance>
در اینجا تنها تغییری که حاصل شده، اضافه شدن ویژگی Enhance به المان EditForm است. این ویژگی به صورت پیش‌فرض غیرفعال است که جزئیات بیشتر آن‌را در قسمت‌های بعدی بررسی خواهیم کرد.


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید: Blazor8x-Server-Normal.zip