مطالب
ASP.NET MVC #5

بررسی نحوه انتقال اطلاعات از یک کنترلر به View‌های مرتبط با آن

در ASP.NET Web forms در فایل code behind یک فرم مثلا می‌توان نوشت Label1.Text و سپس مقداری را به آن انتساب داد. اما اینجا به چه ترتیبی می‌توان شبیه به این نوع عملیات را انجام داد؟ با توجه به اینکه در کنترلر‌ها هیچ نوع ارجاع مستقیمی به اشیاء رابط کاربری وجود ندارد و این دو از هم مجزا شده‌اند.
در پاسخ به این سؤال، همان مثال ساده قسمت قبل را ادامه می‌دهیم. یک پروژه جدید خالی ایجاد شده است به همراه HomeController ایی که به آن اضافه کرده‌ایم. همچنین مطابق روشی که ذکر شد، View ایی به نام Index را نیز به آن اضافه کرده‌ایم. سپس برای ارسال اطلاعات از یک کنترلر به View از یکی از روش‌های زیر می‌توان استفاده کرد:

الف) استفاده از اشیاء پویا

ViewBag یک شیء dynamic است که در دات نت 4 امکان تعریف آن میسر شده است. به این معنا که هر نوع خاصیت دلخواهی را می‌توان به این شیء انتساب داد و سپس این اطلاعات در View نیز قابل دسترسی و استخراج خواهد بود. مثلا اگر در اینجا به شیء ViewBag، خاصیت دلخواه Country را اضافه کنیم و سپس مقداری را نیز به آن انتساب دهیم:

using System.Web.Mvc;

namespace MvcApplication1.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
ViewBag.Country = "Iran";
return View();
}
}
}

این اطلاعات در View مرتبط با اکشنی به نام Index به نحو زیر قابل بازیابی خواهد بود (نحوه اضافه کردن View متناظر با یک اکشن یا متد را هم در قسمت قبل با تصویر مرور کردیم):

@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
<p>
Country : @ViewBag.Country
</p>

در این مثال، @ در View engine جاری که Razor نام دارد، به این معنا می‌باشد که این مقداری است که می‌خواهم دریافت کنی (ViewBag.Country) و سپس آن‌را در حین پردازش صفحه نمایش دهی.


ب) انتقال اطلاعات یک شیء کامل و غیر پویا به View

هر پروژه جدید MVC به همراه پوشه‌ای به نام Models است که در آن می‌توان تعاریف اشیاء تجاری برنامه را قرار داد. در پروژه جاری، یک کلاس ساده را به نام Employee به این پوشه اضافه می‌کنیم:

namespace MvcApplication1.Models
{
public class Employee
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
}
}

اکنون برای نمونه یک وهله از این شیء را در متد Index ایجاد کرده و سپس به view متناظر با آن ارسال می‌کنیم (در قسمت return View کد زیر مشخص است). بدیهی است این وهله سازی در عمل می‌تواند از طریق دسترسی به یک بانک اطلاعاتی یا یک وب سرویس و غیره باشد.

using System.Web.Mvc;
using MvcApplication1.Models;

namespace MvcApplication1.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
ViewBag.Country = "Iran";

var employee = new Employee
{
Email = "name@site.com",
FirstName = "Vahid",
LastName = "N."
};

return View(employee);
}
}
}

امضاهای متفاوت (overloads) متد کمکی View هم به شرح زیر هستند:

ViewResult View(Object model)
ViewResult View(string viewName, Object model)
ViewResult View(string viewName, string masterName, Object model)


اکنون برای دسترسی به اطلاعات این شیء employee در View متناظر با این متد، چندین روش وجود دارد:

@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
<div>
Country: @ViewBag.Country <‪br />
FirstName: @Model.FirstName
</div>

می‌توان از طریق شیء استاندارد دیگری به نام Model (که این هم یک شیء dynamic است مانند ViewBag قسمت قبل)، به خواص شیء یا مدل ارسالی به View جاری دسترسی پیدا کرد که یک نمونه از آن‌را در اینجا ملاحظه می‌کنید.
روش دوم، بر اساس تعریف صریح نوع مدل است به نحو زیر:

@model MvcApplication1.Models.Employee
@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
<div>
Country: @ViewBag.Country
<‪br />
FirstName: @Model.FirstName
</div>

در اینجا در مقایسه با قبل، تنها یک سطر به اول فایل View اضافه شده است که در آن نوع شیء Model تعیین می‌گردد (کلمه model هم در اینجا با حروف کوچک شروع شده است). به این ترتیب اینبار اگر سعی کنیم به خواص این شیء دسترسی پیدا کنیم، Intellisense ویژوال استودیو ظاهر می‌شود. به این معنا که شیء Model بکارگرفته شده اینبار دیگر dynamic نیست و دقیقا می‌داند که چه خواصی را باید پیش از اجرای برنامه در اختیار استفاده کننده قرار دهد.
به این روش، روش Strongly typed view هم گفته می‌شود؛ چون View دقیقا می‌داند که چون نوعی را باید انتظار داشته باشد؛ تحت نظر کامپایلر قرار گرفته و همچنین Intellisense نیز برای آن مهیا خواهد بود.
به همین جهت این روش Strongly typed view، در بین تمام روش‌های مهیا، به عنوان روش توصیه شده و مرجح مطرح است.
به علاوه استفاده از Strongly typed views یک مزیت دیگر را هم به همراه دارد: فعال شدن یک code generator توکار در VS.NET به نام scaffolding. یک مثال ساده:
تا اینجا ما اطلاعات یک کارمند را نمایش دادیم. اگر بخواهیم یک لیست از کارمندها را نمایش دهیم چه باید کرد؟
روش کار با قبل تفاوتی نمی‌کند. اینبار در return View ما، یک شیء لیستی ارائه خواهد شد. در سمت View هم با یک حلقه foreach کار نمایش این اطلاعات صورت خواهد گرفت. راه ساده‌تری هم هست. اجازه دهیم تا خود VS.NET، کدهای مرتبط را برای ما تولید کند.
یک کلاس دیگر به پوشه مدل‌های برنامه اضافه کنید به نام Employees با محتوای زیر:

using System.Collections.Generic;

namespace MvcApplication1.Models
{
public class Employees
{
public IList<Employee> CreateEmployees()
{
return new[]
{
new Employee { Email = "name1@site.com", FirstName = "name1", LastName = "LastName1" },
new Employee { Email = "name2@site.com", FirstName = "name2", LastName = "LastName2" },
new Employee { Email = "name3@site.com", FirstName = "name3", LastName = "LastName3" }
};
}
}
}

سپس متد جدید زیر را به کنترلر Home اضافه کنید.

public ActionResult List()
{
var employeesList = new Employees().CreateEmployees();
return View(employeesList);
}

برای اضافه کردن View متناظر با آن، روی نام متد کلیک راست کرده و گزینه Add view را انتخاب کنید. در صفحه ظاهر شده:


تیک مربوط به Create a strongly typed view را قرار دهید. سپس در قسمت Model class، کلاس Employee را انتخاب کنید (نه Employees جدید را، چون از آن می‌خواهیم به عنوان منبع داده لیست تولیدی استفاده کنیم). اگر این کلاس را مشاهده نمی‌کنید، به این معنا است که هنوز برنامه را یکبار کامپایل نکرده‌اید تا VS.NET بتواند با اعمال Reflection بر روی اسمبلی برنامه آن‌را پیدا کند. سپس در قسمت Scaffold template گزینه List را انتخاب کنید تا Code generator توکار VS.NET فعال شود. اکنون بر روی دکمه Add کلیک نمائید تا View نهایی تولید شود. برای مشاهده نتیجه نهایی مسیر http://localhost/Home/List باید بررسی گردد.


ج) استفاده از ViewDataDictionary

ViewDataDictionary از نوع IDictionary با کلیدی رشته‌ای و مقداری از نوع object است. توسط آن شیء‌ایی به نام ViewData در ASP.NET MVC به نحو زیر تعریف شده است:

public ViewDataDictionary ViewData { get; set; }

این روش در نگارش‌های اولیه ASP.NET MVC بیشتر مرسوم بود. برای مثال:

using System;
using System.Web.Mvc;

namespace MvcApplication1.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
ViewData["DateTime"] = "<‪br/>" + DateTime.Now;
return View();
}
}
}

و سپس جهت استفاده از این ViewData تعریف شده با کلید دلخواهی به نام DateTime در View متناظر با اکشن Index خواهیم داشت:

@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
<div>
DateTime: @ViewData["DateTime"]
</div>

یک نکته امنیتی:
اگر به مقدار انتساب داده شده به شیء ViewDataDictionary دقت کنید، یک تگ br هم به آن اضافه شده است. برنامه را یکبار اجرا کنید. مشاهده خواهید کرد که این تگ به همین نحو نمایش داده می‌شود و نه به صورت یک سطر جدید HTML . چرا؟ چون Razor به صورت پیش فرض اطلاعات را encode شده (فراخوانی متد Html.Encode در پشت صحنه به صورت خودکار) در صفحه نمایش می‌دهد و این مساله از لحاظ امنیتی بسیار عالی است؛ زیرا جلوی بسیاری از حملات cross site scripting یا XSS را خواهد گرفت.
احتمالا الان این سؤال پیش خواهد آمد که اگر «عالمانه» بخواهیم این رفتار نیکوی پیش فرض را غیرفعال کنیم چه باید کرد؟
برای این منظور می‌توان نوشت:
@Html.Raw(myString)

و یا:
<div>@MvcHtmlString.Create("<h1>HTML</h1>")</div>

به این ترتیب خروجی Razor دیگر encode شده نخواهد بود.


د) استفاده از TempData

TempData نیز یک dictionary دیگر برای ذخیره سازی اطلاعات است و به نحو زیر در فریم ورک تعریف شده است:

public TempDataDictionary TempData { get; set; }

TempData در پشت صحنه از سشن‌های ASP.NET جهت ذخیره سازی اطلاعات استفاده می‌کند. بنابراین اطلاعات آن در سایر کنترلرها و View ها نیز در دسترس خواهد بود. البته TempData یک سری تفاوت هم با سشن معمولی ASP.NET دارد:
- بلافاصله پس از خوانده شدن، حذف خواهد شد.
- پس از پایان درخواست از بین خواهد رفت.
هر دو مورد هم به جهت بالابردن کارآیی برنامه‌های ASP.NET MVC و مصرف کمتر حافظه سرور درنظر گرفته‌ شده‌اند.
البته کسانی که برای بار اول هست با ASP.NET مواجه می‌شوند، شاید سؤال بپرسند این مسایل چه اهمیتی دارد؟ پروتکل HTTP، ذاتا یک پروتکل «بدون حالت» است یا Stateless هم به آن گفته می‌شود. به این معنا که پس از ارائه یک صفحه وب توسط سرور، تمام اشیاء مرتبط با آن در سمت سرور تخریب خواهند شد. این مورد متفاوت‌ است با برنامه‌های معمولی دسکتاپ که طول عمر یک شیء معمولی تعریف شده در سطح فرم به صورت یک فیلد، تا زمان باز بودن آن فرم، تعیین می‌گردد و به صورت خودکار از حافظه حذف نمی‌شود. این مساله دقیقا مشکل تمام تازه واردها به دنیای وب است که چرا اشیاء ما نیست و نابود شدند. در اینجا وب سرور قرار است به هزاران درخواست رسیده پاسخ دهد. اگر قرار باشد تمام این اشیاء را در سمت سرور نگهداری کند، خیلی زود با اتمام منابع مواجه می‌گردد. اما واقعیت این است که نیاز است یک سری از اطلاعات را در حافظه نگه داشت. به همین منظور یکی از چندین روش مدیریت حالت در ASP.NET استفاده از سشن‌ها است که در اینجا به نحو بسیار مطلوبی، با سربار حداقل توسط TempData مدیریت شده است.
یک مثال کاربردی در این زمینه:
فرض کنید در متد جاری کنترلر، ابتدا بررسی می‌کنیم که آیا ورودی دریافتی معتبر است یا خیر. در غیراینصورت، کاربر را به یک View دیگر از طریق کنترلری دیگر جهت نمایش خطاها هدایت خواهیم کرد.
همین «هدایت مرورگر به یک View دیگر» یعنی پاک شدن و تخریب اطلاعات کنترلر قبلی به صورت خودکار. بنابراین نیاز است این اطلاعات را در TempData قرار دهیم تا در کنترلری دیگر قابل استفاده باشد:

using System;
using System.Web.Mvc;

namespace MvcApplication1.Controllers
{
public class HomeController : Controller
{
public ActionResult InsertData(string name)
{
// Check for input errors.
if (string.IsNullOrWhiteSpace(name))
{
TempData["error"] = "name is required.";
return RedirectToAction("ShowError");
}
// No errors
// ...
return View();
}

public ActionResult ShowError()
{
var error = TempData["error"] as string;
if (!string.IsNullOrWhiteSpace(error))
{
ViewBag.Error = error;
}
return View();
}
}
}

در همان HomeController دو متد جدید به نام‌های InsertData و ShowError اضافه شده‌اند. در متد InsertData ابتدا بررسی می‌شود که آیا نامی وارد شده است یا خیر. اگر خیر توسط متد RedirectToAction، کاربر به اکشن یا متد ShowError هدایت خواهد شد.
برای انتقال اطلاعات خطایی که می‌خواهیم در حین این Redirect نمایش دهیم نیز از TempData استفاده شده است.
بدیهی است برای اجرا این مثال نیاز است دو View جدید برای متدهای InsertData و ShowError ایجاد شوند (کلیک راست روی نام متد و انتخاب گزینه Add view برای اضافه کردن View مرتبط با آن اکشن).
محتوای View مرتبط با متد افزودن اطلاعات فعلا مهم نیست، ولی View نمایش خطاها در ساده‌ترین حالت مثلا می‌تواند به صورت زیر باشد:

@{
ViewBag.Title = "ShowError";
}

<h2>Error</h2>

@ViewBag.Error

برای آزمایش برنامه هم مطابق مسیریابی پیش فرض و با توجه به قرار داشتن در کنترلری به نام Home، مسیر http://localhost/Home/InsertData ابتدا باید بررسی شود. چون آرگومانی وارد نشده، بلافاصله صفحه به آدرس http://localhost/Home/ShowError به صورت خودکار هدایت خواهد شد.


نکته‌ای تکمیلی در مورد Strongly typed viewها:
عنوان شد که Strongly typed view روش مرجح بوده و بهتر است از آن استفاده شود، زیرا اطلاعات اشیاء و خواص تعریف شده در یک View تحت نظر کامپایلر قرار می‌گیرند که بسیار عالی است. یعنی اگر در View بنویسم FirstName: @Model.FirstName1 چون FirstName1 وجود خارجی ندارد، برنامه نباید کامپایل شود. یکبار این را بررسی کنید. برنامه بدون مشکل کامپایل می‌شود! اما تنها در زمان اجرا است که صفحه زرد رنگ معروف خطاهای ASP.NET ظاهر می‌شود که چنین خاصیتی وجود ندارد (این حالت پیش فرض است؛ یعنی کامپایل یک View‌ در زمان اجرا). البته این باز هم خیلی بهتر است از ViewBag، چون اگر مثلا ViewBag.Country1 را وارد کنیم، در زمان اجرا تنها چیزی نمایش داده نخواهد شد؛‌ اما با روش Strongly typed view، حتما خطای Compilation Error به همراه نمایش محل مشکل نهایی، در صفحه ظاهر خواهد شد.
سؤال: آیا می‌شود پیش از اجرای برنامه هم این بررسی را انجام داد؟
پاسخ: بله. باید فایل پروژه را اندکی ویرایش کرده و مقدار MvcBuildViews را که به صورت پیش فرض false هست، true نمود. یا خارج از ویژوال استودیو با یک ادیتور متنی ساده مثلا فایل csproj را گشوده و این تغییر را انجام دهید. یا داخل ویژوال استودیو، بر روی نام پروژه کلیک راست کرده و سپس گزینه Unload Project را انتخاب کنید. مجددا بر روی این پروژه Unload شده کلیک راست نموده و گزینه edit را انتخاب نمائید. در صفحه باز شده، MvcBuildViews را یافته و آن‌را true کنید. سپس پروژه را Reload کنید.
اکنون اگر پروژه را کامپایل کنید، پیغام خطای زیر پیش از اجرای برنامه قابل مشاهده خواهد بود:

'MvcApplication1.Models.Employee' does not contain a definition for 'FirstName1' 
and no extension method 'FirstName1' accepting a first argument of type 'MvcApplication1.Models.Employee'
could be found (are you missing a using directive or an assembly reference?)
d:\Prog\MvcApplication1\MvcApplication1\Views\Home\Index.cshtml 10 MvcApplication1

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

و یک خبر خوب!
مجوز سورس کد ASP.NET MVC از MS-PL به Apache تغییر کرده و همچنین Razor و یک سری موارد دیگر هم سورس باز شده‌اند. این تغییرات به این معنا خواهند بود که پروژه از حالت فقط خواندنی MS-PL به حالت متداول یک پروژه سورس باز که شامل دریافت تغییرات و وصله‌ها از جامعه برنامه نویس‌ها است، تغییر کرده است (^ و ^).

مطالب
C# 8.0 - Async Streams
امکان تعریف نوع‌های شمارشی async در C# 8.0

فرض کنید قصد دارید یک متد async از نوع IEnumerable را که تعدادی yield return به تاخیر افتاده را به همراه دارد (yield return‌ها فقط زمانی اجرا می‌شوند که بر روی آن‌ها متدهایی مانند ToList و یا حلقه‌ی foreach اجرا شوند) و همچنین توسط await Task.Delay، دریافت اطلاعات به صورت async را نیز شبیه سازی می‌کند، تهیه کنید:
public struct Statement
{
    public int Id { get; }
    public string Description { get; }
    public Statement(int id, string description) => (Id, Description) = (id, description);
    public override string ToString() => Description;
}

public static async Task<IEnumerable<Statement>> GetStatements(bool error)
{
    if (error)
    {
       throw new Exception("Oops, we messed up 😬");
    }

    await Task.Delay(1000); //Simulate waiting for data to come through. 

    yield return new Statement(1, "C# is cool!");
    yield return new Statement(2, "C# orginally named COOL.");
    yield return new Statement(3, "More examples...");
}
این قطعه کد حتی در C# 8.0 نیز چنین خطای کامپایلری را به همراه دارد:
The body of 'AsyncStreams.GetStatements(bool)' cannot be an iterator block because
'Task<IEnumerable<AsyncStreams.Statement>>' is not an iterator interface type (CS1624)
عنوان می‌کند که برای دریافت اطلاعات متد GetStatements باید یک iterator تشکیل شود؛ اما Task IEnumerable از این نوع نیست.

برای رفع یک چنین مشکلی، اکنون در C# 8.0 می‌توان از اینترفیس جدید IAsyncEnumerable بجای Task IEnumerable استفاده کرد. به این ترتیب تنها تغییری که در قطعه کد فوق نیاز است، تغییر امضای آن به صورت زیر است:
static async IAsyncEnumerable<Statement> GetStatements(bool error)


امکان تعریف حلقه‌های async در C# 8.0

مرحله‌ی بعد، ایجاد حلقه‌ای بر روی متد GetStatements است. اکنون مشکل دیگری وجود دارد: حلقه‌ی foreach به خودی خود، یک حلقه‌ی synchronous است و اگر از آن برای کار با یک استریم async استفاده شود، هربار که اطلاعاتی از آن بازگشت داده می‌شود، پایان یک Task نیز گزارش داده خواهد شد که می‌توان سبب خاتمه‌ی حلقه شود. بنابراین انجام اینکار نیز پیش از C# 8.0 میسر نبود که اکنون با امکان تعریف await پیش از یک حلقه‌ی foreach، ممکن شده‌است:
static async IAsyncEnumerable<Statement> GetStatementsAsync(bool error)
{
    await foreach (var statement in GetStatements(error))
    {
      await Task.Delay(1000);
      yield return statement;
    }
}
تا پیش از C# 8.0، از واژه‌ی await تنها برای دریافت یک تک مقدار استفاده می‌شد؛ اما حالا می‌توان از آن برای دریافت استریمی از نتایج (async streams) نیز استفاده کرد.


اینترفیس IAsyncEnumerable چگونه تعریف شده‌است؟

 اینترفیس IAsyncEnumerable متد GetAsyncEnumerator را تعریف می‌کند که یک IAsyncEnumerator را بازگشت می‌دهد و آن نیز به همراه متد MoveNextAsync است. اگر دقت کنید در این حالت از نگارش async اینترفیس IDisposable به نام IAsyncDisposable استفاده کرده‌است:
using System.Threading;

namespace System.Collections.Generic
{
    public interface IAsyncEnumerable<out T>
    {
        IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default);
    }

    public interface IAsyncEnumerator<out T> : IAsyncDisposable
    {
        T Current { get; }

        ValueTask<bool> MoveNextAsync();
    }
}

namespace System
{
    public interface IAsyncDisposable
    {
        ValueTask DisposeAsync();
    }
}
اینترفیس‌های IAsyncDisposable و IAsyncEnumerator یک ValueTask را توسط متدهای DisposeAsync و MoveNextAsync بازگشت می‌دهند و این مورد به C# 7x باز می‌گردد که امکان await را نه تنها بر روی Task، بلکه بر روی هر نوعی که متد GetAwaiter را پیاده سازی می‌کند، میسر می‌کند و ValueTask نیز یکی از آن‌ها است. ValueTask به صورت یک نوع مقدار (value type) تعریف شده‌است؛ بجای نوع ارجاعی Task که سربار کمتری را به همراه دارد.


مثالی از IAsyncDisposable و روش Dispose خودکار آن

با معرفی IAsyncDisposable، اگر یک مثال ساده از پیاده سازی آن به صورت زیر باشد:
public class AwaitUsingTest : IAsyncDisposable
{
   public async ValueTask DisposeAsync()
   {
     await Task.CompletedTask;
   }

   public void Dummy() { }
}
روش فراخوانی using declaration بر روی آن به همراه واژه‌ی کلیدی await در C# 8.0، مانند مثال زیر است:
async Task FooBar()
{
   await using var test = new AwaitUsingTest();
   test.Dummy();
}
مطالب دوره‌ها
تزریق خودکار وابستگی‌ها در ASP.NET Web API به همراه رها سازی خودکار منابع IDisposable
در انتهای مطلب « تزریق خودکار وابستگی‌ها در برنامه‌های ASP.NET MVC » اشاره‌ای کوتاه به روش DependencyResolver توکار Web API شد که این روش پس از بررسی‌های بیشتر (^ و ^) به دلیل ماهیت service locator بودن آن و همچنین از دست دادن Context جاری Web API، مردود اعلام شده و استفاده از IHttpControllerActivator توصیه می‌گردد. در ادامه این روش را توسط Structure map 3 پیاده سازی خواهیم کرد.

پیش نیازها
- شروع یک پروژه‌ی جدید وب با پشتیبانی از Web API
- نصب دو بسته‌ی نیوگت مرتبط با Structure map 3
 PM>install-package structuremap
PM>install-package structuremap.web

پیاده سازی IHttpControllerActivator توسط Structure map

using System;
using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Dispatcher;
using StructureMap;

namespace WebApiDISample.Core
{
    public class StructureMapHttpControllerActivator : IHttpControllerActivator
    {
        private readonly IContainer _container;
        public StructureMapHttpControllerActivator(IContainer container)
        {
            _container = container;
        }

        public IHttpController Create(
                HttpRequestMessage request,
                HttpControllerDescriptor controllerDescriptor,
                Type controllerType)
        {
            var nestedContainer = _container.GetNestedContainer();
            request.RegisterForDispose(nestedContainer);
            return (IHttpController)nestedContainer.GetInstance(controllerType);
        }
    }
}
در اینجا نحوه‌ی پیاده سازی IHttpControllerActivator را توسط StructureMap ملاحظه می‌کنید.
نکته‌ی مهم آن استفاده از NestedContainer آن است. معرفی آن به متد request.RegisterForDispose سبب می‌شود تا کلیه کلاس‌های IDisposable نیز در پایان کار به صورت خودکار رها سازی شده و نشتی حافظه رخ ندهد.


معرفی StructureMapHttpControllerActivator به برنامه

فایل WebApiConfig.cs را گشوده و تغییرات ذیل را در آن اعمال کنید:
using System.Web.Http;
using System.Web.Http.Dispatcher;
using StructureMap;
using WebApiDISample.Core;
using WebApiDISample.Services;

namespace WebApiDISample
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // IoC Config
            ObjectFactory.Configure(c => c.For<IEmailsService>().Use<EmailsService>());

            var container = ObjectFactory.Container;
            GlobalConfiguration.Configuration.Services.Replace(
                typeof(IHttpControllerActivator), new StructureMapHttpControllerActivator(container));


            // Web API routes
            config.MapHttpAttributeRoutes();
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }
}
 در ابتدا تنظیمات متداول کلاس‌ها و اینترفیس‌ها صورت می‌گیرد. سپس نحوه‌ی معرفی  StructureMapHttpControllerActivator را به GlobalConfiguration.Configuration.Services مخصوص Web API ملاحظه می‌کنید. این مورد سبب می‌شود تا به صورت خودکار کلیه وابستگی‌های مورد نیاز یک Web API Controller به آن تزریق شوند.


تهیه سرویسی برای آزمایش برنامه

namespace WebApiDISample.Services
{
    public interface IEmailsService
    {
        void SendEmail();
    }
}

using System;

namespace WebApiDISample.Services
{
    /// <summary>
    /// سرویسی که دارای قسمت دیسپوز نیز هست
    /// </summary>
    public class EmailsService : IEmailsService, IDisposable
    {
        private bool _disposed;

        ~EmailsService()
        {
            Dispose(false);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        public void SendEmail()
        {
            //todo: send email!
        }

        protected virtual void Dispose(bool disposeManagedResources)
        {
            if (_disposed) return;
            if (!disposeManagedResources) return;

            //todo: clean up resources here ...

            _disposed = true;
        }
    }
}
در اینجا یک سرویس ساده ارسال ایمیل را بدون پیاده سازی خاصی مشاهده می‌کنید.
نکته‌ی مهم آن استفاده از IDisposable در این کلاس خاص است (ضروری نیست؛ صرفا جهت بررسی بیشتر اضافه شده‌است). اگر در کدهای برنامه، یک چنین کلاسی وجود داشت، نیاز است متد Dispose آن نیز توسط IoC Container فراخوانی شود. برای آزمایش آن یک break point را در داخل متد Dispose قرار دهید.


استفاده از سرویس تعریف شده در یک Web API Controller

using System.Web.Http;
using WebApiDISample.Services;

namespace WebApiDISample.Controllers
{
    public class ValuesController : ApiController
    {
        private readonly IEmailsService _emailsService;
        public ValuesController(IEmailsService emailsService)
        {
            _emailsService = emailsService;
        }

        // GET api/values/5
        public string Get(int id)
        {
            _emailsService.SendEmail();
            return "_emailsService.SendEmail(); called!";
        }
    }
}
در اینجا مثال ساده‌ای را از نحوه‌ی تزریق سرویس ارسال ایمیل را در ValuesController مشاهده می‌کنید.
تزریق وهله‌ی مورد نیاز آن، به صورت خودکار توسط StructureMapHttpControllerActivator که در ابتدای بحث معرفی شد، صورت می‌گیرد.

فراخوانی متد Get آن‌را نیز توسط کدهای سمت کاربر ذیل انجام خواهیم داد:
<h2>Index</h2>

@section scripts
{
    <script type="text/javascript">
        $(function () {
            $.getJSON('/api/values/1?timestamp=' + new Date().getTime(), function (data) {
                alert(data);
            });
        });
    </script>
}
درون متد Get کنترلر، یک break point قرار دهید. همچنین داخل متد Dispose لایه سرویس نیز جهت بررسی بیشتر یک break point قرار دهید.
اکنون برنامه را اجرا کنید. هنگام فراخوانی متد Get، وهله‌ی سرویس مورد نظر، نال نیست. همچنین متد Dispose نیز به صورت خودکار فراخوانی می‌شود.


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید
WebApiDISample.zip 
مطالب
تغییرات رمزنگاری اطلاعات در NET Core.
در NET Core. به ظاهر دیگر خبری از کلاس‌هایی مانند RNGCryptoServiceProvider برای تولید اعداد تصادفی و یا SHA256Managed (و تمام کلاس‌های Managed_) برای هش کردن اطلاعات نیست. در ادامه این موارد را بررسی کرده و با معادل‌های آن‌ها در NET Core. آشنا خواهیم شد.


تغییرات الگوریتم‌های هش کردن اطلاعات

با حذف و تغییرنام کلاس‌هایی مانند SHA256Managed (و تمام کلاس‌های Managed_) در NET Core.، معادل کدهایی مانند:
using (var sha256 = new SHA256Managed()) 
{ 
   // Crypto code here... 
}
به صورت ذیل درآمده‌است:
public static string GetHash(string text)
{
  using (var sha256 = SHA256.Create())
  {
   var hashedBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(text));
   return BitConverter.ToString(hashedBytes).Replace("-", "").ToLower();
  }
}
البته اگر از یک برنامه‌ی ASP.NET Core استفاده می‌کنید، اسمبلی‌های مرتبط آن به صورت پیش فرض به پروژه الحاق شده‌اند. اما اگر می‌خواهید یک کتابخانه‌ی جدید را ایجاد کنید، نیاز است وابستگی ذیل را نیز به فایل project.json آن اضافه نمائید:
 "dependencies": {
  "System.Security.Cryptography.Algorithms": "4.2.0"
},

به علاوه اگر نیاز به محاسبه‌ی هش حاصل از جمع چندین byte array را دارید، در اینجا می‌توان از الگوریتم‌های IncrementalHash به صورت ذیل استفاده کرد:
using (var md5 = IncrementalHash.CreateHash(HashAlgorithmName.MD5))
{
  md5.AppendData(byteArray1, 0, byteArray1.Length);
  md5.AppendData(byteArray2, 0, byteArray2.Length); 
  var hash = md5.GetHashAndReset();
}
این الگوریتم‌ها شامل MD5، SHA1، SHA256، SHA384 و SHA512 می‌شوند.


تولید اعداد تصادفی Thread safe در NET Core.

روش‌های زیادی برای تولید اعداد تصادفی در برنامه‌های دات نت وجود دارند؛ اما مشکل اکثر آن‌ها این است که thread safe نیستند و نباید از آن‌ها در برنامه‌های چند ریسمانی (مانند برنامه‌های وب)، به نحو متداولی استفاده کرد. در این بین تنها کلاسی که thread safe است، کلاس RNGCryptoServiceProvider می‌باشد؛ آن هم با یک شرط:
   private static readonly RNGCryptoServiceProvider Rand = new RNGCryptoServiceProvider();
از کلاس آن باید تنها یک وهله‌ی static readonly در کل برنامه وجود داشته باشد (مطابق مستندات MSDN).
بنابراین اگر در کدهای خود چنین تعریفی را دارید:
 var rand = new RNGCryptoServiceProvider();
اشتباه است و باید اصلاح شود.
در NET Core. این کلاس به طور کامل حذف شده‌است و معادل جدید آن کلاس RandomNumberGenerator است که به صورت ذیل قابل استفاده است (و در عمل تفاوتی بین کدهای آن با کدهای RNGCryptoServiceProvider نیست):
    public interface IRandomNumberProvider
    {
        int Next();
        int Next(int max);
        int Next(int min, int max);
    }

    public class RandomNumberProvider : IRandomNumberProvider
    {
        private readonly RandomNumberGenerator _rand = RandomNumberGenerator.Create();

        public int Next()
        {
            var randb = new byte[4];
            _rand.GetBytes(randb);
            var value = BitConverter.ToInt32(randb, 0);
            if (value < 0) value = -value;
            return value;
        }

        public int Next(int max)
        {
            var randb = new byte[4];
            _rand.GetBytes(randb);
            var value = BitConverter.ToInt32(randb, 0);
            value = value % (max + 1); // % calculates remainder
            if (value < 0) value = -value;
            return value;
        }

        public int Next(int min, int max)
        {
            var value = Next(max - min) + min;
            return value;
        }
    }
در اینجا نیز یک وهله از کلاس RandomNumberGenerator را ایجاد کرده‌ایم، اما استاتیک نیست. علت اینجا است که چون برنامه‌های ASP.NET Core به همراه یک IoC Container توکار هستند، می‌توان این کلاس را با طول عمر singleton معرفی کرد که همان کار را انجام می‌دهد:
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.TryAddSingleton<IRandomNumberProvider, RandomNumberProvider>();
و پس از این تنظیم، می‌توان سرویس IRandomNumberProvider را در تمام قسمت‌های برنامه، با کمک تزریق وابستگی‌های آن در سازنده‌ی کلاس‌ها، استفاده کرد و نکته‌ی مهم آن thread safe بودن آن جهت کاربردهای چند ریسمانی است. بنابراین دیگر در برنامه‌های وب خود از new Random استفاده نکنید.


نیاز به الگوریتم‌های رمزنگاری متقارن قوی و معادل بهتر آن‌ها در ASP.NET Core

ASP.NET Core به همراه یکسری API جدید است به نام data protection APIs که روش‌هایی را برای پیاده سازی بهتر الگوریتم‌های هش کردن اطلاعات و رمزنگاری اطلاعات، ارائه می‌دهند و برای مثال ASP.NET Core Identity و یا حتی Anti forgery token آن، در پشت صحنه دقیقا از همین API برای انجام کارهای رمزنگاری اطلاعات استفاده می‌کنند.
برای مثال اگر بخواهید کتابخانه‌ای را طراحی کرده و در آن از الگوریتم AES استفاده نمائید، نیاز است تنظیم اضافه‌تری را جهت دریافت کلید عملیات نیز اضافه کنید. اما با استفاده از data protection APIs نیازی به اینکار نیست و مدیریت ایجاد، نگهداری و انقضای این کلید به صورت خودکار توسط سیستم data protection انجام می‌شود. کلیدهای این سیستم موقتی هستند و طول عمری محدود دارند. بنابراین باتوجه به این موضوع، روش مناسبی هستند برای تولید توکن‌های Anti forgery و یا تولید محتوای رمزنگاری شده‌ی کوکی‌ها. بنابراین نباید از آن جهت ذخیره سازی اطلاعات ماندگار در بانک‌های اطلاعاتی استفاده کرد.
فعال سازی این سیستم نیازی به تنظیمات اضافه‌تری در ASP.NET Core ندارد و جزو پیش فرض‌های آن است. در کدهای ذیل، نمونه‌ای از استفاده‌ی از این سیستم را ملاحظه می‌کنید:
    public interface IProtectionProvider
    {
        string Decrypt(string inputText);
        string Encrypt(string inputText);
    }

namespace Providers
{
    public class ProtectionProvider : IProtectionProvider
    {
        private readonly IDataProtector _dataProtector;

        public ProtectionProvider(IDataProtectionProvider dataProtectionProvider)
        {
            _dataProtector = dataProtectionProvider.CreateProtector(typeof(ProtectionProvider).FullName);
        }

        public string Decrypt(string inputText)
        {
                var inputBytes = Convert.FromBase64String(inputText);
                var bytes = _dataProtector.Unprotect(inputBytes);
                return Encoding.UTF8.GetString(bytes);
        }

        public string Encrypt(string inputText)
        {
            var inputBytes = Encoding.UTF8.GetBytes(inputText);
            var bytes = _dataProtector.Protect(inputBytes);
            return Convert.ToBase64String(bytes);
        }
    }
}
کار با تزریق IDataProtectionProvider در سازنده‌ی کلاس شروع می‌شود. سرویس آن به صورت پیش فرض توسط ASP.NET Core در اختیار برنامه قرار می‌گیرد و نیازی به تنظیمات اضافه‌تری ندارد. پس از آن باید یک محافظت کننده‌ی جدید را با فراخوانی متد CreateProtector آن ایجاد کرد و در آخر کار با آن به سادگی فراخوانی متدهای Unprotect و Protect است که ملاحظه می‌کنید.
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.TryAddSingleton<IProtectionProvider, ProtectionProvider>();
پس از طراحی این سرویس جدید نیز می‌توان وابستگی‌های آن‌را به نحو فوق به سیستم معرفی کرد و از سرویس IProtectionProvider آن در تمام قسمت‌های برنامه (جهت کارهای کوتاه مدت رمزنگاری اطلاعات) استفاده نمود.

مستندات مفصل این API را در اینجا می‌توانید مطالعه کنید.


معادل الگوریتم Rijndael در NET Core.

همانطور که عنوان شد، طول عمر کلیدهای data protection API محدود است و به همین جهت برای کارهایی چون تولید توکن‌ها، رمزنگاری کوئری استرینگ‌ها و یا کوکی‌های کوتاه مدت، بسیار مناسب است. اما اگر نیاز به ذخیره سازی طولانی مدت اطلاعات رمزنگاری شده وجود داشته باشد، یکی از الگوریتم‌های مناسب اینکار، الگوریتم AES است.
الگوریتم Rijndael نگارش کامل دات نت، اینبار نام اصلی آن یا AES را در NET Core. پیدا کرده‌است و نمونه‌ای از نحوه‌ی استفاده‌ی از آن، جهت رمزنگاری و رمزگشایی اطلاعات، به صورت ذیل است:
public string Decrypt(string inputText, string key, string salt)
{
    var inputBytes = Convert.FromBase64String(inputText);
    var pdb = new Rfc2898DeriveBytes(key, Encoding.UTF8.GetBytes(salt));
 
    using (var ms = new MemoryStream())
    {
        var alg = Aes.Create();
 
        alg.Key = pdb.GetBytes(32);
        alg.IV = pdb.GetBytes(16);
 
        using (var cs = new CryptoStream(ms, alg.CreateDecryptor(), CryptoStreamMode.Write))
        {
            cs.Write(inputBytes, 0, inputBytes.Length);
        }
        return Encoding.UTF8.GetString(ms.ToArray());
    }
}
 
public string Encrypt(string inputText, string key, string salt)
{
 
    var inputBytes = Encoding.UTF8.GetBytes(inputText);
    var pdb = new Rfc2898DeriveBytes(key, Encoding.UTF8.GetBytes(salt));
    using (var ms = new MemoryStream())
    {
        var alg = Aes.Create();
 
        alg.Key = pdb.GetBytes(32);
        alg.IV = pdb.GetBytes(16);
 
        using (var cs = new CryptoStream(ms, alg.CreateEncryptor(), CryptoStreamMode.Write))
        {
            cs.Write(inputBytes, 0, inputBytes.Length);
        }
        return Convert.ToBase64String(ms.ToArray());
    }
}
همانطور که در کدهای فوق نیز مشخص است، این روش نیاز به قید صریح key و salt را دارد. اما روش استفاده‌ی از data protection APIs مدیریت key و salt را خودکار کرده‌است؛ آن هم با طول عمر کوتاه. در این حالت دیگر نیازی نیست تا جفت کلیدی را که احتمالا هیچگاه در طول عمر برنامه تغییری نمی‌کنند، از مصرف کننده‌ی کتابخانه‌ی خود دریافت کنید و یا حتی نام خودتان را به عنوان کلید در کدها به صورت hard coded قرار دهید!
مطالب
آشنایی با WPF قسمت پنجم : DataContext بخش اول
یکی از مهمترین قسمت‌های برنامه، کار با داده‌های بانک اطلاعاتی (یا در کل منابع اطلاعاتی) است. اینکه چگونه با آن‌ها ارتباط برقرار کنیم و آن‌ها را در یک قالب کاربر پسند به کاربران برنامه نشان دهیم. افزودن شیء DataContext و مفاهیمی چون DataBinding باعث ارتباط سریع‌تر و راحت‌تری با منبع داده‌ها شده است. همچنین این قابلیت وجود دارد که هر گونه به روز آوری در اطلاعات دریافت شده، شما را با خبر سازد تا بتوانید طبق آن چه که می‌خواهید اطلاعات نمایشی را به روز کنید. در این مقاله به نحوه‌ی ارتباط بین منبع داده با DataContext و سپس کنترل‌هایی را چون Grid و ListBox و ... در رابطه با این منابع داده بررسی می‌کنیم.

در مورد بررسی ارتباط با داده‌ها در WPF باید سه مورد را بشناسیم:

  • DataContext: این شیء اتصالش را به منبع داده‌ها برقرار کرده و هر موقع داده‌ای را نیاز داریم، از طریق این شیء تامین می‌شود.
  • DataBinding: یک واسطه بین DataContext و هر آن چیزی است که قرار است از داده‌ها تغذیه کند. در تعریفی رسمی‌تر می‌گوییم: روشی ساده و قدرتمند بوده و واسطی است بین مدل تجاری و رابط کاربری. هر زمانی که داده‌ای تغییر کند، ما را آگاه می‌سازد که می‌تواند یک ارتباط یک طرفه یا دو طرفه باشد.
  • DataTemplate: نحوه‌ی فرمت بندی و نمایش داده‌ها را تعیین می‌کند.
ابتدا کار را با یک مثال ساده آغاز می‌کنیم. قصد داریم فرمی را که در قسمت قبلی ساختیم، با استفاده از یک منبع داده پر کنیم:
ابتدا قبل از هر چیزی کلاس فرم قبلی را پیاده سازی می‌کنیم. در این پیاده سازی از یک enum برای انتخاب زمینه‌های کاری هم کمک گرفته ایم و هچنین با یک متد ایستا، منبع داده‌ی تک رکوردی را جهت تست برنامه آماده کرده‌ایم:
   public enum FieldOfWork
    {
        Actor=0,
        Director=1,
        Producer=2
    }
    public class Person
    {
        public string Name { get; set; }

        public bool Gender { get; set; }

        public string ImageName { get; set; }

        public string Country { get; set; }

        public DateTime Date { get; set; }

        public IList<FieldOfWork> FieldOfWork { get; set; }
        public static Person GetPerson()
        {
            return new Person()
            {
                Name = "Leo",
                Gender = true,
                ImageName ="man.jpg",
                Country = "Italy",
                Date = DateTime.Now
            };
        }
    }

حالا لازم است که این منبع داده را در اختیار DataContext بگذاریم. وارد بخش کد نویسی شده و در سازنده‌ی پنجره کد زیر را می‌نویسیم:
 DataContext = Person.GetPerson();
public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = Person.GetPerson();
        }  
    }
با این کار، ارتباط شما با منبع داده آغاز می‌شود و طبق درخواست‌هایی که از DataBinding به آن می‌رسد، اطلاعات را تحویل DataBinding می‌دهد. برای نمایش داده‌ها در کنترل‌ها و استفاده از DataBinding، به سراغ خصوصیات وابسته می‌رویم. در حال حاضر فعلا برنامه را با دو کنترل عکس و نام که رشته‌ای هستند آغاز می‌کنیم؛ چون بقیه‌ی کنترل‌ها کمی متفاوت هستند.
همانطور که می‌دانید متن کنترل TextBox توسط خصوصیت Text پر می‌شود و برای همین در این خصوصیت می‌نویسیم:
Text="{Binding Name}"
علامت {} را باز کرده و در ابتدا نام Binding را می‌آوریم. سپس بعد از یک فاصله، نام پراپرتی کلاسی را که حاوی اطلاعات مدنظر است، می‌نویسیم و بدین صورت اتصال برقرار می‌شود. برای کنترل عکس هم وضعیت به همین صورت است:
Source="{Binding ImageName}"
حال برنامه را اجرا کرده و دو کنترل textbox و Image را بررسی می‌کنیم:


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

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

کار را از یک کلاس آغاز می‌کنیم. از اینترفیس INotifyPropertyChanged ارث بری کرده و در آن یک رویداد و یک متد را تعریف می‌کنیم و کمی در هم در تعریف Property‌ها دست می‌بریم. فعلا اینکار را فقط برای پراپرتی Name انجام می‌دهیم:
 private string _name;
        public string Name
        {
            get { return _name; }
            set
            {
                _name = value;
                OnPropertyChanged("Name");
            }
        }
        public event PropertyChangedEventHandler PropertyChanged;

        private void OnPropertyChanged(string property)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(property));
            }
        }
در کد بالا یک رویداد از نوع PropertyChangedEventHandler تعریف می‌کنیم که وظیفه‌ی به روزآوری را به عهده دارد؛ ولی صدا زدن این رویداد بر عهده‌ی ماست و خود به خود صدا زده نمی‌شود. پس نیاز است متدی را فراهم کرده تا بدانیم که چه خصوصیتی تغییر یافته‌است و از آن طریق رویداد را فراخوانی کنیم و به رویداد بگوییم که کدام پراپرتی تغییر کرده است. این متد را OnpropertyChanged می‌نامیم که آرگومان ورودی آن نام خصوصیتی است که تغییر یافته است و پس از ارزیابی از صحت آن، رویداد را Invoke می‌کنیم.
در بخش Setter آن خصوصیت هم باید این متد را صدا زده و نام خصوصیت را به آن پاس بدهیم تا موقعی که مدل تغییر پیدا کرد، بگوید که خصوصیت Name بوده است که تغییر کرده است.
برای اینکه بدانیم کد واقعا کار می‌کند و تستی بر آن زده باشیم، فعلا دکمه‌ی Save را به Change تغییر می‌دهیم و کد داخل پنجره را بدین صورت تغییر می‌دهیم:
  public partial class MainWindow : Window
    {
        private Person person;
        public MainWindow()
        {
            InitializeComponent();
            person = Person.GetPerson();
            DataContext = person;

        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            person.Name = "Leonardo Decaperio";
        }  
    }

متغیر کلاسی را از حالت محلی Local به عمومی Global تغییر دادم که از طریق دکمه‌ی منبع داده در دسترس باشد. حال در رویداد دکمه نام بازیگر را تغییر می‌دهم. برنامه را اجرا کنید و بر روی دکمه کلیک کنید. باید بعد از یک لحظه‌ی کوتاه، نام بازیگر از Leo به Leonardo Decaperio تغییر کند.
این کد واقعا کدی مفید جهت به روزرسانی است ولی مشکلی دارد که نام پراپرتی باید به صورت String به آن پاس شود که در یک برنامه بزرگ این مورد یک مشکل خواهد شد و اگر نام خصوصیت تغییر کند باید نام داخل آن هم تغییر کند؛ پس کد را به شکل دیگری بازنویسی می‌کنیم:
 private string _name;
        public string Name
        {
            get { return _name; }
            set
            {
                _name = value;
                OnPropertyChanged();
            }
        } 

  private void OnPropertyChanged([CallerMemberName] string property="")
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(property));
            }
        }

در متد OnPropertyChanged در کنار پارامتر اول، ویژگی attribute به نام CallerMemberName را که در فضای نام system.runtime.compilerservice قرار دارد استفاده می‌کنیم (دات نت 4.5). این ویژگی، نام پراپرتی یا متدی که متد OnpropertyChnaged را صدا زده است، به دست می‌آورد. پارامتر اول را هم اختیاری می‌کنیم که سیستم بر ورود پارامتر اجباری نداشته باشد و نهایتا در هر پراپرتی تنها لازم است همانند بالا، خط زیر ذکر شود:
OnPropertyChanged();
اگر الان یک تست از آن بگیرید، می‌بینید که بدون مشکل کار می‌کند. حالا همین متد را در setter تمام پراپرتی‌هایی که دوست دارید از تغییر آن‌ها آگاه شوید قرار دهید.
کد این قسمت
در قسمت‌های آینده به بررسی تبدیل مقادیر و framework element و کنترل‌ها می‌پردازیم.
مطالب
استفاده از مسیرهای مطلق در حین import ماژول‌ها در برنامه‌های مبتنی بر TypeScript
در حین import ماژول‌های TypeScript ایی پس از مدتی به یک چنین کدهایی خواهیم رسید:
import { SpecialCollection } from "../../special";
import { LoginComponent } from "../login";
import { TextUtils } from ".../../utils/text";
import { Router } from "../../../core/router";
در این حالت، در یک پوشه برای import ماژولی مشخص، چنین import ایی را خواهیم داشت:
import { Data } from '../data';
و در پوشه‌ی تو در توی دیگری، این تعریف به صورت زیر تغییر می‌کند:
import { Data } from '../../../data';
و در آخر برنامه پر می‌شود از مسیرهای نسبی ‘../../../..’ مانند. به این ترتیب جابجا کردن فایل‌ها و Refactoring آن‌ها، مشکل می‌شود.
خوشبختانه کامپایلر TypeScript به همراه تنظیمات baseUrl و paths است که توسط آن‌ها می‌توان این مسیرهای نسبی را به مسیرهای مطلق تبدیل کرد و در این حالت اهمیتی ندارد که ماژول مدنظر از چه سطحی و درون چه پوشه‌ی تو در تویی فراخوانی می‌شود، این مسیر import همواره ثابت خواهد بود.


تنظیمات فایل tsconfig.json برای معرفی مسیرهای مطلق ماژول‌ها

فرض کنید می‌خواهید از یکی از سرویس‌های Core Module استفاده کنید:


بسته به عمق پوشه‌ی استفاده کننده، به یک چنین تعریفی خواهید رسید:
import { BrowserStorageService } from "./../../core/browser-storage.service";
برای بهبود این وضعیت، فایل tsconfig.json و یا همان تنظیمات کامپایلر TypeScript را به نحو ذیل تکمیل می‌کنیم:
{
  "compilerOptions": {
    "baseUrl": "src",
    "paths": {
      "@app/*": [
        "app/*"
      ],
      "@app/core/*": [
        "app/core/*"
      ],
      "@app/shared/*": [
        "app/shared/*"
      ],
      "@env/*": [
        "environments/*"
      ]
    }
  }
}
در اینجا baseUrl به پوشه‌ی src برنامه اشاره می‌کند و مسیرهای بعدی بر این اساس محاسبه می‌شوند. در ادامه در قسمت paths، ابتدا یک نام مستعار ذکر می‌شود و سپس مسیری که ارائه دهنده‌ی آن است. ذکر @ در اینجا اختیاری است؛ اما ذکر */‌ها اجباری است.
پس از این تغییرات، اکنون افزونه‌ی پیشنهاد دهنده‌ی imports، هر دو حالت استفاده‌ی از مسیر مطلق بر اساس نام مستعار تعریف شده:
 import { BrowserStorageService } from "@app/core/browser-storage.service";
و یا استفاده‌ی از مسیر نسبی را نیز پیشنهاد می‌دهد:
 import { BrowserStorageService } from "./../../core/browser-storage.service";


برای مثال اگر دقت کرده باشید، روش import اجزای خود Angular به صورت زیر است:
 import { Component } from '@angular/core';
علت اینجا است که Angular از تعریف مشابهی به صورت زیر برای نگاشت پوشه‌ی node_modules آن به angular@ استفاده می‌کند:
"paths": {
    "@angular/*": ["node_modules/@angular/*"]
},
و ذکر @ اختیاری هم از اینجا اقتباس شده‌است.


یک نکته‌ی مهم: تنظیمات فوق بدون تنظیمات معادل webpack ناقص هستند

اگر از برنامه‌ی Angular CLI استفاده می‌کنید، تنظیمات ذکر شده، تا همینجا به پایان می‌رسند؛ چون webpack جزئی از Angular CLI است و تنظیمات پیش فرض آن، قسمت baseUrl و paths فایل tsconfig.json را به صورت خودکار پردازش می‌کند. اما اگر از TypeScript در محیط‌های دیگری استفاده می‌کنید که از webpack به صورت مجزایی استفاده می‌کنند، نیاز است قسمت resolve.alias فایل webpack.config.js را نیز جهت معرفی این تغییرات، اصلاح کنید. از این جهت که کامپایلر TypeScript این مسیرهای مطلق را در حین تولید فایل‌های نهایی JavaScript ایی معادل، به مسیرهای نسبی بازنویسی نمی‌کند و در این حالت webpack نمی‌داند که چطور باید این ماژول‌ها را یافته و یکی کند.
resolve: {
  extensions: ['*', '.js', '.ts'],
  modules: [
    rootDir,
    path.join(rootDir, 'node_modules')
  ],
  alias: {
    '@app': 'src/app'
  }
},


کوتاه کردن مسیرهای مطلق با معرفی فایل ویژه‌ی index.ts

تا اینجا بجای ذکر مسیر
import { BrowserStorageService } from "./../../core/browser-storage.service";
به مسیر مطلق زیر رسیدیم (صرفنظر از محل قرارگیری ماژولی که قرار است آن‌را import کند):
import { BrowserStorageService } from "@app/core/browser-storage.service";
این را هم می‌خواهیم به صورت زیر کوتاه‌تر کنیم:
import { BrowserStorageService } from "@app/core";
یعنی فقط app/core@ را ذکر کنیم.

برای اینکار نیاز است فایل ویژه‌ای را به نام index.ts، در ریشه‌ی پوشه‌ی core ایجاد کنیم (src\app\core\index.ts)، با این محتوا:
export * from "./browser-storage.service";
export * from "./app-config.service";
export * from "./seo-service";
در اینجا تمام ماژول‌هایی که توسط Core Module ارائه می‌شوند را یکبار export می‌کنیم.
برای نمونه اگر به پوشه‌ی node_modules\@angular خود مجموعه‌ی Angular هم مراجعه کنید، هر پوشه‌ی src آن به همراه یک فایل index.d.ts شبیه تعاریف فوق نیز هست.

پس از افزودن فایل index.ts به ریشه‌ی پوشه‌ی مدنظر، اکنون در لیست پیشنهادات، ذکر app/core@ تنها نیز ظاهر شده و استفاده‌ی از آن مجاز است:

مطالب
API Versioning
فرض کنید امروز یک API را برای استفاده عموم ارائه میدهید. آیا با یک breaking change در منابع شما که باعث تغییر در داده‌های ورودی یا خروجی API شود، باید استفاده کنندگان این API در سیستمی که از آن استفاده کرده‌اند، تغییراتی را اعمال کنند یا خیر؟ جواب خیر می‌باشد؛ اصلی‌ترین استفاده از API Versioning دقیقا برای این منظور است که بدون نگرانی از توسعه‌های بعدی، از ورژن‌های قدیمی API بتوانیم استفاده کنیم. 
در این مقاله با روش‌های مختلف ورژن بندی API آشنا خواهید شد.
سه روش اصلی زیر را میتوان برای این منظور در نظر گرفت:
  1.  URI-based versioning 
  2.  Header-based versioning 
  3.  Media type-based versioning 

پیاده سازی URI-based versioning
حداقل به 3 طریق میتوان این مکانیسم را پیاده کرد:
راه حل اول: اگر از Attribute Routing استفاده نمی‌کنید، با تغییر جزئی در تعاریف مسیریابی خود میتوانید به نتیجه مورد نظر برسید. برای ادامه کار دو ویوومدل و دو کنترلر را که هر کدام مربوط به ورژن خاصی از API ما هستند، پیاده سازی میکنیم:
public class ItemViewModel
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Country { get; set; }
}
public class ItemV2ViewModel : ItemViewModel
{
    public double Price { get; set; }
}
ItemViewModel مربوط به ورژن 1 میباشد.
 public class ItemsController : ApiController   
 {
        [ResponseType(typeof(ItemViewModel))]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country = "Japan" };
            return Ok(viewModel);
        }
    }
 public class ItemsV2Controller : ApiController
    {
        [ResponseType(typeof(ItemV2ViewModel))]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemV2ViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 };
            return Ok(viewModel);
        }
    }
ItemsController مربوط به ورژن 1 میباشد.
اگر قرار باشد از مسیرهای متمرکز استفاده کنیم، کافی است که تغییرات زیر را اعمال کنیم:
config.Routes.MapHttpRoute("ItemsV2", "api/v2/items/{id}", new
{
    controller = "ItemsV2",
    id = RouteParameter.Optional
});
config.Routes.MapHttpRoute("Items", "api/items/{id}", new
{
    controller = "Items",
    id = RouteParameter.Optional
});
config.Routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional }
);
در کد بالا به صراحت برای تک تک کنترلرها مسیریابی متناسب با ورژنی که آن را پشتیبانی میکند، معرفی کرده‌ایم.
نکته: این تنظیمات خاص باید قبل از تنظیمات پیش فرض قرار گیرند.

روش بالا به خوبی کار خواهد کرد ولی آنچنان مطلوب نمیباشد. به همین دلیل یک مسیریابی عمومی و کلی را در نظر میگیریم که شامل ورژن مورد نظر، در قالب یک پارامتر میباشد. در روش جایگزین باید به شکل زیر عمل کنید:
config.Routes.MapHttpRoute(
"defaultVersioned",
"api/v{version}/{controller}/{id}",
new { id = RouteParameter.Optional }, new { version = @"\d+" });

config.Routes.MapHttpRoute(
"DefaultApi",
"api/{controller}/{id}",
new { id = RouteParameter.Optional }
);

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

IHttpControllerSelector مسئول پیدا کردن کنترلر مربوطه با توجه به درخواست رسیده می‌باشد. شکل زیر مربوط است به مراحل ساخت کنترلر بر اساس درخواست رسیده:

 به جای پیاده سازی مستقیم این اینترفیس، از پیاده سازی کننده پیش فرض موجود (DefaultHttpControllerSelector) اسفتاده کرده و HttpControllerSelector جدید ما از آن ارث بری خواهد کرد.

public class VersionFinder
    {
        private static bool NeedsUriVersioning(HttpRequestMessage request, out string version)
        {
            var routeData = request.GetRouteData();
            if (routeData != null)
            {
                object versionFromRoute;
                if (routeData.Values.TryGetValue(nameof(version), out versionFromRoute))
                {
                    version = versionFromRoute as string;
                    if (!string.IsNullOrWhiteSpace(version))
                    {
                        return true;
                    }
                }
            }
            version = null;
            return false;
        }
        private static int VersionToInt(string versionString)
        {
            int version;
            if (string.IsNullOrEmpty(versionString) || !int.TryParse(versionString, out version))
                return 0;
            return version;
        }
        public static int GetVersionFromRequest(HttpRequestMessage request)
        {
            string version;

            return NeedsUriVersioning(request, out version) ? VersionToInt(version) : 0;
        }
    }

کلاس VersionFinder برای یافتن ورژن رسیده در درخواست جاری  مورد استفاده قرار خواهد گرفت. با استفاده از متد NeedsUriVersioning بررسی صورت می‌گیرد که آیا در این درخواست پارامتری به نام version وجود دارد یا خیر که درصورت موجود بودن، مقدار آن واکشی شده و درون پارامتر out قرار می‌گیرد. در متد GetVersionFromRequest بررسی میشود که اگر خروجی متد NeedsUriVersioning برابر با true باشد، با استفاده از متد VersionToInt مقدار به دست آمده را به int تبدیل کند.

 public class VersionAwareControllerSelector : DefaultHttpControllerSelector
    {
        public VersionAwareControllerSelector(HttpConfiguration configuration) : base(configuration) { }
        public override string GetControllerName(HttpRequestMessage request)
        {
            var controllerName = base.GetControllerName(request);
            var version = VersionFinder.GetVersionFromRequest(request);
                
        return version > 0 ? GetVersionedControllerName(request, controllerName, version) : controllerName;
        }
        private string GetVersionedControllerName(HttpRequestMessage request, string baseControllerName,
        int version)
        {
            var versionControllerName = $"{baseControllerName}v{version}";
            HttpControllerDescriptor descriptor;
            if (GetControllerMapping().TryGetValue(versionControllerName, out descriptor))
            {
                return versionControllerName;
            }

            throw new HttpResponseException(request.CreateErrorResponse(
            HttpStatusCode.NotFound,
                $"No HTTP resource was found that matches the URI {request.RequestUri} and version number {version}"));
        }
    }

متد  GetControllerName وظیفه بازگشت دادن نام کنترلر را برعهده دارد. ما نیز با لغو رفتار پیش فرض این متد و تهیه نام ورژن بندی شده کنترلر و معرفی این پیاده سازی از  IHttpControllerSelector به شکل زیر میتوانیم به Web API بگوییم که به چه صورت از پارامتر version موجود در درخواست استفاده کند. 

config.Services.Replace(typeof(IHttpControllerSelector), new VersionAwareControllerSelector(config));

حال با اجرای برنامه به نتیجه زیر خواهیم رسید: 

راه حل دوم: برای زمانیکه Attribute Routing مورد استفاده شما است میتوان به راحتی با تعریف قالب‌های مناسب مسیریابی، API ورژن بندی شده را ارائه دهید.

[RoutePrefix("api/v1/Items")]
    public class ItemsController : ApiController
    {
        [ResponseType(typeof(ItemViewModel))]
        [Route("{id:int}")]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country =        "Japan" };
            return Ok(viewModel);
        }
    }


 [RoutePrefix("api/V2/Items")]
    public class ItemsV2Controller : ApiController
    {
        [ResponseType(typeof(ItemV2ViewModel))]
        [Route("{id:int}")]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemV2ViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 };
            return Ok(viewModel);
        }
    }

اگر توجه کرده باشید در مثال ما، نام‌های کنترلر‌ها متفاوت از هم میباشند. اگر بجای در نظر گرفتن نام‌های مختلف برای یک کنترلر در ورژن‌های مختلف، آن را با یک نام یکسان درون namespace‌های مختلف احاطه کنیم یا حتی آنها را درون Class Library‌های جدا نگهداری کنیم، به مشکل "یافت شدن چندین کنترلر که با درخواست جاری مطابقت دارند" برخواهیم خورد. برای حل این موضوع به راه حل سوم توجه کنید. 

راه حل سوم: استفاده از یک NamespaceControllerSelector که پیاده سازی دیگری از اینترفیس IHttpControllerSelector میباشد. فرض بر این است که قالب پروژه به شکل زیر می‌باشد:

کار با پیاده سازی اینترفیس IHttpRouteConstraint آغاز میشود:

public class VersionConstraint : IHttpRouteConstraint
{
    public VersionConstraint(string allowedVersion)
    {
        AllowedVersion = allowedVersion.ToLowerInvariant();
    }
    public string AllowedVersion { get; private set; }

    public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName,
    IDictionary<string, object> values, HttpRouteDirection routeDirection)
    {
        object value;
        if (values.TryGetValue(parameterName, out value) && value != null)
        {
            return AllowedVersion.Equals(value.ToString().ToLowerInvariant());
        }
        return false;
    }
}

کلاس بالا در واقع برای اعمال محدودیت خاصی که در ادامه توضیح داده میشود، پیاده سازی شده است.

متد Match آن وظیفه چک کردن برابری مقدار کلید parameterName موجود در درخواست را با مقدار allowedVersion ای که API از آن پشتیبانی میکند، برعهده دارد. با استفاده از این Constraint مشخص کرده‌ایم که دقیقا چه زمانی باید Route نوشته شده انتخاب شود.

 به روش استفاده از این Constraint توجه کنید:

namespace UriBasedVersioning.Namespace.Controllers.V1
{
    using Models.V1;

RoutePrefix("api/{version:VersionConstraint(v1)}/items")]
public class ItemsController : ApiController
    {
        [ResponseType(typeof(ItemViewModel))]
        [Route("{id}")]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country = "Japan" };
            return Ok(viewModel);
        }
    }
}
namespace UriBasedVersioning.Namespace.Controllers.V2
{
    using Models.V2;


RoutePrefix("api/{version:VersionConstraint(v2)}/items")]
public class ItemsController : ApiController
    {
        [ResponseType(typeof(ItemViewModel))]
        [Route("{id}")]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 };
            return Ok(viewModel);
        }
    }
}

با توجه به کد بالا می‌توان دلیل استفاده از VersionConstraint را هم درک کرد؛ از آنجایی که ما دو Route شبیه به هم داریم، لذا باید مشخص کنیم که در چه شرایطی و کدام یک از این Route‌ها انتخاب شود. خوب، اگر برنامه را اجرا کرده و یکی از API‌های بالا را تست کنید، با خطا مواجه خواهید شد؛ زیرا فعلا این Constraint به سیستم Web API معرفی نشده است. تنظیمات زیر را انجام دهید:

var constraintsResolver = new DefaultInlineConstraintResolver();
            constraintsResolver.ConstraintMap.Add(nameof(VersionConstraint), typeof
            (VersionConstraint));
config.MapHttpAttributeRoutes(constraintsResolver);

مرحله بعدی کار، پیاده سازی IHttpControllerSelector می‌باشد:

  public class NamespaceControllerSelector : IHttpControllerSelector
    {
        private readonly HttpConfiguration _configuration;
        private readonly Lazy<Dictionary<string, HttpControllerDescriptor>> _controllers;
        public NamespaceControllerSelector(HttpConfiguration config)
        {
            _configuration = config;
            _controllers = new Lazy<Dictionary<string,
                HttpControllerDescriptor>>(InitializeControllerDictionary);
        }
        public HttpControllerDescriptor SelectController(HttpRequestMessage request)
        {
            var routeData = request.GetRouteData();
            if (routeData == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }

            var controllerName = GetControllerName(routeData);
            if (controllerName == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
            var version = GetVersion(routeData);
            if (version == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
            var controllerKey = string.Format(CultureInfo.InvariantCulture, "{0}.{1}",
                version, controllerName);
            HttpControllerDescriptor controllerDescriptor;
            if (_controllers.Value.TryGetValue(controllerKey, out controllerDescriptor))
            {
                return controllerDescriptor;
            }
            throw new HttpResponseException(HttpStatusCode.NotFound);
        }
        public IDictionary<string, HttpControllerDescriptor> GetControllerMapping()
        {
            return _controllers.Value;
        }
        private Dictionary<string, HttpControllerDescriptor> InitializeControllerDictionary()
        {
            var dictionary = new Dictionary<string,
                HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase);
            var assembliesResolver = _configuration.Services.GetAssembliesResolver();
            var controllersResolver = _configuration.Services.GetHttpControllerTypeResolver();
            var controllerTypes = controllersResolver.GetControllerTypes(assembliesResolver);
            foreach (var controllerType in controllerTypes)
            {
                var segments = controllerType.Namespace.Split(Type.Delimiter);
                var controllerName =
                    controllerType.Name.Remove(controllerType.Name.Length -
                                               DefaultHttpControllerSelector.ControllerSuffix.Length);
                var controllerKey = string.Format(CultureInfo.InvariantCulture, "{0}.{1}",
                    segments[segments.Length - 1], controllerName);

                if (!dictionary.Keys.Contains(controllerKey))
                {
                    dictionary[controllerKey] = new HttpControllerDescriptor(_configuration,
                        controllerType.Name,
                        controllerType);
                }
            }
            return dictionary;
        }
        private static T GetRouteVariable<T>(IHttpRouteData routeData, string name)
        {
            object result;
            if (routeData.Values.TryGetValue(name, out result))
            {
                return (T)result;
            }
            return default(T);
        }
        private static string GetControllerName(IHttpRouteData routeData)
        {
            var subroute = routeData.GetSubRoutes().FirstOrDefault();
            var dataTokenValue = subroute?.Route.DataTokens.First().Value;
            var controllerName =
                ((HttpActionDescriptor[])dataTokenValue)?.First().ControllerDescriptor.ControllerName.Replace("Controller", string.Empty);
            return controllerName;
        }
        private static string GetVersion(IHttpRouteData routeData)
        {
            var subRouteData = routeData.GetSubRoutes().FirstOrDefault();
            return subRouteData == null ? null : GetRouteVariable<string>(subRouteData, "version");
        }
    }

سورس اصلی کلاس بالا از این آدرس قابل دریافت است. در تکه کد بالا بخشی که مربوط به چک کردن تکراری بودن آدرس میباشد، برای ساده سازی کار حذف شده است. ولی نکته‌ی مربوط به SubRoutes که برای واکشی مقادیر پارامترهای مرتبط با Attribute Routing می‌باشد، اضافه شده است. روال کار به این صورت است که ابتدا RouteData موجود در درخواست را واکشی کرده و با استفاده از متدهای GetControllerName و GetVersion، پارامتر‌های controller و version را جستجو میکنیم. بعد با استفاده از مقادیر به دست آمده، controllerKey را تشکیل داده و درون کنترلر‌های موجود در برنامه به دنبال کنترلر مورد نظر خواهیم گشت.

کارکرد متد InitializeControllerDictionary :

همانطور که میدانید به صورت پیش‌فرض Web API توجهی به فضای نام مرتبط با کنترلر‌ها ندارد. از طرفی برای پیاده سازی اینترفیس IHttpControllerSelector نیاز است متدی با امضای زیر را داشته باشیم:

public IDictionary<string, HttpControllerDescriptor> GetControllerMapping()

لذا در کلاس پیاده سازی شده، خصوصیتی به نام ‎‎_controllers را که از به صورت Lazy و از نوع بازگشتی متد بالا می‌باشد، تعریف کرده‌ایم. متد InitializeControllerDictionary کار آماده سازی داده‌های مورد نیاز خصوصیت ‎‎_controllers می‌باشد. به این صورت که تمام کنترلر‌های موجود در برنامه را واکشی کرده و این بار کلید‌های مربوط به دیکشنری را با استفاده از نام کنترلر و آخرین سگمنت فضای نام آن، تولید و درون دیکشنری مورد نظر ذخیره کرده‌ایم.

حال تنظیمات زیر را اعمال کنید:

 public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            var constraintsResolver = new DefaultInlineConstraintResolver();
            constraintsResolver.ConstraintMap.Add(nameof(VersionConstraint), typeof
            (VersionConstraint));

            config.MapHttpAttributeRoutes(constraintsResolver);

            config.Services.Replace(typeof(IHttpControllerSelector), new NamespaceControllerSelector(config));

        }
    }

این بار برنامه را اجرا کرده و API‌های مورد نظر را تست کنید؛ بله بدون مشکل کار خواهد کرد.

نکته تکمیلی: سورس مذکور در سایت کدپلکس برای حالتی که از Centralized Routes استفاده میکنید آماده شده است. روش مذکور در این مقاله هم  فقط قسمت Duplicate Routes آن را کم دارد که میتوانید اضافه کنید. پیاده سازی دیگری را از این راه حل هم میتوانید داشته باشید.

پیاده سازی Header-based versioning  

در این روش کافی است هدری را برای دریافت ورژن API درخواستی مشخص کرده باشید. برای مثال هدری به نام api-version، که استفاده کننده از این طریق، ورژن درخواستی خود را اعلام میکند. همانطور که قبلا اشاره کردیم، با استفاده از Constraint‌ها دست شما خیلی باز خواهد بود. برای مثال این بار به جای واکشی پارامتر version از RouteData، کد همان قسمت را برای واکشی آن از هدر درخواستی تغییر خواهیم داد:
public class HeaderVersionConstraint : IHttpRouteConstraint
    {
        private const string VersionHeaderName = "api-version";

        public HeaderVersionConstraint(int allowedVersion)
        {
            AllowedVersion = allowedVersion;
        }

        public int AllowedVersion { get; }

        public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName,
            IDictionary<string, object> values,
            HttpRouteDirection routeDirection)
        {
            if (!request.Headers.Contains(VersionHeaderName)) return false;

            var version = request.Headers.GetValues(VersionHeaderName).FirstOrDefault();

            return VersionToInt(version) == AllowedVersion;
        }
        private static int VersionToInt(string versionString)
        {
            int version;
            if (string.IsNullOrEmpty(versionString) || !int.TryParse(versionString, out version))
                return 0;
            return version;
        }
    }
این بار در متد Match، به دنبال هدر خاصی به نام api-version می‌گردیم و مقدار آن را با AllowdVersion مقایسه میکنیم تا مشخص کنیم که آیا این Route نوشته شده برای ادامه کار واکشی کنترلر مورد نظر استفاده شود یا خیر. در ادامه یک RouteFactoryAttribute شخصی سازی شده را به منظور معرفی این محدودیت تعریف شده، در نظر میگیریم:
public sealed class HeaderVersionedRouteAttribute : RouteFactoryAttribute
    {
        public HeaderVersionedRouteAttribute(string template) : base(template)
        {
            Order = -1;
        }

        public int Version { get; set; }

        public override IDictionary<string, object> Constraints => new HttpRouteValueDictionary
        {
            {"", new HeaderVersionConstraint(Version)}
        };
    }

با استفاده از خصوصیت Constraints، توانستیم محدودیت پیاده سازی شده خود را به سیستم مسیریابی معرفی کنیم. توجه داشته باشید که این بار هیچ پارامتری در Uri تحت عنوان version را نداریم و از این جهت به صراحت کلیدی برای آن مشخص نکرده ایم (‎‎‎‎‏‏‎‎{"", new HeaderVersionConstraint(Version)} ‎‎ ).
کنترلر‌های ما به شکل زیر خواهند بود:
    [RoutePrefix("api/items")]
    public class ItemsController : ApiController
    {
        [ResponseType(typeof(ItemViewModel))]
        [HeaderVersionedRoute("{id}", Version = 1)]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country = "Japan" };
            return Ok(viewModel);
        }
    }
  [RoutePrefix("api/items")]
    public class ItemsV2Controller : ApiController
    {
        [ResponseType(typeof(ItemV2ViewModel))]
        [HeaderVersionedRoute("{id}", Version = 2)]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemV2ViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 };
            return Ok(viewModel);
        }
    }

این بار از VersionedRoute برای اعمال مسیر یابی Attribute-based استفاده کرده ایم و به صراحت ورژن هر کدام را با استفاده از خصوصیت Version آن مشخص کرده‌ایم. نتیجه:


پیاده سازی Media type-based versioning
قرار است ورژن API مورد نظر را که استفاده کننده درخواست میدهد، از دل درخواست‌هایی به فرم زیر واکشی کنیم:
GET /api/Items HTTP 1.1
Accept: application/vnd.mediatype.versioning-v1+json
GET /api/Items HTTP 1.1
Accept: application/vnd.mediatype.versioning-v2+json
در این روش دست ما کمی بازتر است چرا که میتوانیم بر اساس فرمت درخواستی نیز تصمیماتی را برای ارائه ورژن خاصی از API  داشته باشیم. روال کار شبیه به روش قبلی با پیاده سازی یک Constraint و یک RouteFactoryAttribute انجام خواهد گرفت.
 public class MediaTypeVersionConstraint : IHttpRouteConstraint
    {
        private const string VersionMediaType = "vnd.mediatype.versioning";

        public MediaTypeVersionConstraint(int allowedVersion)
        {
            AllowedVersion = allowedVersion;
        }

        public int AllowedVersion { get; }

        public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values,
            HttpRouteDirection routeDirection)
        {
            if (!request.Headers.Accept.Any()) return false;

            var acceptHeaderVersion =
                request.Headers.Accept.FirstOrDefault(x => x.MediaType.Contains(VersionMediaType));

            //Accept: application/vnd.mediatype.versioning-v1+json
            if (acceptHeaderVersion == null || !acceptHeaderVersion.MediaType.Contains("-v") ||
                !acceptHeaderVersion.MediaType.Contains("+"))
                return false;

            var version = acceptHeaderVersion.MediaType.Between("-v", "+");
            return VersionToInt(version)==AllowedVersion;
        }
}
در متد Match کلاس بالا به دنبال مدیا تایپ مشخصی هستیم که با هدر Accept برای ما ارسال میشود. برای آشنایی با فرمت ارسال Media Type‌ها میتوانید به اینجا مراجعه کنید. کاری که در اینجا انجام شده‌است، یافتن ورژن ارسالی توسط استفاده کننده می‌باشد که آن را با فرمت خاصی که نشان داده شد و مابین (v-) و (+) قرار داده، ارسال میشود. ادامه کار به مانند روش Header-based میباشد و از مطرح کردن آن در مقاله خودداری میکنیم.
نتیجه به شکل زیر خواهد بود:


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.


مطالب
معرفی System.Text.Json در NET Core 3.0.
معروفترین کتابخانه‌ی کار با JSON در دات نت، Json.NET است که این روزها، جزء جدایی ناپذیر حداقل، تمام برنامه‌های وب مبتنی بر دات نت می‌باشد. برای مثال ASP.NET Core 2x/1x و همچنین ASP.NET Web API پیش از NET Core.، به صورت پیش‌فرض از این کتابخانه برای کار با JSON استفاده می‌کنند. این کتابخانه 10 سال پیش ایجاد شد و در طول زمان، قابلیت‌های زیادی به آن اضافه شده‌است. همین حجم بالای کار صورت گرفته سبب شده‌است که برای مثال شروع به استفاده‌ی از <Span<T در آن برای بالابردن کارآیی، بسیار مشکل شده و نیاز به تغییرات اساسی در آن داشته باشد. به همین جهت خود تیم CoreFX دات نت Core گزینه‌ی دیگری را برای کار با JSON در فضای نام جدید System.Text.Json ارائه داده‌است که برای کار با آن نیاز به نصب وابستگی ثالثی نیست و همچنین کارآیی آن به علت استفاده‌ی از ویژگی‌های جدید زبان، مانند ref struct و Span، به طور میانگین دو برابر کتابخانه‌ی Json.NET است. برای مثال استفاده‌ی از string (حالت پیش‌فرض کتابخانه‌ی Json.NET) یعنی کار با رشته‌هایی از نوع UTF-16؛ اما کار با Span، امکان دسترسی مستقیم به رشته‌هایی از نوع UTF-8 را میسر می‌کند که نیازی به تبدیل به رشته‌هایی از نوع UTF-16 را ندارند.


ASP.NET Core 3x دیگر به صورت پیش‌فرض به همراه Json.NET ارائه نمی‌شود

در برنامه‌های ASP.NET Core 3x، وابستگی ثالث Json.NET حذف شده‌است و از این پس هر نوع خروجی JSON آن، مانند بازگشت مقادیر مختلف از اکشن متدهای کنترلرها، به صورت خودکار در پشت صحنه از امکانات ارائه شده‌ی در System.Text.Json استفاده می‌کند و دیگر Json.NET، کتابخانه‌ی پیش‌فرض کار با JSON آن نیست. بنابراین برای کار با آن نیاز به تنظیم خاصی نیست. همینقدر که یک پروژه‌ی جدید ASP.NET Core 3x را ایجاد کنید، یعنی در حال استفاده‌ی از System.Text.Json هستید.


روش بازگشت به Json.NET در ASP.NET Core 3x

اگر به هر دلیلی هنوز نیاز به استفاده‌ی از کتابخانه‌ی Json.NET را دارید، آداپتور ویژه‌ی آن نیز تدارک دیده شده‌است. برای اینکار:
الف) ابتدا باید بسته‌ی نیوگت Microsoft.AspNetCore.Mvc.NewtonsoftJson را نصب کنید.
ب) سپس در کلاس Startup، باید این کتابخانه را به صورت یک سرویس جدید، با فراخوانی متد AddNewtonsoftJson، معرفی کرد:
 public void ConfigureServices(IServiceCollection services)
 {
     services.AddControllers()
            .AddNewtonsoftJson()
     // ...
}
یکی از دلایل بازگشت به Json.NET می‌تواند عدم پشتیبانی از OpenAPI / Swagger در حین کار با System.Text.Json باشد و این مورد قرار نیست در نگارش نهایی 3.0، حضور داشته باشد و انطباق با آن به نگارش‌های بعدی موکول شده‌است.


روش کار مستقیم با System.Text.Json

اگر در قسمتی از برنامه‌ی خود نیاز به کار مستقیم با اشیاء JSON را داشته باشید و یا حتی بخواهید از این قابلیت در برنامه‌های کنسول و یا کتابخانه‌ها نیز استفاده کنید، روش انتقال کدهایی که از Json.NET استفاده می‌کنند به System.Text.Json، به صورت زیر است:
public class Person
{
   public string FirstName { get; set; }
   public string LastName { get; set; }
   public DateTime? BirthDay { get; set; }
}
تبدیل رشته‌ی JSON حاوی اطلاعات شخص، به شیء متناظر با آن و یا حالت عکس آن:
using System;
using System.Text.Json.Serialization;

namespace ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Person person = JsonSerializer.Parse<Person>(...);
            string json = JsonSerializer.ToString(person);
        }
    }
}
در اینجا از کلاس System.Text.Json.Serialization.JsonSerializer، روش کار با دو متد Parse را برای Deserialization و ToString را برای Serialization مشاهده می‌کنید.
کلاس JsonSerializer دارای overloadهای زیر برای کار با متدهای Parse و ToString است:
namespace System.Text.Json.Serialization
{
    public static class JsonSerializer
    {
        public static object Parse(ReadOnlySpan<byte> utf8Json, Type returnType, JsonSerializerOptions options = null);
        public static object Parse(string json, Type returnType, JsonSerializerOptions options = null);
        public static TValue Parse<TValue>(ReadOnlySpan<byte> utf8Json, JsonSerializerOptions options = null);
        public static TValue Parse<TValue>(string json, JsonSerializerOptions options = null);

        public static string ToString(object value, Type type, JsonSerializerOptions options = null);
        public static string ToString<TValue>(TValue value, JsonSerializerOptions options = null);
    }
}
یک نکته: کارآیی متد Parse با امضای ReadOnlySpan<byte> utf8Json، بیشتر است از نمونه‌ای که string json را می‌پذیرد. از این جهت که چون با آرایه‌ای از بایت‌های رشته‌ای از نوع UTF-8 کار می‌کند، نیاز به تبدیل به UTF-16 را مانند متدی که string را می‌پذیرد، ندارد. برای تولید آرایه‌ی بایت‌های utf8Json از روی یک شیء، می‌توانید از متد JsonSerializer.ToUtf8Bytes استفاده کنید و یا برای تولید آن از روی یک رشته، از متد Encoding.UTF8.GetBytes استفاده کنید.


سفارشی سازی JsonSerializer جدید

اگر به امضای متدهای Parse و ToString کلاس JsonSerializer دقت کنید، دارای یک پارامتر اختیاری از نوع JsonSerializerOptions نیز هستند که به صورت زیر تعریف شده‌است:
public sealed class JsonSerializerOptions
{
   public bool AllowTrailingCommas { get; set; }
   public int DefaultBufferSize { get; set; }
   public JsonNamingPolicy DictionaryKeyPolicy { get; set; }
   public bool IgnoreNullValues { get; set; }
   public bool IgnoreReadOnlyProperties { get; set; }
   public int MaxDepth { get; set; }
   public bool PropertyNameCaseInsensitive { get; set; }
   public JsonNamingPolicy PropertyNamingPolicy { get; set; }
   public JsonCommentHandling ReadCommentHandling { get; set; }
   public bool WriteIndented { get; set; }
}
برای نمونه معادل تنظیم NullValueHandling در Json.NET:
// Json.NET:
var settings = new JsonSerializerSettings
{
    NullValueHandling = NullValueHandling.Ignore
};
string json = JsonConvert.SerializeObject(person, settings);
اینبار توسط خاصیت IgnoreNullValues صورت می‌گیرد:
// JsonSerializer:
var options = new JsonSerializerOptions
{
    IgnoreNullValues = true
};
string json = JsonSerializer.ToString(person, options);

در برنامه‌های ASP.NET Core که این نوع متدها در پشت صحنه فراخوانی می‌شوند، روش تنظیم JsonSerializerOptions به صورت زیر است:
services.AddControllers()
   .AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true);


نگاشت نام ویژه‌ی خواص در حین عملیات deserialization

در مثال فوق، فرض شده‌است که نام خاصیت BirthDay، دقیقا با اطلاعاتی که از رشته‌ی JSON دریافتی پردازش می‌شود، تطابق دارد. اگر این نام در اطلاعات دریافتی متفاوت است، می‌توان از ویژگی JsonPropertyName برای تعریف این نگاشت استفاده کرد:
[JsonPropertyName("birthdate")]
public DateTime? BirthDay { get; set; }
روش دیگر اینکار، برای نمونه تنظیم PropertyNamingPolicy به حالت CamelCase است:
var options = new JsonSerializerOptions
{
   PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
string json = JsonSerializer.ToString(person, options);
و یا اگر می‌خواهید حساسیت به بزرگی و کوچکی حروف را ندید بگیرید (مانند حالت پیش‌فرض JSON.NET) از تنظیم JsonSerializerOptions.PropertyNameCaseInsensitive استفاده کنید.

در این بین اگر نمی‌خواهید خاصیتی در عملیات serialization و یا برعکس آن پردازش شود، می‌توان از تعریف ویژگی [JsonIgnore] بر روی آن استفاده کرد.
مطالب
معرفی Microsoft.Data.dll یا WebMatrix.Data.dll

مایکروسافت اخیرا علاوه بر تکمیل ORM های خود مانند LINQ to SQL و همچنین Entity framework ، لایه دیگری را نیز بر روی ADO.NET جهت کسانی که به هر دلیلی دوست ندارند با ORMs کار کنند و از نوشتن کوئری‌های مستقیم SQL لذت می‌برند،‌ ارائه داده است که Microsoft.Data library نام دارد و از قابلیت‌های جدید زبان سی شارپ مانند واژه‌ کلیدی dynamic استفاده می‌کند.

در ادامه قصد داریم جهت بررسی توانایی‌های این کتابخانه از بانک اطلاعاتی معروف Northwind استفاده کنیم. این بانک اطلاعاتی را از اینجا می‌توانید دریافت کنید.

مراحل استفاده از Microsoft.Data library:
الف) این اسمبلی جدید به همراه پروژه WebMatrix ارائه شده است. بنابراین ابتدا باید آن‌را دریافت کنید: +
لازم به ذکر است که این کتابخانه اخیرا به WebMatrix.Data.dll تغییر نام یافته است. (اگر وب را جستجو کنید فقط به Microsoft.Data.dll اشاره شده است)

ب) پس از نصب، ارجاعی را از اسمبلی WebMatrix.Data.dll به پروژه خود اضافه نمائید. این اسمبلی در صفحه‌ی Add References ظاهر نمی‌شود و باید کامپیوتر خود را برای یافتن آن جستجو کنید که عموما در آدرس زیر قرار دارد:
C:\Program Files\Microsoft ASP.NET\ASP.NET Web Pages\v1.0\Assemblies\WebMatrix.Data.dll

ج) اتصال به بانک اطلاعاتی
پیش فرض اصلی این کتابخانه بانک اطلاعاتی SQL Server CE است. بنابراین اگر قصد استفاده از پروایدرهای دیگری را دارید باید به صورت صریح آن‌را ذکر نمائید:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="systemData:defaultProvider" value="System.Data.SqlClient" />
</appSettings>
<connectionStrings>
<add name="Northwind"
connectionString="Data Source=(local);Integrated Security = true;Initial Catalog=Northwind"
providerName="System.Data.SqlClient" />
</connectionStrings>
</configuration>

این تعاریف در فایل web.config و یا app.config برنامه وب یا ویندوزی شما قرار خواهند گرفت.

د) نحوه‌ی تعریف کوئری‌ها و دریافت اطلاعات
using System;
using WebMatrix.Data;

namespace TestMicrosoftDataLibrary
{
class Program
{
static void Main(string[] args)
{
getProducts();

Console.Read();
Console.WriteLine("Press a key ...");
}

private static void getProducts()
{
using (var db = Database.Open("Northwind"))
{
foreach (var product in db.Query("select * from products where UnitsInStock < @0", 20))
{
Console.WriteLine(product.ProductName + " " + product.UnitsInStock);
}
}
}
}
}
پس از افزودن ارجاعی به اسمبلی WebMatrix.Data و مشخص سازی رشته‌ی اتصالی به بانک اطلاعاتی، استفاده از آن جهت دریافت اطلاعات کوئری‌ها همانند چند سطر ساده‌ی فوق خواهد بود که از امکانات dynamic زبان سی شارپ 4 استفاده می‌کند؛ به این معنا که product.ProductName و product.UnitsInStock در زمان اجرا مورد ارزیابی قرار خواهند گرفت.
همچنین نکته‌ی مهم دیگر آن نحوه‌ی تعریف پارامتر در آن است (همان 0@ ذکر شده) که نسبت به ADO.NET کلاسیک به شدت ساده شده‌است (و نوشتن کوئری‌های امن و SQL Injection safe را تسهیل می‌کند).
در اینجا Database.Open کار گشودن name ذکر شده در فایل کانفیگ برنامه را انجام خواهد داد. اگر بخواهید این تعاریف را در کدهای خود قرار دهید (که اصلا توصیه نمی‌شود)، می‌توان از متد Database.OpenConnectionString استفاده نمود.

یا مثالی دیگر: استفاده از LINQ حین تعریف کوئری‌ها:
private static void getCustomerFax()
{
using (var db = Database.Open("Northwind"))
{
var product = db.Query("SELECT * FROM [Customers] WHERE City=@0", "Paris").FirstOrDefault();
if (product != null)
Console.WriteLine(product.Fax);
else
Console.WriteLine("not found.");
}
}

ه) اجرای کوئری‌ها بر روی بانک اطلاعاتی
private static void ExecQuery()
{
using (var db = Database.Open("Northwind"))
{
int affectedRecords = db.Execute("UPDATE [Customers] SET fax = fax + '*' WHERE City = @0", "Paris");
Console.WriteLine("Affected records: {0}", affectedRecords);
}
}

با استفاده از متد Execute آن می‌توان کوئری‌های دلخواه خود را بر روی بانک اطلاعاتی اجرا کرد. خروجی آن تعداد رکورد تغییر کرده است.

و) نحوه‌ی اجرای یک رویه ذخیره شده و نمایش خروجی آن
private static void ExecSPShowResult()
{
using (var db = Database.Open("Northwind"))
{
var customer = db.Query("exec CustOrderHist @0", "ALFKI").FirstOrDefault();
if (customer != null)
{
Console.WriteLine(customer.ProductName);
}
}
}
در این مثال رویه ذخیره شده CustOrderHist در بانک اطلاعاتی Northwind اجرا گردیده و سپس اولین خروجی آن نمایش داده شده است.

ز) اجرای یک تابع و نمایش خروجی آن
private static void useFuncs()
{
using (var db = Database.Open("Northwind"))
{
var query = db.Query("SELECT dbo.FN_GET_CATEGORY_TREE(@0) as Rec1", 3);
foreach(var tree in query)
{
Console.WriteLine(tree.Rec1);
}
}
}
در اینجا تابع FN_GET_CATEGORY_TREE موجود در بانک اطلاعاتی Northwind انتخاب گردیده و سپس خروجی آن به کمک یک نام مستعار (برای مثال Rec1) نمایش داده شده است.

سؤال : آیا WebMatrix.Data.dll بهتر است یا استفاده از ORMs ؟

در اینجا چون از قابلیت‌های داینامیک زبان سی شارپ 4 استفاده می‌شود، کامپایلر درکی از اشیاء خروجی و خواص آن‌ها برای مثال tree.Rec1 (در مثال آخر) ندارد و تنها در زمان اجرا است که مشخص می‌شود آیا یک چنین ستونی در خروجی کوئری وجود داشته است یا خیر. اما حین استفاده از ORMs این طور نیست و Schema یک بانک اطلاعاتی پیشتر از طریق نگاشت‌های جداول به اشیاء دات نتی، به کامپایلر معرفی می‌شوند و همین امر سبب می‌شود تا اگر ساختار بانک اطلاعاتی تغییر کرد، پیش از اجرای برنامه و در حین کامپایل بتوان مشکلات را دقیقا مشاهده نمود و سپس برطرف کرد.
ولی در کل استفاده از این کتابخانه نسبت به ADO.NET کلاسیک بسیار ساده‌تر بوده، می‌توان اشیاء و خواص آن‌ها را مطابق نام جداول و فیلدهای بانک اطلاعاتی تعریف کرد و همچنین تعریف پارامترها و برنامه نویسی امن نیز در آن بسیار ساده‌تر شده است.

برای مطالعه بیشتر:
Introduction to Microsoft.Data.dll

مطالب
C# 7 - Local Functions
توابع محلی، امکان تعریف یک تابع را درون یک متد، فراهم می‌کنند. هدف آن‌ها تدارک توابعی کمکی است که به سایر قسمت‌های کلاس مرتبط نمی‌شوند. برای مثال اگر متدی نیاز به کار با یک private method دیگر را دارد و این متد خصوصی در جای دیگری استفاده نمی‌شود، می‌توان جهت بالابردن خوانایی برنامه و سهولت یافتن متد مرتبط، این متد خصوصی را تبدیل به یک تابع محلی، درون همان متد کرد.
static void Main(string[] args)
{
    int Add(int a, int b)
    {
        return a + b;
    }
 
    Console.WriteLine(Add(3, 4)); 
}


بازنویسی کدهای C# 6 با توابع محلی C# 7

کلاس زیر را که بر اساس امکانات C# 6 تهیه شده‌است، در نظر بگیرید:
public class PersonWithPrivateMethod
{
    public string Name { get; set; }
    public int Age { get; set; }

    public override string ToString()
    {
        string ageSuffix = GenerateAgeSuffix(Age);
        return $"{Name} is {Age} year{ageSuffix} old";
    }

    private string GenerateAgeSuffix(int age)
    {
        return age > 1 ? "s" : "";
    }
}
متد خصوصی همین کلاس را توسط Func delegates می‌توان به صورت ذیل خلاصه کرد (باز هم بر اساس امکانات C# 6):
public class PersonWithLocalFuncDelegate
{
    public string Name { get; set; }
    public int Age { get; set; }

    public override string ToString()
    {
        Func<int, string> generateAgeSuffix = age => age > 1 ? "s" : "";
        return $"{Name} is {Age} year{generateAgeSuffix(Age)} old";
    }
}
به این ترتیب نیاز به تعریف یک متد private دیگر کمتر خواهد شد.
اکنون در C# 7 می‌توان این Func delegate را به نحو ذیل تبدیل به یک local function کرد:
public class PersonWithLocalFunction
{
    public string Name { get; set; }
    public int Age { get; set; }

    public override string ToString()
    {
        return $"{Name} is {Age} year{GenerateAgeSuffix(Age)} old";
        // Define a local function:
        string GenerateAgeSuffix(int age)
        {
            return age > 1 ? "s" : "";
        }
    }
}


مزیت کار با local functions نسبت به Func delegates محلی

در قطعه کد فوق، کار انجام شده صرفا استفاده‌ی از یک Syntax جدید نیست؛ بلکه از لحاظ کارآیی نیز سربار کمتری را به همراه دارد. زمانیکه Func Delegates تعریف می‌شوند، کار ایجاد یک anonymous type، وهله سازی و فراخوانی آن‌ها توسط کامپایلر صورت می‌گیرد. اما حین کار با توابع محلی، کامپایلر با یک متد استاندارد سروکار دارد و هیچکدام از مراحل یاد شده و سربارهای آن‌ها رخ نمی‌دهند (هیچگونه GC allocation ایی نخواهیم داشت). به علاوه اینبار کامپایلر فرصت in-line تعریف کردن متد را به نحو بهتری یافته و به این ترتیب کار سوئیچ بین متدهای مختلف کاهش پیدا می‌کند که در نهایت سرعت برنامه را افزایش می‌دهند.


میدان دید توابع محلی

البته با توجه به اینکه متد مثال فوق محلی است، به تمام متغیرها و پارامترهای متد دربرگیرنده‌ی آن نیز دسترسی دارد. بنابراین می‌توان پارامتر int age آن‌را نیز حذف کرد:
public class PersonWithLocalFunctionEnclosing
{
    public string Name { get; set; }
    public int Age { get; set; }

    public override string ToString()
    {
        return $"{Name} is {Age} year{GenerateAgeSuffix()} old";
        // Define a local function:
        string GenerateAgeSuffix()
        {
            return Age > 1 ? "s" : "";
        }
    }
}
به همین جهت نمی‌توانید داخل یک تابع محلی، متغیری را تعریف کنید که هم‌نام یکی از متغیرها یا پارامترهای متد دربرگیرنده‌ی آن باشد.


خلاصه نویسی توابع محلی به کمک expression bodies

می‌توان این متد محلی را به صورت یک expression body ارائه شده‌ی در C# 6 نیز بیان کرد:
public class PersonWithLocalFunctionExpressionBodied
{
    public string Name { get; set; }
    public int Age { get; set; }

    public override string ToString()
    {
        return $"{Name} is {Age} year{GenerateAgeSuffix(Age)} old";
        // Define a local function:
        string GenerateAgeSuffix(int age) => age > 1 ? "s" : "";
    }
}


روش ارسال یک local function به متدی دیگر

امکان ارسال یک تابع محلی به صورت یک Func delegate به متدی دیگر نیز وجود دارد:
public class LocalFunctionsTest
{
    public void PassAnonFunctionToMethod()
    {
        var p = new SimplePerson
        {
            Name = "Name1",
            Age = 42
        };
        OutputSimplePerson(p, GenerateAgeSuffix);
        string GenerateAgeSuffix(int age) => age > 1 ? "s" : "";
    }
 
    private void OutputSimplePerson(SimplePerson person, Func<int, string> suffixFunction)
    {
        Output.WriteLine(
        $"{person.Name} is {person.Age} year{suffixFunction(person.Age)} old");
    }
}
در این مثال GenerateAgeSuffix یک Local function است که به صورت expression body نیز بیان شده‌است. برای ارسال آن به متد OutputSimplePerson، پارامتر دریافتی آن باید به صورت Func تعریف شود.