- Transparency: عدم تغییر سیستم تحت آزمون: مثل افزودن امکانات اضافی به کد محصول جهت سهولت آزمایش
- Scope: قابلیت اجرا بر روی هر دو حالت Debug و Release
- Simplicity: سربار کم و سادگی تست برای تغییر
ترکیب ماژولهای مدیریت شده به یک اسمبلی
اگر حقیقت را بخواهید CLR نمیتواند با ماژولها کار کند، بلکه با اسمبلیها کار میکند. اسمبلی یک مفهوم انتزاعی است که به سختی میتوان برای بار اول آن را درک کرد.
اول از همه: اسمبلی یک گروه منطقی از یک یا چند ماژول یا فایلهای ریسورس (منبع) است.
دوم: اسمبلی کوچکترین واحد استفاده مجدد، امنیت و نسخه بندی است.
بر اساس انتخابی که شما در استفاده از کامپایلرها و ابزارها کردهاید، نسخهی نهایی شامل یک یا چند فایل اسمبلی خواهد شد. در دنیای CLR ما یک اسمبلی را کامپوننت صدا میزنیم.
شکل زیر در مورد اسمبلیها توضیح میدهد. آنچه که شکل زیر توضیح میدهد تعدادی از ماژولهای مدیریت شده به همراه فایلهای منابع یا دیتا توسط ابزارهایی که مورد پردازش قرار گرفتهاند به فایلهای 32 یا 64 بیتی تبدیل شدهاند که داخل یک گروه بندی منطقی از فایلها قرار گرفتهاند. آنچه که اتفاق میافتد این هست که این فایلهای 32 یا 64 بیتی شامل بلوکی از دادههایی است که با نام manifest شناخته میشوند. manifest یک مجموعه دیگر از جداول متادیتاها است. این جداول به توصیف فایلهای تشکیل دهنده اسمبلی میپردازد.
همه کارهای تولید اسمبلی به صورت خودکار اتفاق میافتد. ولی در صورتیکه قصد دارید فایلی را به اسمبلی به طور دستی اضافه کنید نیاز است که به دستورات و ابزارهای کامپایلر آشنایی داشته باشید.
یک اسمبلی به شما اجازه میدهد تا مفاهیم فیزیکی و منطقی کامپوننت را از هم جدا سازید. اینکه چگونه کد و منابع خود را از یکدیگر جدا کنید به خود شما بر میگردد. برای مثال اگر قصد دارید منابع یا نوع دادهای را که به ندرت مورد استفاده قرار میگیرد، در یک فایل جدا از اسمبلی نگهداری کنید، این فایل جدا میتواند بر اساس تقاضای کاربر در زمان اجرای برنامه از اینترنت دریافت شود. حال اگر همین فایل هیچگاه استفاده نشود، در زمان نصب برنامه و مقدار حافظه دیسک سخت صرفه جویی خواهد شد. اسمبلیها به شما اجازه میدهند که فایلهای توزیع برنامه را به چندین قسمت بشکنید، در حالی که همهی آنها متعلق به یک مجموعه هستند.
یک ماژول اسمبلی شامل اطلاعاتی در رابطه با ارجاعاتش است؛ به علاوه ورژن خود اسمبلی. این اطلاعات سبب میشوند که یک اسمبلی خود تعریف self-describing شود که به بیان سادهتر باعث میشود CLR وابستگیهای یک اسمبلی را تشخیص داده تا ترتیب اجرای آنها را پیدا کند. نه دیگر نیازی به اطلاعات اضافی در ریجستری است و نه در Active Directory Domain Service یا به اختصار ADDS.
از آنجایی که هیچ اطلاعاتی اضافی نیست، توزیع ماژولهای مدیریت شده راحتتر از ماژولهای مدیریت نشده است.
مطلب مشابهی نیز در وبلاگ آقای شهروز جعفری برای توصیف اسمبلیها وجود دارد که خیلی خوب هست به قسمت مطالب مرتبط آن هم نگاهی داشته باشید.
JSON.NET یک کتابخانهی سورس باز کار با اشیاء JSON در دات نت است. تاریخچهی آن به 8 سال قبل بر میگردد و توسط یک برنامه نویس نیوزیلندی به نام James Newton King تهیه شدهاست. اولین نگارش آن در سال 2006 ارائه شد؛ مقارن با زمانی که اولین استاندارد JSON نیز ارائه گردید.
این کتابخانه از آن زمان تا کنون، 6 میلیون بار دانلود شدهاست و به علت کیفیت بالای آن، این روزها پایه اصلی بسیاری از کتابخانهها و فریم ورکهای دات نتی میباشد؛ مانند RavenDB تا ASP.NET Web API و SignalR مایکروسافت و همچنین گوگل نیز از آن جهت تدارک کلاینتهای کار با API خود استفاده میکنند.
هرچند دات نت برای نمونه در نگارش سوم آن جهت مصارف WCF کلاسی را به نام DataContractJsonSerializer ارائه کرد، اما کار کردن با آن محدود است به فرمت خاص WCF به همراه عدم انعطاف پذیری و سادگی کار با آن. به علاوه باید درنظر داشت که JSON.NET از دات نت 2 به بعد تا مونو، Win8 و ویندوز فون را نیز پشتیبانی میکند.
برای نصب آن نیز کافی است دستور ذیل را در کنسول پاورشل نیوگت اجرا کنید:
PM> install-package Newtonsoft.Json
معماری JSON.NET
کتابخانهی JSON.NET از سه قسمت عمده تشکیل شدهاست:
الف) JsonSerializer
ب) LINQ to JSON
ج) JSON Schema
الف) JsonSerializer
کار JsonSerializer تبدیل اشیاء دات نتی به JSON و برعکس است. مزیت مهم آن امکانات قابل توجه تنظیم عملکرد و خروجی آن میباشد که این تنظیمات را به شکل ویژگیهای خواص نیز میتوان اعمال نمود. به علاوه امکان سفارشی سازی هر کدام نیز توسط کلاسی به نام JsonConverter، پیش بینی شدهاست.
یک مثال:
var roles = new List<string> { "Admin", "User" }; string json = JsonConvert.SerializeObject(roles, Formatting.Indented);
و یا در مثال ذیل استفاده از یک anonymous object را مشاهده میکنید:
var jsonString = JsonConvert.SerializeObject(new { Id =1, Name = "Test" }, Formatting.Indented);
تنظیمات پیشرفتهتر JSON.NET
مزیت مهم JSON.NET بر سایر کتابخانههای موجود مشابه، قابلیتهای سفارشی سازی قابل توجه آن است. در مثال ذیل نحوهی معرفی JsonSerializerSettings را مشاهده مینمائید:
var jsonData = JsonConvert.SerializeObject(new { Id = 1, Name = "Test", DateTime = DateTime.Now }, new JsonSerializerSettings { Formatting = Formatting.Indented, Converters = { new JavaScriptDateTimeConverter() } });
{ "Id": 1, "Name": "Test", "DateTime": new Date(1409821985245) }
نوشتن خروجی JSON در یک استریم
خروجی متد JsonConvert.SerializeObject یک رشتهاست که در صورت نیاز به سادگی توسط متد File.WriteAllText در یک فایل قابل ذخیره میباشد. اما برای رسیدن به حداکثر کارآیی و سرعت میتوان از استریمها نیز استفاده کرد:
using (var stream = File.CreateText(@"c:\output.json")) { var jsonSerializer = new JsonSerializer { Formatting = Formatting.Indented }; jsonSerializer.Serialize(stream, new { Id = 1, Name = "Test", DateTime = DateTime.Now }); }
تبدیل JSON رشتهای به اشیاء دات نت
اگر رشتهی jsonData ایی را که پیشتر تولید کردیم، بخواهیم تبدیل به نمونهای از شیء User ذیل کنیم:
public class User { public int Id { set; get; } public string Name { set; get; } public DateTime DateTime { set; get; } }
var user = JsonConvert.DeserializeObject<User>(jsonData);
البته در اینجا با توجه به استفاده از JavaScriptDateTimeConverter برای تولید jsonData، نیاز است چنین تنظیمی را نیز در حالت DeserializeObject مشخص کنیم:
var user = JsonConvert.DeserializeObject<User>(jsonData, new JsonSerializerSettings { Converters = { new JavaScriptDateTimeConverter() } });
مقدار دهی یک نمونه یا وهلهی از پیش موجود
متد JsonConvert.DeserializeObject یک شیء جدید را ایجاد میکند. اگر قصد دارید صرفا تعدادی از خواص یک وهلهی موجود، توسط JSON.NET مقدار دهی شوند از متد PopulateObject استفاده کنید:
JsonConvert.PopulateObject(jsonData, user);
کاهش حجم JSON تولیدی
زمانیکه از متد JsonConvert.SerializeObject استفاده میکنیم، تمام خواص عمومی تبدیل به معادل JSON آنها خواهند شد؛ حتی خواصی که مقدار ندارند. این خواص در خروجی JSON، با مقدار null مشخص میشوند. برای حذف این خواص از خروجی JSON نهایی تنها کافی است در تنظیمات JsonSerializerSettings، مقدار NullValueHandling = NullValueHandling.Ignore مشخص گردد.
var jsonData = JsonConvert.SerializeObject(object, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, Formatting = Formatting.Indented });
به علاوه حذف Formatting = Formatting.Indented نیز توصیه میگردد. در این حالت فشردهترین خروجی ممکن حاصل خواهد شد.
مدیریت ارث بری توسط JSON.NET
در مثال ذیل کلاس کارمند و کلاس مدیر را که خود نیز در اصل یک کارمند میباشد، ملاحظه میکنید:
public class Employee { public string Name { set; get; } } public class Manager : Employee { public IList<Employee> Reports { set; get; } }
var employee = new Employee { Name = "User1" }; var manager1 = new Manager { Name = "User2" }; var manager2 = new Manager { Name = "User3" }; manager1.Reports = new[] { employee, manager2 }; manager2.Reports = new[] { employee };
var list = JsonConvert.SerializeObject(manager1, Formatting.Indented);
{ "Reports": [ { "Name": "User1" }, { "Reports": [ { "Name": "User1" } ], "Name": "User3" } ], "Name": "User2" }
- در اینجا مشخص نیست که این اشیاء، کارمند هستند یا مدیر. برای مثال مشخص نیست User2 چه نوعی دارد و باید به کدام شیء نگاشت شود.
- مشکل دوم در مورد کاربر User1 است که در دو قسمت تکرار شدهاست. این شیء JSON اگر به نمونهی معادل دات نتی خود نگاشت شود، به دو وهله از User1 خواهیم رسید و نه یک وهلهی اصلی که سبب تولید این خروجی JSON شدهاست.
برای حل این دو مشکل، تغییرات ذیل را میتوان به JSON.NET اعمال کرد:
var list = JsonConvert.SerializeObject(manager1, new JsonSerializerSettings { Formatting = Formatting.Indented, TypeNameHandling = TypeNameHandling.Objects, PreserveReferencesHandling = PreserveReferencesHandling.Objects });
{ "$id": "1", "$type": "JsonNetTests.Manager, JsonNetTests", "Reports": [ { "$id": "2", "$type": "JsonNetTests.Employee, JsonNetTests", "Name": "User1" }, { "$id": "3", "$type": "JsonNetTests.Manager, JsonNetTests", "Reports": [ { "$ref": "2" } ], "Name": "User3" } ], "Name": "User2" }
- با تنظیم PreserveReferencesHandling = PreserveReferencesHandling.Objects شماره Id خودکاری نیز به خروجی JSON اضافه میگردد. اینبار اگر به گزارش دهندهها با دقت نگاه کنیم، مقدار $ref=2 را خواهیم دید. این مورد سبب میشود تا در حین نگاشت نهایی، دو وهله متفاوت از شیء با Id=2 تولید نشود.
باید دقت داشت که در حین استفاده از JsonConvert.DeserializeObject نیز باید JsonSerializerSettings یاد شده، تنظیم شوند.
ویژگیهای قابل تنظیم در JSON.NET
علاوه بر JsonSerializerSettings که از آن صحبت شد، در JSON.NET امکان تنظیم یک سری از ویژگیها به ازای خواص مختلف نیز وجود دارند.
- برای نمونه ویژگی JsonIgnore معروفترین آنها است:
public class User { public int Id { set; get; } [JsonIgnore] public string Name { set; get; } public DateTime DateTime { set; get; } }
- با استفاده از ویژگی JsonProperty اغلب مواردی را که پیشتر بحث کردیم مانند NullValueHandling، TypeNameHandling و غیره، میتوان تنظیم نمود. همچنین گاهی از اوقات کتابخانههای جاوا اسکریپتی سمت کاربر، از اسامی خاصی که از روشهای نامگذاری دات نتی پیروی نمیکنند، در طراحی خود استفاده میکنند. در اینجا میتوان نام خاصیت نهایی را که قرار است رندر شود نیز صریحا مشخص کرد. برای مثال:
[JsonProperty(PropertyName = "m_name", NullValueHandling = NullValueHandling.Ignore)] public string Name { set; get; }
- استفاده از ویژگی JsonObject به همراه مقدار OptIn آن به این معنا است که از کلیه خواصی که دارای ویژگی JsonProperty نیستند، صرفنظر شود. حالت پیش فرض آن OptOut است؛ یعنی تمام خواص عمومی در خروجی JSON حضور خواهند داشت منهای مواردی که با JsonIgnore مزین شوند.
[JsonObject(MemberSerialization.OptIn)] public class User { public int Id { set; get; } [JsonProperty] public string Name { set; get; } public DateTime DateTime { set; get; } }
- با استفاده از ویژگی JsonConverter میتوان نحوهی رندر شدن مقدار خاصیت را سفارشی سازی کرد. برای مثال:
[JsonConverter(typeof(JavaScriptDateTimeConverter))] public DateTime DateTime { set; get; }
تهیه یک JsonConverter سفارشی
با استفاده از JsonConverterها میتوان کنترل کاملی را بر روی اعمال serialization و deserialization مقادیر خواص اعمال کرد. مثال زیر را در نظر بگیرید:
public class HtmlColor { public int Red { set; get; } public int Green { set; get; } public int Blue { set; get; } } var colorJson = JsonConvert.SerializeObject(new HtmlColor { Red = 255, Green = 0, Blue = 0 }, Formatting.Indented);
public class HtmlColorConverter : JsonConverter { public override bool CanConvert(Type objectType) { return objectType == typeof(HtmlColor); } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { throw new NotSupportedException(); } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { var color = value as HtmlColor; if (color == null) return; writer.WriteValue("#" + color.Red.ToString("X2") + color.Green.ToString("X2") + color.Blue.ToString("X2")); } }
از آنجائیکه این تبدیلگر صرفا قرار است برای حالت serialization استفاده شود، قسمت ReadJson آن پیاده سازی نشدهاست.
در آخر برای استفاده از آن خواهیم داشت:
var colorJson = JsonConvert.SerializeObject(new HtmlColor { Red = 255, Green = 0, Blue = 0 }, new JsonSerializerSettings { Formatting = Formatting.Indented, Converters = { new HtmlColorConverter() } });
اسمبلیهای نام قوی در برابر دستکاری مقاوم هستند
از آنجائیکه محتویات اسمبلی، هش شده و مقدار هش آن امضا میشود، در نتیجه اگر شخصی به دستکاری اسمبلی اقدام کرده باشد یا اینکه فایل مد نظر آسیب دیده باشد، به راحتی قابل شناسایی است و آن اسمبلی به عنوان اسمبلی صحیح شناسایی نخواهد شد و نمیگذارد در GAC ثبت شود.
موقعیکه برنامه نیاز داشته باشد به اسمبلی نام قوی بایند یا متصل شود، از 4 ویژگی گفته شدهی در قسمت قبلی استفاده میکند تا آن را در GAC بیابد. اگر اسمبلی درخواستی موجود باشد، زیر دایرکتوری آن برگشت داده خواهد شد؛ ولی اگر آن را نیابد، ابتدا در داخل دایرکتوری برنامه و سپس در مسیرهایی که در فایل پیکربندی ذکر شدهاند، به دنبال آن خواهد گشت و در نهایت اگر برنامه توسط فایل MSI نصب شده باشد، محلهای توزیع را از طریق آن جویا خواهد شد و اگر باز به نتیجهای نرسد، اتصال ناموفق گزارش شده و خطای زیر را ایجاد خواهد کرد:
System.IO.FileNotFoundException
System.IO.FileLoadExceptio
- جلوگیری از دستکاری و حفظ امنیت آن
- این اسمبلی تنها یکبار از حافظهی فیزیکی استفاده میکند؛ برعکس توزیع خصوصی که برای هر برنامه باید یک فضای دیسکی داشته باشد.
- توزیع سادهتر برای نسخههای آینده فراهم میشود که بعدا در مورد آن توضیح میدهیم.
سیاستهای انتخاب اسمبلی توسط GAC
موقعی که GAC میخواهد یک اسمبلی را برای برنامه شما بازگرداند، از روی خصوصیاتی چون نام اسمبلی، ورژن، فرهنگ، توکن کلید عمومی و در نهایت بسته به معماری ماشین، یک اسمبلی را برمیگرداند. در صورتیکه اسمبلی خاصی را برای ماشین مورد نظر پیدا نکند، از اسمبلی یک ماشین دیگر استفاده خواهد کرد. در واقع این انتخاب، یک سیاست پیش فرض است که میتواند از طریق ناشر یا مدیر سیستم تغییر پیدا کند و رونویسی شود.
کنترل مدیریت پیشرفته (پیکربندی)
در قسمت بیستم با ساخت فایل پیکریندی و نحوه تنظیم کردن اسکن اسمبلیها در CLR آشنا شدیم. این بار قصد داریم در مورد المانهای دیگر این فایل پیکریندی صحبت کنیم. فایل پیکربندی زیر را بررسی میکنیم:
<?xml version="1.0"?> <configuration> <runtime> <assemblyBinding xmlns="urn:schemasmicrosoftcom:asm.v1"> <probing privatePath="AuxFiles;bin\subdir" /> <dependentAssembly> <assemblyIdentity name="SomeClassLibrary" publicKeyToken="32ab4ba45e0a69a1" culture="neutral"/> <bindingRedirect oldVersion="1.0.0.0" newVersion="2.0.0.0" /> <codeBase version="2.0.0.0" href="http://www.Wintellect.com/SomeClassLibrary.dll" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="TypeLib" publicKeyToken="1f2e74e897abbcfe" culture="neutral"/> <bindingRedirect oldVersion="3.0.0.03.5.0.0" newVersion="4.0.0.0" /> <publisherPolicy apply="no" /> </dependentAssembly> </assemblyBinding> </runtime> </configuration>
Probing | در
قسمت بیستم گفتیم که در این المان، مکانهایی را که CLR برای پیدا کردن
اسمبلیهای با نام ضعیف، باید اسکن کند، وارد میکنیم که هر مسیر با , از هم جدا شدهاست. برای اسمبلیهای با نام قوی CLR باید داخل GAC را نگاه
کند و در URL هایی که از طریق المان CodeBase مشخص کردهایم. اگر المان
CodeBase مشخص نشود، CLR برای پیدا کردن اسمبلیهای با نام قوی، داخل
دایرکتوریهای این المان را اسکن خواهد کرد. |
Dependent Assembly اول | در
اولین فرزند این المان، Assembly Identity یک اسمبلی با مشخصاتی مثل
Culture و توکن عمومی معرفی میشود و در BindingRedirect به CLR اطلاع
میدهد موقعی که به دنبال نسخه یک این اسمبلی است، نسخهی دو آن را برای
استفاده جایگزین کند. |
Code Base | این
المان میگوید که وقتی CLR سعی در پیدا کردن نسخهی 2 اسمبلی را دارد، آن را
از طریق آدرس مورد نظر پیدا کند. این المان میتواند برای اسمبلیهای با نام
ضعیف هم کار کند. |
Dependent Assembly دوم | این مورد هم همانند سابق است با این تفاوت که گسترهی نسخه 3 تا 3.5 را به نسخهی 4 تغییر میدهد. |
Publisher Policy | اگر
سازمان تولید کننده این اسمبلی فایلی برای تعیین Policy به همراه اسمبلی
ارائه کرده باشد، این المان باعث میشود این فایل ندیده گرفته شود (در مورد
فایل Policy در آینده صحبت میکنیم). |
با وجود این حالت اگر فرض کنیم مدیر یک سیستم متوجه شود که اسمبلی برنامه دچار مشکل شده است و با ناشر تماس بگیرد و ناشر نسخهی جدیدی از آن اسمبلی را در اختیار او بگذارد، مدیر سیستم میتواند به راحتی از طریق فایل پیکربندی، CLR را به استفادهی از اسمبلی جدید به جای اسمبلی قدیمی هدایت کند.
نکته : اگر مدیر بخواهد تمام برنامههای موجود از این اسمبلی جدید استفاده کنند باید فایل machine.config را ویرایش کند.
پیشنیازها
- مطالعهی سری کار با Angular CLI خصوصا قسمت نصب و قسمت ساخت برنامههای آن، پیش از مطالعهی این مطلب ضروری است.
- همچنین فرض بر این است که سری ASP.NET Core را نیز یکبار مرور کردهاید و با نحوهی برپایی یک برنامهی MVC آن و ارائهی فایلهای استاتیک توسط یک پروژهی ASP.NET Core آشنایی دارید.
ایجاد یک پروژهی جدید ASP.NET Core در VS 2017
در ابتدا یک پروژهی خالی ASP.NET Core را در VS 2017 ایجاد خواهیم کرد. برای این منظور:
- ابتدا از طریق منوی File -> New -> Project (Ctrl+Shift+N) گزینهی ایجاد یک ASP.NET Core Web application را انتخاب کنید.
- در صفحهی بعدی آن هم گزینهی «empty template» را انتخاب نمائید.
تنظیمات یک برنامهی ASP.NET Core خالی برای اجرای یک برنامهی Angular CLI
برای اجرای یک برنامهی مبتنی بر Angular CLI، نیاز است بر روی فایل csproj برنامهی ASP.NET Core کلیک راست کرده و گزینهی Edit آنرا انتخاب کنید.
سپس محتوای این فایل را به نحو ذیل تکمیل نمائید:
الف) درخواست عدم کامپایل فایلهای TypeScript
<PropertyGroup> <TargetFramework>netcoreapp1.1</TargetFramework> <TypeScriptCompileBlocked>true</TypeScriptCompileBlocked> </PropertyGroup>
ب) مشخص کردن پوشههایی که باید الحاق و یا حذف شوند
<ItemGroup> <Folder Include="Controllers\" /> <Folder Include="wwwroot\" /> </ItemGroup> <ItemGroup> <Compile Remove="node_modules\**" /> <Content Remove="node_modules\**" /> <EmbeddedResource Remove="node_modules\**" /> <None Remove="node_modules\**" /> </ItemGroup> <ItemGroup> <Compile Remove="src\**" /> <Content Remove="src\**" /> <EmbeddedResource Remove="src\**" /> </ItemGroup>
سپس دو پوشهی node_modules و src واقع در ریشهی پروژه را نیز به طور کامل از سیستم ساخت و توزیع VS 2017 حذف کردهایم. پوشهی node_modules وابستگیهای Angular را به همراه دارد و src همان پوشهی برنامهی Angular ما خواهد بود.
ج) افزودن وابستگیهای سمت سرور مورد نیاز
<ItemGroup> <PackageReference Include="Microsoft.AspNetCore" Version="1.1.1" /> <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="1.1.2" /> <PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="1.1.1" /> </ItemGroup> <ItemGroup> <DotNetCliToolReference Include="Microsoft.DotNet.Watcher.Tools" Version="1.0.0" /> </ItemGroup> <ItemGroup> <!-- extends watching group to include *.js files --> <Watch Include="**\*.js" Exclude="node_modules\**\*;**\*.js.map;obj\**\*;bin\**\*" /> </ItemGroup>
در اینجا Watcher.Tools هم به همراه تنظیمات آن اضافه شدهاند که در ادامهی بحث به آن اشاره خواهد شد.
افزودن یک کنترلر Web API جدید
با توجه به اینکه دیگر در اینجا قرار نیست با فایلهای cshtml و razor کار کنیم، کنترلرهای ما نیز از نوع Web API خواهند بود. البته در ASP.NET Core، کنترلرهای معمولی آن، توانایی ارائهی Web API و همچنین فایلهای Razor را دارند و از این لحاظ تفاوتی بین این دو نیست و یکپارچگی کاملی صورت گرفتهاست.
using System.Collections.Generic; using Microsoft.AspNetCore.Mvc; namespace ASPNETCoreIntegrationWithAngularCLI.Controllers { [Route("api/[controller]")] public class ValuesController : Controller { // GET: api/values [HttpGet] [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] public IEnumerable<string> Get() { return new string[] { "Hello", "DNT" }; } } }
تنظیمات فایل آغازین یک برنامهی ASP.NET Core جهت ارائهی برنامههای Angular
در ادامه به فایل Startup.cs برنامهی خالی جاری، مراجعه کرده و آنرا به نحو ذیل تغییر دهید:
using System; using System.IO; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace ASPNETCoreIntegrationWithAngularCLI { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddMvc(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { loggerFactory.AddConsole(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.Use(async (context, next) => { await next(); if (context.Response.StatusCode == 404 && !Path.HasExtension(context.Request.Path.Value) && !context.Request.Path.Value.StartsWith("/api/", StringComparison.OrdinalIgnoreCase)) { context.Request.Path = "/index.html"; await next(); } }); app.UseMvcWithDefaultRoute(); app.UseDefaultFiles(); app.UseStaticFiles(); } } }
در قسمت app.Use آن، تنظیمات URL Rewriting مورد نیاز جهت کار با مسیریابی برنامههای Angular را مشاهده میکنید. برای نمونه اگر کاربری در ابتدای کار آدرس /products را درخواست کند، این درخواست به سمت سرور ارسال میشود و چون چنین صفحهای در سمت سرور وجود ندارد، خطای 404 بازگشت داده میشود و کار به پردازش برنامهی Angular نخواهد رسید. اینجا است که تنظیم میانافزار فوق، کار مدیریت خروجیهای 404 را بر عهده گرفته و کاربر را به فایل index.html برنامهی تک صفحهای وب، هدایت میکند. به علاوه در اینجا اگر درخواست وارد شده، دارای پسوند باشد (یک فایل باشد) و یا با api/ شروع شود (اشاره کنندهی به کنترلرهای Web API برنامه)، از این پردازش و هدایت به صفحهی index.html معاف خواهد شد.
ایجاد ساختار اولیهی برنامهی Angular CLI در داخل پروژهی جاری
اکنون از طریق خط فرمان به پوشهی ریشهی برنامهی ASP.NET Core، جائیکه فایل Startup.cs قرار دارد، وارد شده و دستور ذیل را اجرا کنید:
>ng new ClientApp --routing --skip-install --skip-git --skip-commit
پس از تولید ساختار برنامهی Angular CLI، به پوشهی آن وارد شده و تمام فایلهای آن را Cut کنید. سپس به پوشهی ریشهی برنامهی ASP.NET Core جاری، وارد شده و این فایلها را در آنجا paste نمائید. به این ترتیب به حداکثر سازگاری ساختار پروژههای Angular CLI و VS 2017 خواهیم رسید. زیرا اکثر فایلهای تنظیمات آنرا میشناسد و قابلیت پردازش آنها را دارد.
پس از این cut/paste، پوشهی خالی ClientApp را نیز حذف کنید.
تنظیم محل خروجی نهایی Angular CLI به پوشهی wwwroot
برای اینکه سیستم Build پروژهی Angular CLI جاری، خروجی خود را در پوشهی wwwroot قرار دهد، تنها کافی است فایل .angular-cli.json را گشوده و outDir آنرا به wwwroot تنظیم کنیم:
"apps": [ { "root": "src", "outDir": "wwwroot",
فراخوانی کنترلر Web API برنامه در برنامهی Angular CLI
در ادامه صرفا جهت آزمایش برنامه، فایل src\app\app.component.ts را گشوده و به نحو ذیل تکمیل کنید:
import { Component, OnInit } from '@angular/core'; import { Http } from '@angular/http'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent implements OnInit { constructor(private _httpService: Http) { } apiValues: string[] = []; ngOnInit() { this._httpService.get('/api/values').subscribe(values => { this.apiValues = values.json() as string[]; }); } }
سپس این آرایه را در فایل قالب این کامپوننت (src\app\app.component.html) استفاده خواهیم کرد:
<h1>Application says:</h1> <ul *ngFor="let value of apiValues"> <li>{{value}}</li> </ul> <router-outlet></router-outlet>
نصب وابستگیهای برنامهی Angular CLI
در ابتدای ایجاد پوشهی ClientApp، از پرچم skip-install استفاده شد، تا صرفا ساختار پروژه، جهت cut/paste آن با سرعت هر چه تمامتر، ایجاد شود. اکنون برای نصب وابستگیهای آن یا میتوان در solution explorer به گره dependencies مراجعه کرده و npm را انتخاب کرد. در ادامه با کلیک راست بر روی آن، گزینهی restore packages ظاهر میشود. و یا میتوان به روش متداول این نوع پروژهها، از طریق خط فرمان به پوشهی ریشهی پروژه وارد شد و دستور npm install را صادر کرد. بهتر است اینکار را از طریق خط فرمان انجام دهید تا مطمئن شوید که از آخرین نگارشهای این ابزار که بر روی سیستم نصب شدهاست، استفاده میکنید.
روش اول اجرای برنامههای مبتنی بر ASP.NET Core و Angular CLI
تا اینجا اگر برنامه را از طریق VS 2017 اجرا کنید، خروجی را مشاهده نخواهید کرد. چون هنوز فایل index.html آن تولید نشدهاست.
بنابراین روش اول اجرای این نوع برنامهها، شامل مراحل ذیل است:
الف) ساخت پروژهی Angular CLI در حالت watch
> ng build --watch
ب) اجرای برنامه از طریق ویژوال استودیو
اکنون که کار ایجاد محتوای پوشهی wwwroot برنامه انجام شدهاست، میتوان برنامه را از طریق VS 2017 به روش متداولی اجرا کرد:
یک نکته: میتوان قسمت الف را تبدیل به یک Post Build Event هم کرد. برای این منظور باید فایل csproj را به نحو ذیل تکمیل کرد:
<Target Name="AngularBuild" AfterTargets="Build"> <Exec Command="ng build" /> </Target>
تنها مشکل روش Post Build Event، کند بودن آن است. زمانیکه از روش ng build --watch به صورت مستقل استفاده میشود، برای بار اول اجرا، اندکی زمان خواهد برد؛ اما اعمال تغییرات بعدی به آن بسیار سریع هستند. چون صرفا نیاز دارد این تغییرات اندک و تدریجی را کامپایل کند و نه کامپایل کل پروژه را از ابتدا.
روش دوم اجرای برنامههای مبتنی بر ASP.NET Core و Angular CLI
روش دومی که در اینجا بررسی خواهد شد، مستقل است از قسمت «ب» روش اول که توضیح داده شد. برنامههای NET Core. نیز به همراه CLI خاص خودشان هستند و نیازی نیست تا حتما از VS 2017 برای اجرای آنها استفاده کرد. به همین جهت وابستگی Microsoft.DotNet.Watcher.Tools را نیز در ابتدای کار به وابستگیهای برنامه اضافه کردیم.
الف) در ادامه، VS 2017 را به طور کامل ببندید؛ چون نیازی به آن نیست. سپس دستور ذیل را در خط فرمان، در ریشهی پروژه، صادر کنید:
> dotnet watch run
>dotnet watch run [90mwatch : [39mStarted Hosting environment: Production Now listening on: http://localhost:5000 Application started. Press Ctrl+C to shut down.
ب) در اینجا چون برنامه بر روی پورت 5000 ارائه شدهاست، بهتر است دستور ng serve -o را صادر کرد تا بتوان به نحو سادهتری از سرور وب ASP.NET Core استفاده نمود. در این حالت برنامهی Angular CLI بر روی پورت 4200 ارائه شده و بلافاصله در مرورگر نیز نمایش داده میشود.
مشکل! سرور وب ما بر روی پورت 5000 است و سرور آزمایشی Angular CLI بر روی پورت 4200. اکنون برنامهی Angular ما، یک چنین درخواستهایی را به سمت سرور، جهت دریافت اطلاعات ارسال میکند: localhost:4200/api
برای رفع این مشکل میتوان فایلی را به نام proxy.config.json با محتویات ذیل ایجاد کرد:
{ "/api": { "target": "http://localhost:5000", "secure": false } }
>ng serve --proxy-config proxy.config.json -o
مزیت این روش، به روز رسانی خودکار مرورگر با انجام هر تغییری در کدهای قسمت Angular برنامه است.
نکته 1: بدیهی است میتوان قسمت «ب» روش دوم را با قسمت «الف» روش اول نیز جایگزین کرد (ساخت پروژهی Angular CLI در حالت watch). اینبار گشودن مرورگر بر روی پورت 5000 (و یا آدرس http://localhost:5000) را باید به صورت دستی انجام دهید. همچنین هربار تغییر در کدهای Angular، سبب refresh خودکار مرورگر نیز نمیشود که آنرا نیز باید خودتان به صورت دستی انجام دهید (کلیک بر روی دکمهی refresh پس از هر بار پایان کار ng build).
نکته 2: میتوان قسمت «الف» روش دوم را حذف کرد (حذف dotnet run در حالت watch). یعنی میخواهیم هنوز هم ویژوال استودیو کار آغاز IIS Express را انجام دهد. به علاوه میخواهیم برنامه را توسط ng serve مشاهده کنیم (با همان پارامترهای قسمت «ب» روش دوم). در این حالت تنها موردی را که باید تغییر دهید، پورتی است که برای IIS Express تنظیم شدهاست. عدد این پورت را میتوان در فایل Properties\launchSettings.json مشاهده کرد و سپس به تنظیمات فایل proxy.config.json اعمال نمود.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: ASPNETCoreIntegrationWithAngularCLI.zip
به همراه این کدها تعدادی فایل bat نیز وجود دارند که جهت ساده سازی عملیات یاد شدهی در این مطلب، میتوان از آنها استفاده کرد:
- فایل restore.bat کار بازیابی و نصب وابستگیهای پروژهی دات نتی و همچنین Angular CLI را انجام میدهد.
- دو فایل ng-build-dev.bat و ng-build-prod.bat بیانگر قسمت «الف» روش اول هستند. فایل dev مخصوص حالت توسعه است و فایل prod مخصوص ارائهی نهایی.
- دو فایل dotnet_run.bat و ng-serve-proxy.bat خلاصه کنندهی قسمتهای «الف» و «ب» روش دوم هستند.
نحوهی اعمال تنظیمات کامپایلر TypeScript
روشهای متفاوتی جهت اعمال تنظیمات کامپایلر TypeScript وجود دارند:
الف) ذکر پارامترها و سوئیچهای کامپایلر خط فرمان tsc به صورت مستقیم.
ب) بعضی از ادیتورها و IDEها این پارامترها را به صورت گزینهها و دیالوگهایی ارائه میدهند.
ج) استفاده از یک Build task، همانند روشی که در تنظیمات VSCode در مطلب «چرا TypeScript» مشاهده کردید.
د) ذکر تنظیمات کامپایلر، در فایل مخصوصی به نام tsconfig.json.
گزینههای متداول کامپایلر TypeScript
گزینههای کامپایلر TypeScript نسبتا قابل توجه هستند و لیست کامل و به روز آنها را در هندبوک تایپاسکریپت میتوانید مشاهده کنید. در اینجا تعدادی از مهمترینها را بررسی خواهیم کرد:
- سوئیچ module-- جهت مشخص سازی فرمت خروجی ماژولهای TypeScript بکار میرود. در مطلب بررسی ماژولها عنوان شد که TypeScript قادر است ماژولهای تعریف شده را با سایر فرمتهای متداول جاوا اسکریپت مانند common.js و amd نیز تولید کند. سوئیچ module-- جهت تنظیم این گزینه درنظر گرفته شدهاست. خلاصهای این سوئیچ نیز m-- است. این سوئیچ یکی از مقادیر commonjs, amd, system, es2015 را میپذیرد. اگر es2015 را مشخص کردید، نیاز است target را نیز به ES6 تنظیم کنید.
- سوئیچ moduleResolution-- نحوهی یافتن ارجاعات به ماژولها را مشخص میکند. در اینجا روشهای node.js و کلاسیک را میتوان قید کرد.
- سوئیچ target-- برای تعیین نگارش خروجی جاوا اسکریپت تولیدی بکار میرود. حالت پیش فرض آن ES3 است و ES5 و ES6 را نیز پشتیبانی میکند.
- گزینهی watch-- کامپایلر را در حالت watch نگه میدارد. در این حالت تغییرات آخرین تاریخ نوشته شدن در فایلهای ts بررسی شده و در صورت یافتن تغییری، بلافاصله خروجی js آنها تهیه میشود.
- سوئیچ outDir-- برای مشخص کردن پوشهی فایلهای تولیدی نهایی بکار میرود.
- گزینهی noImplicitAny-- برای ممنوع کردن نوعهای Any متغیرها به صورت پیش فرض است و در این حالت خطای کامپایلری را مشاهده خواهید کرد. استفاده از این گزینه به این معنا نیست که دیگر نمیتوان از نوع Any استفاده کرد؛ بلکه به این معنا است که در صورت نیاز باید آنرا به صورت صریح قید کنید.
یک مثال:
در VSCode و در پوشهی vscode. آن، در تنظیمات فایل tasks.json، چنین گزینههایی را میتوان برای کامپایلر tsc، ذکر کرد:
"args": ["--target", "ES5", "--outDir", "js", "--module", "commonjs", "--sourceMap", "--watch", "app.ts"],
بررسی کاربرد فایل tsconfig.json
فایل ویژهی tsconfig.json در نگارش 1.5 تایپاسکریپت معرفی گردید. هدف از این فایل، ساده کردن تعریف پارامترهای کامپایلر است؛ البته الزامی به استفادهی از آن وجود ندارد.
این فایل مزایای ذیل را به همراه دارد:
الف) محل قرارگیری آن، ریشهی پروژهی TypeScript را مشخص میکند.
ب) تنظیمات ذکر شدهی در این فایل، به تمام فایلهای موجود در پوشه و زیر پوشههای محل قرارگیری آن به صورت پیش فرض اعمال میشوند.
هنگامیکه در تنظیمات کامپایلر tsc، نام فایل یا فایلهای ts ایی را ذکر نمیکنید، این کامپایلر در ابتدا به دنبال فایل tsconfig.json میگردد و بر این اساس فایلهای ts را پردازش خواهد کرد. اگر مانند مثال فوق، در انتهای پارامترها، نام فایلی را ذکر کنید، از فایل tsconfig.json صرفنظر خواهد شد.
یک نکته: برای ذکر صریح محل فایل tsconfig از پارامتر project استفاده کنید:
tsc --project ./lib
در این حالت میتوان کامپایلر tsc را بدون پارامتری اجرا کرد و این برنامه اطلاعات مورد نیاز خود را از فایل tsconfig.json دریافت خواهد کرد. باید دقت داشت، هر سوئیچی که در خط فرمان ذکر شود، پارامترهای معادل ذکر شدهی در فایل tsconfig.json را بازنویسی میکند. بنابراین در صورت وجود این فایل، میتوان خاصیت args مثال قبل را به یک آرایهی خالی تنظیم کرد.
د) امکان مشخص سازی الحاق و عدم الحاق فایلهای ts را به همراه دارد.
{ "compilerOptions": { "target": "es5", "outDir": "js", "module":"commonjs", "sourceMap":true }, "files": [ "app.ts", "classes.ts" ] }
کار خاصیت files الحاق و include است. اگر میخواهید از پوشهها و یا فایلهایی صرفنظر شود، از خاصیت exclude استفاده کنید:
{ "compilerOptions": { "target": "es5", "outDir": "js" }, "exclude": [ "node_modules", "lib" ] }
یک نکته
در VSCode داخل فایل tsconfig.json با فشردن ctrl+space، به یک intellisense حاوی گزینههای تکمیل کنندهی آن خواهید رسید.
ساده سازی الحاق فایلهای تعاریف نوعها
در مطلب «مبانی TypeScript؛ تهیه فایلهای تعاریف نوعها» با فایلهای ویژهی d.ts. آشنا شدیم. استفادهی از این فایلها به همراه ذکر اجباری reference path مرتبط در ابتدای هر فایل ts است. اینکار اضافی را با استفاده از فایل tsconfig.json میتوان حذف کرد:
{ "compilerOptions": { "target": "es5", "outDir": "js", "module": "commonjs", "sourceMap": true, "watch": true }, "files": [ "app.ts", "typings/main.d.ts" ] }
تغییرات الگوریتمهای هش کردن اطلاعات
با حذف و تغییرنام کلاسهایی مانند 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(); } }
"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(); }
تولید اعداد تصادفی Thread safe در NET Core.
روشهای زیادی برای تولید اعداد تصادفی در برنامههای دات نت وجود دارند؛ اما مشکل اکثر آنها این است که thread safe نیستند و نباید از آنها در برنامههای چند ریسمانی (مانند برنامههای وب)، به نحو متداولی استفاده کرد. در این بین تنها کلاسی که thread safe است، کلاس RNGCryptoServiceProvider میباشد؛ آن هم با یک شرط:
private static readonly RNGCryptoServiceProvider Rand = new RNGCryptoServiceProvider();
بنابراین اگر در کدهای خود چنین تعریفی را دارید:
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; } }
public class Startup { public void ConfigureServices(IServiceCollection services) { services.TryAddSingleton<IRandomNumberProvider, RandomNumberProvider>();
نیاز به الگوریتمهای رمزنگاری متقارن قوی و معادل بهتر آنها در 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); } } }
public class Startup { public void ConfigureServices(IServiceCollection services) { services.TryAddSingleton<IProtectionProvider, ProtectionProvider>();
مستندات مفصل این 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()); } }
نگاشت APM به یک Task
در قسمت اول، نمونه مثالی را از APM، که در آن کار با BeginGetResponse آغاز شده و سپس در callback نهایی توسط EndGetResponse، نتیجهی عملیات به دست میآید، مشاهده کردید. در ادامه میخواهیم یک محصور کنندهی جدید را برای این نوع API قدیمی تهیه کنیم، تا آنرا به صورت یک Task ارائه دهد.
public static class ApmWrapper { public static Task<int> ReadAsync(this Stream stream, byte[] data, int offset, int count) { return Task<int>.Factory.FromAsync(stream.BeginRead, stream.EndRead, data, offset, count, null); } }
در مثال فوق BeginRead و EndRead استفاده شده از نوع delegate هستند. چون خروجی EndRead از نوع int است، خروجی متد نیز از نوع Task of int تعیین شدهاست. همچنین سه پارامتر ابتدایی BeginRead ، دقیقا data، offset و count هستند. دو پارامتر آخر آن callback و state نام دارند. پارامتر callback توسط متد FromAsync فراهم میشود و state نیز در اینجا null درنظر گرفته شدهاست.
یک مثال استفاده از آنرا در ادامه مشاهده میکنید:
using System; using System.IO; using System.Threading.Tasks; namespace Async06 { public static class ApmWrapper { public static Task<int> ReadAsync(this Stream stream, byte[] data, int offset, int count) { return Task<int>.Factory.FromAsync(stream.BeginRead, stream.EndRead, data, offset, count, null); } } class Program { static void Main(string[] args) { using (var stream = File.OpenRead(@"..\..\program.cs")) { var data = new byte[10000]; var task = stream.ReadAsync(data, 0, data.Length); Console.WriteLine("Read bytes: {0}", task.Result); } } } }
البته همانطور که پیشتر نیز عنوان شد، استفاده از خاصیت Result، اجرای کد را بجای غیرهمزمان بودن، به حالت همزمان تبدیل میکند.
در اینجا چون خروجی متد ReadAsync یک Task است، میتوان از متد ContinueWith نیز بر روی آن جهت دریافت نتیجه استفاده کرد:
using (var stream = File.OpenRead(@"..\..\program.cs")) { var data = new byte[10000]; var task = stream.ReadAsync(data, 0, data.Length); task.ContinueWith(t => Console.WriteLine("Read bytes: {0}", t.Result)).Wait(); }
یک نکته
پروژهی سورس بازی به نام Async Generator در GitHub، سعی کردهاست برای ساده سازی نوشتن محصور کنندههای مبتنی بر Task روش APM، یک Code generator تولید کند. فایلهای آنرا از آدرس ذیل میتوانید دریافت کنید:
نگاشت EAP به یک Task
نمونهای از Event based asynchronous pattern یا EAP را در قسمت اول، زمانیکه روال رخدادگردان webClient.DownloadStringCompleted را بررسی کردیم، مشاهده نمودید. کار کردن با آن نسبت به APM بسیار سادهتر است و نتیجهی نهایی عملیات غیرهمزمان را در یک روال رخدادگران، در اختیار استفاده کننده قرار میدهد. همچنین در روش EAP، اطلاعات در همان Synchronization Context ایی که عملیات شروع شدهاست، بازگشت داده میشود. به این ترتیب اگر آغاز کار در ترد UI باشد، نتیجه نیز در همان ترد دریافت خواهد شد. به این ترتیب دیگر نگران دسترسی به مقدار آن در کارهای UI نخواهیم بود؛ اما در APM چنین ضمانتی وجود ندارد.
متاسفانه TPL همانند روش FromAsync معرفی شده در ابتدای بحث، راه حل توکاری را برای محصور سازی متدهای روش EAP ارائه ندادهاست. اما با استفاده از امکانات TaskCompletionSource آن میتوان چنین کاری را انجام داد. در ادامه سعی خواهیم کرد همان متد الحاقی توکار DownloadStringTaskAsync ارائه شده در دات نت 4.5 را از صفر بازنویسی کنیم.
public static class WebClientExtensions { public static Task<string> DownloadTextTaskAsync(this WebClient web, string url) { var tcs = new TaskCompletionSource<string>(); DownloadStringCompletedEventHandler handler = null; handler = (sender, args) => { web.DownloadStringCompleted -= handler; if (args.Cancelled) { tcs.SetCanceled(); } else if(args.Error!=null) { tcs.SetException(args.Error); } else { tcs.SetResult(args.Result); } }; web.DownloadStringCompleted += handler; web.DownloadStringAsync(new Uri(url)); return tcs.Task; } }
سپس از TaskCompletionSource برای تبدیل این عملیات به یک Task کمک میگیریم. اگر args.Cancelled مساوی true باشد، یعنی عملیات دریافت فایل لغو شدهاست. بنابراین متد SetCanceled منبع Task ایجاد شده را فراخوانی خواهیم کرد. این مورد استثنایی را در کدهای فراخوان سبب میشود. به همین دلیل بررسی خطا با یک if else پس از آن انجام شدهاست. برای بازگشت خطای دریافت شده از متد SetException و برای بازگشت نتیجهی واقعی دریافتی، از متد SetResult میتوان استفاده کرد.
به این ترتیب متد الحاقی غیرهمزمان جدیدی را به نام DownloadTextTaskAsync برای محصور سازی متد EAP ایی به نام DownloadStringAsync و همچنین رخدادگران آن تهیه کردیم.