مطالب
ASP.NET MVC #22

تهیه سایت‌های چند زبانه و بومی سازی نمایش اطلاعات در ASP.NET MVC

زمانیکه دات نت فریم ورک نیاز به انجام اعمال حساس به مسایل بومی را داشته باشد،‌ ابتدا به مقادیر تنظیم شده دو خاصیت زیر دقت می‌کند:
الف) System.Threading.Thread.CurrentThread.CurrentCulture
بر این اساس دات نت می‌تواند تشخیص دهد که برای مثال خروجی متد DateTime.Now.ToString در کانادا و آمریکا باید با هم تفاوت داشته باشند. مثلا در آمریکا ابتدا ماه، سپس روز و در آخر سال نمایش داده می‌شود و در کانادا ابتدا سال، بعد ماه و در آخر روز نمایش داده خواهد شد. یا نمونه‌ی دیگری از این دست می‌تواند نحوه نمایش علامت واحد پولی کشورها باشد.
ب) System.Threading.Thread.CurrentThread.CurrentUICulture
مقدار CurrentUICulture بر روی بارگذاری فایل‌های مخصوصی به نام Resource، تاثیر گذار است.

این خواص را یا به صورت دستی می‌توان تنظیم کرد و یا ASP.NET، این اطلاعات را از هدر Accept-Language دریافتی از مرورگر کاربر به صورت خودکار مقدار دهی می‌کند. البته برای این منظور نیاز است یک سطر زیر را به فایل وب کانفیگ برنامه اضافه کرد:

<system.web>
<globalization culture="auto" uiCulture="auto" />

یا اگر نیاز باشد تا برنامه را ملزم به نمایش اطلاعات Resource مرتبط با فرهنگ بومی خاصی کرد نیز می‌توان در همین قسمت مقادیر culture و uiCulture را دستی تنظیم نمود و یا اگر همانند برنامه‌هایی که چند لینک را بالای صفحه نمایش می‌دهند که برای مثال به نگارش‌های فارسی/عربی/انگلیسی اشاره می‌کند، اینکار را با کد نویسی نیز می‌توان انجام داد:

System.Threading.Thread.CurrentThread.CurrentCulture =
System.Globalization.CultureInfo.CreateSpecificCulture("fa");


جهت آزمایش این مطلب، ابتدا تنظیم globalization فوق را به فایل وب کانفیگ برنامه اضافه کنید. سپس به مسیر زیر در IE مراجعه کنید:

IE -> Tools -> Intenet options -> Genarl tab -> Languages

در اینجا می‌توان هدر Accept-Language را مقدار دهی کرد. برای نمونه اگر مقدار زبان پیش فرض را به فرانسه تنظیم کنیم (به عنوان اولین زبان تعریف شده در لیست) و سپس سعی در نمایش مقدار decimal زیر را داشته باشیم:

string.Format("{0:C}", 10.5M)

اگر زبان پیش فرض، انگلیسی آمریکایی باشد، $ نمایش داده خواهد شد و اگر زبان به فرانسه تنظیم شود، یورو در کنار عدد مبلغ نمایش داده می‌شود.
تا اینجا تنها با تنظیم culture=auto به این نتیجه رسیده‌ایم. اما سایر قسمت‌های صفحه چطور؟ برای مثال برچسب‌های نمایش داده شده را چگونه می‌توان به صورت خودکار بر اساس Accept-Language مرجح کاربر تنظیم کرد؟ خوشبختانه در دات نت، زیر ساخت مدیریت برنامه‌های چند زبانه به صورت توکار وجود دارد که در ادامه به بررسی آن خواهیم پرداخت.


آشنایی با ساختار فایل‌های Resource


فایل‌های Resource یا منبع، در حقیقت فایل‌هایی هستند مبتنی بر XML با پسوند resx و هدف آن‌ها ذخیره سازی رشته‌های متناظر با فرهنگ‌های مختلف می‌باشد و برای استفاده از آن‌ها حداقل یک فایل منبع پیش فرض باید تعریف شود. برای نمونه فایل mydata.resx را در نظر بگیرید. برای ایجاد فایل منبع اسپانیایی متناظر، باید فایلی را به نام mydata.es.resx تولید کرد. البته نوع فرهنگ مورد استفاده را کاملتر نیز می‌توان ذکر کرد برای مثال mydata.es-mex.resx جهت فرهنگ اسپانیایی مکزیکی بکارگرفته خواهد شد، یا mydata.fr-ca.resx به فرانسوی کانادایی اشاره می‌کند. سپس مدیریت منابع دات نت فریم ورک بر اساس مقدار CurrentUICulture جاری، اطلاعات فایل متناظری را بارگذاری خواهد کرد. اگر فایل متناظری وجود نداشت، از اطلاعات همان فایل پیش فرض استفاده می‌گردد.
حین تهیه برنامه‌ها نیازی نیست تا مستقیما با فایل‌های XML منابع کار کرد. زمانیکه اولین فایل منبع تولید می‌شود، به همراه آن یک فایل cs یا vb نیز ایجاد خواهد شد که امکان دسترسی به کلیدهای تعریف شده در فایل‌های XML را به صورت strongly typed میسر می‌کند. این فایل‌های خودکار، تنها برای فایل پیش فرض mydata.resx تولید می‌شوند،‌از این جهت که تعاریف اطلاعات سایر فرهنگ‌های متناظر نیز باید با همان کلیدهای فایل پیش فرض آغاز شوند. تنها «مقادیر» کلیدهای تعریف شده در کلاس‌های منبع متفاوت هستند.
اگر به خواص فایل‌های resx در VS.NET دقت کنیم، نوع Build action آن‌ها به embedded resource تنظیم شده است.


مثالی جهت بررسی استفاده از فایل‌های Resource

یک پروژه جدید خالی ASP.NET MVC را آغاز کنید. فایل وب کانفیگ آن‌را ویرایش کرده و تنظیمات globalization ابتدای بحث را به آن اضافه کنید. سپس مدل، کنترلر و View متناظر با متد Index آن‌را با محتوای زیر به پروژه اضافه نمائید:

namespace MvcApplication19.Models
{
public class Employee
{
public int Id { set; get; }
public string Name { set; get; }
}
}

using System.Web.Mvc;
using MvcApplication19.Models;

namespace MvcApplication19.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
var employee = new Employee { Name = "Name 1" };
return View(employee);
}
}
}

@model MvcApplication19.Models.Employee
@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
<fieldset>
<legend>Employee</legend>
<div class="display-label">
Name
</div>
<div class="display-field">
@Html.DisplayFor(model => model.Name)
</div>
</fieldset>
<fieldset>
<legend>Employee Info</legend>
@Html.DisplayForModel()
</fieldset>

قصد داریم در View فوق بر اساس uiCulture کاربر مراجعه کننده به سایت، برچسب Name را مقدار دهی کنیم. اگر کاربری از ایران مراجعه کند، «نام کارمند» نمایش داده شود و سایر کاربران، «Employee Name» را مشاهده کنند. همچنین این تغییرات باید بر روی متد Html.DisplayForModel نیز تاثیرگذار باشد.
برای این منظور بر روی پوشه Views/Home که محل قرارگیری فایل Index.cshtml فوق است کلیک راست کرده و گزینه Add|New Item را انتخاب کنید. سپس در صفحه ظاهر شده، گزینه «Resources file» را انتخاب کرده و برای مثال نام Index_cshtml.resx را وارد کنید.
به این ترتیب اولین فایل منبع مرتبط با View جاری که فایل پیش فرض نیز می‌باشد ایجاد خواهد شد. این فایل، به همراه فایل Index_cshtml.Designer.cs تولید می‌شود. سپس همین مراحل را طی کنید، اما اینبار نام Index_cshtml.fa.resx را حین افزودن فایل منبع وارد نمائید که برای تعریف اطلاعات بومی ایران مورد استفاده قرار خواهد گرفت. فایل دومی که اضافه شده است، فاقد فایل cs همراه می‌باشد.
اکنون فایل Index_cshtml.resx را در VS.NET باز کنید. از بالای صفحه، به کمک گزینه Access modifier، سطح دسترسی متدهای فایل cs همراه آن‌را به public تغییر دهید. پیش فرض آن internal است که برای کار ما مفید نیست. از این جهت که امکان دسترسی به متدهای استاتیک تعریف شده در فایل خودکار Index_cshtml.Designer.cs را در View های برنامه، نخواهیم داشت. سپس دو جفت «نام-مقدار» را در فایل resx وارد کنید. مثلا نام را Name و مقدار آن‌را «Employee Name» و سپس نام دیگر را NameIsNotRight و مقدار آن‌را «Name is required» وارد نمائید.
در ادامه فایل Index_cshtml.fa.resx را باز کنید. در اینجا نیز دو جفت «نام-مقدار» متناظر با فایل پیش فرض منبع را باید وارد کرد. کلیدها یا نام‌ها یکی است اما قسمت مقدار اینبار باید فارسی وارد شود. مثلا نام را Name و مقدار آن‌را «نام کارمند» وارد نمائید. سپس کلید یا نام NameIsNotRight و مقدار «لطفا نام را وارد نمائید» را تنظیم نمائید.
تا اینجا کار تهیه فایل‌های منبع متناظر با View جاری به پایان می‌رسد.
در ادامه با کمک فایل Index_cshtml.Designer.cs که هربار پس از تغییر فایل resx متناظر آن به صورت خودکار توسط VS.NET تولید و به روز می‌شود، می‌توان به کلیدها یا نام‌هایی که تعریف کرده‌ایم، در قسمت‌های مختلف برنامه دست یافت. برای نمونه تعریف کلید Name در این فایل به نحو زیر است:

namespace MvcApplication19.Views.Home {
public class Index_cshtml {
public static string Name {
get {
return ResourceManager.GetString("Name", resourceCulture);
}
}
}
}

بنابراین برای استفاده از آن در هر View ایی تنها کافی است بنویسیم:

@MvcApplication19.Views.Home.Index_cshtml.Name

به این ترتیب بر اساس تنظیمات محلی کاربر، اطلاعات به صورت خودکار از فایل‌های Index_cshtml.fa.resx فارسی یا فایل پیش فرض Index_cshtml.resx، دریافت می‌گردد.
علاوه بر امکان دسترسی مستقیم به کلیدهای تعریف شده در فایل‌های منبع، امکان استفاده از آن‌ها توسط data annotations نیز میسر است. در این حالت می‌توان مثلا پیغام‌های اعتبار سنجی را بومی کرد یا حین استفاده از متد Html.DisplayForModel، بر روی برچسب نمایش داده شده خودکار، تاثیر گذار بود. برای اینکار باید اندکی مدل برنامه را ویرایش کرد:

using System.ComponentModel.DataAnnotations;

namespace MvcApplication19.Models
{
public class Employee
{
[ScaffoldColumn(false)]
public int Id { set; get; }

[Display(ResourceType = typeof(MvcApplication19.Views.Home.Index_cshtml),
Name = "Name")]
[Required(ErrorMessageResourceType = typeof(MvcApplication19.Views.Home.Index_cshtml),
ErrorMessageResourceName = "NameIsNotRight")]
public string Name { set; get; }
}
}

همانطور که ملاحظه می‌کنید، حین تعریف ویژگی‌های Display یا Required، امکان تعریف نام کلاس متناظر با فایل resx خاصی وجود دارد. به علاوه ErrorMessageResourceName به نام یک کلید در این فایل و یا پارامتر Name ویژگی Display نیز به نام کلیدی در فایل منبع مشخص شده، اشاره می‌کنند. این اطلاعات توسط متدهای Html.DisplayForModel، Html.ValidationMessageFor، Html.LabelFor و امثال آن به صورت خودکار مورد استفاده قرار خواهند گرفت.


نکته‌ای در مورد کش کردن اطلاعات
در این مثال اگر فیلتر OutputCache را بر روی متد Index تعریف کنیم، حتما نیاز است به هدر Accept-Language نیز دقت داشت. در غیراینصورت تمام کاربران، صرفنظر از تنظیمات بومی آن‌ها، یک صفحه را مشاهده خواهند کرد:

[OutputCache(Duration = 60, VaryByHeader = "Accept-Language")]
public ActionResult Index()


نظرات مطالب
سفارشی سازی ASP.NET Core Identity - قسمت دوم - سرویس‌های پایه
در اینترفیس IApplicationRoleManager متدی با امضاء :
 Task<PagedUsersListViewModel> GetPagedApplicationUsersInRoleListAsync
تعریف شده است و همچنین در کلاس ApplicationRoleManager این متد بصورت زیر پیاده سازی شده است :
public async Task<PagedUsersListViewModel> GetPagedApplicationUsersInRoleListAsync
و برنامه خطای زمان کامپایل زیر رو صادر می‌کنه :
does not implement interface member 'IApplicationRoleManager.GetPagedApplicationUsersInRoleListAsync
با توجه به اینکه در بدنه پیاده سازی شده تابع await استفاده شده امکان حذف عبارت async وجود ندارد.این مورد رو چگونه می‌تواند حل کرد؟
مطالب
اصول طراحی شی گرا SOLID - #بخش اول اصل SRP
مخاطب چه کسی است؟
این مقاله برای کسانی در نظر گرفته شده است که حداقل پیش زمینه ای در مورد برنامه نویسی شی گرا داشته باشند.کسانی که تفاوت بین کلاس‌ها و اشیاء را میدانند و میتوانند در مورد ارکان پایه ای برنامه نویسی شی گرایی نظیر : کپسوله سازی (Encapsulation) ، کلاس‌های انتزاعی (Abstraction) ، چند ریختی (Polymorphism ) ، ارث بری (Inheritance) و... صحبت کنند.

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

وقتی کسانی شروع به ایجاد معماری نرم افزار میکنند، نشان میدهند که اهداف خوبی در سر دارند.آنها سعی میکنند از تجارب خود برای ساخت یک طراحی زیبا و تمیز استفاده کنند.  
 
اما با گذشت زمان، نرم افزار کارایی خود را از دست میدهد و بلااستفاده میشود. با هر درخواست ایجاد ویژگی جدید در نرم افزار ، به تدریج نرم افزار شکل خود را از دست میدهد و درنهایت ساده‌ترین تغییرات در نرم افزار موجب تلاش و دقت زیاد، زمان طولانی و مهمتر از همه بالا رفتن تعداد باگها در نرم افزار میشود.
چه کسی مقصر است؟
"تغییرات" یک قسمت جدایی ناپذیر از جهان نرم افزار هستند بنابراین ما نمتوانیم "تغییر" را مقصر بدانیم و در حقیقت این طراحی ما است که مشکل دارد.
یکی از بزرگترین دلایل مخرب کننده‌ی نرم افزار، تعریف وابستگی‌های ناخواسته و بیخود در قسمت‌های مختلف سیستم است. در این گونه طراحی‌ها ، هر قسمت از سیستم وابسته به چندین قسمت دیگر است ، بنابراین تغییر یک قسمت، بر روی قسمت‌های دیگر نیز تاثیر میگذارد و باعث این چنین مشکلاتی میشود. ولی در صورتی که ما قادر به مدیریت این وابستگی باشیم در آینده خواهیم توانست از این سیستم نرم افزاری به آسانی نگهداری کنیم.
 مثال: 

راه حل : اصول ، الگوهای طراحی و معماری نرم افزار
معماری نرم افزار به عنوان مثال MVC, MVP, 3-Tire به ما میگویند که پروژه‌ها از از چه ساختاری استفاده میکنند.
الگوهای طراحی یک سری راه حل‌های قابل استفاده‌ی مجدد را برای مسائلی که به طور معمول اتفاق می‌افتند، فراهم میکند. یا به عبارتی دیگر الگوهای طراحی راه کارهایی را به ما معرفی میکنند که میتوانند برای حل مشکلات کد نویسی بارها مورد استفاده قرار بگیرند.
اصول به ما میگوید اینها را انجام بده تا به آن دست پیدا کنی واینکه چطور انجامش میدهی به خودت بستگی دارد. هر کس یک سری اصول را در زندگی خود تعریف میکند مانند : "من هرگز دروغ نمیگویم" یا "من هرگز سیگار نمیکشم" و از این قبیل. او با دنبال کردن این اصول زندگی آسانی را برای خودش ایجاد میکند. به همین شکل، طراحی شی گرا هم مملو است از اصولی که به ما اجازه میدهد تا با طراحی مناسب مشکلاتمان را مدیریت کنیم.
آقای رابرت مارتین(Robert Martin) این موارد را به صورت زیر طبقه بندی کرده است :
1- اصول طراحی کلاس‌ها که SOLID نامیده می‌شوند.
2- اصول انسجام بسته بندی
3- اصول اتصال بسته بندی
در این مقاله ما در مورد اصول SOLID به همراه مثال‌های کاربردی صحبت خواهیم کرد.
SOLID مخففی از 5 اصول معرفی شده توسط آقای مارتین است:
S -> Single responsibility Principle
O-> Open Close Principle
L-> Liskov substitution principle
I -> Interface Segregation principle
D-> Dependency Inversion principle
اصل 1) S - SRP - Single responsibility Principle 
به کد زیر توجه کنید :
public class Employee
{
    public string EmployeeName { get; set; }
    public int EmployeeNo { get; set; }

    public void Insert(Employee e) 
    {
        //Database Logic written here
    }
    public void GenerateReport(Employee e)
    {
        //Set report formatting
    }
}
در کد بالا هر زمان تغییری در یک قسمت از کد ایجاد شود این احتمال وجود دارد که قسمت دیگری از آن مورد تاثیر این تغییر قرار بگیرد و به مشکل برخورد کنید. دلیل نیز مشخص است : هر دو در یک خانه‌ی مشابه و دارای یک والد یکسان هستند.
برای مثال با تغییر یک پراپرتی ممکن است متدهای هم خانه که از آن استفاده میکنند با مشکل مواجه شوند و باید این تغییرات را نیز در آنها انجام داد. در هر صورت خیلی مشکل است که همه چیز را کنترل کنیم.  بنابراین تنها تغییر موجب دوبرابر شدن عملیات تست میشود و شاید بیشتر.
اصل SRP برای رفع این مشکل میگوید "هر ماژول نرم افزاری میبایست تنها یک دلیل برای تغییر داشته باشد".
(منظور از ماژول نرم افزاری همان کلاس‌ها ، توابع و ... است و عبارت "دلیل برای تغییر" همان مسئولیت است.) به عبارتی هر شی باید یک مسئولیت بیشتر بر عهده نداشته باشد.  هدف این قانون جدا سازی مسئولیت­های چسبیده به هم است. به عنوان مثال کلاسی که هم مسئول ذخیره سازی و هم مسئول ارتباط با واسط کاربر است، این اصل را نقض می­کند و باید به دو کلاس مجزا تقسیم شود.  
 برای رسیدن به این منظور میتوانیم مثال بالا را به صورت 3 کلاس مختلف ایجاد کنیم :
1- Employee  : که حاوی خاصیت‌ها است.
2- EmployeeDB : عملیات دیتابیسی نظیر درج رکورد و واکشی رکوردها از دیتابیس را انجام میدهد.
3- EmployeeReport : وظایف مربوط به ایجاد گزارش‌ها را انجام میدهد.
کد حاصل :
public class Employee
{
    public string EmployeeName { get; set; }
    public int EmployeeNo { get; set; }
}

public class EmployeeDB
{
    public void Insert(Employee e) 
    {
        //Database Logic written here
    }
    public Employee Select() 
    {
        //Database Logic written here
    }
}

public class EmployeeReport
{
    public void GenerateReport(Employee e)
    {
        //Set report formatting
    }
}
این روش برای متد‌ها نیز صدق میکند به طوری که هر متد باید مسئولیت واحدی داشته باشد.
برای مثال قطعه کد زیر اصل SRP را نقض میکند :
//Method with multiple responsibilities – violating SRP
public void Insert(Employee e)
{     
    string StrConnectionString = "";       
    SqlConnection objCon = new SqlConnection(StrConnectionString); 
    SqlParameter[] SomeParameters=null;//Create Parameter array from values
    SqlCommand objCommand = new SqlCommand("InertQuery", objCon);
    objCommand.Parameters.AddRange(SomeParameters);
    ObjCommand.ExecuteNonQuery();
}
این متد وظایف مختلفی را انجام میدهد مانند اتصال به دیتابیس ، ایجاد پارامتر‌ها برای مقادیر، ایجاد کوئری و در نهایت اجرای آن بر روی دیتابیس.
اما با توجه به اصل SRP میتوان آن را به صورت زیر بازنویسی کرد :
//Method with single responsibility – follow SRP
public void Insert(Employee e)
{            
    SqlConnection objCon = GetConnection();
    SqlParameter[] SomeParameters=GetParameters();
    SqlCommand ObjCommand = GetCommand(objCon,"InertQuery",SomeParameters);
    ObjCommand.ExecuteNonQuery();
}

private SqlCommand GetCommand(SqlConnection objCon, string InsertQuery, SqlParameter[] SomeParameters)
{
    SqlCommand objCommand = new SqlCommand(InsertQuery, objCon);
    objCommand.Parameters.AddRange(SomeParameters);
    return objCommand;
}

private SqlParameter[] GetParaeters()
{
    //Create Paramter array from values
}

private SqlConnection GetConnection()
{
    string StrConnectionString = "";
    return new SqlConnection(StrConnectionString);
}
مطالب
آشنایی با Refactoring - قسمت 10

یکی دیگر از روش‌هایی که جهت بهبود کیفیت کدها مورد استفاده قرار می‌گیرد، «طراحی با قراردادها» است؛ به این معنا که «بهتر است» متدهای تعریف شده پیش از استفاده از آرگومان‌های خود، آن‌ها را دقیقا بررسی کنند و به این نوع پیش شرط‌ها، قرارداد هم گفته می‌شود.
نمونه‌ای از آن‌را در قسمت 9 مشاهده کردید که در آن اگر آرگومان‌های متد AddRole، خالی یا نال باشند، یک استثناء صادر می‌شود. این نوع پیغام‌های واضح و دقیق در مورد عدم اعتبار ورودی‌های دریافتی، بهتر است از پیغام‌های کلی و نامفهوم null reference exception که بدون بررسی stack trace و سایر ملاحظات، علت بروز آن‌ها مشخص نمی‌شوند.
در دات نت 4، جهت سهولت این نوع بررسی‌ها، مفهوم Code Contracts ارائه شده است. (این نام هم از این جهت بکارگرفته شده که Design by Contract نام تجاری شرکت ثبت شده‌ای در آمریکا است!)


یک مثال:
متد زیر را در نظر بگیرید. اگر divisor مساوی صفر باشد، استثنای کلی DivideByZeroException صادر می‌شود:

namespace Refactoring.Day10.DesignByContract.Before
{
public class MathMehods
{
public double Divide(int dividend, int divisor)
{
return dividend / divisor;
}
}
}

روش متداول «طراحی با قراردادها» جهت بهبود کیفیت کد فوق پیش از دات نت 4 به صورت زیر است:

using System;

namespace Refactoring.Day10.DesignByContract.After
{
public class MathMehods
{
public double Divide(int dividend, int divisor)
{
if (divisor == 0)
throw new ArgumentException("divisor cannot be zero", "divisor");

return dividend / divisor;
}
}
}

در اینجا پس از بررسی آرگومان divisor، قرارداد خود را به آن اعمال خواهیم کرد. همچنین در استثنای تعریف شده، پیغام واضح‌تری به همراه نام آرگومان مورد نظر، ذکر شده است که از هر لحاظ نسبت به استثنای استاندارد و کلی DivideByZeroException مفهوم‌تر است.

در دات نت 4 ، به کمک امکانات مهیای در فضای نام System.Diagnostics.Contracts، این نوع بررسی‌ها نام و امکانات درخور خود را یافته‌اند:

using System.Diagnostics.Contracts;

namespace Refactoring.Day10.DesignByContract.After
{
public class MathMehods
{
public double Divide(int dividend, int divisor)
{
Contract.Requires(divisor != 0, "divisor cannot be zero");

return dividend / divisor;
}
}
}

البته اگر قطعه کد فوق را به همراه divisor=0 اجرا کنید، هیچ پیغام خاصی را مشاهده نخواهید کرد؛ از این لحاظ که نیاز است تا فایل‌های مرتبط با آن‌را از این آدرس دریافت و نصب کنید. این کتابخانه با VS2008 و VS2010 سازگار است. پس از آن، برگه‌ی Code contracts به عنوان یکی از برگه‌های خواص پروژه در دسترس خواهد بود و به کمک آن می‌توان مشخص کرد که برنامه حین رسیدن به این نوع بررسی‌ها چه عکس العملی را باید بروز دهد.

برای مطالعه بیشتر:
مطالب
بررسی Source Generators در #C - قسمت چهارم - روش دسترسی به تنظیمات برنامه
در حین توسعه‌ی Source Generators، نیاز می‌شود تا بتوان تنظیماتی را از استفاده کننده دریافت کرد؛ برای مثال تعیین فضای نام ویژه‌ای، فعال و غیرفعال کردن قابلیتی و یا حتی دریافت فایل‌های تکمیلی. این تنظیمات سفارشی از طریق تعریف آن‌ها در فایل‌های csproj. و خواص MSBuild قابل دسترسی هستند که روش کار با آن‌ها را در ادامه مرور خواهیم کرد.


روش تعریف خواص سفارشی MSBuild در پروژه‌ی Source Generator

در مثال همین سری، به پوشه‌ی NotifyPropertyChangedGenerator مراجعه کرده و فایل جدید NotifyPropertyChangedGenerator.props را با محتوای زیر به آن اضافه می‌کنیم:
<Project>
    <ItemGroup>
        <CompilerVisibleProperty Include="SourceGenerator_CustomRootNamespace"/>        
    </ItemGroup>
</Project>
هدف این است که خاصیت جدیدی به نام SourceGenerator_CustomRootNamespace، توسط استفاده کننده در فایل csproj. برنامه‌، قابل تعریف شود. علت قرار دادن این تعاریف در یک فایل props. مجزا، آماده کردن این پروژه جهت ارائه‌ی به صورت یک بسته‌ی نیوگت است که نیاز به تنظیمات ذیل را نیز دارد:
<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <GeneratePackageOnBuild>true</GeneratePackageOnBuild> <!-- Generates a package at build -->
        <IncludeBuildOutput>false</IncludeBuildOutput> <!-- Do not include the generator as a lib dependency -->
    </PropertyGroup>

    <ItemGroup>
        <!-- Package the generator in the analyzer directory of the nuget package -->
        <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false"/>

        <!-- Package the props file -->
        <None Include="NotifyPropertyChangedGenerator.props" Pack="true" PackagePath="build" Visible="true"/>
    </ItemGroup>
</Project>
با این تنظیمات، فایل props. یاد شده، در پوشه‌ی build بسته‌ی نیوگت قرار می‌گیرد و با سیستم build پروژه‌ی استفاده کننده یکی می‌شود. همچنین باید در تنظیمات دیگری، مقدار PackagePath را به analyzers/dotnet/cs و IncludeBuildOutput را به false تنظیم کرد تا تولید کننده‌ی کد، در پوشه‌ی مخصوص آنالایزرها قرار گیرد و نه در جای دیگری. اگر این موارد رعایت نشوند، بسته‌ی نیوگت نهایی، سبب تولید کدی نخواهد شد و کار نمی‌کند.

یک نکته‌ی مهم! اگر بخواهیم مستقیما از پروژه‌ی source generator خود مثلا در پروژه‌ی NotifyPropertyChangedGenerator.Demo این سری همانند قبل استفاده کنیم، تنظیمات ذکر شده‌ی در فایل props. فوق، در آن قابل دسترسی نخواهند بود و با پروسه‌ی build یکی نمی‌شوند. تنظیماتی که تا اینجا ذکر شدند، فقط مخصوص بسته‌ی نیوگت نهایی است. برای استفاده‌ی مستقیم از آن‌ها در پروژه‌ی Demo، نیاز است یکبار دیگر محتویات props. تولید کننده‌ی کد را داخل فایل csproj. پروژه‌ی Demo، تعریف کرد. یا می‌توان از روش استفاده از فایل ویژه‌ی Directory.Build.props و قابلیت‌های ارث‌بری آن استفاده کرد. یعنی یک فایل Directory.Build.props را در بالاترین سطح ممکن قرار داد و CompilerVisiblePropertyها را در آن تعریف کرد تا در تمام پروژه‌های برنامه قابل دسترسی شوند.


روش تعریف خواص سفارشی MSBuild در پروژه‌ی استفاده کننده از Source Generator

در مثال این سری چون از بسته‌ی نیوگت تولید کننده‌ی کد استفاده نمی‌کنیم، نیاز است خاصیت سفارشی تعریف شده را یکبار دیگر داخل فایل csproj. پروژه‌ی Demo تعریف کنیم. پس از آن می‌توان این خاصیت را در قسمت PropertyGroup مقدار دهی کرد:
<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <SourceGenerator_CustomRootNamespace>ThisIsTest</SourceGenerator_CustomRootNamespace>
    </PropertyGroup>


    <ItemGroup>
        <CompilerVisibleProperty Include="SourceGenerator_CustomRootNamespace"/>
    </ItemGroup>
</Project>
البته بدیهی است اگر از بسته‌ی نیوگت تولید کننده‌ی کد استفاده شود، نیازی به ذکر قسمت CompilerVisibleProperty نخواهد بود و آن‌را به صورت خودکار از فایل props. به همراه بسته‌ی نیوگت دریافت می‌کند.


روش دسترسی به مقدار خاصیت سفارشی MSBuild در پروژه‌ی Source Generator

پس از این مقدمات، خواص عمومی MSBuild از طریق خاصیت AnalyzerConfigOptions.GlobalOptions و متد TryGetValue آن با فرمول زیر قابل دسترسی هستند. قسمت build_property ثابت بوده و جزو موارد توکار MSBuild است:
internal static class SourceGeneratorContextExtensions
{
    public static string GetMSBuildProperty(
        this GeneratorExecutionContext context,
        string property,
        string defaultValue = "")
    {
        return !context.AnalyzerConfigOptions.GlobalOptions.TryGetValue($"build_property.{property}", out var value)
            ? defaultValue
            : value;
    }
}
برای نمونه روش دسترسی به خاصیت SourceGenerator_CustomRootNamespace تعریف شده، در متد Execute به صورت زیر است:
[Generator]
public class NotifyPropertyChangedGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
    }

    public void Execute(GeneratorExecutionContext context)
    {
        var customRootNamespace = context.GetMSBuildProperty("SourceGenerator_CustomRootNamespace", "Test");


معرفی خاصیت ویژه‌ی AdditionalFiles

تا اینجا روش تعریف یک خاصیت جدید MSBuild و روش دسترسی به آن‌را بررسی کردیم. خاصیت توکاری به نام AdditionalFiles نیز در MSBuild تعریف شده‌است که در پروژه‌های Source Generator جهت دسترسی به فایل‌ها و محتوای آن‌ها قابل استفاده است. برای نمونه می‌توان در فایل csproj. پروژه‌ی Demo تعریف زیر را ارائه کرد:
<Project Sdk="Microsoft.NET.Sdk">
    <ItemGroup>
        <AdditionalFiles Include="file1.txt" Visible="false"/> 
    </ItemGroup>
</Project>
که در اینجا file1.txt، مسیر فایلی است که در پروژه‌ی Source Generator از طریق خاصیت context.AdditionalFiles قابل دسترسی است. AdditionalFiles یک آرایه‌است؛ یعنی می‌توان در پروژه‌ی Demo، چندین AdditionalFiles را تعریف و استفاده کرد. هر AdditionalText که معرف اجزای AdditionalFiles است، به همراه مسیر فایل معرفی شده و همچنین متد GetText است.
خاصیت Visible در اینجا مشخص می‌کند که آیا file1.txt در IDE، در کنار لیست سایر فایل‌ها نمایش داده شود یا خیر.


امکان تعریف خواص سفارشی بر روی AdditionalFiles

فرض کنید علاقمندیم خاصیت ویژه‌ای را به AdditionalFiles اضافه کنیم؛ برای مثال به نام SourceGenerator_EnableLogging مانند مثال زیر:
<Project Sdk="Microsoft.NET.Sdk">
    <ItemGroup>
        <AdditionalFiles Include="file2.txt" SourceGenerator_EnableLogging="true" Visible="false"/> 
    </ItemGroup>
</Project>
روش انجام اینکار به صورت زیر است:
الف) فایل NotifyPropertyChangedGenerator.props تعریف شده را به صورت زیر تکمیل می‌کنیم:
<Project>
    <ItemGroup>
        <CompilerVisibleProperty Include="SourceGenerator_CustomRootNamespace"/>
        
        <CompilerVisibleProperty Include="SourceGenerator_EnableLogging"/>
        <CompilerVisibleItemMetadata Include="AdditionalFiles" MetadataName="SourceGenerator_EnableLogging"/>
    </ItemGroup>
</Project>
یعنی در اینجا هم می‌توان خاصیت SourceGenerator_EnableLogging را به صورت سراسری در قسمت PropertyGroupهای استفاده کننده تعریف کرد و همچنین توسط CompilerVisibleItemMetadata و MetadataName آن، آن‌‌را به AdditionalFiles نیز انتساب داد. برای مثال اگر AdditionalFiles ای به همراه ویژگی SourceGenerator_EnableLogging نبود، اگر مقدار سراسری استفاده شود.

ب) در فایل csproj. پروژه‌ی Demo، از این خواص و متادیتاها استفاده می‌کنیم:
<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <SourceGenerator_EnableLogging>true</SourceGenerator_EnableLogging>
    </PropertyGroup>

    <ItemGroup>
        <CompilerVisibleProperty Include="SourceGenerator_EnableLogging"/>
        <CompilerVisibleItemMetadata Include="AdditionalFiles" MetadataName="SourceGenerator_EnableLogging"/>

        <AdditionalFiles Include="file1.txt" Visible="false"/>  <!-- logging will be controlled by default, or global value -->
        <AdditionalFiles Include="file2.txt" SourceGenerator_EnableLogging="true" Visible="false"/>  <!-- always enable logging for this file -->
        <AdditionalFiles Include="file3.txt" SourceGenerator_EnableLogging="false" Visible="false"/> <!-- never enable logging for this file -->
    </ItemGroup>
</Project>
همانطور که عنوان شد چون از بسته‌ی نیوگت تولید کننده‌ی کد استفاده نمی‌کنیم، نیاز است خواص جدید تعریف شده را یکبار دیگر هم در اینجا تکرار کنیم. پس از آن امکان مقدار دهی SourceGenerator_EnableLogging میسر می‌شود.

ج) برای خواندن این خواص در پروژه‌ی Source generator به صورت زیر عمل می‌شود:
internal static class SourceGeneratorContextExtensions
{
    public static string GetAdditionalFilesOption(
        this GeneratorExecutionContext context,
        AdditionalText additionalText,
        string property,
        string defaultValue = "")
    {
        return !context.AnalyzerConfigOptions.GetOptions(additionalText)
            .TryGetValue($"build_metadata.AdditionalFiles.{property}", out var value)
            ? defaultValue
            : value;
    }
}
- در اینجا build_metadata.AdditionalFiles ثابت بوده و جزو تنظیمات MSBuild است.
- روش تامین AdditionalText این متد را نیز در مثال زیر مشاهده می‌کنید که حاصل ایجاد حلقه‌ای بر روی context.AdditionalFiles‌های دریافتی است:
[Generator]
public class NotifyPropertyChangedGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
    }

    public void Execute(GeneratorExecutionContext context)
    {
        var customRootNamespace = context.GetMSBuildProperty("SourceGenerator_CustomRootNamespace", "Test");

        var globalLoggingSwitch = context.GetMSBuildProperty("SourceGenerator_EnableLogging", "false");
        var emitLoggingGlobal = globalLoggingSwitch.Equals("true", StringComparison.OrdinalIgnoreCase);

        foreach (var file in context.AdditionalFiles)
        {
            var perFileLoggingSwitch = context.GetAdditionalFilesOption(file, "SourceGenerator_EnableLogging");
            var emitLogging = string.IsNullOrWhiteSpace(perFileLoggingSwitch)
                ? emitLoggingGlobal // allow the user to override the global logging on a per-file basis
                : perFileLoggingSwitch.Equals("true", StringComparison.OrdinalIgnoreCase);

            // add the source with or without logging...
        }
در این مثال یکبار به مقدار سراسری SourceGenerator_EnableLogging ارجاع شده که در قسمت PropertyGroupها تعریف شده و سپس درون حلقه، به متادیتای تعریف شده‌ی بر روی AdditionalFiles اشاره می‌کند.
مطالب
استفاده از EF در اپلیکیشن های N-Tier : قسمت اول
تمام اپلیکیشن‌ها را نمی‌توان در یک پروسس بسته بندی کرد، بدین معنا که تمام اپلیکیشن روی یک سرور فیزیکی قرار گیرد. در عصر حاظر معماری بسیاری از اپلیکیشن‌ها چند لایه است و هر لایه روی سرور مجزایی توزیع می‌شود. بعنوان مثال یک معماری کلاسیک شامل سه لایه نمایش (presentation)، اپلیکیشن (application) و داده (data) است. لایه بندی منطقی (logical layering) یک اپلیکیشن می‌تواند در یک App Domain واحد پیاده سازی شده و روی یک کامپیوتر میزبانی شود. در این صورت لازم نیست نگران مباحثی مانند پراکسی ها، مرتب سازی (serialization)، پروتوکل‌های شبکه و غیره باشیم. اما اپلیکیشن‌های بزرگی که چندین کلاینت دارند و در مراکز داده میزبانی می‌شوند باید تمام این مسائل را در نظر بگیرند. خوشبختانه پیاده سازی چنین اپلیکیشن هایی با استفاده از Entity Framework و دیگر تکنولوژی‌های مایکروسافت مانند WCF, Web API ساده‌تر شده است. منظور از n-Tier معماری اپلیکیشن هایی است که لایه‌های نمایش، منطق تجاری و دسترسی داده هر کدام روی سرور مجزایی میزبانی می‌شوند. این تفکیک فیزیکی لایه‌ها به بسط پذیری، مدیریت و نگهداری اپلیکیشن‌ها در دراز مدت کمک می‌کند، اما معمولا تاثیری منفی روی کارایی کلی سیستم دارد. چرا که برای انجام عملیات مختلف باید از محدوده ماشین‌های فیریکی عبور کنیم.

معماری N-Tier چالش‌های بخصوصی را برای قابلیت‌های change-tracking در EF اضافه می‌کند. در ابتدا داده‌ها توسط یک آبجکت EF Context بارگذاری می‌شوند اما این آبجکت پس از ارسال داده‌ها به کلاینت از بین می‌رود. تغییراتی که در سمت کلاینت روی داده‌ها اعمال می‌شوند ردیابی (track) نخواهند شد. هنگام بروز رسانی، آبجکت Context جدیدی برای پردازش اطلاعات ارسالی باید ایجاد شود. مسلما آبجکت جدید هیچ چیز درباره Context پیشین یا مقادیر اصلی موجودیت‌ها نمی‌داند.

در نسخه‌های قبلی Entity Framework توسعه دهندگان با استفاده از قالب ویژه ای بنام Self-Tracking Entities می‌توانستند تغییرات موجودیت‌‌ها را ردیابی کنند. این قابلیت در نسخه EF 6 از رده خارج شده است و گرچه هنوز توسط ObjectContext پشتیبانی می‌شود، آبجکت DbContext از آن پشتیبانی نمی‌کند.

در این سری از مقالات روی عملیات پایه CRUD تمرکز می‌کنیم که در اکثر اپلیکیشن‌های n-Tier استفاده می‌شوند. همچنین خواهیم دید چگونه می‌توان تغییرات موجودیت‌ها را ردیابی کرد. مباحثی مانند همزمانی (concurrency) و مرتب سازی (serialization) نیز بررسی خواهند شد. در قسمت یک این سری مقالات، به بروز رسانی موجودیت‌های منفصل (disconnected) توسط سرویس‌های Web API نگاهی خواهیم داشت.


بروز رسانی موجودیت‌های منفصل با Web API

سناریویی را فرض کنید که در آن برای انجام عملیات CRUD از یک سرویس Web API استفاده می‌شود. همچنین مدیریت داده‌ها با مدل Code-First پیاده سازی شده است. در مثال جاری یک کلاینت Console Application خواهیم داشت که یک سرویس Web API را فراخوانی می‌کند. توجه داشته باشید که هر اپلیکیشن در Solution مجزایی قرار دارد. تفکیک پروژه‌ها برای شبیه سازی یک محیط n-Tier انجام شده است.

فرض کنید مدلی مانند تصویر زیر داریم.

همانطور که می‌بینید مدل جاری، سفارشات یک اپلیکیشن فرضی را معرفی می‌کند. می‌خواهیم مدل و کد دسترسی به داده‌ها را در یک سرویس Web API پیاده سازی کنیم، تا هر کلاینتی که از HTTP استفاده می‌کند بتواند عملیات CRUD را انجام دهد. برای ساختن سرویس مورد نظر مراحل زیر را دنبال کنید.

  • در ویژوال استودیو پروژه جدیدی از نوع ASP.NET Web Application بسازید و قالب پروژه را Web API انتخاب کنید. نام پروژه را به Recipe1.Service تغییر دهید.
  • کنترلر جدیدی از نوع WebApi Controller با نام OrderController به پروژه اضافه کنید.
  • کلاس جدیدی با نام Order در پوشه مدل‌ها ایجاد کنید و کد زیر را به آن اضافه نمایید.
public class Order
{
    public int OrderId { get; set; }
    public string Product { get; set; }
    public int Quantity { get; set; }
    public string Status { get; set; }
    public byte[] TimeStamp { get; set; }
}
  • با استفاده از NuGet Package Manager کتابخانه Entity Framework 6 را به پروژه اضافه کنید.
  • حال کلاسی با نام Recipe1Context ایجاد کنید و کد زیر را به آن اضافه نمایید.
public class Recipe1Context : DbContext
{
    public Recipe1Context() : base("Recipe1ConnectionString") { }
    
    public DbSet<Order> Orders { get; set; }
    
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Order>().ToTable("Orders");
        // Following configuration enables timestamp to be concurrency token
        modelBuilder.Entity<Order>().Property(x => x.TimeStamp)
            .IsConcurrencyToken()
            .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Computed);
    }
}

  • فایل Web.config پروژه را باز کنید و رشته اتصال زیر را به قسمت ConnectionStrings اضافه نمایید.
<connectionStrings>
  <add name="Recipe1ConnectionString"
    connectionString="Data Source=.;
    Initial Catalog=EFRecipes;
    Integrated Security=True;
    MultipleActiveResultSets=True"
    providerName="System.Data.SqlClient" />
</connectionStrings>
  • فایل Global.asax را باز کنید و کد زیر را به آن اضافه نمایید. این کد بررسی Entity Framework Compatibility را غیرفعال می‌کند.
protected void Application_Start()
{
    // Disable Entity Framework Model Compatibilty
    Database.SetInitializer<Recipe1Context>(null);
    ...
}
  • در آخر کد کنترلر Order را با لیست زیر جایگزین کنید.
public class OrderController : ApiController
{
    // GET api/order
    public IEnumerable<Order> Get()
    {
        using (var context = new Recipe1Context())
        {
            return context.Orders.ToList();
        }
    }

    // GET api/order/5
    public Order Get(int id)
    {
        using (var context = new Recipe1Context())
        {
            return context.Orders.FirstOrDefault(x => x.OrderId == id);
        }
    }

    // POST api/order
    public HttpResponseMessage Post(Order order)
    {
        // Cleanup data from previous requests
        Cleanup();
        
        using (var context = new Recipe1Context())
        {
            context.Orders.Add(order);
            context.SaveChanges();
            // create HttpResponseMessage to wrap result, assigning Http Status code of 201,
            // which informs client that resource created successfully
            var response = Request.CreateResponse(HttpStatusCode.Created, order);
            // add location of newly-created resource to response header
            response.Headers.Location = new Uri(Url.Link("DefaultApi",
                new { id = order.OrderId }));
            return response;
        }
    }

    // PUT api/order/5
    public HttpResponseMessage Put(Order order)
    {
        using (var context = new Recipe1Context())
        {
            context.Entry(order).State = EntityState.Modified;
            context.SaveChanges();
            // return Http Status code of 200, informing client that resouce updated successfully
            return Request.CreateResponse(HttpStatusCode.OK, order);
        }
    }

    // DELETE api/order/5
    public HttpResponseMessage Delete(int id)
    {
        using (var context = new Recipe1Context())
        {
            var order = context.Orders.FirstOrDefault(x => x.OrderId == id);
            context.Orders.Remove(order);
            context.SaveChanges();
            // Return Http Status code of 200, informing client that resouce removed successfully
            return Request.CreateResponse(HttpStatusCode.OK);
        }
    }

    private void Cleanup()
    {
        using (var context = new Recipe1Context())
        {
            context.Database.ExecuteSqlCommand("delete from [orders]");
        }
    }
}

قابل ذکر است که هنگام استفاده از Entity Framework در MVC یا Web API، بکارگیری قابلیت Scaffolding بسیار مفید است. این فریم ورک‌های ASP.NET می‌توانند کنترلرهایی کاملا اجرایی برایتان تولید کنند که صرفه جویی چشمگیری در زمان و کار شما خواهد بود.

در قدم بعدی اپلیکیشن کلاینت را می‌سازیم که از سرویس Web API استفاده می‌کند.

  • در ویژوال استودیو پروژه جدیدی از نوع Console Application بسازید و نام آن را به Recipe1.Client تغییر دهید.
  • کلاس موجودیت Order را به پروژه اضافه کنید. همان کلاسی که در سرویس Web API ساختیم.

نکته: قسمت هایی از اپلیکیشن که باید در لایه‌های مختلف مورد استفاده قرار گیرند - مانند کلاس‌های موجودیت‌ها - بهتر است در لایه مجزایی قرار داده شده و به اشتراک گذاشته شوند. مثلا می‌توانید پروژه ای از نوع Class Library بسازید و تمام موجودیت‌ها را در آن تعریف کنید. سپس لایه‌های مختلف این پروژه را ارجاع خواهند کرد.

فایل program.cs را باز کنید و کد زیر را به آن اضافه نمایید.

private HttpClient _client;
private Order _order;

private static void Main()
{
    Task t = Run();
    t.Wait();
    
    Console.WriteLine("\nPress <enter> to continue...");
    Console.ReadLine();
}

private static async Task Run()
{
    // create instance of the program class
    var program = new Program();
    program.ServiceSetup();
    program.CreateOrder();
    // do not proceed until order is added
    await program.PostOrderAsync();
    program.ChangeOrder();
    // do not proceed until order is changed
    await program.PutOrderAsync();
    // do not proceed until order is removed
    await program.RemoveOrderAsync();
}

private void ServiceSetup()
{
    // map URL for Web API cal
    _client = new HttpClient { BaseAddress = new Uri("http://localhost:3237/") };
    // add Accept Header to request Web API content
    // negotiation to return resource in JSON format
    _client.DefaultRequestHeaders.Accept.
        Add(new MediaTypeWithQualityHeaderValue("application/json"));
}

private void CreateOrder()
{
    // Create new order
    _order = new Order { Product = "Camping Tent", Quantity = 3, Status = "Received" };
}

private async Task PostOrderAsync()
{
    // leverage Web API client side API to call service
    var response = await _client.PostAsJsonAsync("api/order", _order);
    Uri newOrderUri;
    
    if (response.IsSuccessStatusCode)
    {
        // Capture Uri of new resource
        newOrderUri = response.Headers.Location;
        // capture newly-created order returned from service,
        // which will now include the database-generated Id value
        _order = await response.Content.ReadAsAsync<Order>();
        Console.WriteLine("Successfully created order. Here is URL to new resource: {0}",  newOrderUri);
    }
    else
        Console.WriteLine("{0} ({1})", (int)response.StatusCode, response.ReasonPhrase);
}

private void ChangeOrder()
{
    // update order
    _order.Quantity = 10;
}

private async Task PutOrderAsync()
{
    // construct call to generate HttpPut verb and dispatch
    // to corresponding Put method in the Web API Service
    var response = await _client.PutAsJsonAsync("api/order", _order);
    
    if (response.IsSuccessStatusCode)
    {
        // capture updated order returned from service, which will include new quanity
        _order = await response.Content.ReadAsAsync<Order>();
        Console.WriteLine("Successfully updated order: {0}", response.StatusCode);
    }
    else
        Console.WriteLine("{0} ({1})", (int)response.StatusCode, response.ReasonPhrase);
}

private async Task RemoveOrderAsync()
{
    // remove order
    var uri = "api/order/" + _order.OrderId;
    var response = await _client.DeleteAsync(uri);

    if (response.IsSuccessStatusCode)
        Console.WriteLine("Sucessfully deleted order: {0}", response.StatusCode);
    else
        Console.WriteLine("{0} ({1})", (int)response.StatusCode, response.ReasonPhrase);
}

اگر اپلیکیشن کلاینت را اجرا کنید باید با خروجی زیر مواجه شوید:

Successfully created order: http://localhost:3237/api/order/1054
Successfully updated order: OK
Sucessfully deleted order: OK

شرح مثال جاری

با اجرای اپلیکیشن Web API شروع کنید. این اپلیکیشن یک کنترلر Web API دارد که پس از اجرا شما را به صفحه خانه هدایت می‌کند. در این مرحله اپلیکیشن در حال اجرا است و سرویس‌های ما قابل دسترسی هستند.

حال اپلیکیشن کنسول را باز کنید. روی خط اول کد program.cs یک breakpoint تعریف کرده و اپلیکیشن را اجرا کنید. ابتدا آدرس سرویس Web API را پیکربندی کرده و خاصیت Accept Header را مقدار دهی می‌کنیم. با این کار از سرویس مورد نظر درخواست می‌کنیم که داده‌ها را با فرمت JSON بازگرداند. سپس یک آبجکت Order می‌سازیم و با فراخوانی متد PostAsJsonAsync آن را به سرویس ارسال می‌کنیم. این متد روی آبجکت HttpClient تعریف شده است. اگر به اکشن متد Post در کنترلر Order یک breakpoint اضافه کنید، خواهید دید که این متد سفارش جدید را بعنوان یک پارامتر دریافت می‌کند و آن را به لیست موجودیت‌ها در Context جاری اضافه می‌نماید. این عمل باعث می‌شود که آبجکت جدید بعنوان Added علامت گذاری شود، در این مرحله Context جاری شروع به ردیابی تغییرات می‌کند. در آخر با فراخوانی متد SaveChanges داده‌ها را ذخیره می‌کنیم. در قدم بعدی کد وضعیت 201 (Created) و آدرس منبع جدید را در یک آبجکت HttpResponseMessage قرار می‌دهیم و به کلاینت ارسال می‌کنیم. هنگام استفاده از Web API باید اطمینان حاصل کنیم که کلاینت‌ها درخواست‌های ایجاد رکورد جدید را بصورت POST ارسال می‌کنند. درخواست‌های HTTP Post بصورت خودکار به اکشن متد متناظر نگاشت می‌شوند.

در مرحله بعد عملیات بعدی را اجرا می‌کنیم، تعداد سفارش را تغییر می‌دهیم و موجودیت جاری را با فراخوانی متد PutAsJsonAsync به سرویس Web API ارسال می‌کنیم. اگر به اکشن متد Put در کنترلر سرویس یک breakpoint اضافه کنید، خواهید دید که آبجکت سفارش بصورت یک پارامتر دریافت می‌شود. سپس با فراخوانی متد Entry و پاس دادن موجودیت جاری بعنوان رفرنس، خاصیت State را به Modified تغییر می‌دهیم، که این کار موجودیت را به Context جاری می‌چسباند. حال فراخوانی متد SaveChanges یک اسکریپت بروز رسانی تولید خواهد کرد. در مثال جاری تمام فیلدهای آبجکت Order را بروز رسانی می‌کنیم. در شماره‌های بعدی این سری از مقالات، خواهیم دید چگونه می‌توان تنها فیلدهایی را بروز رسانی کرد که تغییر کرده اند. در آخر عملیات را با بازگرداندن کد وضعیت 200 (OK) به اتمام می‌رسانیم.

در مرحله بعد، عملیات نهایی را اجرا می‌کنیم که موجودیت Order را از منبع داده حذف می‌کند. برای اینکار شناسه (Id) رکورد مورد نظر را به آدرس سرویس اضافه می‌کنیم و متد DeleteAsync را فراخوانی می‌کنیم. در سرویس Web API رکورد مورد نظر را از دیتابیس دریافت کرده و متد Remove را روی Context جاری فراخوانی می‌کنیم. این کار موجودیت مورد نظر را بعنوان Deleted علامت گذاری می‌کند. فراخوانی متد SaveChanges یک اسکریپت Delete تولید خواهد کرد که نهایتا منجر به حذف شدن رکورد می‌شود.

در یک اپلیکیشن واقعی بهتر است کد دسترسی داده‌ها از سرویس Web API تفکیک شود و در لایه مجزایی قرار گیرد.

مطالب
فیلدهای پویا در NHibernate

یکی از قابلیت‌های جالب NHibernate امکان تعریف فیلدها به صورت پویا هستند. به این معنا که زیرساخت طراحی یک برنامه "فرم ساز" هم اکنون در اختیار شما است! سیستمی که امکان افزودن فیلدهای سفارشی را دارا است که توسط برنامه نویس در زمان طراحی اولیه آن ایجاد نشده‌اند. در ادامه نحوه‌ی تعریف و استفاده از این قابلیت را توسط Fluent NHibernate بررسی خواهیم کرد.

در اینجا کلاسی که قرار است توانایی افزودن فیلدهای سفارشی را داشته باشد به صورت زیر تعریف می‌شود:
using System.Collections;

namespace TestModel
{
public class DynamicEntity
{
public virtual int Id { get; set; }
public virtual IDictionary Attributes { set; get; }
}
}

Attributes در عمل همان فیلدهای سفارشی مورد نظر خواهند بود. جهت معرفی صحیح این قابلیت نیاز است تا نگاشت آن‌را از نوع dynamic component تعریف کنیم:
using FluentNHibernate.Automapping;
using FluentNHibernate.Automapping.Alterations;

namespace TestModel
{
public class DynamicEntityMapping : IAutoMappingOverride<DynamicEntity>
{
public void Override(AutoMapping<DynamicEntity> mapping)
{
mapping.Table("tblDynamicEntity");
mapping.Id(x => x.Id);
mapping.IgnoreProperty(x => x.Attributes);
mapping.DynamicComponent(x => x.Attributes,
c =>
{
c.Map(x => (string)x["field1"]);
c.Map(x => (string)x["field2"]).Length(300);
c.Map(x => (int)x["field3"]);
c.Map(x => (double)x["field4"]);
});
}
}
}
و مهم‌ترین نکته‌ی این بحث هم همین نگاشت فوق است.
ابتدا از IgnoreProperty جهت ندید گرفتن Attributes استفاده کردیم. زیرا درغیراینصورت در حالت Auto mapping ، یک رابطه چند به یک به علت وجود IDictionary به صورت خودکار ایجاد خواهد شد که نیازی به آن نیست (یافتن این نکته نصف روز کار برد ....! چون مرتبا خطای An association from the table DynamicEntity refers to an unmapped class: System.Collections.Idictionary ظاهر می‌شد و مشخص نبود که مشکل از کجاست).
سپس Attributes به عنوان یک DynamicComponent معرفی شده است. در اینجا چهار فیلد سفارشی را اضافه کرده‌ایم. به این معنا که اگر نیاز باشد تا فیلد سفارشی دیگری به سیستم اضافه شود باید یکبار Session factory ساخته شود و SchemaUpdate فراخوانی گردد و خوشبختانه با وجود Fluent NHibernate ، تمام این تغییرات در کدهای برنامه قابل انجام است (بدون نیاز به سر و کار داشتن با فایل‌های XML نگاشت‌ها و ویرایش دستی آن‌ها). از تغییر نام جدول که برای مثال در اینجا tblDynamicEntity در نظر گرفته شده تا افزودن فیلدهای دیگر در قسمت DynamicComponent فوق.
همچنین باتوجه به اینکه این نوع تغییرات (ساخت دوبار سشن فکتوری) مواردی نیستند که قرار باشد هر ساعت انجام شوند، بنابراین سربار آنچنانی را به سیستم تحمیل نمی‌کنند.
   drop table tblDynamicEntity

create table tblDynamicEntity (
Id INT IDENTITY NOT NULL,
field1 NVARCHAR(255) null,
field2 NVARCHAR(300) null,
field3 INT null,
field4 FLOAT null,
primary key (Id)
)

اگر با SchemaExport، اسکریپت خروجی معادل با نگاشت فوق را تهیه کنیم به جدول فوق خواهیم رسید. نوع و طول این فیلدهای سفارشی بر اساس نوعی که برای اشیاء دیکشنری مشخص می‌کنید، تعیین خواهند شد.

چند مثال جهت کار با این فیلدهای سفارشی یا پویا :

نحوه‌ی افزودن رکوردهای جدید بر اساس خاصیت‌های سفارشی:
//insert
object savedId = 0;
using (var session = sessionFactory.OpenSession())
{
using (var tx = session.BeginTransaction())
{
var obj = new DynamicEntity();
obj.Attributes = new Hashtable();
obj.Attributes["field1"] = "test1";
obj.Attributes["field2"] = "test2";
obj.Attributes["field3"] = 1;
obj.Attributes["field4"] = 1.1;

savedId = session.Save(obj);
tx.Commit();
}
}

با خروجی
INSERT
INTO
tblDynamicEntity
(field1, field2, field3, field4)
VALUES
(@p0, @p1, @p2, @p3);
@p0 = 'test1' [Type: String (0)], @p1 = 'test2' [Type: String (0)], @p2 = 1
[Type: Int32 (0)], @p3 = 1.1 [Type: Double (0)]

نحوه‌ی کوئری گرفتن از این اطلاعات (فعلا پایدارترین روشی را که برای آن یافته‌ام استفاده از HQL می‌باشد ...):
//query
using (var session = sessionFactory.OpenSession())
{
using (var tx = session.BeginTransaction())
{
//using HQL
var list = session
.CreateQuery("from DynamicEntity d where d.Attributes.field1=:p0")
.SetString("p0", "test1")
.List<DynamicEntity>();

if (list != null && list.Any())
{
Console.WriteLine(list[0].Attributes["field2"]);
}
tx.Commit();
}
}
با خروجی:
    select
dynamicent0_.Id as Id1_,
dynamicent0_.field1 as field2_1_,
dynamicent0_.field2 as field3_1_,
dynamicent0_.field3 as field4_1_,
dynamicent0_.field4 as field5_1_
from
tblDynamicEntity dynamicent0_
where
dynamicent0_.field1=@p0;
@p0 = 'test1' [Type: String (0)]

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

نحوه‌ی به روز رسانی و حذف اطلاعات بر اساس فیلدهای پویا:
//update
using (var session = sessionFactory.OpenSession())
{
using (var tx = session.BeginTransaction())
{
var entity = session.Get<DynamicEntity>(savedId);
if (entity != null)
{
entity.Attributes["field2"] = "new-val";
tx.Commit(); // Persist modification
}
}
}

//delete
using (var session = sessionFactory.OpenSession())
{
using (var tx = session.BeginTransaction())
{
var entity = session.Get<DynamicEntity>(savedId);
if (entity != null)
{
session.Delete(entity);
tx.Commit();
}
}
}

بازخوردهای دوره
مدیریت تغییرات گریدی از اطلاعات به کمک استفاده از الگوی واحد کار مشترک بین ViewModel و لایه سرویس
 کد SelectedItem را در صورت نیاز می‌توانید به صورت زیر عمل کنید خاصیت زیر را به کلاس ViewModel اضافه کنید 
public User SelectedUser { get; set; }
و در سمت View
SelectedItem="{Binding SelectedUser}"
و در نهایت برای تابع Delete
UsersList.Remove(SelectedUser);

مطالب
توسعه برنامه‌های Cross Platform با Xamarin Forms & Bit Framework - قسمت هجدهم
در این قسمت می‌خواهیم با Rest Api ارتباط برقرار کنیم. به جای نوشتن سمت سرور، از یک سرور آماده استفاده می‌کنیم که مثال اول آن، LIST USERS است و لیست کاربران را نمایش می‌دهد. توضیحات این قسمت به فراخوانی سرویس‌های Rest ارتباط دارد، با پروتکل HTTP و دیتای JSON. البته فراخوانی سرویس‌های SOAP نیز ساده است که در این آموزش به آنها نمی‌پردازیم.
برای این کار از HttpClient استفاده می‌کنیم. استفاده کردن از WebClient و WebRequest اشتباه محض هست و این دو را کلا فراموش کنید. مطمئن باشید هر کدی که با آن دو در اینترنت پیدا می‌کنید، با HttpClient هم قابلیت پیاده سازی را دارند و مطمئن باشید که اگر از آن دو کلاس استفاده کنید، حتما به دردسر بدی میافتید. در زمان استفاده از HttpClient هم در نظر بگیرید که نباید مدام HttpClient را new و dispose کنید. این کار اشتباه است و یک HTTP client برای شما کافی است. ساختن HTTP client نکات  بسیاری دارد که در همین سایت به آنها پرداخته شده‌است. در Xamarin دغدغه‌های استفاده از Network Stack هر سیستم عامل نیز به لیست مواردی که باید به آنها دقت کنید اضافه می‌شوند. می‌توانید درگیر تمامی این موارد شوید، یا برای سادگی بیشتر، ضمن نصب پکیج Bit.CSharpClient.Rest که کدهای آن نیز در GitHub قرار داده شده‌اند، صرفا HTTP Client را بگیرید و به هر جایی که دوست دارید Request بزنید. لزومی به اینکه در سمت سرور از Bit استفاده کرده باشید تا بتوانید از Bit.CSharpClient.Rest استفاده کنید نیست.

خب، پس Package مربوطه را نصب و در App.xaml.cs کدهای زیر را استفاده کنید:
//قرار دهید containerBuilder.RegisterRequiredServices(); این دو را بعد از
containerBuilder.RegisterHttpClient();
containerBuilder.RegisterIdentityClient();
در View Model ای که قصد استفاده از Http Client را دارید، یک Property از جنس Http Client تعریف کنید و برای خواندن اطلاعات مثال، کد زیر را بزنید:
توضیحات این کد در ادامه آمده است.
public virtual HttpClient HttpClient { get; set; } 

async Task CallUsersListApiUsingHttpClient()
{
    HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "https://reqres.in/api/users");
    // Use request.Headers to set jwt token, ...
    // Use request.Content to send body. You can use StringContent, StreamContent, etc.
    HttpResponseMessage response = await HttpClient.SendAsync(request);
    response.EnsureSuccessStatusCode();
    using (StreamReader streamReader = new StreamReader(await response.Content.ReadAsStreamAsync()))
    using (JsonReader jsonReader = new JsonTextReader(streamReader))
    {
        List<UserDto> users = (await JToken.LoadAsync(jsonReader))["data"].ToObject<List<UserDto>>();
    }
}
برای درک بهتر این کد، بعد از Clone/Pull کردن آخرین وضعیت پروژه XamApp به سراغ کلاس RestSamplesViewModel بروید. فراخوانی https://reqres.in/api/users چنین JSON ای را بر می‌گرداند: 
{
    "page": 2,
    "per_page": 3,
    "total": 12,
    "total_pages": 4,
    "data": [
        {
            "id": 4,
            "first_name": "Eve",
            "last_name": "Holt",
            "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/marcoramires/128.jpg"
        },
        {
            "id": 5,
            "first_name": "Charles",
            "last_name": "Morris",
            "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/stephenmoon/128.jpg"
        }
    ]
}

قسمت‌های مختلف این JSON برای ما اهمیتی ندارند و تنها قسمت data آن که اطلاعات user‌ها را شامل می‌شود، برای ما اهمیت دارند. صد البته که هر سروری، دیتای JSON را با ساختاری که دوست داشته باشد بر می‌گرداند. در کدی که نوشته‌ایم، ابتدا یک HttpRequestMessage را ساخته‌ایم. این HttpRequestMessage از نوع Get و به آدرس https://reqres.in/api/users است. می‌توان روی HttpRequestMessage هم هدرهای مختلفی را تنظیم نمود و هم می‌توان به آن Content داد.
سپس آن را با HttpClient.SendAsync ارسال می‌کنیم و با فراخوانی EnsureSuccessStatusCode مطمئن می‌شویم که خطا نداده‌است. برای خواندن Response با بالاترین Performance ممکن، ابتدا از StreamReader برای خواندن Stream دریافتی استفاده می‌کنیم. با توجه به JSON بودن Response دریافتی، از JsonTextReader و JToken استفاده می‌کنیم (این مورد هیچ ربطی به JWT یا Json Web Token ندارد!). بعد از Load کردن آن، قسمت ["data"] را به لیستی از کلاس UserDto تبدیل می‌کنیم. Dto مخفف Data Transfer Object است و دیتایی است که ما یا ارسال می‌کنیم یا در همین سناریو مثال، از سرور دریافت می‌کنیم. کد کلاس UserDto:
public class UserDto
{
    [JsonProperty("id")]
    public int Id { get; set; }
    [JsonProperty("first_name")]
    public string FirstName { get; set; }
    [JsonProperty("last_name")]
    public string LastName { get; set; }
    [JsonProperty("avatar")]
    public string Avatar { get; set; }
}
البته Http Client فقط برای ارسال یا دریافت JSON نیست. می‌توان با آن فایل و Xml و ... نیز ارسال و دریافت نمود. در این قسمت مهم نبود که سرور شما با چه تکنولوژی ای توسعه داده شده‌است. صرف بودن سرور روی پروتکل Http کافی است. واضح است که شما دارید از HttpClient استفاده می‌کنید. در صورتیکه سرور TCP باشد، شما در CSharp می‌توانید از TcpClient و Socket استفاده کنید. اگر سمت سرور شما Wcf یا OData یا Graphql باشد نیز کلاینت‌های خودشان را در CSharp دارید و می‌توانید در پروژه‌تان از تمامی آنها استفاده کنید که آموزش همه این موارد از حوصله این قسمت خارج است؛ اما در صورتیکه سمت سرور شما نیز با Bit توسعه داده شده باشد، می‌توانید با روش‌های خیلی بهتری به سرور خود وصل شوید که این موضوع قسمت‌های بعدی آموزش است.
مطالب
آشنایی با Refactoring - قسمت 7


یکی دیگر از روش‌های Refactoring ، معرفی کردن یک کلاس بجای پارامترها است. عموما تعریف متدهایی با بیش از 5 پارامتر مزموم است:

using System;
using System.Collections.Generic;

namespace Refactoring.Day7.IntroduceParameterObject.Before
{
public class Registration
{
public void Create(string name, DateTime date, DateTime validUntil,
IEnumerable<string> courses, decimal credits)
{
// do work
}
}
}

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

using System;
using System.Collections.Generic;

namespace Refactoring.Day7.IntroduceParameterObject.After
{
public class RegistrationContext
{
public string Name {set;get;}
public DateTime Date {set;get;}
public DateTime ValidUntil {set;get;}
public IEnumerable<string> Courses {set;get;}
public decimal Credits { set; get; }
}
}

namespace Refactoring.Day7.IntroduceParameterObject.After
{
public class Registration
{
public void Create(RegistrationContext registrationContext)
{
// do work
}
}
}

یکی از مزایای این روش، منعطف شدن معرفی متدها است؛ به این صورت که اگر نیاز به افزودن پارامتر دیگری باشد، تنها کافی است یک خاصیت جدید به کلاس RegistrationContext اضافه شود و امضای متد Create،‌ ثابت باقی خواهد ماند.

روش دیگر تشخیص نیاز به این نوع Refactoring ، یافتن پارامترهایی هستند که در یک گروه قرار می‌گیرند. برای مثال:

public int GetIndex(int pageSize, int pageNumber, ...) { ...

همانطور که ملاحظه می‌کنید تعدادی از پارامترها در اینجا با کلمه page شروع شده‌اند. بهتر است این‌ پارامترهای مرتبط را به یک کلاس مجزا به نام Page انتقال داد.