مطالب
بررسی نکات دریافت فایل‌های حجیم توسط HttpClient
HttpClient به عنوان جایگزینی برای HttpWebRequest API قدیمی، به همراه NET 4.5. ارائه شد و هدف آن یکپارچه کردن پیاده سازی‌های متفاوت موجود به همراه ارائه را‌ه‌حلی چندسکویی است که از WPF/UWP ، ASP.NET تا NET Core. و iOS/Android را نیز پشتیبانی می‌کند. تمام قابلیت‌های جدید پروتکل HTTP مانند HTTP/2 نیز از این پس تنها به همراه این API ارائه می‌شوند.
در مطلب «روش استفاده‌ی صحیح از HttpClient در برنامه‌های دات نت» با روش استفاده‌ی تک وهله‌ای آن آشنا شدیم. در این مطلب نکات ویژه‌ی دریافت فایل‌های حجیم آن‌را بررسی خواهیم کرد. بدون توجه به این نکات، یا OutOfMemoryException را دریافت خواهید کرد و یا پیش از پایان کار، با خطای Timeout این پروسه به پایان خواهد رسید.


مشکل اول: نیاز به تغییر Timeout پیش فرض

فرض کنید می‌خواهیم فایل حجیمی را با تنظیمات پیش‌فرض HttpClient دریافت کنیم:
using System;
using System.Net.Http;
using System.Threading.Tasks;

namespace HttpClientTips.LargeFiles
{
    class Program
    {
        private static readonly HttpClient _client = new HttpClient();

        static async Task Main(string[] args)
        {
            var bytes = await DownloadLargeFileAsync();
        }

        public static async Task<byte[]> DownloadLargeFileAsync()
        {
            Console.WriteLine("Downloading a 4K content - too much bytes.");
            var response = await _client.GetAsync("http://downloads.4ksamples.com/downloads/sample-Elysium.2013.2160p.mkv");
            var bytes = await response.Content.ReadAsByteArrayAsync();
            return bytes;
        }
    }
}
در این حالت باتوجه به اینکه Timeout پیش‌فرض HttpClient به 100 ثانیه تنظیم شده‌است، اگر سرعت دریافت بالایی را نداشته باشید و نتوانید این فایل را پیش از 2 دقیقه دریافت کنید، برنامه با استثنای TaskCancelledException متوقف خواهد شد.
بنابراین اولین تغییر مورد نیاز، تنظیم صریح Timeout آن است:
private static readonly HttpClient _client = new HttpClient
{
    Timeout = Timeout.InfiniteTimeSpan
};


مشکل دوم: دریافت استثنای OutOfMemoryExceptions

روش دریافت پیش‌فرض اطلاعات توسط HttpClient، نگهداری و بافر تمام آن‌ها در حافظه‌ی سیستم است. این روش برای اطلاعات کم حجم، مشکلی را به همراه نخواهد داشت. بنابراین در حین دریافت فایل‌های چندگیگابایتی با آن، حتما با استثنای OutOfMemoryException مواجه خواهیم شد.
namespace HttpClientTips.LargeFiles
{
    class Program
    {
        private static readonly HttpClient _client = new HttpClient
        {
            Timeout = Timeout.InfiniteTimeSpan
        };

        static async Task Main(string[] args)
        {
            await DownloadLargeFileAsync();
        }

        public static async Task DownloadLargeFileAsync()
        {
            Console.WriteLine("Downloading a 4K content. too much bytes.");
            var response = await _client.GetAsync("http://downloads.4ksamples.com/downloads/sample-Elysium.2013.2160p.mkv");
            using (var streamToReadFrom = await response.Content.ReadAsStreamAsync())
            {
                string fileToWriteTo = Path.GetTempFileName();
                Console.WriteLine($"Save path: {fileToWriteTo}");
                using (var streamToWriteTo = File.Open(fileToWriteTo, FileMode.Create))
                {
                    await streamToReadFrom.CopyToAsync(streamToWriteTo);
                }
            }
        }
    }
}
در این حالت برای رفع مشکل، از متد ReadAsStreamAsync آن استفاده می‌کنیم. به این ترتیب بجای یک آرایه‌ی بزرگ از بایت‌ها، با استریمی از آن‌ها سر و کار داشته و به این صورت مشکل مواجه شدن با کمبود حافظه برطرف می‌شود.
مشکل: در این حالت اگر برنامه را اجرا کنید، تا پایان کار متد DownloadLargeFileAsync، حجم فایل دریافتی تغییری نخواهد کرد. یعنی هنوز هم کل فایل در حافظه بافر می‌شود و سپس استریم آن در اختیار FileStream نهایی برای نوشتن قرار خواهد گرفت.
علت این‌جا است که متد client.GetAsync تا زمانیکه کل Response ارسالی از طرف سرور خوانده نشود (headers + content)، عملیات را سد کرده و منتظر می‌ماند. بنابراین با این تغییرات عملا به نتیجه‌ی دلخواه نرسیده‌ایم.


دریافت اطلاعات Header و سپس استریم کردن Content

چون متد client.GetAsync تا دریافت کامل headers + content متوقف می‌ماند، می‌توان به آن اعلام کرد تنها هدر را به صورت کامل دریافت کن و سپس باقیمانده‌ی عملیات دریافت بدنه‌ی Response را به صورت Stream در اختیار ادامه‌ی برنامه قرار بده. برای اینکار نیاز است پارامتر HttpCompletionOption را تکمیل کرد:
var response = await _client.GetAsync(
                "http://downloads.4ksamples.com/downloads/sample-Elysium.2013.2160p.mkv",
                HttpCompletionOption.ResponseHeadersRead);
پارامتر HttpCompletionOption.ResponseHeadersRead به متد GetAsync اعلام می‌کند که پس از خواندن هدر Response، ادامه‌ی عملیات را در اختیار سطرهای بعدی کد قرار بده و عملیات را تا پایان خواندن کامل Response در حافظه، متوقف نکن.


مشکل سوم: برنامه در دریافت سومین فایل از یک سرور هنگ می‌کند.

تعداد اتصالات همزمانی را که می‌توان توسط HttpClient به یک سرور گشود، محدود هستند. برای مثال این عدد در Full .NET Framework مساوی 2 است. بنابراین اگر اتصال سوم موازی را شروع کنیم، چون Timeout را به بی‌نهایت تنظیم کرده‌ایم، این قسمت از برنامه هیچگاه تکمیل نخواهد شد.
روش تنظیم تعداد اتصالات مجاز به یک سرور:
- در Full .NET Framework با تنظیم خاصیت ServicePointManager.DefaultConnectionLimit است که به 2 تنظیم شده‌است.
- این مورد در NET Core. توسط پارامتر HttpClientHandler و خاصیت MaxConnectionsPerServer آن تنظیم می‌شود:
private static readonly HttpClientHandler _handler = new HttpClientHandler
{
    MaxConnectionsPerServer = int.MaxValue, // default for .NET Core
    UseDefaultCredentials = true
};
private static readonly HttpClient _client = new HttpClient(_handler)
{
    Timeout = Timeout.InfiniteTimeSpan
};
البته مقدار پیش‌فرض آن int.MaxValue است که نسبت به حالت Full .NET Framework عدد بسیار بزرگتری است.
نظرات مطالب
احراز هویت و اعتبارسنجی کاربران در برنامه‌های Angular - قسمت ششم - کار با منابع محافظت شده‌ی سمت سرور
یک نکته‌ی تکمیلی: بهبود کنترل نمایش و مخفی سازی قسمت‌های مختلف

یک روش «نمایش و یا مخفی کردن قسمت‌های مختلف صفحه بر اساس نقش‌های کاربر وارد شده‌ی به سیستم» را در مطلب جاری مطالعه کردید. روش دیگر اینکار، تهیه‌ی یک دایرکتیو و سپس اعمال آن به المان‌های مختلف صفحه است. به علاوه با توجه به اینکه Auth Service ما رخ‌داد خروج کاربر را نیز گزارش می‌کند، روش ارائه شده‌ی در اینجا نیاز به اندکی بهبود هم دارد:
  ngOnInit() {
    this.isAdmin = this.authService.isAuthUserInRole("Admin");
    this.isUser = this.authService.isAuthUserInRole("User");
  }
نتیجه‌ی این بررسی، حتی با خروج کاربر نیز تغییری نخواهد کرد و ثابت است. بنابراین بهتر است مشترک this.authService.authStatus شد و نسبت به رخ‌دادهای صادر شده‌ی توسط سرویس اعتبارسنجی، همانند کامپوننت هدر، واکنش نشان داد.
برای پیاده سازی آن و همچنین کپسوله سازی این عملیات تکراری، دایرکتیو جدیدی را در مسیر src\app\shared\directives\is-visible-for-auth-user.directive.ts ایجاد می‌کنیم:
import { Directive, ElementRef, Input, OnDestroy, OnInit } from "@angular/core";
import { Subscription } from "rxjs/Subscription";

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

@Directive({
  selector: "[isVisibleForAuthUser]"
})
export class IsVisibleForAuthUserDirective implements OnInit, OnDestroy {

  private subscription: Subscription;

  @Input() isVisibleForRoles: string[];

  constructor(private elem: ElementRef, private authService: AuthService) { }

  ngOnDestroy(): void {
    this.subscription.unsubscribe();
  }

  ngOnInit(): void {
    this.subscription = this.authService.authStatus$.subscribe(status => this.changeVisibility(status));
    this.changeVisibility(this.authService.isAuthUserLoggedIn());
  }

  private changeVisibility(status: boolean) {
    const isInRoles = !this.isVisibleForRoles ? true : this.authService.isAuthUserInRoles(this.isVisibleForRoles);
    this.elem.nativeElement.style.display = isInRoles && status ? "" : "none";
  }
}
در اینجا علاوه بر بررسی isAuthUserLoggedIn و isAuthUserInRoles، نسبت به تغییرات this.authService.authStatus نیز واکنش نشان داده می‌شود.

سپس تعریف آن‌را به قسمت‌های declarations و exports مربوط به SharedModule اضافه می‌کنیم:
import { IsVisibleForAuthUserDirective } from "./directives/is-visible-for-auth-user.directive";

@NgModule({
  declarations: [
    IsVisibleForAuthUserDirective
  ],
  exports: [
    IsVisibleForAuthUserDirective
  ]
})
export class SharedModule {}

اکنون ماژول Dashboard برای استفاده‌ی از این امکانات تنها کافی است SharedModule را دریافت کند (یا هر ماژول دیگری نیز به همین ترتیب است):
import { SharedModule } from "../shared/shared.module";

@NgModule({
  imports: [
    SharedModule
  ]
})
export class DashboardModule { }

پس از آن برای مخفی سازی یک المان از دید کاربران وارد نشده‌ی به سیستم، فقط کافی است دایرکتیو isVisibleForAuthUser را به المان اعمال کنیم:
<div class="alert alert-info" isVisibleForAuthUser>
      Is-Visible-For-AuthUser
</div>
و یا اگر نیاز به اعمال نقش‌ها نیز وجود داشت می‌توان از خاصیت isVisibleForRoles آن استفاده کرد:
<div class="alert alert-success" isVisibleForAuthUser [isVisibleForRoles]="['Admin','User']">
      Is-Visible-For-Roles = ['Admin','User']
</div>

خلاصه‌ی این تغییرات به کدهای نهایی این سری اعمال شده‌اند.
مطالب
Blazor 5x - قسمت ششم - مبانی Blazor - بخش 3 - چرخه‌های حیات کامپوننت‌ها
در این قسمت می‌خواهیم انواع رویدادهای چرخه‌ی حیات یک کامپوننت را بررسی کنیم. به همین جهت ابتدا دو کامپوننت جدید Lifecycle.razor و Lifecycle‍Child.razor را به مثالی که تا این قسمت تکمیل کرده‌ایم، اضافه کرده و آن‌ها‌را به صورت زیر جهت نمایش رویدادهای چرخه‌ی حیات، تغییر می‌دهیم:

کدهای کامل کامپوننت Pages\LearnBlazor\Lifecycle.razor
@page "/lifecycle"
@using System.Threading

<div class="border">
    <h3>Lifecycles Parent Component</h3>

    <div class="border">
        <LifecycleChild CountValue="CurrentCount"></LifecycleChild>
    </div>

    <p>Current count: @CurrentCount</p>

    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
    <br /><br />
    <button class="btn btn-primary" @onclick=StartCountdown>Start Countdown</button> @MaxCount
</div>

@code
{
    int CurrentCount = 0;
    int MaxCount = 5;

    private void IncrementCount()
    {
        CurrentCount++;
        Console.WriteLine("Parnet - IncrementCount is called");
    }

    protected override void OnInitialized()
    {
        Console.WriteLine("Parnet - OnInitialized is called");
    }

    protected override async Task OnInitializedAsync()
    {
        await Task.Delay(100);
        Console.WriteLine("Parnet - OnInitializedAsync is called");
    }

    protected override void OnParametersSet()
    {
        Console.WriteLine("Parnet - OnParameterSet is called");
    }

    protected override async Task OnParametersSetAsync()
    {
        await Task.Delay(100);
        Console.WriteLine("Parnet - OnParametersSetAsync is called");
    }

    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender)
        {
            Console.WriteLine("Parnet - OnAfterRender(firstRender == true) is called");
            CurrentCount = 111;
        }
        else
        {
            CurrentCount = 999;
            Console.WriteLine("Parnet - OnAfterRender(firstRender == false) is called");
        }
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await Task.Delay(100);
        Console.WriteLine("Parnet - OnAfterRenderAsync is called");
    }

    protected override bool ShouldRender()
    {
        Console.WriteLine("Parnet - ShouldRender is called");
        return true;
    }

    void StartCountdown()
    {
        Console.WriteLine("Parnet - StartCountdown()");
        var timer = new Timer(TimeCallBack, null, 1000, 1000);
    }

    void TimeCallBack(object state)
    {
        if (MaxCount > 0)
        {
            MaxCount--;
            Console.WriteLine("Parnet - InvokeAsync(StateHasChanged)");
            InvokeAsync(StateHasChanged);
        }
    }
}

و کدهای کامل کامپوننت Pages\LearnBlazor\LearnBlazor‍Components\Lifecycle‍Child.razor
<h3 class="ml-3 mr-3">Lifecycles Child Componenet</h3>

@code
{
    [Parameter]
    public int CountValue { get; set; }

    protected override void OnInitialized()
    {
        Console.WriteLine("  Child - OnInitialized is called");
    }

    protected override async Task OnInitializedAsync()
    {
        await Task.Delay(100);
        Console.WriteLine("  Child - OnInitializedAsync is called");
    }

    protected override void OnParametersSet()
    {
        Console.WriteLine("  Child - OnParameterSet is called");
    }

    protected override async Task OnParametersSetAsync()
    {
        await Task.Delay(100);
        Console.WriteLine("  Child - OnParametersSetAsync is called");
    }

    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender)
        {
            Console.WriteLine("  Child - OnAfterRender(firstRender == true) is called");
        }
        else
        {
            Console.WriteLine("  Child - OnAfterRender(firstRender == false) is called");
        }
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await Task.Delay(100);
        Console.WriteLine("  Child - OnAfterRenderAsync is called");
    }

    protected override bool ShouldRender()
    {
        Console.WriteLine("  Child - ShouldRender is called");
        return true;
    }
}
و همچنین برای دسترسی به آن‌ها، مدخل منوی زیر را به کامپوننت Shared\NavMenu.razor اضافه می‌کنیم:
<li class="nav-item px-3">
    <NavLink class="nav-link" href="lifecycle">
        <span class="oi oi-list-rich" aria-hidden="true"></span> Lifecycles
    </NavLink>
</li>
با توجه به اینکه برنامه‌ی جاری از نوع Blazor Server است، Console.WriteLine‌های آن، در صفحه‌ی کنسول اجرای برنامه ظاهر می‌شوند و نه در developer tools مرورگر:





رویدادهای OnInitialized و OnInitializedAsync

@code
{
    protected override void OnInitialized()
    {
        Console.WriteLine("Parnet - OnInitialized is called");
    }

    protected override async Task OnInitializedAsync()
    {
        await Task.Delay(100);
        Console.WriteLine("Parnet - OnInitializedAsync is called");
    }
همانطور که در تصویر فوق نیز ملاحظه می‌کنید، اولین رویدادی که فراخوانی می‌شود، OnInitialized نام دارد و پس از آن نمونه‌ی async آن به نام OnInitializedAsync. این رویدادها زمانیکه یک کامپوننت و اجزای UI آن کاملا بارگذاری شده‌اند، فراخوانی می‌شوند. مهم‌ترین کاربرد آن‌ها، دریافت اطلاعات از سرویس‌های برنامه‌است.
در کامپوننت Lifecycle.razor، یک کامپوننت دیگر نیز به نام Lifecycle‍Child.razor فراخوانی شده‌است. در این حالت ابتدا OnInitialized کامپوننت والد فراخوانی شده‌است و پس از آن بلافاصله فراخوانی OnInitialized کامپوننت فرزند را مشاهده می‌کنیم.


رویدادهای OnParametersSet و OnParametersSetAsync

این رویدادها یکبار در زمان بارگذاری اولیه‌ی کامپوننت و بار دیگر هر زمانیکه کامپوننت فرزند، پارامتر جدیدی را از طریق کامپوننت والد دریافت می‌کند، فراخوانی می‌شوند. برای نمونه کامپوننت LifecycleChild، پارامتر CurrentCount را از والد خود دریافت می‌کند:
<LifecycleChild CountValue="CurrentCount"></LifecycleChild>
هرچند این پارامتر در UI کامپوننت فرزند مثال تهیه شده استفاده نمی‌شود، اما مقدار دهی آن از طرف والد، سبب بروز رویدادهای OnParametersSet و OnParametersSetAsync خواهد شد. برای آزمایش آن اگر بر روی دکمه‌ی click me در کامپوننت والد کلیک کنیم، این رویدادهای جدید را مشاهده خواهیم کرد:
Parnet - IncrementCount is called
Parnet - ShouldRender is called
  Child - OnParameterSet is called
  Child - ShouldRender is called
Parnet - OnAfterRender(firstRender == false) is called
  Child - OnAfterRender(firstRender == false) is called
  Child - OnParametersSetAsync is called
  Child - ShouldRender is called
  Child - OnAfterRender(firstRender == false) is called
  Child - OnAfterRenderAsync is called
Parnet - OnAfterRenderAsync is called
  Child - OnAfterRenderAsync is called
ابتدا متد IncrementCount کامپوننت والد فراخوانی شده‌است که سبب تغییر مقدار پارامتر CurrentCount ارسالی به کامپوننت Lifecycle‍Child می‌شود و پس از آن، رویداد OnParameterSet کامپوننت فرزند را مشاهده می‌کنید که عکس العملی است به این تغییر مقدار. یکی از کاربردهای آن، دریافت مقدار جدید پارامترهای کامپوننت و سپس به روز رسانی قسمت خاصی از UI بر اساس آن‌ها است.



رویدادهای OnAfterRender و OnAfterRenderAsync

پس از هر بار رندر کامپوننت، این متدها فراخوانی می‌شوند. در این مرحله کار بارگذاری کامپوننت، دریافت اطلاعات و نمایش آن‌ها به پایان رسیده‌است. یکی از کاربردهای آن، آغاز کامپوننت‌های جاوا اسکریپتی است که برای کار، نیاز به DOM را دارند؛ مانند نمایش یک modal بوت استرپی.

یک نکته: هر تغییری که در مقادیر فیلدها در این رویدادها صورت گیرند، به UI اعمال نمی‌شوند؛ چون در مرحله‌ی آخر رندر UI قرار دارند.

@code
{
    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender)
        {
            Console.WriteLine("Parnet - OnAfterRender(firstRender == true) is called");
            CurrentCount = 111;
        }
        else
        {
            CurrentCount = 999;
            Console.WriteLine("Parnet - OnAfterRender(firstRender == false) is called");
        }
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await Task.Delay(100);
        Console.WriteLine("Parnet - OnAfterRenderAsync is called");
    }
}
در مثال‌های فوق، پارامتر firstRender را نیز مشاهده می‌کنید. یک کامپوننت چندین بار می‌تواند رندر شود. برای مثال هربار که توسط رویدادگردانی مقدار فیلدی را که در UI استفاده می‌شود، تغییر دهیم، کامپوننت مجددا رندر می‌شود. برای نمونه با کلیک بر روی دکمه‌ی click me، سبب تغییر مقدار فیلد CurrentCount می‌شویم. این تغییر و فراخوانی ضمنی StateHasChanged در پایان کار متد و در پشت صحنه، سبب رندر مجدد UI شده و در نتیجه‌ی آن، مقدار جدیدی را در صفحه مشاهده می‌کنیم. در اینجا اگر خواستیم بدانیم که رندر انجام شده برای بار اول است که صورت می‌گیرد یا خیر، می‌توان از پارامتر firstRender استفاده کرد.

سؤال: با توجه به مقدار دهی‌های 111 و 999 صورت گرفته‌ی در متد OnAfterRender، در اولین بار نمایش کامپوننت، چه عددی به عنوان CurrentCount نمایش داده می‌شود؟
در اولین بار نمایش صفحه، لحظه‌ای عدد 111 و سپس عدد 999 نمایش داده می‌شود. عدد 111 را در بار اول رندر و عدد 999 را در بار دوم رندر که پس از مقدار دهی پارامتر کامپوننت فرزند است، می‌توان مشاهده کرد.
اما ... اگر پس از نمایش اولیه‌ی صفحه، چندین بار بر روی دکمه‌ی click me کلیک کنیم، همواره عدد 1000 مشاهده می‌شود. علت اینجا است که تغییرات مقادیر فیلدها در متد OnAfterRender، به UI اعمال نمی‌شوند؛ چون در این مرحله، رندر UI به پایان رسیده‌است. در اینجا فقط مقدار فیلد CurrentCount به 999 تغییر می‌کند و به همین صورت باقی می‌ماند. دفعه‌ی بعدی که بر روی دکمه‌ی click me کلیک می‌کنیم، یک واحد به آن اضافه شده و اکنون است که کار رندر UI، مجددا شروع خواهد شد (در واکشن به یک رخ‌داد و فراخوانی ضمنی StateHasChanged در پشت صحنه) و اینبار حاصل 999+1 را در UI مشاهده می‌کنیم و باز هم در پایان کار رندر، مجددا مقدار CurrentCount به 999 تغییر می‌کند که ... دیگر به UI منعکس نمی‌شود تا زمان کلیک بعدی و همینطور الی آخر.


رویدادهای StateHasChanged و ShouldRender

- اگر خروجی رویداد ShouldRender مساوی true باشد، اجازه‌ی اعمال تغییرات به UI داده خواهد شد و برعکس. بنابراین اگر حالت UI تغییر کند و خروجی این متد false باشد، این تغییرات نمایش داده نخواهند شد.
- اگر رویداد StateHasChanged فراخوانی شود، به معنای درخواست رندر مجدد UI است. کاربرد آن در مکان‌هایی است که نیاز به اطلاع رسانی دستی تغییرات UI وجود دارد؛ درست پس از زمانیکه رندر UI به پایان رسیده‌است. برای آزمایش این مورد و فراخوانی دستی StateHasChanged، کدهای تایمر زیر تهیه شده‌اند:
@page "/lifecycle"
@using System.Threading

button class="btn btn-primary" @onclick=StartCountdown>Start Countdown</button> @MaxCount

@code
{
    int MaxCount = 5;

    void StartCountdown()
    {
        Console.WriteLine("Parnet - StartCountdown()");
        var timer = new Timer(TimeCallBack, null, 1000, 1000);
    }

    void TimeCallBack(object state)
    {
        if (MaxCount > 0)
        {
            MaxCount--;
            Console.WriteLine("Parnet - InvokeAsync(StateHasChanged)");
            InvokeAsync(StateHasChanged);
        }
    }
}
تایمر تعریف شده، یک thread timer است. یعنی callback آن بر روی یک ترد جدید و مجزای از ترد UI اجرا می‌شود. در این حالت اگر StateHasChanged را جهت اطلاع رسانی تغییر حالت UI فراخوانی نکنیم، در حین کار تایمر، هیچ تغییری را در UI مشاهده نخواهیم کرد.


یک نکته: متدهای رویدادگردان در Blazor، می‌توانند sync و یا async باشند؛ مانند متدهای OnClick و OnClickAsync زیر که هر دو پس از پایان متدها، سبب فراخوانی ضمنی StateHasChanged نیز می‌شوند (به این دلیل است که با کلیک بر روی دکمه‌ای، UI هم به روز رسانی می‌شود). البته متدهای رویدادگردان async، دوبار سبب فراخوانی ضمنی StateHasChanged می‌شوند؛ یکبار زمانیکه قسمت sync متد به پایان می‌رسد و یکبار هم زمانیکه کار فراخوانی کلی متد به پایان خواهد رسید:
<button @onclick="OnClick">Synchronous</button>
<button @onclick="OnClickAsync">Asynchronous</button>
@code{
    void OnClick()
    {
    } // StateHasChanged is called after the method

    async Task OnClickAsync()
    {
        text = "click1";
        // StateHasChanged is called here as the synchronous part of the method ends

        await Task.Delay(1000);
        await Task.Delay(2000);
        text = "click2";
    } // StateHasChanged is called after the method
}
بنابراین یکی دیگر از دلایل نیاز به فراخوانی صریح InvokeAsync(StateHasChanged) در callback تایمر تعریف شده، عدم فراخوانی خودکار آن، در پایان کار رویداد callback تایمر است.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-06.zip
مطالب
Globalization در ASP.NET MVC
اگر بازار هدف یک محصول شامل چندین کشور، منطقه یا زبان مختلف باشد، طراحی و پیاده سازی آن برای پشتیبانی از ویژگی‌های چندزبانه یک فاکتور مهم به حساب می‌آید. یکی از بهترین روشهای پیاده سازی این ویژگی در دات نت استفاده از فایلهای Resource است. درواقع هدف اصلی استفاده از فایلهای Resource نیز Globalization است. Globalization برابر است با Internationalization + Localization که به اختصار به آن g11n میگویند. در تعریف، Internationalization (یا به اختصار i18n) به فرایند طراحی یک محصول برای پشتیبانی از فرهنگ(culture)‌ها و زبانهای مختلف و Localization (یا L10n) یا بومی‌سازی به شخصی‌سازی یک برنامه برای یک فرهنگ یا زبان خاص گفته میشود. (اطلاعات بیشتر در اینجا).
استفاده از این فایلها محدود به پیاده سازی ویژگی چندزبانه نیست. شما میتوانید از این فایلها برای نگهداری تمام رشته‌های موردنیاز خود استفاده کنید. نکته دیگری که باید بدان اشاره کرد این است که تقرببا تمامی منابع مورد استفاده در یک محصول را میتوان درون این فایلها ذخیره کرد. این منابع در حالت کلی شامل موارد زیر است:
- انواع رشته‌های مورد استفاده در برنامه چون لیبل‌ها و پیغام‌ها و یا مسیرها (مثلا نشانی تصاویر یا نام کنترلرها و اکشنها) و یا حتی برخی تنظیمات ویژه برنامه (که نمیخواهیم براحتی قابل نمایش یا تغییر باشد و یا اینکه بخواهیم با تغییر زبان تغییر کنند مثل direction و امثال آن)
- تصاویر و آیکونها و یا فایلهای صوتی و انواع دیگر فایل ها
- و ...
 نحوه بهره برداری از فایلهای Resource در دات نت، پیاده سازی نسبتا آسانی را در اختیار برنامه نویس قرار میدهد. برای استفاده از این فایلها نیز روشهای متنوعی وجود دارد که در مطلب جاری به چگونگی استفاده از آنها در پروژه‌های ASP.NET MVC پرداخته میشود.

Globalization در دات نت
فرمت نام یک culture دات نت (که در کلاس CultureInfo پیاده شده است) بر اساس استاندارد RFC 4646 (^ و ^) است. (در اینجا اطلاعاتی راجع به RFC یا Request for Comments آورده شده است). در این استاندارد نام یک فرهنگ (کالچر) ترکیبی از نام زبان به همراه نام کشور یا منطقه مربوطه است. نام زبان برپایه استاندارد ISO 639 که یک عبارت دوحرفی با حروف کوچک برای معرفی زبان است مثل fa برای فارسی و en برای انگلیسی و نام کشور یا منطقه نیز برپایه استاندارد ISO 3166 که یه عبارت دوحرفی با حروف بزرگ برای معرفی یک کشور یا یک منطقه است مثل IR برای ایران یا US برای آمریکاست. برای نمونه میتوان به fa-IR برای زبان فارسی کشور ایران و یا en-US برای زبان انگلیسی آمریکایی اشاره کرد. البته در این روش نامگذاری یکی دو مورد استثنا هم وجود دارد (اطلاعات کامل کلیه زبانها: National Language Support (NLS) API Reference). یک فرهنگ خنثی (Neutral Culture) نیز تنها با استفاده از دو حرف نام زبان و بدون نام کشور یا منطقه معرفی میشود. مثل fa برای فارسی یا de برای آلمانی. در این بخش نیز دو استثنا وجود دارد (^).
در دات نت دو نوع culture وجود دارد: Culture و UICulture. هر دوی این مقادیر در هر Thread مقداری منحصربه فرد دارند. مقدار Culture بر روی توابع وابسته به فرهنگ (مثل فرمت رشته‌های تاریخ و اعداد و پول) تاثیر میگذارد. اما مقدار UICulture تعیین میکند که سیستم مدیریت منابع دات نت (Resource Manager) از کدام فایل Resource برای بارگذاری داده‌ها استفاده کند. درواقع در دات نت با استفاده از پراپرتی‌های موجود در کلاس استاتیک Thread برای ثرد جاری (که عبارتند از CurrentCulture و CurrentUICulture) برای فرمت کردن و یا انتخاب Resource مناسب تصمیم گیری میشود. برای تعیین کالچر جاری به صورت دستی میتوان بصورت زیر عمل کرد:
Thread.CurrentThread.CurrentUICulture = new CultureInfo("fa-IR");
Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture("fa-IR");
دراینجا باید اشاره کنم که کار انتخاب Resource مناسب با توجه به کالچر ثرد جاری توسط ResourceProviderFactory پیشفرض دات نت انجام میشود. در مطالب بعدی به نحوه تعریف یک پرووایدر شخصی سازی شده هم خواهم پرداخت.

پشتیبانی از زبانهای مختلف در MVC
برای استفاده از ویژگی چندزبانه در MVC دو روش کلی وجود دارد.
1. استفاده از فایلهای Resource برای تمامی رشته‌های موجود
2. استفاده از View‌های مختلف برای هر زبان
البته روش سومی هم که از ترکیب این دو روش استفاده میکند نیز وجود دارد. انتخاب روش مناسب کمی به سلیقه‌ها و عادات برنامه نویسی بستگی دارد. اگر فکر میکنید که استفاده از ویوهای مختلف به دلیل جداسازی مفاهیم درگیر در کالچرها (مثل جانمایی اجزای مختلف ویوها یا بحث Direction) باعث مدیریت بهتر و کاهش هزینه‌های پشتیبانی میشود بهتر است از روش دوم یا ترکیبی از این دو روش استفاده کنید. خودم به شخصه سعی میکنم از روش اول استفاده کنم. چون معتقدم استفاده از ویوهای مختلف باعث افزایش بیش از اندازه حجم کار میشود. اما در برخی موارد استفاده از روش دوم یا ترکیبی از دو روش میتواند بهتر باشد.

تولید فایلهای Resource
بهترین مکان برای نگهداری فایلهای Resource در یک پروژه جداگانه است. در پروژه‌های از نوع وب‌سایت پوشه‌هایی با نام App_GlobalResources یا App_LocalResources وجود دارد که میتوان از آنها برای نگهداری و مدیریت این نوع فایلها استفاده کرد. اما همانطور که در اینجا توضیح داده شده است این روش مناسب نیست. بنابراین ابتدا یک پروژه مخصوص نگهداری فایلهای Resource ایجاد کنید و سپس اقدام به تهیه این فایلها نمایید. سعی کنید که عنوان این پروژه به صورت زیر باشد. برای کسب اطلاعات بیشتر درباره نحوه نامگذاری اشیای مختلف در دات نت به این مطلب رجوع کنید.
<SolutionName>.Resources
برای افزودن فایلهای Resource به این پروژه ابتدا برای انتخاب زبان پیش فرض محصول خود تصمیم بگیرید. پیشنهاد میکنم که از زبان انگلیسی (en-US) برای اینکار استفاده کنید. ابتدا یک فایل Resource (با پسوند resx.) مثلا با نام Texts.resx به این پروژه اضافه کنید. با افزودن این فایل به پروژه، ویژوال استودیو به صورت خودکار یک فایل cs. حاوی کلاس متناظر با این فایل را به پروژه اضافه میکند. این کار توسط ابزار توکاری به نام ResXFileCodeGenerator انجام میشود. اگر به پراپرتی‌های این فایل resx. رجوع کنید میتوانید این عنوان را در پراپرتی Custom Tool ببینید. البته ابزار دیگری برای تولید این کلاسها نیز وجود دارد. این ابزارهای توکار برای سطوح دسترسی مخنلف استفاده میشوند. ابزار پیش فرض در ویژوال استودیو یعنی همان ResXFileCodeGenerator، این کلاسها را با دسترسی internal تولید میکند که مناسب کار ما نیست. ابزار دیگری که برای اینکار درون ویژوال استودیو وجود دارد PublicResXFileCodeGenerator است و همانطور که از نامش پیداست از سطح دسترسی public استفاده میکند. برای تغییر این ابزار کافی است تا عنوان آن را دقیقا در پراپرتی Custom Tool تایپ کنید.

نکته: درباره پراپرتی مهم Build Action این فایلها در مطالب بعدی بیشتر بحث میشود.
برای تعیین سطح دسترسی Resource موردنظر به روشی دیگر، میتوانید فایل Resource را باز کرده و Access Modifier آن را به Public تغییر دهید.

سپس برای پشتیبانی از زبانی دیگر، یک فایل دیگر Resource به پروژه اضافه کنید. نام این فایل باید همنام فایل اصلی به همراه نام کالچر موردنظر باشد. مثلا برای زبان فارسی عنوان فایل باید Texts.fa-IR.resx یا به صورت ساده‌تر برای کالچر خنثی (بدون نام کشور) Texts.fa.resx باشد. دقت کنید اگر نام فایل را در همان پنجره افزودن فایل وارد کنید ویژوال استودیو این همنامی را به صورت هوشمند تشخیص داده و تغییراتی را در پراپرتی‌های پیش فرض فایل Resource ایجاد میکند.
نکته: این هوشمندی مرتبه نسبتا بالایی دارد. بدین صورت که تنها درصورتیکه عبارت بعد از نام فایل اصلی Resource (رشته بعد از نقطه مثلا fa در اینجا) متعلق به یک کالچر معتبر باشد این تغییرات اعمال خواهد شد.
مهمترین این تغییرات این است که ابزاری را برای پراپرتی Custom Tool این فایلها انتخاب نمیکند! اگر به پراپرتی فایل Texts.fa.resx مراجعه کنید این مورد کاملا مشخص است. در نتیجه دیگر فایل cs. حاوی کلاسی جداگانه برای این فایل ساخته نمیشود. همچنین اگر فایل Resource جدید را باز کنید میبنید که برای Access Modifier آن گزینه No Code Generation انتخاب شده است.
در ادامه شروع به افزودن عناوین موردنظر در این دو فایل کنید. در اولی (بدون نام زبان) رشته‌های مربوط به زبان انگلیسی و در دومی رشته‌های مربوط به زبان فارسی را وارد کنید. سپس در هرجایی که یک لیبل یا یک رشته برای نمایش وجود دارد از این کلیدهای Resource استفاده کنید مثل:
<SolutionName>.Resources.Texts.Save
<SolutionName>.Resources.Texts.Cancel

استفاده از Resource در ویومدل ها
دو خاصیت معروفی که در ویومدلها استفاده میشوند عبارتند از: DisplayName و Required. پشتیبانی از کلیدهای Resource به صورت توکار در خاصیت Required وجود دارد. برای استفاده از آنها باید به صورت زیر عمل کرد:
[Required(ErrorMessageResourceName = "ResourceKeyName", ErrorMessageResourceType = typeof(<SolutionName>.Resources.<ResourceClassName>))]
در کد بالا باید از نام فایل Resource اصلی (فایل اول که بدون نام کالچر بوده و به عنوان منبع پیشفرض به همراه یک فایل cs. حاوی کلاس مربوطه نیز هست) برای معرفی ErrorMessageResourceType استفاده کرد. چون ابزار توکار ویژوال استودیو از نام این فایل برای تولید کلاس مربوطه استفاده میکند.
متاسفانه خاصیت DisplayName که در فضای نام System.ComponentModel (در فایل System.dll) قرار دارد قابلیت استفاده از کلیدهای Resource را به صورت توکار ندارد. در دات نت 4 خاصیت دیگری در فضای نام System.ComponentModel.DataAnnotations به نام Display (در فایل System.ComponentModel.DataAnnotations.dll) وجود دارد که این امکان را به صورت توکار دارد. اما قابلیت استفاده از این خاصیت تنها در MVC 3 وجود دارد. برای نسخه‌های قدیمیتر MVC امکان استفاده از این خاصیت حتی اگر نسخه فریمورک هدف 4 باشد وجود ندارد، چون هسته این نسخه‌های قدیمی امکان استفاده از ویژگی‌های جدید فریمورک با نسخه بالاتر را ندارد. برای رفع این مشکل میتوان کلاس خاصیت DisplayName را برای استفاده از خاصیت Display به صورت زیر توسعه داد:
public class LocalizationDisplayNameAttribute : DisplayNameAttribute
  {
    private readonly DisplayAttribute _display;
    public LocalizationDisplayNameAttribute(string resourceName, Type resourceType)
    {
      _display = new DisplayAttribute { ResourceType = resourceType, Name = resourceName };
    }
    public override string DisplayName
    {
      get
      {
        try
        {
          return _display.GetName();
        }
        catch (Exception)
        {
          return _display.Name;
        }
      }
    }
  }
در این کلاس با ترکیب دو خاصیت نامبرده امکان استفاده از کلیدهای Resource فراهم شده است. در پیاده سازی این کلاس فرض شده است که نسخه فریمورک هدف حداقل برابر 4 است. اگر از نسخه‌های پایین‌تر استفاده میکنید در پیاده سازی این کلاس باید کاملا به صورت دستی کلید موردنظر را از Resource معرفی شده بدست آورید. مثلا به صورت زیر:
public class LocalizationDisplayNameAttribute : DisplayNameAttribute
{
    private readonly PropertyInfo nameProperty;
    public LocalizationDisplayNameAttribute(string displayNameKey, Type resourceType = null)
        : base(displayNameKey)
    {
        if (resourceType != null)
            nameProperty = resourceType.GetProperty(base.DisplayName, BindingFlags.Static | BindingFlags.Public);
    }
    public override string DisplayName
    {
        get
        {
            if (nameProperty == null) base.DisplayName;
            return (string)nameProperty.GetValue(nameProperty.DeclaringType, null);
        }
    }
}
برای استفاده از این خاصیت جدید میتوان به صورت زیر عمل کرد:
[LocalizationDisplayName("ResourceKeyName", typeof(<SolutionName>.Resources.<ResourceClassName>))]
البته بیشتر خواص متداول در ویومدلها از ویژگی موردبحث پشتیبانی میکنند.
نکته: به کار گیری این روش ممکن است در پروژه‌های بزرگ کمی گیج کننده و دردسرساز بوده و باعث پیچیدگی بی‌مورد کد و نیز افزایش بیش از حد حجم کدنویسی شود. در مقاله آقای فیل هک (Model Metadata and Validation Localization using Conventions) روش بهتر و تمیزتری برای مدیریت پیامهای این خاصیت‌ها آورده شده است.

پشتیبانی از ویژگی چند زبانه
مرحله بعدی برای چندزبانه کردن پروژه‌های MVC تغییراتی است که برای مدیریت Culture جاری برنامه باید پیاده شوند. برای اینکار باید خاصیت CurrentUICulture در ثرد جاری کنترل و مدیریت شود. یکی از مکانهایی که برای نگهداری زبان جاری استفاده میشود کوکی است. معمولا برای اینکار از کوکی‌های دارای تاریخ انقضای طولانی استفاده میشود. میتوان از تنظیمات موجود در فایل کانفیگ برای ذخیره زبان پیش فرض سیستم نیز استفاه کرد.
روشی که معمولا برای مدیریت زبان جاری میتوان از آن استفاده کرد پیاده سازی یک کلاس پایه برای تمام کنترلرها است. کد زیر راه حل نهایی را نشان میدهد:
public class BaseController : Controller
  {
    private const string LanguageCookieName = "MyLanguageCookieName";
    protected override void ExecuteCore()
    {
      var cookie = HttpContext.Request.Cookies[LanguageCookieName];
      string lang;
      if (cookie != null)
      {
        lang = cookie.Value;
      }
      else
      {
        lang = ConfigurationManager.AppSettings["DefaultCulture"] ?? "fa-IR";
        var httpCookie = new HttpCookie(LanguageCookieName, lang) { Expires = DateTime.Now.AddYears(1) };
        HttpContext.Response.SetCookie(httpCookie);
      }
      Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(lang);
      base.ExecuteCore();
    }
  }
راه حل دیگر استفاده از یک ActionFilter است که نحوه پیاده سازی یک نمونه از آن در زیر آورده شده است:
public class LocalizationActionFilterAttribute : ActionFilterAttribute
  {
    private const string LanguageCookieName = "MyLanguageCookieName";
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
      var cookie = filterContext.HttpContext.Request.Cookies[LanguageCookieName];
      string lang;
      if (cookie != null)
      {
        lang = cookie.Value;
      }
      else
      {
        lang = ConfigurationManager.AppSettings["DefaultCulture"] ?? "fa-IR";
        var httpCookie = new HttpCookie(LanguageCookieName, lang) { Expires = DateTime.Now.AddYears(1) };
        filterContext.HttpContext.Response.SetCookie(httpCookie);
      }
      Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(lang);
      base.OnActionExecuting(filterContext);
    }
  }
نکته مهم: تعیین زبان جاری (یعنی همان مقداردهی پراپرتی CurrentCulture ثرد جاری) در یک اکشن فیلتر بدرستی عمل نمیکند. برای بررسی بیشتر این مسئله ابتدا به تصویر زیر که ترتیب رخ‌دادن رویدادهای مهم در ASP.NET MVC را نشان میدهد دقت کنید:

همانطور که در تصویر فوق مشاهده میکنید رویداد OnActionExecuting که در یک اکشن فیلتر به کار میرود بعد از عملیات مدل بایندینگ رخ میدهد. بنابراین قبل از تعیین کالچر جاری، عملیات validation و یافتن متن خطاها از فایلهای Resource انجام میشود که منجر به انتخاب کلیدهای مربوط به کالچر پیشفرض سرور (و نه آنچه که کاربر تنظیم کرده) خواهد شد. بنابراین استفاده از یک اکشن فیلتر برای تعیین کالچر جاری مناسب نیست. راه حل مناسب استفاده از همان کنترلر پایه است، زیرا متد ExecuteCore قبل از تمامی این عملیات صدا زده میشود. بنابرابن همیشه کالچر تنظیم شده توسط کاربر به عنوان مقدار جاری آن در ثرد ثبت میشود.

امکان تعیین/تغییر زبان توسط کاربر
برای تعیین یا تغییر زبان جاری سیستم نیز روشهای گوناگونی وجود دارد. استفاده از زبان تنظیم شده در مرورگر کاربر، استفاده از عنوان زبان در آدرس صفحات درخواستی و یا تعیین زبان توسط کاربر در تنظیمات برنامه/سایت و ذخیره آن در کوکی یا دیتابیس و مواردی از این دست روشهایی است که معمولا برای تعیین زبان جاری از آن استفاده میشود. در کدهای نمونه ای که در بخشهای قبل آورده شده است فرض شده است که زبان جاری سیستم درون یک کوکی ذخیره میشود بنابراین برای استفاده از این روش میتوان از قطعه کدی مشابه زیر (مثلا در فایل Layout.cshtml_) برای تعیین و تغییر زبان استفاه کرد:
<select id="langs" onchange="languageChanged()">
  <option value="fa-IR">فارسی</option>
  <option value="en-US">انگلیسی</option>
</select>
<script type="text/javascript">
  function languageChanged() {
    setCookie("MyLanguageCookieName", $('#langs').val(), 365);
    window.location.reload();
  }
  document.ready = function () {
    $('#langs').val(getCookie("MyLanguageCookieName"));
  };
  function setCookie(name, value, exdays, path) {
    var exdate = new Date();
    exdate.setDate(exdate.getDate() + exdays);
    var newValue = escape(value) + ((exdays == null) ? "" : "; expires=" + exdate.toUTCString()) + ((path == null) ? "" : "; path=" + path) ;
    document.cookie = name + "=" + newValue;
  }
  function getCookie(name) {
    var i, x, y, cookies = document.cookie.split(";");
    for (i = 0; i < cookies.length; i++) {
      x = cookies[i].substr(0, cookies[i].indexOf("="));
      y = cookies[i].substr(cookies[i].indexOf("=") + 1);
      x = x.replace(/^\s+|\s+$/g, "");
      if (x == name) {
        return unescape(y);
      }
    }
  }
</script> 
متدهای setCookie و getCookie جاوا اسکریپتی در کد بالا از اینجا گرفته شده اند البته پس از کمی تغییر.
نکته: مطلب Cookieها بحثی نسبتا مفصل است که در جای خودش باید به صورت کامل آورده شود. اما در اینجا تنها به همین نکته اشاره کنم که عدم توجه به پراپرتی path کوکی‌ها در این مورد خاص برای خود من بسیار گیج‌کننده و دردسرساز بود. 
به عنوان راهی دیگر میتوان به جای روش ساده استفاده از کوکی، تنظیماتی در اختیار کاربر قرار داد تا بتواند زبان تنظیم شده را درون یک فایل یا دیتابیس ذخیره کرد البته با درنظر گرفتن مسائل مربوط به کش کردن این تنظیمات.
راه حل بعدی میتواند استفاده از تنظیمات مرورگر کاربر برای دریافت زبان جاری تنظیم شده است. مرورگرها تنظیمات مربوط به زبان را در قسمت Accept-Languages در HTTP Header درخواست ارسالی به سمت سرور قرار میدهند. بصورت زیر:
GET https://www.dntips.ir HTTP/1.1
...
Accept-Language: fa-IR,en-US;q=0.5
...
این هم تصویر مربوط به Fiddler آن:

نکته: پارامتر q در عبارت مشخص شده در تصویر فوق relative quality factor نام دارد و به نوعی مشخص کننده اولویت زبان مربوطه است. مقدار آن بین 0 و 1 است و مقدار پیش فرض آن 1 است. هرچه مقدار این پارامتر بیشتر باشد زبان مربوطه اولویت بالاتری دارد. مثلا عبارت زیر را درنظر بگیرید:
Accept-Language: fa-IR,fa;q=0.8,en-US;q=0.5,ar-BH;q=0.3
در این حالت اولویت زبان fa-IR برابر 1 و fa برابر 0.8 (fa;q=0.8) است. اولویت دیگر زبانهای تنظیم شده نیز همانطور که نشان داده شده است در مراتب بعدی قرار دارند. در تنظیم نمایش داده شده برای تغییر این تنظیمات در IE میتوان همانند تصویر زیر اقدام کرد:

در تصویر بالا زبان فارسی اولویت بالاتری نسبت به انگلیسی دارد. برای اینکه سیستم g11n دات نت به صورت خودکار از این مقادیر جهت زبان ثرد جاری استفاده کند میتوان از تنظیم زیر در فایل کانفیگ استفاده کرد:
<system.web>
    <globalization enableClientBasedCulture="true" uiCulture="auto" culture="auto"></globalization>
</system.web>
در سمت سرور نیز برای دریافت این مقادیر تنظیم شده در مرورگر کاربر میتوان از کدهای زیر استفاه کرد. مثلا در یک اکشن فیلتر:
var langs = filterContext.HttpContext.Request.UserLanguages;
پراپرتی UserLanguages از کلاس Request حاوی آرایه‌ای از استرینگ است. این آرایه درواقع از Split کردن مقدار Accept-Languages با کاراکتر ',' بدست می‌آید. بنابراین اعضای این آرایه رشته‌ای از نام زبان به همراه پارامتر q مربوطه خواهند بود (مثل "fa;q=0.8").
راه دیگر مدیریت زبانها استفاده از عنوان زبان در مسیر درخواستی صفحات است. مثلا آدرسی شبیه به www.MySite.com/fa/Employees نشان میدهد کاربر درخواست نسخه فارسی از صفحه Employees را دارد. نحوه استفاده از این عناوین و نیز موقعیت فیزیکی این عناوین در مسیر صفحات درخواستی کاملا به سلیقه برنامه نویس و یا کارفرما بستگی دارد. روش کلی بهره برداری از این روش در تمام موارد تقریبا یکسان است.
برای پیاده سازی این روش ابتدا باید یک route جدید در فایل Global.asax.cs اضافه کرد:
routes.MapRoute(
    "Localization", // Route name
    "{lang}/{controller}/{action}/{id}", // URL with parameters
    new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
دقت کنید که این route باید قبل از تمام routeهای دیگر ثبت شود. سپس باید کلاس پایه کنترلر را به صورت زیر پیاده سازی کرد:
public class BaseController : Controller
{
  protected override void ExecuteCore()
  {
    var lang = RouteData.Values["lang"];
    if (lang != null && !string.IsNullOrWhiteSpace(lang.ToString()))
    {
      Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(lang.ToString());
    }
    base.ExecuteCore();
  }
}
این کار را در یک اکشن فیلتر هم میتوان انجام داد اما با توجه به توضیحاتی که در قسمت قبل داده شد استفاده از اکشن فیلتر برای تعیین زبان جاری کار مناسبی نیست.
نکته: به دلیل آوردن عنوان زبان در مسیر درخواستها باید کتترل دقیقتری بر کلیه مسیرهای موجود داشت!

استفاده از ویوهای جداگانه برای زبانهای مختلف
برای اینکار ابتدا ساختار مناسبی را برای نگهداری از ویوهای مختلف خود درنظر بگیرید. مثلا میتوانید همانند نامگذاری فایلهای Resource از نام زبان یا کالچر به عنوان بخشی از نام فایلهای ویو استفاده کنید و تمام ویوها را در یک مسیر ذخیره کنید. همانند تصویر زیر:

البته اینکار ممکن است به مدیریت این فایلها را کمی مشکل کند چون به مرور زمان تعداد فایلهای ویو در یک فولدر زیاد خواهد شد. روش دیگری که برای نگهداری این ویوها میتوان به کار برد استفاده از فولدرهای جداگانه با عناوین زبانهای موردنظر است. مانند تصویر زیر:

روش دیگری که برای نگهداری و مدیریت بهتر ویوهای زبانهای مختلف از آن استفاده میشود به شکل زیر است:

استفاه از هرکدام از این روشها کاملا به سلیقه و راحتی مدیریت فایلها برای برنامه نویس بستگی دارد. درهر صورت پس از انتخاب یکی از این روشها باید اپلیکشن خود را طوری تنظیم کنیم که با توجه به زبان جاری سیستم، ویوی مربوطه را جهت نمایش انتخاب کند.
مثلا برای روش اول نامگذاری ویوها میتوان از روش دستکاری متد OnActionExecuted در کلاس پایه کنترلر استفاده کرد:
public class BaseController : Controller
{
  protected override void OnActionExecuted(ActionExecutedContext context)
  {
    var view = context.Result as ViewResultBase;
    if (view == null) return; // not a view
    var viewName = view.ViewName;
    view.ViewName = GetGlobalizationViewName(viewName, context);
    base.OnActionExecuted(context);
  }
  private static string GetGlobalizationViewName(string viewName, ControllerContext context)
  {
    var cultureName = Thread.CurrentThread.CurrentUICulture.Name;
    if (cultureName == "en-US") return viewName; // default culture
    if (string.IsNullOrEmpty(viewName))
      return context.RouteData.Values["action"] + "." + cultureName; // "Index.fa"
    int i;
    if ((i = viewName.IndexOf('.')) > 0) // ex: Index.cshtml
      return viewName.Substring(0, i + 1) + cultureName + viewName.Substring(i); // "Index.fa.cshtml"
    return viewName + "." + cultureName; // "Index" ==> "Index.fa"
  }
}
همانطور که قبلا نیز شرح داده شد، چون متد ExecuteCore قبل از OnActionExecuted صدا زده میشود بنابراین از تنظیم درست مقدار کالچر در ثرد جاری اطمینان داریم.
روش دیگری که برای مدیریت انتخاب ویوهای مناسب استفاده از یک ویوانجین شخصی سازی شده است. مثلا برای روش سوم نامگذاری ویوها میتوان از کد زیر استفاده کرد:
public sealed class RazorGlobalizationViewEngine : RazorViewEngine
  {
    protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
    {
      return base.CreatePartialView(controllerContext, GetGlobalizationViewPath(controllerContext, partialPath));
    }
    protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
    {
      return base.CreateView(controllerContext, GetGlobalizationViewPath(controllerContext, viewPath), masterPath);
    }
    private static string GetGlobalizationViewPath(ControllerContext controllerContext, string viewPath)
    {
      //var controllerName = controllerContext.RouteData.GetRequiredString("controller");
      var request = controllerContext.HttpContext.Request;
      var lang = request.Cookies["MyLanguageCookie"];
      if (lang != null && !string.IsNullOrEmpty(lang.Value) && lang.Value != "en-US")
      {
        var localizedViewPath = Regex.Replace(viewPath, "^~/Views/", string.Format("~/Views/Globalization/{0}/", lang.Value));
        if (File.Exists(request.MapPath(localizedViewPath))) viewPath = localizedViewPath;
      }
      return viewPath;
    }
و برای ثبت این ViewEngine در فایل Global.asax.cs خواهیم داشت:
protected void Application_Start()
{
  ViewEngines.Engines.Clear();
  ViewEngines.Engines.Add(new RazorGlobalizationViewEngine());
}

محتوای یک فایل Resource
ساختار یک فایل resx. به صورت XML استاندارد است. در زیر محتوای یک نمونه فایل Resource با پسوند resx. را مشاهده میکنید:
<?xml version="1.0" encoding="utf-8"?>
<root>
  <!-- 
    Microsoft ResX Schema ...
    -->
  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
   ...
  </xsd:schema>
  <resheader name="resmimetype">
    <value>text/microsoft-resx</value>
  </resheader>
  <resheader name="version">
    <value>2.0</value>
  </resheader>
  <resheader name="reader">
    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <resheader name="writer">
    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <data name="RightToLeft" xml:space="preserve">
    <value>false</value>
    <comment>RightToleft is false in English!</comment>
  </data>
</root>
در قسمت ابتدایی تمام فایلهای resx. که توسط ویژوال استودیو تولید میشود کامنتی طولانی وجود دارد که به صورت خلاصه به شرح محتوا و ساختار یک فایل Resource میپردازد. در ادامه تگ نسبتا طولانی xsd:schema قرار دارد. از این قسمت برای معرفی ساختار داده ای فایلهای XML استفاده میشود. برای آشنایی بیشتر با XSD (یا XML Schema) به اینجا مراجعه کنید. به صورت خلاصه میتوان گفت که XSD برای تعیین ساختار داده‌ها یا تعیین نوع داده ای اطلاعات موجود در یک فایل XML به کار میرود. درواقع تگهای XSD به نوعی فایل XML ما را Strongly Typed میکند. با توجه به اطلاعات این قسمت، فایلهای resx. شامل 4 نوع گره اصلی هستند که عبارتند از: metadata و assembly و data و resheader. در تعریف هر یک از گره‌ها در این قسمت مشخصاتی چون نام زیر گره‌های قابل تعریف در هر گره و نام و نوع خاصیتهای هر یک معرفی شده است.
بخش موردنظر ما در این مطلب قسمت انتهایی این فایلهاست (تگهای resheader و data). همانطور در بالا مشاهده میکنید تگهای reheader شامل تنظیمات مربوط به فایل resx. با ساختاری ساده به صورت name/value است. یکی از این تنظیمات resmimetype فایل resource را معرفی میکند که درواقع مشخص کننده نوع محتوای (Content Type) فایل XML است(^). برای فایلهای resx این مقدار برابر text/microsoft-resx است. تنظیم بعدی نسخه مربوط به فایل resx (یا Microsoft ResX Schema) را نشان میدهد. در حال حاضر نسخه جاری (در VS 2010) برابر 2.0 است. تنظیم بعدی مربوط به کلاسهای reader و writer تعریف شده برای استفاده از این فایلهاست. به نوع این کلاسهای خواننده و نویسنده فایلهای resx. و مکان فیزیکی و فضای نام آنها دقت کنید که در مطالب بعدی از آنها برای ویرایش و بروزرسانی فایلهای resource در زمان اجرا استفاده خواهیم کرد.
در پایان نیز تگهای data که برای نگهداری داده‌ها از آنها استفاده میشود. هر گره data شامل یک خاصیت نام (name) و یک زیرگره مقدار (value) است. البته امکان تعیین یک کامنت در زیرگره comment نیز وجود دارد که اختیاری است. هر گره data مینواند شامل خاصیت type و یا mimetype نیز باشد. خاصیت type مشخص کننده نوعی است که تبدیل text/value را با استفاده از ساختار TypeConverter پشتیبانی میکند. البته اگر در نوع مشخص شده این پشتیبانی وجود نداشته باشد، داده موردنظر پس از سریالایز شدن با فرمت مشخص شده در خاصیت mimetype ذخیره میشود. این mimetype اطلاعات موردنیاز را برای کلاس خواننده این فایلها (ResXResourceReader به صورت پیشفرض) جهت چگونگی بازیابی آبجکت موردنظر فراهم میکند. مشخص کردن این دو خاصیت برای انواع رشته ای نیاز نیست. انواع mimetype قابل استفاده عبارتند از:
- application/x-microsoft.net.object.binary.base64: آبجکت موردنظر باید با استفاده از کلاس System.Runtime.Serialization.Formatters.Binary.BinaryFormatter سریالایز شده و سپس با فرمت base64 به یک رشته انکد شود (راجع به انکدینگ base64 ^ و ^).
- application/x-microsoft.net.object.soap.base64: آبجکت موردنظر باید با استفاده از کلاس System.Runtime.Serialization.Formatters.Soap.SoapFormatter سریالایز شده و سپس با فرمت base64 به یک رشته انکد شود.
- application/x-microsoft.net.object.bytearray.base64: آبجکت ابتدا باید با استفاده از یک System.ComponentModel.TypeConverter به آرایه ای از بایت سریالایز شده و سپس با فرمت base64 به یک رشته انکد شود.
نکته: امکان جاسازی کردن (embed) فایلهای resx. در یک اسمبلی یا کامپایل مستقیم آن به یک سَتِلایت اسمبلی (ترجمه مناسبی برای satellite assembly پیدا نکردم، چیزی شبیه به اسمبلی قمری یا وابسته و از این قبیل ...) وجود ندارد. ابتدا باید این فایلهای resx. به فایلهای resources. تبدیل شوند. اینکار با استفاده از ابزار Resource File Generator (نام فایل اجرایی آن resgen.exe است) انجام میشود (^ و ^). سپس میتوان با استفاده از Assembly Linker ستلایت اسمبلی مربوطه را تولید کرد (^). کل این عملیات در ویژوال استودیو با استفاده از ابزار msbuild به صورت خودکار انجام میشود!

نحوه یافتن کلیدهای Resource در بین فایلهای مختلف Resx توسط پرووایدر پیش فرض در دات نت
عملیات ابتدا با بررسی خاصیت CurrentUICulture از ثرد جاری آغاز میشود. سپس با استفاده از عنوان استاندارد کالچر جاری، فایل مناسب Resource یافته میشود. در نهایت بهترین گزینه موجود برای کلید درخواستی از منابع موجود انتخاب میشود. مثلا اگر کالچر جاری fa-IR و کلید درخواستی از کلاس Texts باشد ابتدا جستجو برای یافتن فایل Texts.fa-IR.resx آغاز میشود و اگر فایل موردنظر یا کلید درخواستی در این فایل یافته نشد جستجو در فایل Texts.fa.resx ادامه می‌یابد. اگر باز هم یافته نشد درنهایت این عملیات جستجو در فایل resource اصلی خاتمه می‌یابد و مقدار کلید منبع پیش فرض به عنوان نتیجه برگشت داده میشود. یعنی در تمامی حالات سعی میشود تا دقیقترین و بهترین و نزدیکترین نتیجه انتخاب شود. البته درصورتیکه از یک پرووایدر شخصی سازی شده برای کار خود استفاده میکنید باید چنین الگوریتمی را جهت یافتن کلیدهای منابع خود از فایلهای Resource (یا هرمنبع دیگر مثل دیتابیس یا حتی یک وب سرویس) درنظر بگیرید.

Globalization در کلاینت (javascript g11n)
یکی دیگر از موارد استفاده g11n در برنامه نویسی سمت کلاینت است. با وجود استفاده گسترده از جاوا اسکریپت در برنامه نویسی سمت کلاینت در وب اپلیکیشنها، متاسفانه تا همین اواخر عملا ابزار یا کتابخانه مناسبی برای مدیریت g11n در این زمینه وجود نداشته است. یکی از اولین کتابخانه‌های تولید شده در این زمینه کتابخانه jQuery Globalization است که توسط مایکروسافت توسعه داده شده است (برای آشنایی بیشتر با این کتابخانه به ^ و ^ مراجعه کنید). این کتابخانه بعدا تغییر نام داده و اکنون با عنوان Globalize شناخته میشود. Globalize یک کتابخانه کاملا مستقل است که وابستگی به هیچ کتابخانه دیگر ندارد (یعنی برای استفاده از آن نیازی به jQuery نیست). این کتابخانه حاوی کالچرهای بسیاری است که عملیات مختلفی چون فرمت و parse انواع داده‌ها را نیز در سمت کلاینت مدیریت میکند. همچنین با فراهم کردن منابعی حاوی جفتهای key/culture میتوان از مزایایی مشابه مواردی که در این مطلب بحث شد در سمت کلاینت نیز بهره برد. نشانی این کتابخانه در github اینجا است. با اینکه خود این کتابخانه ابزار کاملی است اما در بین کالچرهای موجود در فایلهای آن متاسفانه پشتیبانی کاملی از زبان فارسی نشده است. ابزار دیگری که برای اینکار وجود دارد پلاگین jquery localize است که برای بحث g11n رشته‌ها پیاده‌سازی بهتر و کاملتری دارد.

در مطالب بعدی به مباحث تغییر مقادیر کلیدهای فایلهای resource در هنگام اجرا با استفاده از روش مستقیم تغییر محتوای فایلها و کامپایل دوباره توسط ابزار msbuild و نیز استفاده از یک ResourceProvider شخصی سازی شده به عنوان یک راه حل بهتر برای اینکار میپردازم.
در تهیه این مطلب از منابع زیر استفاده شده است:

مطالب
آموزش Cache در ASP.NET Core - (قسمت دوم : EasyCaching)
در قسمت اول، درمورد سیستم Cache پیش‌فرض موجود در Asp.Net Core و مزیت‌ها و معایب آن گفتیم. اگر قسمت اول را نخواندید، قسمت اول مقاله را میتوانید از این لینک بخوانید. 
 در این قسمت میخواهیم یک پکیج محبوب و کاربردی را برای پیاده سازی کش، در Asp.Net Core را بررسی کنیم.
در دنیای امروز، برنامه نویسی پکیج‌ها و فریمورک‌ها، نقش بسیار مهمی را ایفا میکنند؛ بطوریکه در بسیاری از این موارد، استفاده از این پکیج‌ها، عمل عاقلانه‌تری نسبت به دوباره نویسی فیچر‌های مربوطه است. برای عمل کشینگ در Asp.Net Core نیز پکیج‌های فوق‌العاده‌ای وجود دارند که در این مقاله، به بررسی و استفاده پکیج این میپردازیم.
در این پکیج، هر یک از متد‌های موجود در عملیات کشینگ، بصورت بهینه‌ای تعریف شده و قابل استفاده‌اند. سیستمی که این پکیج برای کش کردن داده‌ها استفاده میکند، همان سیستم کش Asp.Net Core هست و به‌نوعی، سوار بر این سیستم، قابلیت‌های بیشتر و بهتری را ارائه میدهد و این متد‌ها شامل موارد زیر هستند:
  1.  Get/GetAsync(with data retriever)
  2.  Get/GetAsync(without data retriever)
  3.  Set/SetAsync
  4.  Remove/RemoveAsync
  5.  ~~Refresh/RefreshAsync (was removed)~~
  6.  RemoveByPrefix/RemoveByPrefixAsync
  7.  SetAll/SetAllAsync
  8.  GetAll/GetAllAsync
  9.  GetByPrefix/GetByPrefixAsync
  10.  RemoveAll/RemoveAllAsync
  11.  GetCount
  12.  Flush/FlushAsync
  13.  TrySet/TrySetAsync
  14.  GetExpiration/GetExpirationAsync

مفهوم استفاده از این متد‌ها، با همان مفهوم متد‌های کش در core، برابری میکند که در قسمت اول این مقاله به آن پرداختیم. همانطور که می‌بینید، این پکیج از Async Method‌‌ها هم پشتیبانی میکند و میتوانید کش‌های خود را بصورت Async بنویسید.
یکی از قابلیت‌های دیگر این پکیج، سازگاری آن با انواع Cache Provider‌های موجود است. بطور خلاصه Cache Provider‌ها، همان ارائه دهندگان حافظه‌ی Ram، در قالب‌ها و ابزارهای مختلف هستند. برخی از این‌ها با داشتن الگوریتم‌های بهینه‌تر، سرعت بالاتری از رد و بدل کردن اطلاعات در Ram را در اختیار ما قرار میدهند و Local بودن یا Distributed بودن را کنترل میکنند. Cache provider‌های گوناگونی وجود دارند که هریک به شکلی کار میکند؛ برای مثال شما میتوانید با Provider ای مستقیما با خود Ram، برای Get و Set کردن کش‌های خود در ارتباط باشید و یا در روشی دیگر، از یک دیتابیس(Redis)، جدا از دیتابیس اصلی برنامه که حافظه مصرفی آن Ram هست و منابع حافظه شما را نیز مدیریت میکند، برای کش‌های خود استفاده کنید و اطلاعات را بصورت ایندکس گذاری شده در Ram ذخیره کنید که به سرعت واکشی آن می‌افزاید.

بطور کل Cache Provider هایی که پکیج EasyCaching با آن‌ها سازگار است شامل موارد زیر است:
  1. In-Memory
  2. Memcached
  3. Redis(Based on StackExchange.Redis)
  4. Redis(Based on csredis)
  5. SQLite
  6. Hybrid
  7. Disk
  8. LiteDb

یکی دیگر از مزیت‌های این پکیج، سازگاری آن با Serializer‌های مختلف است. همانطور که میدانید دیتا‌های ورودی و خروجی در برنامه، نیاز به Serialize شدن دارند. وقتی میخواهید دیتایی را در دیتابیس ذخیره کنید، آن را در قالب یک شی (Model) از کاربر دریافت میکنید و شما باید برای ذخیره این دیتا، اطلاعات درون شیء را به قالبی که قابل ذخیره شدن باشد، در آورید که این عمل Serialize نام دارد. دقیقا برعکس این روند، بعد از واکشی اطلاعات از دیتابیس، اطلاعات را در قالب اشیایی که قابل نمایش به کاربر باشد (DeSerialize) در میاوریم.
در کش کردن هم چیزی که شما با آن سروکار دارید، دیتا است؛ پس برای ذخیره و واکشی این دیتا، از هر حافظه‌ای، چه دیتابیس و چه Ram، باید از یک Serializer استفاده کنید تا عملیات Serialize و DeSerialize را برایتان انجام دهد. Serializer‌های مختلفی وجود دارند که بصورت پکیج‌هایی ارائه شده‌اند و اما Serializer هایی که سیستم EasyCaching آن‌هارا پشتیبانی میکند، شامل موارد ذیل هستند:
  1. BinaryFormatter
  2. MessagePack
  3. Newtonsoft.Json
  4. Protobuf
  5. System.Text.Json

در ادامه به پیاده سازی کش، با استفاده از EasyCaching در سه Provider مختلف از این پکیج می‌پردازیم.

 1_ پروایدر InMemory :
پروایدر InMemory، یک سیستم Local Caching را برای ما به وجود میاورد. در قسمت قبلی مقاله سیستم‌های Local(InMemory) و Distributed را بررسی کردیم و تفاوت‌های میان آن‌ها را گفتیم.

برای استفاده از پروایدر InMemory در EasyCaching باید پکیج زیر را نصب کنید: 
Install-Package EasyCaching.InMemory
در مرحله بعد، کانفیگ‌های مربوط به این پکیج را در کلاس Startup برنامه خود میاوریم. راحت‌ترین روش افزودن این پکیج به Startup، صرفا افزودن حالت پیشفرض آن به متد ConfigureServices است که به شرح زیر عمل میکنیم: 
  services.AddEasyCaching(options =>
 {
       // use memory cache with a simple way
        options.UseInMemory();
 }
این حالت از کانفیگ، پکیج تنظیمات پیش‌فرض خود پکیج را برای برنامه قرار میدهد؛ شما میتوانید با استفاده از option‌های دیگری که در متد ()UseInMemory وجود دارند، تنظیمات شخصی سازی شده از سیستم کشینگ خود را اعمال کنید. 
و تمام. هم اکنون میتوان با استفاده از اینترفیس IEasyCachingProvider که این سرویس در اختیارمان قرار داده و عمل تزریق وابستگی آن در کلاس‌ها و کنترلر‌های مان دیتای در حال عبور را کش کنیم. متد‌های موجود در این اینترفیس به شرح زیر میباشد : 
// تنظیم یک کش با کلید - مقدار - زمان انقضا
void Set<T>(string cacheKey, T cacheValue, TimeSpan expiration);
Task SetAsync<T>(string cacheKey, T cacheValue, TimeSpan expiration);

// تنظیم یک کش با مقدار و زمان انقضا که تایپ مقدار از نوع دیکشنری هست و کلید دیکشنری بعنوان کلید کش قرار میگیرد
void SetAll<T>(IDictionary<string, T> value, TimeSpan expiration);
Task SetAllAsync<T>(IDictionary<string, T> value, TimeSpan expiration);

// تنظیم یک کش با کلید - مقدار - زمان انقضا
// اگر کلیدی همنام وجود داشته باشد مقدار نادرست و در غیر اینصورت مقدار نادرست را برمیگرداند
bool TrySet<T>(string cacheKey, T cacheValue, TimeSpan expiration);
Task<bool> TrySetAsync<T>(string cacheKey, T cacheValue, TimeSpan expiration);
 
// گرفتن یک کش با کلید
CacheValue<T> Get<T>(string cacheKey);
Task<CacheValue<T>> GetAsync<T>(string cacheKey);

// 
CacheValue<T> Get<T>(string cacheKey, Func<T> dataRetriever, TimeSpan expiration);
Task<CacheValue<T>> GetAsync<T>(string cacheKey, Func<Task<T>> dataRetriever, TimeSpan expiration);
 
// گرفتن یک کش با چند کاراکتر پیشین کلید آن
// برای مثال یک کلید با نام
// MyKey
// تنها با داشتن چند حرف اول 
// MyK
// میتوانیم این کش را دریافت کنیم
IDictionary<string, CacheValue<T>> GetByPrefix<T>(string prefix);
Task<IDictionary<string, CacheValue<T>>> GetByPrefixAsync<T>(string prefix);

// 
IDictionary<string, CacheValue<T>> GetAll<T>(IEnumerable<string> cacheKeys);
Task<IDictionary<string, CacheValue<T>>> GetAllAsync<T>(IEnumerable<string> cacheKeys);

// گرفتن تعداد کش‌های با کاراکتر‌های پیشین کلید که میان چند کلید یکسان است 
int GetCount(string prefix = "");
Task<int> GetCountAsync(string prefix = "");

// گرفتن زمان انقضا باقیمانده از یک کش با کلید آن
TimeSpan GetExpiration(string cacheKey);
Task<TimeSpan> GetExpirationAsync(string cacheKey);

// حذف کردن یک کش با کلید
void Remove(string cacheKey);
Task RemoveAsync(string cacheKey);

// حذف کردن یک کش با چند کاراکتر پیشین کلید
void RemoveByPrefix(string prefix);
Task RemoveByPrefixAsync(string prefix);
 
// حذف کردن چند کش با لیستی از کلید‌ها void RemoveAll(IEnumerable<string> cacheKeys);
Task RemoveAllAsync(IEnumerable<string> cacheKeys);

// بررسی وجود یا عدم وجود یک کش با کلید
bool Exists(string cacheKey);
Task<bool> ExistsAsync(string cacheKey);

// حذف کردن همه کش‌ها void Flush();
Task FlushAsync();

همانطور که قبلا گفته شد، سیستم کش، با دیتا مرتبط است و نیازمند یک Object Serializer جهت Serialize کردن اطلاعات ورودی و ذخیره آن در Target Storage مشخص شده است. پکیج EasyCaching برای Provider‌های خود، یک Object Serializer پیش‌فرض قرار داده‌است و تا وقتی که شما آن را طبق نیازی خاص، بصورت سفارشی تغییر نداده باشید، از آن استفاده میکند.
در میان پنج Serializer معرفی شده که EasyCaching آن‌ها را پشتیبانی میکند، BinaryFormatter بصورت پیش‌فرض در همه‌ی Provider‌ها برقرار است و تا وقتی یک Serializer انتخابی به EasyCaching معرفی نکنید، این پکیج از این Serializer استفاده میکند.
برای استفاده از Serializer‌های دیگری که معرفی شده میتوانید از لینک‌های زیر کمک بگیرید :

2 - پروایدر Redis :
ردیس، یک دیتابیس Key Value محور هست که محل ذخیره سازی آن Ram است و اطلاعات، بصورت موقت در آن ذخیره میشوند. بطور خلاصه، Key Value یعنی یکبار کلید و مقداری برای آن کلید تعریف میشود و هر وقت نام کلید تعریف شده، صدا زده شد، مقدار نسبت داده شده به آن، در اختیار ما قرار میگیرد. برای مثال کلید "Name" و مقدار "James". با این انتساب، هروقت "Name" فراخوانده شود، مقدار "James" را خواهیم داشت. سیستم Key Value بخاطر عدم پیچیدگی و سادگی‌ای که دارد، بسیار سریع عمل میکند و همچنین ایندکس گذاری‌هایی که ردیس روی دیتا‌ها انجام میدهد، باعث افزایش سرعت آن نیز خواهد شد که ردیس را به سریع‌ترین دیتابیس Key Value دنیا تبدیل کرده.
در اینجا با توجه به قابلیت هایی که ردیس داراست، یکی از بهترین گزینه‌ها برای انتخاب بعنوان فضای ذخیره سازی کش‌ها بصورت Distributed است.
برای استفاده از این دیتابیس قدرتمند ابتدا باید از طریق یکی از روش‌های معمول اقدام به نصب آن کنید. میتوانید فایل نصبی را از وبسایت رسمی آن دانلود کنید و یا یا با استفاده از Docker اقدام به نصب آن نمایید.
پس از نصب این دیتابیس روی سیستم خود ، برای استفاده از آن در EasyCaching ابتدا باید پکیج مورد نیاز را نصب کنید. 
Install-Package EasyCaching.Redis
ادامه کار به همان سادگی پروایدر قبلی هست و فقط کافیست EasyCaching و option ردیس را به کلاس Startup اضافه کنید. 
 services.AddEasyCaching(option =>
{
       option.UseRedis(config =>
      {
             config.DBConfig.Endpoints.Add(new ServerEndPoint("127.0.0.1", 6379));
      });
});
با استفاده از متد UseRedis شما قابلیت استفاده از ردیس را در EasyCaching فعال میکنید و سپس باید اطلاعات Host و Port ردیس نصب شده‌ی روی سیستم خود را به این متد معرفی کنید.
اگر ردیس را بدون تنظیمات شخصی سازی شده و در همان حالت پیش‌فرض خودش نصب کرده باشید، Host و Port شما مانند نمونه بالا 127.0.0.1 و 6379 خواهد بود و نیازی به تغییر نیست.
در مرحله بعد برای استفاده از پروایدر ردیس ، اینترفیس IRedisCachingProvider در سرتاسر برنامه در دسترس خواهد بود. این اینترفیس علاوه بر اینکه متد‌های اصلی موجود در EasyCaching را ساپورت کرده ، بخاطر ساختار دیتابیسی که خود ردیس در اختیار ما قرار میدهد قابلیت‌های بیشتری نیز اراعه خواهد داد. این قابلیت‌ها خصیصه‌های ردیس هست چرا که این دیتابیس هم دقیقا شبیه به ساختار سیستم کش Key , Value را پشتیبانی میکند و در پی آن قابلیت هایی برای مدیریت بهتر کلید‌ها و مقادیر اراعه میدهد.
اینترفیس IRedisCachingProvider شامل تعداد زیادی از متد‌ها برای پشتیبانی از قابلیت‌های ردیس است که در ادامه همه آنهارا نام برده و برخی را توضیح مختصری خواهیم داد:
  • متد‌های Keys 
// حذف کردن یک کلید در صورت وجود
bool KeyDel(string cacheKey);
Task<bool> KeyDelAsync(string cacheKey);

// تنظیم تاریخ انتضا به یک کلید موجود بر حسب ثانیه
bool KeyExpire(string cacheKey, int second);
Task<bool> KeyExpireAsync(string cacheKey, int second);

// بررسی وجود یا عدم وجود یک کلید
bool KeyExists(string cacheKey);
Task<bool> KeyExistsAsync(string cacheKey);

// گرفتن زمان انتقضا باقیمانده یک کلید
long TTL(string cacheKey);
Task<long> TTLAsync(string cacheKey);

// جستجو بین همه کلید‌ها براساس فیلتر شامل بودن نام کلید از مقدار ورودی
List<string> SearchKeys(string cacheKey, int? count = null);
  • متد‌های String 
// افزودن یک عدد (پیشقرض 1) به مقدار نوع عددی یک کلید
long IncrBy(string cacheKey, long value = 1);
Task<long> IncrByAsync(string cacheKey, long value = 1);

// افزودن یک عدد (پیشقرض 1) به مقدار نوع عددی یک کلید
double IncrByFloat(string cacheKey, double value = 1);
Task<double> IncrByFloatAsync(string cacheKey, double value = 1);

// تنظیم یک کلید و مقدار وقتی مقدار از نوع رشته باشد
bool StringSet(string cacheKey, string cacheValue, TimeSpan? expiration = null, string when = "");
Task<bool> StringSetAsync(string cacheKey, string cacheValue, TimeSpan? expiration = null, string when = "");

// گرفتن کلید و مقدار آن وقتی مقدار از نوع رشته باشد
string StringGet(string cacheKey);
Task<string> StringGetAsync(string cacheKey);

// گرفتن تعداد کاراکتر‌های مقدار یک کلید وقتی مقدار از نوع رشته باشد
long StringLen(string cacheKey);
Task<long> StringLenAsync(string cacheKey);

// جایگزاری یک رشته درون رشته مقدار یک کلید بعد از شماره کاراکتر مشخص شده در ورودی برای مثال 
// "Hello World"
// 6 , jack
// "Hello jack"
long StringSetRange(string cacheKey, long offest, string value);
Task<long> StringSetRangeAsync(string cacheKey, long offest, string value);

// گرفتن یک بازه از رشته مقدار یک کلید با شماره کاراکتر شروع و پایان
string StringGetRange(string cacheKey, long start, long end);
Task<string> StringGetRangeAsync(string cacheKey, long start, long end);
  • متد‌های Hashes
// شما میتوانید دو کلید با نام‌های یکسان داشته باشید که در کلید تایپ دیکشنری مقدار خود باهم متفاوت هستند
bool HMSet(string cacheKey, Dictionary<string, string> vals, TimeSpan? expiration = null);
Task<bool> HMSetAsync(string cacheKey, Dictionary<string, string> vals, TimeSpan? expiration = null);

// شما میتوانید دو کلید با نام‌های یکسان داشته باشید که در ورودی فیلد باهم متفاوت هستند
bool HSet(string cacheKey, string field, string cacheValue);
Task<bool> HSetAsync(string cacheKey, string field, string cacheValue);

// بررسی وجود یا عدم وجود یک کلید و فیلد
bool HExists(string cacheKey, string field);
Task<bool> HExistsAsync(string cacheKey, string field);

// حذف کردن کلید‌های همنام موجود با همه فیلد‌های متفاوت در حالت پیشفرض مگر اینکه کلید و نام فیلد را بهمراه آن مشخص کنید
long HDel(string cacheKey, IList<string> fields = null);
Task<long> HDelAsync(string cacheKey, IList<string> fields = null);

// گرفتن مقدار با نام کلید و نام فیلد
string HGet(string cacheKey, string field);
Task<string> HGetAsync(string cacheKey, string field);

// گرفتن فیلد و مقدار با کلید
Dictionary<string, string> HGetAll(string cacheKey);
Task<Dictionary<string, string>> HGetAllAsync(string cacheKey);

//  افزودن یک عدد (پیشقرض 1) به مقدار نوع عددی یک کلید و فیلد
long HIncrBy(string cacheKey, string field, long val = 1);
Task<long> HIncrByAsync(string cacheKey, string field, long val = 1);

// گرفتن فیلد‌های متفاوت یک کلید
List<string> HKeys(string cacheKey);
Task<List<string>> HKeysAsync(string cacheKey);

// گرفتن تعداد فیلد‌های متفاوت یک کلید
long HLen(string cacheKey);
Task<long> HLenAsync(string cacheKey);

// گرفتن مقادیر یک کلید بدون در نظر گرفتن فیلد‌های متفاوت
List<string> HVals(string cacheKey);
Task<List<string>> HValsAsync(string cacheKey);

// گرفتن مقدار دیکشنری با کلید و نام فیلد‌ها Dictionary<string, string> HMGet(string cacheKey, IList<string> fields);
Task<Dictionary<string, string>> HMGetAsync(string cacheKey, IList<string> fields);
  • متد‌های List
// گرفتن یک مقدار از لیست مقادیر با شماره ایندکس آن
T LIndex<T>(string cacheKey, long index);
Task<T> LIndexAsync<T>(string cacheKey, long index);

// گرفتن تعداد مقادیر در لیست یک کلید
long LLen(string cacheKey);
Task<long> LLenAsync(string cacheKey);

// گرفتن اولین مقدار از مقادیر یک لیست در یک کلید
T LPop<T>(string cacheKey);
Task<T> LPopAsync<T>(string cacheKey);

// ایجاد یک کلید که لیستی از مقادیر را پشتیبانی میکند و میتوانید هر بار مقدار جدید به لیست آن اضافه کنید
long LPush<T>(string cacheKey, IList<T> cacheValues);
Task<long> LPushAsync<T>(string cacheKey, IList<T> cacheValues);

// گرفتن مقادیر یک لیست از داده بر اساس شماره ایندکس شروع و پایان برای مثال مقادیر ۳ تا ۷ از ۱۰ مقدار
List<T> LRange<T>(string cacheKey, long start, long stop);
Task<List<T>> LRangeAsync<T>(string cacheKey, long start, long stop);

// حذف کردن مقادیر یک لیست بر اساس تعداد وارد شده که بعد از مقدار وارد شده شروع به شمارش میشود
long LRem<T>(string cacheKey, long count, T cacheValue);
Task<long> LRemAsync<T>(string cacheKey, long count, T cacheValue);

// افزودن یک مقدار به لیستی از مقادیر یک کلید با گرفتن شماره ایندکس
bool LSet<T>(string cacheKey, long index, T cacheValue);
Task<bool> LSetAsync<T>(string cacheKey, long index, T cacheValue);

// بررسی میکند که لیست مقداری برای شماره ایندکس شروع و پایان درون خودش دارد یا خیر
bool LTrim(string cacheKey, long start, long stop);
Task<bool> LTrimAsync(string cacheKey, long start, long stop);

//  https://redis.io/commands/lpushx
long LPushX<T>(string cacheKey, T cacheValue);
Task<long> LPushXAsync<T>(string cacheKey, T cacheValue);

// https://redis.io/commands/linsert
long LInsertBefore<T>(string cacheKey, T pivot, T cacheValue);
Task<long> LInsertBeforeAsync<T>(string cacheKey, T pivot, T cacheValue);

// https://redis.io/commands/linsert
long LInsertAfter<T>(string cacheKey, T pivot, T cacheValue);
Task<long> LInsertAfterAsync<T>(string cacheKey, T pivot, T cacheValue);

// https://redis.io/commands/rpushx
long RPushX<T>(string cacheKey, T cacheValue);
Task<long> RPushXAsync<T>(string cacheKey, T cacheValue);

// https://redis.io/commands/rpush
long RPush<T>(string cacheKey, IList<T> cacheValues);
Task<long> RPushAsync<T>(string cacheKey, IList<T> cacheValues);

// https://redis.io/commands/rpop
T RPop<T>(string cacheKey);
Task<T> RPopAsync<T>(string cacheKey);
  • متد‌های Set
// https://redis.io/commands/SAdd
long SAdd<T>(string cacheKey, IList<T> cacheValues, TimeSpan? expiration = null);
Task<long> SAddAsync<T>(string cacheKey, IList<T> cacheValues, TimeSpan? expiration = null);
       
// https://redis.io/commands/SCard
long SCard(string cacheKey);
Task<long> SCardAsync(string cacheKey);

// https://redis.io/commands/SIsMember
bool SIsMember<T>(string cacheKey, T cacheValue);
Task<bool> SIsMemberAsync<T>(string cacheKey, T cacheValue);

// https://redis.io/commands/SMembers
List<T> SMembers<T>(string cacheKey);
Task<List<T>> SMembersAsync<T>(string cacheKey);

// https://redis.io/commands/SPop
T SPop<T>(string cacheKey);
Task<T> SPopAsync<T>(string cacheKey);

// https://redis.io/commands/SRandMember
List<T> SRandMember<T>(string cacheKey, int count = 1);
Task<List<T>> SRandMemberAsync<T>(string cacheKey, int count = 1);

// https://redis.io/commands/SRem
long SRem<T>(string cacheKey, IList<T> cacheValues = null);
Task<long> SRemAsync<T>(string cacheKey, IList<T> cacheValues = null);
  • متد‌های Stored Set
// https://redis.io/commands/ZAdd
long ZAdd<T>(string cacheKey, Dictionary<T, double> cacheValues);
Task<long> ZAddAsync<T>(string cacheKey, Dictionary<T, double> cacheValues);
       
// https://redis.io/commands/ZCard       
long ZCard(string cacheKey);
Task<long> ZCardAsync(string cacheKey);

// https://redis.io/commands/ZCount
long ZCount(string cacheKey, double min, double max);
Task<long> ZCountAsync(string cacheKey, double min, double max);

// https://redis.io/commands/ZIncrBy
double ZIncrBy(string cacheKey, string field, double val = 1);
Task<double> ZIncrByAsync(string cacheKey, string field, double val = 1);

// https://redis.io/commands/ZLexCount
long ZLexCount(string cacheKey, string min, string max);
Task<long> ZLexCountAsync(string cacheKey, string min, string max);

// https://redis.io/commands/ZRange
List<T> ZRange<T>(string cacheKey, long start, long stop);
Task<List<T>> ZRangeAsync<T>(string cacheKey, long start, long stop);

// https://redis.io/commands/ZRank
long? ZRank<T>(string cacheKey, T cacheValue);
Task<long?> ZRankAsync<T>(string cacheKey, T cacheValue);

// https://redis.io/commands/ZRem
long ZRem<T>(string cacheKey, IList<T> cacheValues);
Task<long> ZRemAsync<T>(string cacheKey, IList<T> cacheValues);

// https://redis.io/commands/ZScore
double? ZScore<T>(string cacheKey, T cacheValue);
Task<double?> ZScoreAsync<T>(string cacheKey, T cacheValue);
  • متد‌های Hyperloglog
// https://redis.io/commands/PfAdd
bool PfAdd<T>(string cacheKey, List<T> values);
Task<bool> PfAddAsync<T>(string cacheKey, List<T> values);

// https://redis.io/commands/PfCount
long PfCount(List<string> cacheKeys);
Task<long> PfCountAsync(List<string> cacheKeys);

// https://redis.io/commands/PfMerge
bool PfMerge(string destKey, List<string> sourceKeys);
Task<bool> PfMergeAsync(string destKey, List<string> sourceKeys);
  • متد‌های Geo
// https://redis.io/commands/GeoAdd
long GeoAdd(string cacheKey, List<(double longitude, double latitude, string member)> values);
Task<long> GeoAddAsync(string cacheKey, List<(double longitude, double latitude, string member)> values);

// https://redis.io/commands/GeoDist
double? GeoDist(string cacheKey, string member1, string member2, string unit = "m");
Task<double?> GeoDistAsync(string cacheKey, string member1, string member2, string unit = "m");

// https://redis.io/commands/GeoHash
List<string> GeoHash(string cacheKey, List<string> members);
Task<List<string>> GeoHashAsync(string cacheKey, List<string> members);

// https://redis.io/commands/GeoPos
List<(decimal longitude, decimal latitude)?> GeoPos(string cacheKey, List<string> members);
Task<List<(decimal longitude, decimal latitude)?>> GeoPosAsync(string cacheKey, List<string> members);
برای اطلاعات بیشتر از متد‌های دیگر موجود در ردیس میتوانید از این لینک استفاده کنید. 

3 - پروایدر Hybrid :
این پروایدر، روشی از کشینگ را مابین local caching و distributed caching، ارائه میدهد و میتوانید از یک پروایدر Local مثل InMemory و پروایدر Distributed مثل Redis، همزمان باهم استفاده کنید که در یک کانال باهم و در راستای هم کار میکنند.
اما سوال اینجاست که این قابلیت دقیقا چه کاری انجام میدهد؟
همانطور که قبلا گفته شد، کش In-Memory سرعت بالاتری نسبت به کش Distributed دارد؛ اما دچار معایبی در حالت چند سروری هست که این معایب از جمله حذف شدن دیتای یک سرور، در صورت Down شدن آن، Sync نبودن کش سرور‌ها باهم دیگر و دو نسخه، کش کردن دیتا در هر سرور و موارد دیگری که میتوان نام برد. اما از طرفی کش Distributed مشکلات چند سروری را با قرار دادن یک مرکزیت واحد کش در حافظه شبکه شده سرور‌ها برطرف میکند و اطلاعات سرور‌ها، از یک منبع خوانده میشود و طبعا مشکلات In-Memory را نخواهیم داشت؛ اما به دلیل رد و بدل شدن دیتا در محیط شبکه و عمل Serialize , Deserialize که هنگام عبور دیتا روی آن صورت میگیرد، بخشی از سرعت، کاهش خواهد یافت و درنهایت Performance کمتری را نسبت به In-Memory ارائه میدهد.
حالا برای اینکه بتوانیم سیستم کش خودمان را طوری طراحی کنیم که عیب‌های (Local)In-Memory و Distributed را نداشته باشیم و بتوانیم از هریک به شکلی درست استفاده کنیم که هم اطلاعاتمان Sync باشد و هم از سرعت بالای In-Memory برخوردار شویم، میتوانیم از پروایدر Hybrid استفاده کنیم. 

شیوه کار این پروایدر به این صورت است که وقتی برنامه برای بار اول به کش In-Memory درخواستی را ارسال میکند و کش مورد نظر در آن وجود ندارد، برنامه یک درخواست دیگر را به کش Distributed ارسال میکند و دیتای مورد نظر را به کاربر بازگشت میدهد و علاوه بر آن یک کپی از کش آن دیتا، در کش In-Memory هم ایجاد میکند. با این ساختار از دفعات بعد که کاربر درخواستی را ارسال کند، دیتای درخواستی در In-Memory نیز موجود خواهد بود و سریع‌تر از بار اول پاسخ را ارسال خواهد کرد.
از طرفی نیز وقتی کاربر دیتای جدیدی را ذخیره میکند، ابتدا آن دیتا در In-Memory کش شده و سپس با درخواست خود پروایدر، در کش Distributed هم اعمال میشود تا در نهایت دیتابیس نیز آن را ذخیره کند.
وقتی این اتفاق می‌افتد، پروایدر Hybrid با کمک پکیج Bus.Redis به کش In-Memory سرور‌های دیگر دستور Pull کردن دیتا کش‌های جدید را ارسال میکند و در نهایت همه سرور‌ها نیز به کمک Distributed مرکزی باهم Sync خواهند بود.

برای فعال سازی این پروایدر باید پکیج‌های زیر را در برنامه خود نصب کنید: 
Install-Package EasyCaching.HybridCache
Install-Package EasyCaching.InMemory
Install-Package EasyCaching.Redis
Install-Package EasyCaching.Bus.Redis
در این مجموعه از پکیج‌ها، از یک پروایدر Local(InMemory) و یک پروایدر distributed(Redis) استفاده شده و همانطور که گفته شد، مدیریت هماهنگ سازی این دو، توسط پکیج دیگری بنام EasyCaching.Bus.Redis صورت میگیرد.

تنظیمات فعالسازی این پروایدر هم متشکل از تنظیمات دو پروایدر In-Memory و Redis، بعلاوه معرفی این دو به هم در متد UseHybrid خواهد بود. 
   services.AddEasyCaching(option =>
       // local
       option.UseInMemory("c1");

       // distributed
       option.UseRedis(config =>
                config.DBConfig.Endpoints.Add(new ServerEndPoint("127.0.0.1", 6379));
       }, "c2");

       // combine local and distributed
        option.UseHybrid(config =>
                 // specify the local cache provider name after v0.5.4
                   config.LocalCacheProviderName = "c1"
                // specify the distributed cache provider name after v0.5.4
                   config.DistributedCacheProviderName = "c2"
        });

          // use redis bus
           .WithRedisBus(busConf =>
                   busConf.Endpoints.Add(new ServerEndPoint("127.0.0.1", 6379));
           });
});
برای استفاده از این پروایدر، متفاوت با پروایدر‌های قبلی، باید اینترفیس IHybridCachingProvider را فراخوانی کنیم. متد‌های موجود در این اینترفیس، همان متدهایی است که در اینترفیس IEasyCachingProvider وجود دارند و از نظر نام متد و روش استفاده، تفاوتی میان آن نیست.

 پیشنهاد شخصی در Distributed Cache‌ها 
همانطور که گفته شد Distributed کش‌ها، گزینه مناسب‌تری برای برنامه‌های چند سروری هستند؛ اما در این حالت مواردی مثل Round Trip شبکه و جابجایی اطلاعات در این محیط بعلاوه Serialize , Deserialize هایی که باید انجام شوند دلیلی میشود تا سرعت آن در پاسخ به درخواست‌های برنامه، نسبت به حالت تک سروری(In-Memory) کمتر باشد. Hybrid Provider یکی از روش‌های حل این مشکل بوده که معرفی کردیم. اما برای اینکه تیر خلاص را به پیکره سیستم Distributed Cache خود بزنید و تریک فنی آخر را نیز روی آن اجرا کنید، پیشنهاد میکنم از پکیج EasyCaching.Extensions.EasyCompressor که بر پایه پکیج EasyCaching نوشته شده استفاده کنید. این پکیج، اطلاعات را قبل از کش شدن، فشرده سازی میکند و حجم اطلاعات را به طور محسوسی کاهش میدهد که میزان فضای اشغالی Ram را کم کرده و همچنین عمل جابجایی اطلاعات را نیز تسریع می‌بخشد. میتوانید از این پکیج هم در Redis و هم در Hybrid استفاده کنید. چگونگی استفاده از آن نیز در لینک Github ذکر شده موجود است.

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

هنگامی که درباره‌ی علم و یادگیری ماشینی فکر می‌کنیم، دو زبان برنامه‌نویسی بلافاصله به ذهن متبادر می‌شوند: پایتون و R. این دو زبان به شکل عمومی از بسیاری از الگوریتم‌های یادگیری ماشین رایج، تکنیکهای پیش‌پردازش داده‌ها و خیلی بیشتر از اینها پشتیبانی می‌کنند؛ بنابراین برای -تقریباً- هر مساله‌ی یادگیری ماشینی مورد استفاده قرار می‌گیرند. 
 با این‌حال، گاهی فرد یا شرکتی نمی‌تواند از پایتون یا R استفاده کند که می‌تواند به یکی از دلایل متعدد، از جمله وجود کد منبع در زبان دیگر یا نداشتن هیچ تجربه‌ای در پایتون یا R باشد. یکی از محبوب‌ترین زبان‌های امروزی، #C است که برای بسیاری از کاربردها مورد استفاده قرار می‌گیرد. مایکروسافت برای استفاده از قدرت یادگیری ماشین در #C، یک بسته را به نام ML.NET ایجاد کرده که همه‌ی قابلیت‌های یادگیری ماشین پایه را فراهم می‌کند. 
در این مقاله، به شما نشان خواهم داد که چگونه از ML.NET برای ایجاد یک مدل دسته‌بندی دوتایی بهره ببرید؛ قابلیت‌های AutoML را مورد استفاده قرار داده و از یک مدل Tensorflow با ML.NET استفاده کنید. کد کامل مخصوص مدل دسته‌بندی دوتایی را می‌توانید در GitHub بیابید.

افزودن ML.NET به پروژه‌ی #C
اضافه کردن ML.NET به یک پروژه‌ی #C یا #F آسان است. تنها کار لازم، اضافه کردن بسته‌ی Microsoft.ML یا در برخی موارد، -بسته به نیازمندی‌های پروژه- بسته‌های اضافی مانند: Microsoft.ML.ImageAnalytics, Microsoft.ML.TensorFlow یا Microsoft.ML.OnnxTransformer است. 


بارگذاری در یک دیتاست و ایجاد pipeline داده‌ها
بارگذاری و پیش‌پردازش یک مجموعه داده در ML.NET کاملا ً متفاوت از زمانی است که با دیگر بسته‌ها / چارچوب‌های یادگیری ماشین کار می‌کنیم. چون ما نیاز داریم به طور واضح، ساختار داده‌ها را بیان کنیم. برای انجام این کار، فایلی به نام ModelInput.cs را درون یک پوشه به نام DataModels ایجاد کرده و داخل این فایل، همه‌ی ستون‌های مجموعه داده‌های خود را ثبت خواهیم کرد. برای این مقاله، ما از مجموعه داده‌های ردیابی کلاه‌برداری کارت اعتباری استفاده می‌کنیم که می‌تواند آزادانه از Kaggle بارگیری شود. این مجموعه‌داده‌ها شامل ۳۱ ستون است. کلاس تراکنش (۰ یا ۱)، مقدار تراکنش، زمان تراکنش و نیز ۲۸ ویژگی بی‌نام (anonymous). 


using Microsoft.ML.Data;

namespace CreditCardFraudDetection.DataModels
{
    public class ModelInput
    {
        [ColumnName("Time"), LoadColumn(0)]
        public float Time { get; set; }

        [ColumnName("V1"), LoadColumn(1)]
        public float V1 { get; set; }

        [ColumnName("V2"), LoadColumn(2)]
        public float V2 { get; set; }

        [ColumnName("V3"), LoadColumn(3)]
        public float V3 { get; set; }

        [ColumnName("V4"), LoadColumn(4)]
        public float V4 { get; set; }

        [ColumnName("V5"), LoadColumn(5)]
        public float V5 { get; set; }

        [ColumnName("V6"), LoadColumn(6)]
        public float V6 { get; set; }

        [ColumnName("V7"), LoadColumn(7)]
        public float V7 { get; set; }

        [ColumnName("V8"), LoadColumn(8)]
        public float V8 { get; set; }

        [ColumnName("V9"), LoadColumn(9)]
        public float V9 { get; set; }

        [ColumnName("V10"), LoadColumn(10)]
        public float V10 { get; set; }

        [ColumnName("V11"), LoadColumn(11)]
        public float V11 { get; set; }

        [ColumnName("V12"), LoadColumn(12)]
        public float V12 { get; set; }

        [ColumnName("V13"), LoadColumn(13)]
        public float V13 { get; set; }

        [ColumnName("V14"), LoadColumn(14)]
        public float V14 { get; set; }

        [ColumnName("V15"), LoadColumn(15)]
        public float V15 { get; set; }

        [ColumnName("V16"), LoadColumn(16)]
        public float V16 { get; set; }

        [ColumnName("V17"), LoadColumn(17)]
        public float V17 { get; set; }

        [ColumnName("V18"), LoadColumn(18)]
        public float V18 { get; set; }

        [ColumnName("V19"), LoadColumn(19)]
        public float V19 { get; set; }

        [ColumnName("V20"), LoadColumn(20)]
        public float V20 { get; set; }

        [ColumnName("V21"), LoadColumn(21)]
        public float V21 { get; set; }

        [ColumnName("V22"), LoadColumn(22)]
        public float V22 { get; set; }

        [ColumnName("V23"), LoadColumn(23)]
        public float V23 { get; set; }

        [ColumnName("V24"), LoadColumn(24)]
        public float V24 { get; set; }

        [ColumnName("V25"), LoadColumn(25)]
        public float V25 { get; set; }

        [ColumnName("V26"), LoadColumn(26)]
        public float V26 { get; set; }

        [ColumnName("V27"), LoadColumn(27)]
        public float V27 { get; set; }

        [ColumnName("V28"), LoadColumn(28)]
        public float V28 { get; set; }

        [ColumnName("Amount"), LoadColumn(29)]
        public float Amount { get; set; }

        [ColumnName("Class"), LoadColumn(30)]
        public bool Class { get; set; }
    }
} 
در اینجا یک فیلد را برای هر یک از ستون‌های داخل مجموعه داده‌مان ایجاد می‌کنیم. نکته‌ی مهم، تعیین شاخص (Index)، نوع و ستون، به شکل صحیح است. حالا که داده‌های ما مدل‌سازی شده‌اند، باید قالب و شکل داده‌های خروجی خود را مدل کنیم. این کار می‌تواند به روشی مشابه با کدهای بالا انجام شود. 
 using Microsoft.ML.Data;

namespace CreditCardFraudDetection.DataModels
{
    public class ModelOutput
    {
        [ColumnName("PredictedLabel")]
        public bool Prediction { get; set; }

        public float Score { get; set; }
    }
}  
ما در این‌جا ۲ فیلد داریم. فیلد score نشان‌دهنده‌ی خروجی به شکل درصد است؛ در حالیکه فیلد prediction از نوع بولی است. اکنون که هر دو داده ورودی و خروجی را مدل‌سازی کرده‌ایم، می‌توانیم داده‌های واقعی خود را با استفاده از روش مونت‌کارلو بارگذاری کنیم.
IDataView trainingDataView = mlContext.Data.LoadFromTextFile<ModelInput>(
                                            path: dataFilePath,
                                            hasHeader: true,
                                            separatorChar: ',',
                                            allowQuoting: true,
                                            allowSparse: false);

ساخت و آموزش مدل
برای ایجاد و آموزش مدل، نیاز به ایجاد یک pipeline داریم که شامل پیش‌پردازش داده‌های مورد نیاز و الگوریتم آموزش است. برای این مجموعه داده‌ی خاص، انجام هر پیش‌پردازش بسیار دشوار است زیرا ۲۸ ویژگی بی‌نام دارد. بنابراین تصمیم گرفتم که آن را ساده نگه دارم و تنها همه‌ی ویژگی‌ها را الحاق کنم (این کار باید در ML.NET انجام شود).
var dataProcessPipeline = mlContext.Transforms.Concatenate("Features", new[] { "Time", "V1", "V2", "V3", "V4", "V5", "V6", "V7", "V8", "V9", "V10", "V11", "V12", "V13", "V14", "V15", "V16", "V17", "V18", "V19", "V20", "V21", "V22", "V23", "V24", "V25", "V26", "V27", "V28", "Amount" });
برای مدل، الگوریتم LightGBM را انتخاب می‌کنم. این الگوریتم در واقع در Microsoft.ML از ابتدا وجود ندارد؛ بنابراین شما باید Microsoft.ML.LightGbm را نصب کنید تا قادر باشید از آن استفاده کنید.
// Choosing algorithm
var trainer = mlContext.BinaryClassification.Trainers.LightGbm(labelColumnName: "Class", featureColumnName: "Features");

// Appending algorithm to pipeline
var trainingPipeline = dataProcessPipeline.Append(trainer);
اکنون می‌توانیم مدل را با متد Fit، آموزش داده سپس با استفاده از mlContext.model.save ذخیره کنیم:
ITransformer model = trainingPipeline.Fit(trainingDataView);
mlContext.Model.Save(model , trainingDataView.Schema, <path>);

ارزیابی مدل
حالا که مدل ما آموزش دیده است، باید عملکرد آن را بررسی کنیم. ساده‌ترین راه برای انجام این کار، استفاده از اعتبارسنجی متقاطع (cross-validation) است. ML.Net به ما روش‌های اعتبارسنجی متقاطع را برای انواع مختلف داده‌های مختلف، ارایه می‌دهد. از آنجا که مجموعه داده‌های ما یک مجموعه داده دسته‌بندی دودویی است، ما از روش mlContext.BinaryClassification.CrossValidateNonCalibrated برای امتیازدهی به مدل خود استفاده خواهیم کرد:
var crossValidationResults = mlContext.BinaryClassification.CrossValidateNonCalibrated(trainingDataView, trainingPipeline, numberOfFolds: 5, labelColumnName: "Class");

انجام پیش‌بینی
پیش بینی داده‌های جدید با استفاده از ML.NET واقعاً سرراست و راحت است. ما فقط باید یک PredictionEngine، نمایشی دیگر را از مدل خود که به طور خاص، برای استنباط ساخته شده است، ایجاد کنیم و متد Predict آن را به عنوان یک شی ModelInput فراخوانی کنیم. 
var predEngine = mlContext.Model.CreatePredictionEngine<ModelInput, ModelOutput>(mlModel);

ModelInput sampleData = new ModelInput() {
    time = 0,
    V1 = -1.3598071336738,
    ...
};

ModelOutput predictionResult = predEngine.Predict(sampleData);

Console.WriteLine($"Actual value: {sampleData.Class} | Predicted value: {predictionResult.Prediction}"); 

Auto-ML 
نکته جالب دیگر در مورد ML.NET اجرای عالی Auto ML است. با استفاده از  Auto ML  فقط با مشخص کردن اینکه روی چه مشکلی کار می‌کنیم و ارائه داده‌های خود، می‌توانیم راه‌حل‌های اساسی و پایه‌ی یادگیری ماشین را بسازیم.
برای شروع کار با ML خودکار در ML.NET، باید  Visual Studio Extension - ML.NET Model Builder (Preview)  را بارگیری کنیم. این کار را می‌توان از طریق تب extensions انجام داد.
پس از نصب موفقیت آمیز افزونه، با کلیک راست روی پروژه‌ی خود در داخل Solution Ex می‌توانیم از Auto ML استفاده کنیم.

با این کار پنجره Model Builder باز می‌شود. سازنده‌ی مدل، ما را در روند ساخت یک مدل یادگیری ماشین راهنمایی می‌کند.
   
برای کسب اطلاعات در مورد چگونگی گذراندن مراحل مختلف، حتماً آموزش رسمی شروع کار را در سایت مایکروسافت، بررسی کنید. بعد از تمام مراحل، Model Builder به طور خودکار کد را تولید می‌کند.
 
استفاده از یک مدل پیش‌آموزش‌داده‌شده‌ی تنسورفلو (pre-trained) 
نکته‌ی جالب دیگر در مورد ML.NET این است که به ما امکان استفاده از مدل‌های Tensorflow و ONNX را برای استنباط ( inference ) می‌دهد. برای استفاده از مدل Tensorflow باید Microsoft.ML.TensorFlow را با استفاده از NuGet نصب کنیم. پس از نصب بسته‌های لازم، می‌توانیم با فراخوانی متد Model.LoadTensorFlowModel، یک مدل Tensorflow را بارگذاری کنیم. پس از آن، باید متد ScoreTensorFlowModel را فراخوانی کرده و نام لایه‌ی ورودی و خروجی را به آن ارسال کنیم.  
private ITransformer SetupMlnetModel(string tensorFlowModelFilePath)
{
    var pipeline = _mlContext.<preprocess-data>
           .Append(_mlContext.Model.LoadTensorFlowModel(tensorFlowModelFilePath)
                                               .ScoreTensorFlowModel(
                                                      outputColumnNames: new[]{TensorFlowModelSettings.outputTensorName },
                                                      inputColumnNames: new[] { TensorFlowModelSettings.inputTensorName },
                                                      addBatchDimensionInput: false));
 
    ITransformer mlModel = pipeline.Fit(CreateEmptyDataView());
 
    return mlModel;
}

اطلاعات بیشتر در مورد نحوه استفاده از مدل Tensorflow در ML.NET:
مطالب دوره‌ها
تزریق وابستگی‌های AutoMapper در لایه سرویس برنامه
اگر مطلب «Refactoring به تزریق وابستگی‌ها» را به خاطر داشته باشید، جهت تشخیص وابستگی‌های یک کلاس، کار از بررسی کلمات new و همچنین فراخوانی‌های استاتیک، شروع می‌شود و ... متد استاتیک Mapper.Map کتابخانه‌ی AutoMapper نیز از همین دست است. در ادامه قصد داریم بجای فراخوانی مستقیم Mapper.Map از اینترفیس IMappingEngine به عنوان تامین کننده‌ی متد Map استفاده کنیم. همچنین کلاس‌های Profile نوشته شده را نیز به صورت خودکار به برنامه اضافه نمائیم.


تنظیمات IoC Container مختص به AutoMapper

در ذیل یک کلاس Registry مخصوص StructureMap را مشاهده می‌کنید که جهت کپسوله کردن اطلاعات خاص AutoMapper تهیه شده‌است. می‌توان این اطلاعات را در داخل تنظیمات new Container خود قرار داد و یا می‌توان آن‌ها را جهت شلوغ نشدن سایر تنظیمات IoC Container، به یک کلاس Registry منتقل کرد:
public class AutomapperRegistry : Registry
{
    public AutomapperRegistry()
    {
        var platformSpecificRegistry = PlatformAdapter.Resolve<IPlatformSpecificMapperRegistry>();
        platformSpecificRegistry.Initialize();
 
        For<ConfigurationStore>().Singleton().Use<ConfigurationStore>()
            .Ctor<IEnumerable<IObjectMapper>>().Is(MapperRegistry.Mappers);
 
        For<IConfigurationProvider>().Use(ctx => ctx.GetInstance<ConfigurationStore>());
 
        For<IConfiguration>().Use(ctx => ctx.GetInstance<ConfigurationStore>());
 
        For<ITypeMapFactory>().Use<TypeMapFactory>();
 
        For<IMappingEngine>().Singleton().Use<MappingEngine>()
                             .SelectConstructor(() => new MappingEngine(null));
 
        this.Scan(scanner =>
        {
            scanner.AssembliesFromApplicationBaseDirectory();
 
            scanner.ConnectImplementationsToTypesClosing(typeof(ITypeConverter<,>))
                   .OnAddedPluginTypes(t => t.HybridHttpOrThreadLocalScoped());
 
            scanner.ConnectImplementationsToTypesClosing(typeof(ValueResolver<,>))
                .OnAddedPluginTypes(t => t.HybridHttpOrThreadLocalScoped());
        });
    }
}
هدف اصلی، وهله سازی خودکار IMappingEngine است و برای رسیدن به آن، باید تمام وابستگی‌های کلاس MappingEngine را مانند IConfigurationProvider و سایر مواردی که مشاهده می‌کنید، مشخص کرد. پس از این تنظیمات، کلاس ObjectFactory سفارشی برنامه به شکل ذیل جهت معرفی AutomapperRegistry تغییر خواهد کرد:
public static class SmObjectFactory
{
    private static readonly Lazy<Container> _containerBuilder =
        new Lazy<Container>(defaultContainer, LazyThreadSafetyMode.ExecutionAndPublication);
 
    public static IContainer Container
    {
        get { return _containerBuilder.Value; }
    }
 
    private static Container defaultContainer()
    {
        var container = new Container(cfg =>
        {
            cfg.AddRegistry<AutomapperRegistry>();
            cfg.Scan(scan =>
            {
                scan.TheCallingAssembly();
                scan.WithDefaultConventions();
                scan.AddAllTypesOf<Profile>().NameBy(item => item.FullName);
            });
        });
 
        configureAutoMapper(container);
 
        return container;
    }
 
    private static void configureAutoMapper(IContainer container)
    {
        var configuration = container.TryGetInstance<IConfiguration>();
        if (configuration == null) return;
        //saying AutoMapper how to resolve services
        configuration.ConstructServicesUsing(container.GetInstance);
        foreach (var profile in container.GetAllInstances<Profile>())
        {
            configuration.AddProfile(profile);
        }
    }
}
در اینجا علاوه بر معرفی AutomapperRegistry، یک مورد دیگر نیز اضافه شده‌است: یافتن خودکار کلاس‌هایی از نوع Profile و همچنین فراخوانی متد AddProfile کتابخانه‌ی AutoMapper به صورت خودکار. به این ترتیب دیگر نیازی نخواهد بود تا در ابتدای کار برنامه، متد Mapper.Initialize را جهت معرفی کلاس‌های Profile فراخوانی کرد و این‌کار به صورت خودکار توسط متد configureAutoMapper انجام می‌شود.


تغییرات لایه سرویس برنامه جهت استفاده از IoC Container

اکنون که IoC Container ما با نحوه‌ی یافتن وابستگی‌های IMappingEngine آشنا شده‌است، تنها کافی است این اینترفیس را در سازنده‌ی کلاس سرویس خود تزریق کنیم:
public class UsersService : IUsersService
{
    private readonly IMappingEngine _mappingEngine;
 
    public UsersService(IMappingEngine mappingEngine)
    {
        _mappingEngine = mappingEngine;
    }
 
    public UserViewModel GetName(int id)
    {
        var dbUser1 = new User
        {
            Id = 1,
            Name = "Test",
            RegistrationDate = DateTime.Now.AddDays(-10)
        };
 
        var uiUser = new UserViewModel();
        _mappingEngine.Map(source: dbUser1, destination: uiUser);
        return uiUser;
    }
}
و پس از آن از متد Map این اینترفیس بجای فراخوانی مستقیم Mapper.Map می‌توان استفاده کرد. به این ترتیب وابستگی مورد نیاز این کلاس، از طریق سازنده‌ی آن به آن تزریق شده‌است و دیگر فراخوانی‌های استاتیک را در اینجا مشاهده نمی‌کنیم.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید:
AM_Sample03.zip
نظرات مطالب
بررسی روش آپلود فایل‌ها در ASP.NET Core
چند نکته‌ی تکمیلی
چگونه فایل دریافتی را تبدیل به آرایه‌ای از بایت‌ها کنیم؟
public async Task<IActionResult> FileUpload(IFormFile file)
        {
            if (file == null || file.Length == 0)
            {
                return BadRequest();
            }

            using (var memoryStream = new MemoryStream())
            {
                await file.CopyToAsync(memoryStream);
                var fileBytes = memoryStream.ToArray();
                // ... save it
            }
        }

چگونه فایل تصویر دریافتی را تبدیل به یک شیء تصویر کنیم؟
public async Task<IActionResult> FileUpload(IFormFile file)
        {
            if (file == null || file.Length == 0)
            {
                return BadRequest();
            }

            using (var memoryStream = new MemoryStream())
            {
                await file.CopyToAsync(memoryStream);
                using (var img = Image.FromStream(memoryStream))
                {
                  // TODO: ResizeImage(img, 100, 100);
                }
            }
        }
مطالب
اضافه کردن OData به پروژه‌های ASP.NET Core 3.1 با اضافه کردن فقط 20 کلمه به کد!
به مناسبت ارائه‌ی نسخه 7.4 از Microsoft.AspNetCore.OData که دیروز صورت پذیرفت، تصمیم گرفتم آموزش استفاده از OData را در پروژه‌های ASP.NET Core 3.1 به بالا که دارای endpoint routing هستند (روش توصیه شده)، تهیه کنم تا در آن، پروژه کمترین تغییر ممکن را برای اضافه شدن OData داشته باشد و ببینیم که استفاده از آن در نسخه‌های جدید، به چه میزان آسان شده است.

ابتدا با dotnet --version و یا dotnet --info و یا هر روش دیگری، از نصب بودن dot net core 3.1 sdk مطمئن می‌شویم. سپس دستور
dotnet new webapi -o SampleApi
را می‌زنیم.

در ویژوال استودیو، این دستور معادل ساخت پروژه‌ای جدید از نوع ASP.NET Core Web Application است که در دیالوگ بعدی، از بین گزینه‌های Empty، Api و Web Application و ... ما گزینه‌ی Api را انتخاب می‌کنیم.
این یک پروژه‌ی Web Api و با استفاده از endpoint routing است. در این پروژه یک WeatherForecast وجود دارد که نقش مدل را ایفا می‌کند و یک WeatherForecastController که در Get خود، تعدادی از WeatherForecastها را ساخته و باز می‌گرداند. این پروژه فاقد دیتابیس است.

حال چه کنیم که این پروژه OData enabled شود؟
1- دو Package زیر را نصب می‌کنیم:
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.3" />
<PackageReference Include="Microsoft.AspNetCore.OData" Version="7.4.0" />

2- در Startup.cs ابتدا
using Microsoft.AspNet.OData.Extensions;
را در بالای فایل اضافه کرده، و کدهای ConfigureServices را به شکل زیر تغییر می‌دهیم:
services.AddControllers().AddNewtonsoftJson(); // Add .AddNewtonsoftJson() to services.AddControllers();
services.AddOData(); // add this new line
همچنین کد app.UseEndpoints را به صورت زیر تغییر می‌دهیم:
app.UseEndpoints(endpoints => // Existing code
{   
    endpoints.MapControllers(); // Existing code
    endpoints.EnableDependencyInjection(); // Add this new line
    endpoints.Select().Expand().Filter().OrderBy().Count().MaxTop(20); // Add this new line
});

سپس در WeatherForecastController و متد Get، کد را به شکل زیر تغییر دهید:
// replace:
[HttpGet] // Existing code  
// with:
[HttpGet, EnableQuery] // new code
برنامه را اجرا کنید و آدرس زیر را بزنید:
وجود orderby=TemperatureC باعث می‌شود که WeatherForcastها، مرتب شده بر اساس درجه سانتیگرادی که دارند، برای کلاینت ارسال شوند.

همانطور که مشاهده کردید، به ساده‌ترین شکل ممکن، OData به پروژه اضافه شد!

به صورت کلی OData امکان فیلتر کردن رکوردهای بازگشتی، Projection، مرتب سازی، Paging، گروه بندی، Aggregation و ... را دارد که در ادامه چند مثالی را با هم می‌بینیم. 
فقط با توجه به اینکه مثال پیش فرض ASP.NET Core، یعنی WeatherForecast کمی گنگ و غیر متداول است، از آن میگذریم و با فرض لیستی از مشتریان با ساختار زیر پیش می‌رویم:
public class Customer
{
    public int Id { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public Gender Gender { get; set; }

    public AddressInfo Address { get; set; }
}

public class AddressInfo
{
    public int StreetNo { get; set; }

    public string PostalCode { get; set; }
}

public enum Gender
{
    Man, Woman, Other
}  
برای بازگردادن مشتریان خانم، داریم: (آموزش فیلتر بر روی enum)
?$filter=Gender eq 'Woman'
برای مرتب سازی مشتریان خانم بر اساس شماره خیابان آدرس آنها: (آموزش مرتب سازی بر روی Nested Properties و ترکیب orderby و filter)
?$filter=Gender eq 'Woman'&$orderby=Address/StreetNo
برای بازگرداندن مشتریانی که در اسم آنها کلمه‌ی ali وجود دارد:
?$filter=contains(FirstName,'Ali')
برای مشاهده لیست امکانات کوئری گیری و مثال‌های بیشتر می‌توانید به این لینک مراجعه کنید.

امکان کوئری گیری به صورت مفصل وجود دارد و می‌توانید مواردی چون Any / All را نیز در فیلترهای خود بگنجانید.
به علاوه اگر به جای یک آرایه یا لیست، یک IQueryable از EntityFrameworkCore گرفته و ... بازگردانید، OData کوئری ارسالی را روی IQueryable مربوطه اعمال و paging و orderby و ... تماما روی دیتابیس انجام و فقط دیتای لازم و مورد نیاز از دیتابیس خوانده شده و به کلاینت باز می‌گردد که این مهم در بهبود عملکرد برنامه بسیار موثر است.

نکته مهم این است که در این روش، امنیت برنامه به خطر نمیافتد؛ زیرا اگر شما بر اساس منطق برنامه، یک Where را سمت سرور اعمال کنید، Client فقط میتواند روی دیتایی که حق دارد ببیند Paging و OrderBy و... اعمال کند، نه اینکه دیتای بیشتری را از سرور دریافت کند.
همچنین در این روش، استفاده از Swagger، Routing و ... تماما به روشی است که همین الان آن را بلدید، در کنار رعایت best practiceهایی چون بازگرداندن Dto به جای Entity و ... نیز کاملا امکان پذیر است.
نظرات مطالب
اشیاء تغییر ناپذیر (Immutable Object)
یک نکته تکمیلی
 public class SallowMutable
    {
        public int Value { get; }
        
        //Mutable
        public StringBuilder NameBuilder { get; }

        public SallowMutable(int value, StringBuilder nameBuilder)
        {
            Value = value;
            NameBuilder = nameBuilder;
        }
    }
کلاس بالا به ظاهر به صورت Immutable پیاده سازی شده است  و اگر کلاینت به صورت زیر از کلاس بالا استفاده کند متوجه خواهید شد این پیاده سازی Immutable نیست زیرا StringBuilder از نوع Mutable است.
 public class UsageOfSallowMutable
    {
        public static void Main()
        {
            StringBuilder x=new StringBuilder();
            var sm=new SallowMutable(10,x);
            x.Append("Ali");
            sm.NameBuilder.Append(" Reza");
            Console.WriteLine(sm.NameBuilder);// Ali Reza
        }
    }