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

پی نوشت: برای داشتن نیوگت شخصی سایت‌های نظیر Nuget Server و Myget ( به همراه پشتیبانی از مخازن npm و Bower ) هم این سرویس را ارائه میکنند. ولی باید هزینه‌ی آن را پرداخت کنید. البته سایت GemFury مخازن رایگان مختلفی چون Nuget را نیز پشتیبانی می‌کند.


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


در اولین قدم، شما باید یک دایرکتوری را در سیستم خود درست کنید تا پکیج‌های خود را داخل آن قرار دهید. پنجره‌ی Package manager Settings را باز کنید و در آن گزینه‌ی Package Sources را انتخاب کنید. سپس در کادر باز شده،  بر روی دکمه‌ی افزودن، در بالا کلیک کنید تا در پایین کادر، از شما نام محل توزیع بسته و آدرس آن را بپرسد:


بعد از ورود اطلاعات، بر روی دکمه‌ی Update کلیک کنید. از این پس این دایرکتوری، منبع پکیج‌های شماست و برای دریافت پکیج‌ها از این آدرس می‌توانید از طریق منوی کشویی موجود در کنسول، پکیج جدید خودتان را انتخاب کنید:


اگر میخواهید می‌توانید این دایرکتوری را به اشتراک بگذارید تا دیگر افراد حاضر در شبکه‌ی محلی هم بتوانند آن را به عنوان منبع توزیع خود به سیستم اضافه کنند.

مرحله‌ی بعدی این است که از طریق ابزار خط فرمان نیوگت نسخه 3.3 پکیج‌هایتان را به دایرکتوری مربوطه انتقال دهید. نحوه‌ی صدا زدن این دستور به شکل زیر است:
nuget init e:\nuget\ e:\nuget\test
این دستور تمام پکیج‌هایی را که شما در مسیر اولی قرار داده‌اید، به داخل دایرکتوری test منتقل می‌کند. ولی اگر قصد دارید که فقط یکی از پکیج‌ها را به این دایرکتوری انتقال دهید از دستور زیر استفاده کنید:
nuget add GMap.Net.1.0.1.nupkg -source e:\nuget\test
اینبار با کلمه‌ی رزرو شده‌ی add و بعد از آن نام پکیچ، دایرکتوری منبع را به آن معرفی می‌کنیم.


ساخت منبع راه دور (اینترنت)


شما با استفاده از ویژوال استودیو و انجام چند عمل ساده می‌توانید پکیج‌های خودتان را مدیریت کنید. برای شروع، یک پروژه‌ی تحت وب خام (Empty) را ایجاد کنید و در کنسول Nuget دستور زیر را وارد کنید:
Install-Package NuGet.Server
شما با نصب این بسته و وابستگی‌هایش، به راحتی یک سیستم مدیریت بسته را دارید. ممکن است مدتی برای نصب طول بکشد و در نهایت از شما بخواهد که فایل web.config را رونویسی کند که شما اجازه‌ی آن را صادر خواهید کرد. بعد از اتمام نصب، فایل web.config را گشوده و در خط زیر، خصوصیت Value را به یک دایرکتوری که دلخواه که مدنظر شماست تغییر دهید. این آدرس دهی می‌تواند به صورت مطلق باشد، یا آدرس مجازی آن را وارد کنید؛ یا اگر هم خالی بگذارید به طور پیش فرض دایرکتوری Packages را در نظر میگیرد:
<appSettings>
    <!-- Set the value here to specify your custom packages folder. -->
    <add key="packagesPath" value="×\Packages" />
</appSettings>
حال فایل‌های دایرکتوری محلی خود را به دایرکتوری Packages انتقال دهید و سپس سایت را بر روی IIS، هاست نمایید. از این پس منبع شما به صورت آنلاین مانند آدرس زیر در دسترس خواهد بود.
(هر محلی که نصب کنید طبق الگوی مسیریابی، آدرس nuget را در انتها وارد کنید)
Dotnettips.info/nuget

در صورتی که قصد دارید مستقیما بسته‌ای را به سمت سرور push کنید، از یک رمز عبور قدرتمند که آن را می‌توانید در web.config، بخش apiKey وارد نمایید، استفاده کنید. اگر هم نمی‌خواهید، می‌توانید در تگ RequiredApiKey در خصوصیت Value، مقدار false را وارد نمایید.
برای اینکار می‌توانید از دستور زیر استفاده کنید:
nuget push GMap.Net.1.0.1.nupkg -source http://Dotenettips.info/nuget (ApiKey)
البته اگر قبل از دستور بالا، دستور زیر را وار کنید، دیگر نیازی نیست تا برای دستورات بعدی تا مدتی ApiKey را وارد نمایید.
nuget setapikey -source https://www.dntips.ir/Nuget (ApiKey)
مطالب
Pro Agile .NET Development With Scrum - قسمت دوم

داستان‌های کاربر

توسعه‌دهندگان، ویژگی‌های مورد نظر پروژه را با جمع‌آوری نیازمندی‌ها، در قالب داستانهای کاربر احصاء می‌کنند و به هرکدام متناسب با پیچیدگی‌اش امتیازی اختصاص می‌دهند. با لیستی از داستان‌های دارای ابعادی مشخص و بودجه و زمان مورد نیاز برای هرکدام، مشتریان قادر به این انتخابند که کدام ویژگی‌ها در تکرار (iteration) بعدی باقی بماند. مشخص‌کردن بودجه و زمان، یعنی تعیین حجم کاری که تیم توسعه برای انجام آن ویژگی، نیاز می‌داند. برآورد بودجۀ مورد نیاز تکرار اول به صورت تجربی خواهد بود و ممکن است این تخمین در ابتدا نادرست باشد؛ اما با شروع تکرار بعدی درست خواهد شد. در پایان هر تکرار، امتیازات به دست آمده از داستان‌های کامل شده را جمع کنید. مجموع این امتیازات، نشانگر سرعت شما خواهد بود. این سرعت شاخص خوبی جهت چگونگی بودجه‌بندی مرحلۀ بعد است. هنگامیکه امتیازات جمع‌آوری شده به حد مطلوبی رسید، «سرعت پیشرَوی»، شاخص مناسب دیگری برای بودجه‌بندی است که عبارت است از متوسط سرعت سه تکرار آخر.

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

برنامه‌ریزی (planning game) دو فاز دارد: فاز شناسایی و فاز برنامه‌ریزی. در فاز شناسایی، توسعه‌دهندگان و مشتریان را دور هم جمع می‌کنند تا دربارۀ نیازمندیهای سیستم در حال طراحی، گفتگو کنند. به خاطر داشته باشید که این کار تا وقتی انجام می‌شود که به ویژگی‌هایی (features) کافی برای شروع انجام کار برسیم و البته واضح است که چنین لیستی از ویژگی‌های احصاء شده، هرچقدر هم که تلاش شود، کامل نخواهد بود. مشتریان اغلب اوقات، خواسته‌ی خود را یا نمی‌دانند یا نمی‌توانند به خوبی توضیح دهند. بنابراین معمولاً این لیست به مرور تغییر می‌کند. در ضمن آنکه برخی ویژگیها دقیق‌تر می‌شود، مواردی نیز ممکن است به لیست افزوده شوند یا حتی می‌توان برخی ویژگی‌های نامربوط را از لیست حذف کرد. در مرحلۀ شناسایی، ویژگی‌ها به داستانهای کاربر تجزیه شد و ثبت می‌شوند.

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

به عنوان «نوع کاربر»، من می‌خواهم «یک فعل» تا «منفعتی برای کسب و کار» 

یا به صورت:

به منظور «یک دلیل» به عنوان «نقش کاربر» من می‌خواهم «یک فعل»

داستانهای کاربر معمولاً در جلسه‌ی گفتگو با مشتری بر روی کارت‌های راهنما نوشته شده و در آن از واژگان و ادبیاتی استفاده می‌شود که برای مشتری قابل فهم باشد. ممکن است چنین بیاندیشید که ثبت نیازمندی‌ها، خلاف مزیت‌های چابک‌سازی است؛ چرا که تولید نرم‌افزار کارآمد و چابک مبتنی بر مستندسازی گسترده و فراگیر خواهد بود. در واقع، داستان‌های کاربر به طور ساده فقط یادآورندۀ جزئیات بیشتری از گفتگوی انجام شده‌اند که به عمد به‌صورت کوتاه و دقیق نوشته شده‌اند. فهم دقیق‌تر جزئیات کار، مستلزم ارتباط بیشتر میان توسعه‌دهندگان و مشتری است. در واقع همسو با این اصل چابک که می‌گوید: «مؤثرترین و کارآمدترین شیوۀ انتقال اطلاعات در میان تیم توسعه و به خارج از آن، گفتگوی چهره به چهره است.»

هنگام احصاء ویژگی‌های پروژه تحت عنوان داستان‌های کاربری، از اصول INVEST (که پیش‌تر گفته شد) جهت کنترل مناسب بودن این داستانها استفاده کنید. شکل 2-3 مثالی از یک داستان کاربر را که توصیف‌کنندۀ ویژگی «افزودن یک بن تخفیف به سبد خرید» است، نشان می‌دهد. «تخفیف گرفتن»، یک منفعت کسب و کار است برای عامل (actor) اصلی، یعنی مشتری. «یک بن تخفیف به سبد بیفزا» نام فرآیند یا «use case» مربوط است.

 


از معیار پذیرش (acceptance criteria) نیز می‌توان در هنگام تولید داستان‌ها استفاده کرد. معیار پذیرش را می‌توان در پشت کارت داستان، آن طور که در شکل 3-3 نشان داده شده است، نوشت. استفاده از طرف مقابل کارت این اجازه را می‌دهد که اعضای تیم و مشتریان، اطلاعات خودشان را در یک جا جمع کنند.  


معیار پذیرش همچنین به تشخیص جزئیات بیشتر یا شناسایی وابستگی‌ها کمک می‌کند. مثلاً در شکل 3-3 تعریف «in date» چیست و چه چیزی حدود یک بن تخفیف را مشخص می‌کند؟ معمولاً باید حداقل سه معیار پذیرش وجود داشته باشد. در فصل بعد در یک مطالعۀ موردی، مطالب بیشتری را دربارۀ داستانهای کاربر خواهید آموخت.

هنگامیکه تیم و مشتریان حس‌کنند که حدود 75 درصد از ویژگی‌های اصلی احصاء شده است، توسعه‌دهندگان ابعاد داستان‌ها را تخمین زده و آنها را برای اولویت‌بندی توسط مشتری آماده می‌کنند.


تخمین 

شکی در آن نیست که تخمین‌زدن کار سختی است. تخمین‌زدن هم دانش است هم هنر. تخمین‌زدن در یک پروژۀ تازه شروع شده، بسیار سخت است زیرا مجهولات بسیاری در آن وجود دارد. 

یکی از روش‌های تخمین گروهی، روش «Planning Poker» نام دارد. در این روش همه‌ی اعضای فنی تیم، متشکل از توسعه‌دهندگان نرم‌افزار، تحلیل‌گران، متخصصان امنیت و زیرساخت، مشارکت می‌کنند. نقش مشتری در این حالت پاسخ‌گویی به سؤالات احتمالی اعضای تیم است تا ایشان بهتر بتوانند تخمین بزنند.

شیوۀ انجام کار به این صورت است که عضوی از تیم، یک داستان کاربر را برداشته و آن را برای تیم توضیح می‌دهد. تیم دربارۀ آن ویژگی با مشتری گفتگو کرده تا جزئیات بیشتری را دریابد. وقتی که تیم به درک خوبی از آن رسید، رأی‌گیری آغاز می‌شود. هر عضو تیم با یک کارت، از مجموعه‌ای ازکارتهایی با شماره‌های 0، 1 ، 2، 3، 5، 8، 13، 20، 40 و 100 رأی خود را اعلام می‌کند.

تیم باید از داستانی شروع کند که نسبتاً کوچک و ساده باشد. این داستان به عنوان مبنا انتخاب می‌شود. هر تخمین داستان کاربر، باید به نسبت این داستان کوچک انجام شود. اگر داستان مبنا به خوبی انتخاب نشود، بقیۀ تخمین‌ها نادرست خواهد بود.

اگر همه‌ی اعضای تیم به یک صورت رأی دهند، آن رأی، تخمین آن داستان خواهد شد. اگر اختلاف آراء وجود داشت، ناظر یعنی کسی که رأی نمی‌دهد، از افرادی که بالاترین و پایین‌ترین امتیاز را داده‌اند، می‌خواهد که علل خود را توضیح دهند. سپس تیم مجدداً گفتگو کرده و دوباره رأی‌گیری می‌کند. طبق تجربه، خوب است که زمان معقولی، برای هر گفتگو در نظر گرفته شود. 

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

داستان‌هایی که بیش از یک هفته کار نیاز داشته باشند با عنوان داستانهای حماسی (epic stories) شناخته می‌شوند و معمولاً برای تخمین بسیار بزرگ هستند. در واقع، این داستان‌ها به چند داستان کوچک‌تر که قابل فهم‌تر و به آسانی قابل تخمین باشند، تجزیه می‌شوند. این بدان معناست که ایجاد یک داستان کاربر از تعداد انبوهی ویژگی موجب کاهش کارآیی خواهد شد. 

تخمین در تیمی که افراد آن تاکنون با همدیگر سابقۀ همکاری نداشته باشند، خیلی پایین یا خیلی بالاست. اما با استمرار هر تکرار و تجربه و دانش بیشتر افراد، تخمین داستان‌ها بهتر می‌شود.

استفاده از ابزار Planning Poker مزایای بسیاری دربردارد. دقت تخمین بالا می‌رود؛ زیرا مسأله از منظر تخصص‌های گوناگون مورد بررسی قرار گرفته است. همچنین به تیم کمک می‌کند که هم رأی شوند و گفتگو میان اعضاء را تسهیل می‌کند. پس از آنکه داستان‌ها تخمین زده شدند، مشتری و صاحب محصول با تیم توسعه در تولید چگونگی انتشار نسخه‌ها، همکاری می‌کنند.


برنامه انتشار 

اگرچه کدهای قابل ارسال، قابلیت انتشار در پایان هر تکرار را دارند، اما یک پروژه XP در چند سری منتشر شده است. یک نسخۀ منتشرشده، متشکل از تعداد مناسبی داستان برای عرضۀ ارزش کسب وکاری است که به کوچک نگه داشتن آن کمک می‌کند. بسیار مناسب است که یک موضوع یا هدف خاص را در ضمن هرنسخۀ انتشار، مد نظر قرار داد تا کمک کند که هر نسخۀ انتشار بر برخی ارزشهای کسب و کاری متمرکز شده و آن را هدایت کند. معمولاً یک نسخۀ انتشار، متشکل از چهار تکرار است؛ همانطور که در شکل 4-3 نشان داده شده است.

 


در برنامه‌ریزی نسخه‌های انتشار، طول یک تکرار نیز تعیین می‌شود که معمولاً بین دو تا چهار هفته است. مطابق تجربه، اگر محیط کار شما دچار بی‌نظمی و اختلالات دائمی است، می‌توانید دورۀ تکرار را به یک هفته محدود کنید.

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

اگر قصد دارید به صورت دقیق بر روی حلقۀ بازخورد متمرکز شوید، دوره‌ی تکرار یک هفته‌ای، مدل خوبی است. اما این مدل سربار زیادی را به دلیل ضرورت تقسیم داستانهای کاربر باید به بخش‌های کوچک‌تری تا آن اندازه که در یک دوره تکمیل شوند، بر پروژه تحمیل می‌کند. در ادامه خواهیم گفت که هر تکرار شامل برنامۀ ملاقات و بازبینی نیز هست.

 بعد از مدتی که تیم با فرآیند کار آشناتر شد و نوبت به مشکلات با اولویت کم‌تر رسید، می‌توان دورۀ تکرار را دو هفته‌ای در نظر گرفت. اما اگر پروژه به گونه‌ای است که ویژگی‌های بزرگ‌تر را نمی‌توان به موارد کوچک‌تری که قابل انجام در دوره‌های یک هفته‌ای باشد، تجزیه کرد و تیم هنوز در حال یادگیری است، دوره‌های بلندمدت‌تر قابل پذیرش است.

مشتری با توجه به طول دورۀ تکرار و بودجۀ داستان آغازین، انتخاب می‌کند که کدام داستان در هنگام انتشار نسخۀ اوّل، در تکرار اوّل کامل شود. 

این مشتری است که داستان‌ها را به گونه‌ای اولویت‌بندی می‌کند تا مشخص شود که کدام‌یک بیشترین ارزش کسب و کار را فراهم می‌کند. از آنجایی که مشتری مسؤول داستانهای کاربر است، تیم باید به وی توضیح دهد که داستانهایی وجود دارند که صرفاً باید به جهت دلایل فنی ایجاد شوند. 

معمولاً باید به داستانهای کاربری‌ای که مستلزم ریسک بالا بوده یا دربرگیرندۀ مجهولات زیادی باشند، بیش از یک یا دو تکرار اختصاص داد. 


برنامۀ تکرار

مشتری داستان‌هایی را که می‌خواهد در تکرار باشند، انتخاب می‌کند. برای هر داستان کاربر، مجموعه‌ای از معیارهای پذیرش، تعریف شده است. همان طور که متوجه شده‌اید ما در هر فاز، وقت بیشتر و بیشتری را صرف جمع‌آوری جزئیات هر داستان کاربر کرده و بصورت عمیق‌تری در آن غور می‌کنیم. این کار مفید است، زیرا اگر یک داستان کاربر ایجاد شده در ابتدای پروژه، ممکن است بعداً به عنوان داستانی کم اهمیت یا غیر مهم دیده‌شود و بدون آنکه وقت خاصی برای آن صرف شده باشد، کنار گذاشته شود. اما اگر در ابتدای کار وقت زیادی صرف دقیق‌تر کردن داستان‌های کاربر شود و بعداً بعضی از آنها کنار گذاشته شوند، در واقع وقت تلف شده است. بنابراین دقیق‌تر کردن یک داستان در جایی که مورد نیاز است، باید اتفاق بیفتد. در سطح برنامۀ تکرار، مجموعه‌ای از معیارهای پذیرش را برای هر داستان کاربر تعریف می‌کنیم. معیار پذیرش به توسعه‌دهنده کمک می‌کند تا بداند که یک داستان کاربر به طور کامل انجام می‌شود. این معیارها به صورت مؤلفه‌هایی از بافرض/هنگامی که/درنتیجه، نوشته می‌شود. 

مثالهای زیر چگونگی انجام این کار را توصیف می‌کند:

عنوان ویژگی: افزودن کالایی به سبد

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

سناریو: سبد  خالی

با فرض اینکه یک سبد خالی دارم، در نتیجه جمع تعداد کالایی که برای سفارش در سبد من وجود دارد، صفر است.

سناریو: افزودن یک کالا به سبد

با فرض اینکه یک سبد خالی دارم هنگامی که کالایی با شناسۀ 1 به سبدم اضافه می‌کنم، در نتیجه جمع کالاهای قابل سفارش در سبدم 1 می‌شود.

سناریو: افزودن کالاهایی به سبد

با فرض اینکه یک سبد خالی دارم، هنگامی که کالایی با شناسۀ 1 و کالایی با شناسۀ 2 به سبدم اضافه می‌کنم، در نتیجه جمع کالاهای قابل سفارش در سبدم 2 می‌شود.

سناریو: دو بار افزودن یک کالا

با فرض اینکه یک سبد خالی دارم هنگامی که کالایی با شناسۀ 1 به سبدم اضافه می‌کنم و هنگامی که کالایی با شناسۀ 1 را مجدداً به سبدم اضافه می‌کنم، در نتیجه تعداد کالاهای با شناسۀ 1 در سبد من باید 2 باشد.

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

با فرض اینکه یک سبد خالی دارم و کالایی با شناسۀ 2 در انبار وجود نداشته باشد، هنگامی که من کالایی با شناسۀ 2  را به سبد خودم اضافه می‌کنم، در نتیجه جمع تعداد کالای قابل سفارش در سبد من باید 0 باشد و به کاربر، موجود نبودن آن کالا را هشدار دهد.

یک آزمون پذیرش (acceptance) به زبان متعارف در قوانین کسب و کار نوشته می‌شود. در مثال سبد خرید، این سؤال پیش می‌آید که چگونه می‌توان یک محصول را از سبد کالا، حذف کرد و اگر یک جنس اکنون در انبار نیست و کاربر پیام هشدار دریافت کرده است، در ادامه چه اتفاقی باید بیفتد؟ سناریوها به تیم در کشف ملزومات کسب و کار و تصریح آن‌ها کمک می‌کند.

این سناریوها توسط توسعه‌دهنده به عنوان نقطۀ شروع آزمونهای واحد در توسعۀ آزمون محور و رفتار محور استفاده می‌شود. سناریوها همچنین در آزمودن معیارهای پذیرش به توسعه‌دهنده کمک کرده و توسعه‌دهنده و تست‌کننده را قادر می‌سازند که بر روی اتمام داستان اتفاق نظر داشته باشند.

بعد از آنکه سناریوهای معیار پذیرش تعیین شد، تیم توسعه، هر داستان را به تعدادی وظیفه تقسیم می‌کند و وظایف مرتبط به یک داستان، در تابلوی وظایف قرارگرفته و تیم توسعه تخمین‌های خود را در قالب یکی از واحدهای اندازه‌گیری، مثلاً نفرساعت  اعلام می‌کند. شکل 5-3 یک تابلوی وظیفه را نمایش می‌دهد.

به عنوان مثال وظایف می‌توانند شامل ایجاد طرح یک بانک اطلاعاتی برای یک داستان یا یکپارچه‌سازی آن با بخشی موجود در سیستم باشند. وظایف شامل مؤلفه‌های فنی مانند تهیۀ گزارش از زیرسیستم‌ها یا چارچوب مدیریت استثنائات نیز می‌باشد. اغلب این‌گونه وظایف نادیده‌گرفته می‌شود. یک داستان کاربر با وظایف گوناگونی گره خورده است. مثلاً:

داستان کاربر : به عنوان یک کاربر می‌خواهم بتوانیم یک کاربر را مدیریت کنم.

وظایف زیر از این داستان قابل استخراج است:

  • طرحی برای بانک اطلاعات جهت ذخیره‌سازی اطلاعات کاربر ایجاد کن.
  • یک کلاس کاربر، برای مدیریت کاربر از درون برنامه ایجاد کن.

هر عضو تیم می‌تواند بر روی هر وظیفه‌ای که بر روی تخته است، کار کند. هنگامیکه یک عضو گروه، وظیفه‌ای را برمی‌دارد، باید نشانی از خود روی کارت آن وظیفه قراردهد ( مثلاً حروف اوّل اسمش) تا بقیۀ افراد بدانند که وی بر روی آن وظیفه، مشغول به کار است. معمولاً اما نه همیشه، یک توسعه‌دهنده همۀ وظایف مربوط به یک داستان را برمی‌دارد. این کار بدین معناست که آن توسعه‌دهنده با پشتیبانی تیم، مسؤول اتمام آن کار است.

 

شکل 5-3. تختۀ وظایف نشان‌دهندۀ چگونگی پیشرفت پروژه

به محض اینکه یک تکرار آغاز شد، داستان‌هایی را که کار بر روی آنها شروع شده است، دیگر نمی‌توان تغییر داد. این مهم است که برنامۀ تکرار را در حین انجام آن، تغییر ندهید؛ زیرا این کار منجر به سوئیچنگ زمینه (context switching) می‌شود. برای توسعه‌دهندگان سوئیچینگ زمینه، هم به لحاظ زمانی و هم به لحاظ مالی، بسیار پرهزینه است.
به جای آنکه تلاش کنید در ضمن یک تکرار، تغییراتی را ایجاد کنید، مشخص کنید که آیا این کار اضافه، یا داستان اضافه را می‌توان تا تکرار بعدی به تعویق انداخت. مشتریان یا مدیران معمولاً می‌توانند چنین تعویقی را بپذیرند؛ زیرا این پذیرش مستلزم به تاخیر انداختن کار، مثلاً تا یک ماه دیگر نیست. اگر این کار جدید را که اضافه شده است، نمی‌توان به تعویق انداخت، باید ریسک خارج ساختن کدهای موجود و رفتن به سمت کدنویسی برای کارکرد جدید را به همراه تیم بررسی کرد. همچنین تیم باید بداند که اگر کار اضافه‌ای به یک دورۀ تکرار افزوده شد، بخشی از کارهای این دوره باید به تکرار بعدی موکول شوند. قاعدۀ کلی این است که اگر چیزهای جدیدی به کار وارد شد و تعویق آن ممکن نبود، باید کارهایی با همان ابعاد یا بزرگتر از تکرار، خارج شود. 
سرعت به ما نشان می‌دهد که تیم چه حجم کاری را در طول یک دوره کامل کرده است. از سرعت، در برنامه‌ریزی تکرارهای آتی استفاده می‌شود. برای درک چگونگی سرعت کار از نمودار burn-down استفاده می‌‌شود. یک نمودار burn-down (شکل 6-3) داستان‌های باقی‌ماندۀ یک پروژه و داستانهای تکمیل شده را در یک تکرار نمایش می‌دهد. سرعت در پایان هر تکرار محاسبه می‌شود و تعریف آن عبارت است از تعداد داستان‌های تکمیل شده در آخرین تکرار. بر اساس سرعت کنونی و تعداد داستان‌های باقی‌مانده، می‌توان تخمین زد که چقدر طول می‌کشد تا همه‌ی داستانها تکمیل شود. همانند آنچه در شکل 6-3 با خط چین نمایش داده شده است.
نمودار burn-down ابزار خوبی برای فهم آن است که آیا تیم می‌تواند پروژه را در زمان مقتضی به پایان برساند یا خیر و اگر نمی‌تواند، مدیر چگونه باید نسبت به آن تصمیم‌گیری کند. آیا افراد بیشتری باید به پروژه وارد شوند؟ آیا باید از ویژگی‌های مدنظر پروژه کاهش داد، یا باید زمان پایان کار را تغییر داد؟ 
 


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


با تشکر از آقای سید مجتبی حسینی
مطالب
EF Code First #4

آشنایی با Code first migrations

ویژگی Code first migrations برای اولین بار در EF 4.3 ارائه شد و هدف آن سهولت هماهنگ سازی کلاس‌های مدل برنامه با بانک اطلاعاتی است؛ به صورت خودکار یا با تنظیمات دقیق دستی.

همانطور که در قسمت‌های قبل نیز به آن اشاره شد، تا پیش از EF 4.3، پنج روال جهت آغاز به کار با بانک اطلاعاتی در EF code first وجود داشت و دارد:
1) در اولین بار اجرای برنامه، در صورتیکه بانک اطلاعاتی اشاره شده در رشته اتصالی وجود خارجی نداشته باشد، نسبت به ایجاد خودکار آن اقدام می‌گردد. اینکار پس از وهله سازی اولین DbContext و همچنین صدور یک کوئری به بانک اطلاعاتی انجام خواهد شد.
2) DropCreateDatabaseAlways : همواره پس از شروع برنامه، ابتدا بانک اطلاعاتی را drop کرده و سپس نمونه جدیدی را ایجاد می‌کند.
3) DropCreateDatabaseIfModelChanges : اگر EF Code first تشخیص دهد که تعاریف مدل‌های شما با بانک اطلاعاتی مشخص شده توسط رشته اتصالی، هماهنگ نیست، آن‌را drop کرده و نمونه جدیدی را تولید می‌کند.
4) با مقدار دهی پارامتر متد System.Data.Entity.Database.SetInitializer به نال، می‌توان فرآیند آغاز خودکار بانک اطلاعاتی را غیرفعال کرد. در این حالت شخص می‌تواند تغییرات انجام شده در کلاس‌های مدل برنامه را به صورت دستی به بانک اطلاعاتی اعمال کند.
5) می‌توان با پیاده سازی اینترفیس IDatabaseInitializer، یک آغاز کننده بانک اطلاعاتی سفارشی را نیز تولید کرد.

اکثر این روش‌ها در حین توسعه یک برنامه یا خصوصا جهت سهولت انجام آزمون‌های خودکار بسیار مناسب هستند، اما به درد محیط کاری نمی‌خورند؛ زیرا drop یک بانک اطلاعاتی به معنای از دست دادن تمام اطلاعات ثبت شده در آن است. برای رفع این مشکل مهم، مفهومی به نام «Migrations» در EF 4.3 ارائه شده است تا بتوان بانک اطلاعاتی را بدون تخریب آن، بر اساس اطلاعات تغییر کرده‌ی کلاس‌های مدل برنامه، تغییر داد. البته بدیهی است زمانیکه توسط NuGet نسبت به دریافت و نصب EF اقدام می‌شود، همواره آخرین نگارش پایدار که حاوی اطلاعات و فایل‌های مورد نیاز جهت کار با «Migrations» است را نیز دریافت خواهیم کرد.


تنظیمات ابتدایی Code first migrations

در اینجا قصد داریم همان مثال قسمت قبل را ادامه دهیم. در آن مثال از یک نمونه سفارشی سازی شده DropCreateDatabaseAlways استفاده شد.
نیاز است از منوی Tools در ویژوال استودیو، گزینه‌ Library package manager آن، گزینه package manager console را انتخاب کرد تا کنسول پاورشل NuGet ظاهر شود.
اطلاعات مرتبط با پاورشل EF، به صورت خودکار توسط NuGet نصب می‌شود. برای مثال جهت مشاهده آن‌ها به مسیر packages\EntityFramework.4.3.1\tools در کنار پوشه پروژه خود مراجعه نمائید.
در ادامه در پایین صفحه، زمانیکه کنسول پاورشل NuGet ظاهر می‌شود، ابتدا باید دقت داشت که قرار است فرامین را بر روی چه پروژه‌ای اجرا کنیم. برای مثال اگر تعاریف DbContext را به یک اسمبلی و پروژه class library مجزا انتقال داده‌اید، گزینه Default project را در این قسمت باید به این پروژه مجزا، تغییر دهید.
سپس در خط فرمان پاور شل، دستور enable-migrations را وارد کرده و دکمه enter را فشار دهید.
پس از اجرای این دستور، یک سری اتفاقات رخ خواهد داد:
الف) پوشه‌ای به نام Migrations به پروژه پیش فرض مشخص شده در کنسول پاورشل، اضافه می‌شود.
ب) دو کلاس جدید نیز در آن پوشه تعریف خواهند شد به نام‌های Configuration.cs و یک نام خودکار مانند number_InitialCreate.cs
ج) در کنسول پاور شل، پیغام زیر ظاهر می‌گردد:
Detected database created with a database initializer. Scaffolded migration '201205050805256_InitialCreate' 
corresponding to current database schema. To use an automatic migration instead, delete the Migrations
folder and re-run Enable-Migrations specifying the -EnableAutomaticMigrations parameter.

با توجه به اینکه در مثال قسمت سوم، از آغاز کننده سفارشی سازی شده DropCreateDatabaseAlways استفاده شده بود، اطلاعات آن در جدول سیستمی dbo.__MigrationHistory در بانک اطلاعاتی برنامه موجود است (تصویری از آن‌را در قسمت اول این سری مشاهده کردید). سپس با توجه به ساختار بانک اطلاعاتی جاری، دو کلاس خودکار زیر را ایجاد کرده است:

namespace EF_Sample02.Migrations
{
using System;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Linq;

internal sealed class Configuration : DbMigrationsConfiguration<EF_Sample02.Sample2Context>
{
public Configuration()
{
AutomaticMigrationsEnabled = false;
}

protected override void Seed(EF_Sample02.Sample2Context context)
{
// This method will be called after migrating to the latest version.

// You can use the DbSet<T>.AddOrUpdate() helper extension method
// to avoid creating duplicate seed data. E.g.
//
// context.People.AddOrUpdate(
// p => p.FullName,
// new Person { FullName = "Andrew Peters" },
// new Person { FullName = "Brice Lambson" },
// new Person { FullName = "Rowan Miller" }
// );
//
}
}
}

namespace EF_Sample02.Migrations
{
using System.Data.Entity.Migrations;

public partial class InitialCreate : DbMigration
{
public override void Up()
{
CreateTable(
"Users",
c => new
{
Id = c.Int(nullable: false, identity: true),
Name = c.String(),
LastName = c.String(),
Email = c.String(),
Description = c.String(),
Photo = c.Binary(),
RowVersion = c.Binary(nullable: false, fixedLength: true, timestamp: true, storeType: "rowversion"),
Interests_Interest1 = c.String(maxLength: 450),
Interests_Interest2 = c.String(maxLength: 450),
AddDate = c.DateTime(nullable: false),
})
.PrimaryKey(t => t.Id);

CreateTable(
"Projects",
c => new
{
Id = c.Int(nullable: false, identity: true),
Title = c.String(maxLength: 50),
Description = c.String(),
RowVesrion = c.Binary(nullable: false, fixedLength: true, timestamp: true, storeType: "rowversion"),
AddDate = c.DateTime(nullable: false),
AdminUser_Id = c.Int(),
})
.PrimaryKey(t => t.Id)
.ForeignKey("Users", t => t.AdminUser_Id)
.Index(t => t.AdminUser_Id);

}

public override void Down()
{
DropIndex("Projects", new[] { "AdminUser_Id" });
DropForeignKey("Projects", "AdminUser_Id", "Users");
DropTable("Projects");
DropTable("Users");
}
}
}


در این کلاس خودکار، نحوه ایجاد جداول بانک اطلاعاتی تعریف شده‌اند. در متد تحریف شده Up، کار ایجاد بانک اطلاعاتی و در متد تحریف شده Down، دستورات حذف جداول و قیود ذکر شده‌اند.
به علاوه اینبار متد Seed را در کلاس مشتق شده از DbMigrationsConfiguration، می‌توان تحریف و مقدار دهی کرد.
علاوه بر این‌ها جدول سیستمی dbo.__MigrationHistory نیز با اطلاعات جاری مقدار دهی می‌گردد.


فعال سازی گزینه‌های مهاجرت خودکار

برای استفاده از این کلاس‌ها، ابتدا به فایل Configuration.cs مراجعه کرده و خاصیت AutomaticMigrationsEnabled را true‌ کنید:

internal sealed class Configuration : DbMigrationsConfiguration<EF_Sample02.Sample2Context>
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
}

پس از آن EF به صورت خودکار کار استفاده و مدیریت «Migrations» را عهده‌دار خواهد شد. البته برای این منظور باید نوع آغاز کننده بانک اطلاعاتی را از DropCreateDatabaseAlways قبلی به نمونه جدید MigrateDatabaseToLatestVersion نیز تغییر دهیم:
//Database.SetInitializer(new Sample2DbInitializer());
Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample2Context, Migrations.Configuration>());

یک نکته:
کلاس Migrations.Configuration که باید در حین وهله سازی از MigrateDatabaseToLatestVersion قید شود (همانند کدهای فوق)، از نوع internal sealed معرفی شده است. بنابراین اگر این کلاس را در یک اسمبلی جداگانه قرار داده‌اید، نیاز است فایل را ویرایش کرده و internal sealed آن‌را به public تغییر دهید.

روش دیگر معرفی کلاس‌های Context و Migrations.Configuration، حذف متد Database.SetInitializer و استفاده از فایل app.config یا web.config است به نحو زیر ( در اینجا حرف ` اصطلاحا back tick نام دارد. فشردن دکمه ~ در حین تایپ انگلیسی):

<entityFramework>
<contexts>
<context type="EF_Sample02.Sample2Context, EF_Sample02">
<databaseInitializer
type="System.Data.Entity.MigrateDatabaseToLatestVersion`2[[EF_Sample02.Sample2Context, EF_Sample02],
[EF_Sample02.Migrations.Configuration, EF_Sample02]], EntityFramework"
/>
</context>
</contexts>
</entityFramework>

آزمودن ویژگی مهاجرت خودکار

اکنون برای آزمایش این موارد، یک خاصیت دلخواه را به کلاس Project به نام public string SomeProp اضافه کنید. سپس برنامه را اجرا نمائید.
در ادامه به بانک اطلاعاتی مراجعه کرده و فیلدهای جدول Projects را بررسی کنید:

CREATE TABLE [dbo].[Projects](
---...
[SomeProp] [nvarchar](max) NULL,
---...

بله. اینبار فیلد SomeProp بدون از دست رفتن اطلاعات و drop بانک اطلاعاتی، به جدول پروژه‌ها اضافه شده است.


عکس العمل ویژگی مهاجرت خودکار در مقابل از دست رفتن اطلاعات

در ادامه، خاصیت public string SomeProp را که در قسمت قبل به کلاس پروژه اضافه کردیم، حذف کنید. اکنون مجددا برنامه را اجرا نمائید. برنامه بلافاصله با استثنای زیر متوقف خواهد شد:

Automatic migration was not applied because it would result in data loss.

از آنجائیکه حذف یک خاصیت مساوی است با حذف یک ستون در جدول بانک اطلاعاتی، امکان از دست رفتن اطلاعات در این بین بسیار زیاد است. بنابراین ویژگی مهاجرت خودکار دیگر اعمال نخواهد شد و این مورد به نوعی یک محافظت خودکار است که درنظر گرفته شده است.
البته در EF Code first این مساله را نیز می‌توان کنترل نمود. به کلاس Configuration اضافه شده توسط پاورشل مراجعه کرده و خاصیت AutomaticMigrationDataLossAllowed را به true تنظیم کنید:

internal sealed class Configuration : DbMigrationsConfiguration<EF_Sample02.Sample2Context>
{
public Configuration()
{
this.AutomaticMigrationsEnabled = true;
this.AutomaticMigrationDataLossAllowed = true;
}

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


استفاده از Code first migrations بر روی یک بانک اطلاعاتی موجود

تفاوت یک دیتابیس موجود با بانک اطلاعاتی تولید شده توسط EF Code first در نبود جدول سیستمی dbo.__MigrationHistory است.
به این ترتیب زمانیکه فرمان enable-migrations را در یک پروژه EF code first متصل به بانک اطلاعاتی قدیمی موجود اجرا می‌کنیم، پوشه Migration در آن ایجاد خواهد شد اما تنها حاوی فایل Configuration.cs است و نه فایلی شبیه به number_InitialCreate.cs .
بنابراین نیاز است به صورت صریح به EF اعلام کنیم که نیاز است تا جدول سیستمی dbo.__MigrationHistory و فایل number_InitialCreate.cs را نیز تولید کند. برای این منظور کافی است دستور زیر را در خط فرمان پاورشل NuGet پس از فراخوانی enable-migrations اولیه، اجرا کنیم:
add-migration Initial -IgnoreChanges

با بکارگیری پارامتر IgnoreChanges، متد Up در فایل number_InitialCreate.cs تولید نخواهد شد. به این ترتیب نگران نخواهیم بود که در اولین بار اجرای برنامه، تعاریف دیتابیس موجود ممکن است اندکی تغییر کند.
سپس دستور زیر را جهت به روز رسانی جدول سیستمی dbo.__MigrationHistory اجرا کنید:
update-database

پس از آن جهت سوئیچ به مهاجرت خودکار، خاصیت AutomaticMigrationsEnabled = true را در فایل Configuration.cs همانند قبل مقدار دهی کنید.


مشاهده دستوارت SQL به روز رسانی بانک اطلاعاتی

اگر علاقمند هستید که دستورات T-SQL به روز رسانی بانک اطلاعاتی را نیز مشاهده کنید، دستور Update-Database را با پارامتر Verbose آغاز نمائید:
Update-Database -Verbose

و اگر تنها نیاز به مشاهده اسکریپت تولیدی بدون اجرای آن‌ها بر روی بانک اطلاعاتی مدنظر است، از پارامتر Script باید استفاده کرد:
update-database -Script



نکته‌ای در مورد جدول سیستمی dbo.__MigrationHistory

تنها دلیلی که این جدول در SQL Server البته (ونه برای مثال در SQL Server CE) به صورت سیستمی معرفی می‌شود این است که «جلوی چشم نباشد»! به این ترتیب در SQL Server management studio در بین سایر جداول معمولی بانک اطلاعاتی قرار نمی‌گیرد. اما برای EF تفاوتی نمی‌کند که این جدول سیستمی است یا خیر.
همین سیستمی بودن آن ممکن است بر اساس سطح دسترسی کاربر اتصالی به بانک اطلاعاتی مساله ساز شود. برای نمونه ممکن است schema کاربر متصل dbo نباشد. همینجا است که کار به روز رسانی این جدول متوقف خواهد شد.
بنابراین اگر قصد داشتید خواص سیستمی آن‌را لغو کنید، تنها کافی است دستورات T-SQL زیر را در SQL Server اجرا نمائید:

SELECT * INTO [TempMigrationHistory]
FROM [__MigrationHistory]
DROP TABLE [__MigrationHistory]
EXEC sp_rename [TempMigrationHistory], [__MigrationHistory]


ساده سازی پروسه مهاجرت خودکار

کل پروسه‌ای را که در این قسمت مشاهده کردید، به صورت ذیل نیز می‌توان خلاصه کرد:

using System;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Data.Entity.Migrations.Infrastructure;
using System.IO;

namespace EF_Sample02
{
public class Configuration<T> : DbMigrationsConfiguration<T> where T : DbContext
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
AutomaticMigrationDataLossAllowed = true;
}
}

public class SimpleDbMigrations
{
public static void UpdateDatabaseSchema<T>(string SQLScriptPath = "script.sql") where T : DbContext
{
var configuration = new Configuration<T>();
var dbMigrator = new DbMigrator(configuration);
saveToFile(SQLScriptPath, dbMigrator);
dbMigrator.Update();
}

private static void saveToFile(string SQLScriptPath, DbMigrator dbMigrator)
{
if (string.IsNullOrWhiteSpace(SQLScriptPath)) return;

var scriptor = new MigratorScriptingDecorator(dbMigrator);
var script = scriptor.ScriptUpdate(sourceMigration: null, targetMigration: null);
File.WriteAllText(SQLScriptPath, script);
Console.WriteLine(script);
}
}
}

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

SimpleDbMigrations.UpdateDatabaseSchema<Sample2Context>();

در این کلاس ذخیره سازی اسکریپت تولیدی جهت به روز رسانی بانک اطلاعاتی جاری در یک فایل نیز درنظر گرفته شده است.



تا اینجا مهاجرت خودکار را بررسی کردیم. در قسمت بعدی Code-Based Migrations را ادامه خواهیم داد.
مطالب
امکان تعریف توابع خاص بانک‌های اطلاعاتی در EF Core
یکی از اهداف کار با ORMها، رسیدن به کدی قابل ترجمه و استفاده‌ی توسط تمام بانک‌های اطلاعاتی ممکن است و یکی از الزامات رسیدن به این هدف، صرفنظر کردن از قابلیت‌های بومی بانک‌های اطلاعاتی است که در سایر بانک‌های اطلاعاتی دیگر معادلی ندارند. برای مثال SQL Server به همراه توابع توکاری مانند datediff و datepart برای کار با زمان و تاریخ است؛ اما این توابع را به صورت مستقیم نمی‌توان در ORMها استفاده کرد. چون به محض استفاده‌ی از آن‌ها، کد تهیه شده دیگر قابلیت انتقال به سایر بانک‌های اطلاعاتی را نخواهد داشت. اما ... اگر این هدف را نداشته باشیم، چطور؟ آیا می‌توان یک تابع DateDiff سفارشی را برای EF Core تهیه نمود و از تمام قابلیت‌های بومی آن در کوئری‌های LINQ استفاده کرد؟ بله! یک چنین قابلیتی تحت عنوان DbFunctions در EF Core پشتیبانی می‌شود که روش تهیه‌ی آن‌ها را در این مطلب بررسی خواهیم کرد.


معرفی موجودیت Person

در مثال این مطلب قصد داریم، معادل توابع بومی مخصوص SQL Server را که امکان کار با DateTime را مهیا می‌کنند، در EF Core تعریف کنیم. به همین جهت نیاز به موجودیتی داریم که دارای خاصیتی از این نوع باشد:
using System;

namespace EFCoreDbFunctionsSample.Entities
{
    public class Person
    {
        public int Id { get; set; }

        public string Name { get; set; }

        public DateTime AddDate { get; set; }
    }
}


گزارشگیری بر اساس تعداد روز گذشته‌ی از ثبت نام

اکنون فرض کنید می‌خواهیم گزارشی را از تمام کاربرانی که در طی 10 روز قبل ثبت نام کرده‌اند، تهیه کنیم. اگر کوئری زیر را برای این منظور تهیه کنیم:
var usersInfo = context.People.Where(person => (DateTime.Now - person.AddDate).Days <= 10).ToList();
با استثنای زیر متوقف خواهیم شد:
'The LINQ expression 'DbSet<Person>.Where(p => (DateTime.Now - p.AddDate).Days <= 10)'
could not be translated. Either rewrite the query in a form that can be translated,
or switch to client evaluation explicitly by inserting a call to either
AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync().
See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.'
عنوان می‌کند که یک چنین کوئری LINQ ای قابلیت ترجمه‌ی به SQL را ندارد. اما ... نکته‌ی مهم اینجا است که خود SQL Server یک چنین توانمندی را به صورت توکار دارا است:
SELECT [p].[Id], [p].[AddDate], [p].[Name]
FROM [People] AS [p]
WHERE DATEDIFF(Day, [p].[AddDate], GETDATE()) <= 10
برای انجام کوئری مدنظر فقط کافی است از تابع DATEDIFF توکار آن با پارامتر Day، استفاده کنیم تا لیست تمام کاربران ثبت نام کرده‌ی در طی 10 روز قبل را بازگشت دهد. اکنون سؤال اینجا است که آیا می‌توان چنین تابعی را به EF Core معرفی کرد؟


روش تعریف تابع DATEDIFF سفارشی در EF Core

برای تعریف متد DateDiff مخصوص EF Core، ابتدا باید یک کلاس static را تعریف کرد و سپس تنها امضای این متد را، معادل امضای تابع توکار SQL Server تعریف کرد. این متد نیازی نیست تا پیاده سازی را داشته باشد. به همین جهت بدنه‌ی آن‌را صرفا با یک throw new InvalidOperationException مقدار دهی می‌کنیم. هدف از این متد، استفاده‌ی از آن در LINQ Expressions است و قرار نیست به صورت مستقیمی بکار گرفته شود:
namespace EFCoreDbFunctionsSample.DataLayer
{
    public enum SqlDateDiff
    {
        Year,
        Quarter,
        Month,
        DayOfYear,
        Day,
        Week,
        Hour,
        Minute,
        Second,
        MilliSecond,
        MicroSecond,
        NanoSecond
    }

    public static class SqlDbFunctionsExtensions
    {
        public static int SqlDateDiff(SqlDateDiff interval, DateTime initial, DateTime end)
            => throw new InvalidOperationException($"{nameof(SqlDateDiff)} method cannot be called from the client side.");
        public static readonly MethodInfo SqlDateDiffMethodInfo = typeof(SqlDbFunctionsExtensions)
            .GetRuntimeMethod(
                nameof(SqlDbFunctionsExtensions.SqlDateDiff),
                new[] { typeof(SqlDateDiff), typeof(DateTime), typeof(DateTime) }
            );
    }
}
در اینجا علاوه بر تعریف امضای متد DateDiff که در اینجا SqlDateDiff نام گرفته‌است، فیلد SqlDateDiffMethodInfo را نیز مشاهده می‌کنید. در حین تعریف و معرفی DbFunctions سفارشی به EF Core، متدهایی که اینکار را انجام می‌دهند، پارامترهای ورودی از نوع MethodInfo دارند. به همین جهت یک چنین تعریفی انجام شده‌است.


روش معرفی تابع DATEDIFF سفارشی به EF Core

پس از تعریف امضای متد معادل DateDiff، اکنون نوبت به معرفی آن به EF Core است:
namespace EFCoreDbFunctionsSample.DataLayer
{
    public class ApplicationDbContext : DbContext
    {
        // ...
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder.HasDbFunction(SqlDbFunctionsExtensions.SqlDateDiffMethodInfo)
                .HasTranslation(args =>
                {
                    var parameters = args.ToArray();
                    var param0 = ((SqlConstantExpression)parameters[0]).Value.ToString();
                    return SqlFunctionExpression.Create("DATEDIFF",
                        new[]
                        {
                            new SqlFragmentExpression(param0), // It should be written as DateDiff(day, ...) and not DateDiff(N'day', ...) .
                            parameters[1],
                            parameters[2]
                        },
                        SqlDbFunctionsExtensions.SqlDateDiffMethodInfo.ReturnType,
                        typeMapping: null);
                });
        }
    }
}
کار تعریف DbFunctions سفارشی توسط متد HasDbFunction صورت می‌گیرد. پارامتر این متد، همان MethodInfo معادل امضای تابع توکار مدنظر است.
سپس توسط متد HasTranslation، مشخص می‌کنیم که این متد به چه نحوی قرار است به یک عبارت SQL ترجمه شود. پارامتر args ای که در اینجا در اختیار ما قرار می‌گیرد، دقیقا همان پارامترهای متد public static int SqlDateDiff(SqlDateDiff interval, DateTime initial, DateTime end) هستند که در این مثال خاص، شامل سه پارامتر می‌شوند. پارامترهای دوم و سوم آن‌را به همان نحوی که دریافت می‌کنیم، به SqlFunctionExpression.Create ارسال خواهیم کرد. اما پارامتر اول را از نوع enum تعریف کرده‌ایم و همچنین قرار نیست به صورت 'N'day و رشته‌ای به سمت بانک اطلاعاتی ارسال شود، بلکه باید به همان نحو اصلی آن (یعنی day)، در کوئری نهایی درج گردد، به همین جهت ابتدا Value آن‌را استخراج کرده و سپس توسط SqlFragmentExpression عنوان می‌کنیم آن‌را باید به همین نحو درج کرد.
پارامتر اول متد SqlFunctionExpression.Create، باید دقیقا معادل نام متد توکار مدنظر باشد. پارامتر دوم آن، لیست پارامترهای این تابع است. پارامتر سوم آن، نوع خروجی این تابع است که از طریق MethodInfo معادل، قابل استخراج است.


استفاده‌ی از DbFunction سفارشی جدید در برنامه

پس از این تعاریف و معرفی‌ها، اکنون می‌توان متد سفارشی SqlDateDiff تهیه شده را به صورت مستقیمی در کوئری‌های LINQ استفاده کرد تا قابلیت ترجمه‌ی به SQL را پیدا کنند:
var sinceDays = 10;
users = context.People.Where(person =>
      SqlDbFunctionsExtensions.SqlDateDiff(SqlDateDiff.Day, person.AddDate, DateTime.Now) <= sinceDays).ToList();
/*
SELECT [p].[Id], [p].[AddDate], [p].[Name]
FROM [People] AS [p]
WHERE DATEDIFF(Day, [p].[AddDate], GETDATE()) <= @__sinceDays_0
*/


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید: EFCoreDbFunctionsSample.zip
این کدها به همراه چند تابع سفارشی دیگر نیز هستند.
مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایل‌های config
یکی دیگر از تغییرات ASP.NET Core با نگارش‌های قبلی آن، تغییرات اساسی در مورد نحوه‌ی کار با تنظیمات برنامه و فایل‌های مرتبط با آن‌ها است. در ASP.NET Core می‌توانید:
- تنظیمات برنامه را از چندین منبع مختلف خوانده و آن‌ها را یکی کنید.
- تنظیمات را بر اساس تنظیمات مختلف محیطی برنامه، بارگذاری کنید.
- امکان نگاشت اطلاعات خوانده شده‌ی از فایل‌های کانفیگ به کلاس‌ها پیش بینی شده‌است.
- امکان بارگذاری مجدد فایل‌های کانفیگ درصورت تغییر، بدون ری‌استارت کل برنامه وجود دارد.
- امکان تزریق وابستگی‌های تنظیمات برنامه، به قسمت‌های مختلف آن پیش بینی شده‌است.


نصب پیشنیاز خواندن تنظیمات برنامه از یک فایل JSON

برای شروع به کار با خواندن تنظیمات برنامه در ASP.NET Core، نیاز است ابتدا بسته‌ی نیوگت Microsoft.Extensions.Configuration.Json را نصب کنیم.
برای این منظور بر روی گره references کلیک راست کرده و گزینه‌ی manage nuget packages را انتخاب کنید. سپس در برگه‌ی browse آن Microsoft.Extensions.Configuration.Json را جستجو کرده و نصب نمائید:


البته همانطور که در تصویر مشاهده می‌کنید، اگر صرفا Microsoft.Extensions.Configuration را جستجو کنید (بدون ذکر JSON)، بسته‌های مرتبط با خواندن فایل‌های کانفیگ از نوع XML و یا حتی INI را هم خواهید یافت.
انجام این مراحل معادل هستند با افزودن یک سطر ذیل به فایل project.json برنامه:
{
    "dependencies": {
         //same as before  
         "Microsoft.Extensions.Configuration.Json": "1.0.0"
    },

 
افزودن یک فایل کانفیگ JSON دلخواه

بر روی پروژه کلیک راست کرده و از طریق منوی add->new item یک فایل خالی جدید را به نام appsettings.json ایجاد کنید (نام این فایل دلخواه است)؛ با این محتوا:
{
    "Key1": "Value1",
    "Auth": {
        "Users": [ "Test1", "Test2", "Test3" ]
    },
    "Logging": {
        "IncludeScopes": false,
        "LogLevel": {
            "Default": "Debug",
            "System": "Information",
            "Microsoft": "Information"
        }
    }
}
در نگارش‌های پیشین ASP.NET که از web.config برای تعریف تنظیمات برنامه استفاده می‌شد، حالت پیش فرض ذکر تنظیمات برنامه می‌توانست تنها یک سطحی و با ساختار ذیل باشد (البته امکان کدنویسی و نوشتن مداخل سفارشی هم وجود داشت؛ ولی حالت پیش فرض appSettings، تنها key/valueهای یک سطحی هستند):
<appSettings>
   <add key="Logging-IncludeScopes" value="false" />
   <add key="Logging-Level-Default" value="verbose" />
   <add key="Logging-Level-System" value="Information" />
   <add key="Logging-Level-Microsoft" value="Information" />
</appSettings>
اما اکنون یک فایل JSON را با هر تعداد سطح مورد نیاز می‌توان تعریف و استفاده کرد و برای اینکار نیازی به نوشتن کدهای سفارشی تعریف مداخل خاص، وجود ندارد.
در فایل JSON فوق، نمونه‌ای از key/valueها، آرایه‌ها و اطلاعات چندین سطحی را مشاهده می‌کنید.


خواندن فایل تنظیمات appsettings.json در برنامه

پس از نصب پیشنیاز خواندن فایل‌های کانفیگ از نوع JSON، به فایل آغازین برنامه مراجعه کرده و سازنده‌ی جدیدی را به آن اضافه کنید:
public class Startup
{
    public IConfigurationRoot Configuration { set; get; }
 
    public Startup(IHostingEnvironment env)
    {
        var builder = new ConfigurationBuilder()
                            .SetBasePath(env.ContentRootPath)
                            .AddJsonFile("appsettings.json");
        Configuration = builder.Build();
    }
در اینجا نحوه‌ی خواندن فایل کانفیگ جدید appsettings.json را مشاهده می‌کنید. چند نکته در اینجا حائز اهمیت هستند:
الف) این خواندن، در سازنده‌ی کلاس آغازین برنامه و پیش از تمام تنظیمات دیگر باید انجام شود.
ب) جهت در معرض دید قرار دادن اطلاعات خوانده شده، آن‌را به یک خاصیت عمومی انتساب داده‌ایم.
ج) متد SetBasePath جهت مشخص کردن محل یافتن فایل appsettings.json ذکر شده‌است. این اطلاعات را می‌توان از سرویس توکار IHostingEnvironment و خاصیت ContentRootPath آن دریافت کرد. همانطور که ملاحظه می‌کنید، این تزریق وابستگی نیز به صورت خودکار توسط ASP.NET Core مدیریت می‌شود.


دسترسی به تنظیمات خوانده شده توسط اینترفیس IConfigurationRoot

تا اینجا موفق شدیم تا تنظیمات خوانده شده را به خاصیت عمومی Configuration از نوع IConfigurationRoot انتساب دهیم. اما ساختار ذخیره شده‌ی در این اینترفیس به چه صورتی است؟


همانطور که مشاهده می‌کنید، هر سطح از سطح قبلی آن با : جدا شده‌است. همچنین اعضای آرایه، دارای ایندکس‌های 0: و 1: و 2: هستند. بنابراین برای خواندن این اطلاعات می‌توان نوشت:
var key1 = Configuration["Key1"];
var user1 = Configuration["Auth:Users:0"];
var authUsers = Configuration.GetSection("Auth:Users").GetChildren().Select(x => x.Value).ToArray();
var loggingIncludeScopes = Configuration["Logging:IncludeScopes"];
var loggingLoggingLogLevelDefault = Configuration["Logging:LogLevel:Default"];
خاصیت Configuration نیز در نهایت بر اساس key/valueها کار می‌کند و این keyها اگر چند سطحی بودند، با : از هم جدا می‌شوند و اگر نیاز به دسترسی اعضای خاصی از آرایه‌ها وجود داشت می‌توان آن ایندکس خاص را در انتهای زنجیره ذکر کرد. همچنین در اینجا نحوه‌ی استخراج تمام اعضای یک آرایه را نیز مشاهده می‌کنید.

یک نکته: خاصیت Configuration، دارای متد GetValue نیز هست که توسط آن می‌توان نوع مقدار دریافتی و یا حتی مقدار پیش فرضی را در صورت عدم وجود این key، مشخص کرد:
 var val = Configuration.GetValue<int>("key-name", defaultValue: 10);
در متد GetValue، آرگومان جنریک آن، یک کلاس را نیز می‌پذیرد. یعنی می‌توان خواص تو در توی مشخص شده‌ی با : را به یک کلاس نیز نگاشت کرد. در اینجا مقدار کلید معرفی شده، اولین سطحی خواهد بود که باید این اطلاعات از آن استخراج و نگاشت شوند.


سرویس IConfigurationRoot قابل تزریق است

در قسمت قبل، سرویس‌ها و تزریق وابستگی‌ها را بررسی کردیم. نکته‌ی جالبی را که می‌توان به آن اضافه کرد، قابلیت تزریق خاصیت عمومی
public class Startup
{
    public IConfigurationRoot Configuration { set; get; }
به تمام قسمت‌های برنامه است. برای نمونه در همان مثال قسمت قبل، قصد داریم تنظیمات برنامه را در لایه سرویس آن خوانده و مورد استفاده قرار دهیم. برای اینکار باید مراحل ذیل طی شوند:
الف) اعلام موجودیت IConfigurationRoot به IoC Container
اگر از استراکچرمپ استفاده می‌کنید، باید مشخص کنید، زمانیکه IConfigurationRoot درخواست شد، آن‌را چگونه باید از خاصیت مرتبط با آن دریافت کند:
var container = new Container();
container.Configure(config =>
{
    config.For<IConfigurationRoot>().Singleton().Use(() => Configuration);
و یا اگر از همان IoC Container توکار ASP.NET Core استفاده می‌کنید، روش انجام این‌کار در متد ConfigureServices به صورت زیر است:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IConfigurationRoot>(provider => { return Configuration; });
طول عمر آن هم singleton مشخص شده‌است تا تنها یکبار وهله سازی و سپس کش شود (مناسب برای کار با تنظیمات سراسری برنامه).

ب) فایل project.json کتابخانه‌ی Core1RtmEmptyTest.Services را گشوده و وابستگی Microsoft.Extensions.Configuration.Abstractions را به آن اضافه کنید:
{ 
    "dependencies": {
        //same as before 
        "Microsoft.Extensions.Configuration.Abstractions": "1.0.0"
    }
این وابستگی امکان دسترسی به اینترفیس IConfigurationRoot را در اسمبلی‌های دیگر میسر می‌کند.

ج) سپس فایل MessagesService.cs را گشوده و این اینترفیس را به سازنده‌ی سرویس MessagesService تزریق می‌کنیم:
public interface IMessagesService
{
    string GetSiteName();
}
 
public class MessagesService : IMessagesService
{
    private readonly IConfigurationRoot _configurationRoot;
 
    public MessagesService(IConfigurationRoot configurationRoot)
    {
        _configurationRoot = configurationRoot;
    }
 
    public string GetSiteName()
    {
        var key1 = _configurationRoot["Key1"];
        return $"DNT {key1}";
    }
}
در ادامه، نحوه‌ی استفاده‌ی از آن، همانند نکاتی است که در قسمت «دسترسی به تنظیمات خوانده شده توسط اینترفیس IConfigurationRoot» عنوان شد.
اکنون اگر برنامه را اجرا کنید، با توجه به اینکه میان افزار Run از این سرویس سفارشی استفاده می‌کند:
public void Configure(
    IApplicationBuilder app,
    IHostingEnvironment env,
    IMessagesService messagesService)
{ 
    app.Run(async context =>
    {
        var siteName = messagesService.GetSiteName();
        await context.Response.WriteAsync($"Hello {siteName}");
    });
}
چنین خروجی را خواهیم داشت:



خواندن تنظیمات از حافظه

الزاما نیازی به استفاده از فایل‌های JSON و یا XML در اینجا وجود ندارد. ابتدایی‌ترین حالت کار با بسته‌ی Microsoft.Extensions.Configuration، متد AddInMemoryCollection آن است که در اینجا می‌توان لیستی از key/value‌ها را ذکر کرد:
var builder = new ConfigurationBuilder()
                    .AddInMemoryCollection(new[]
                                {
                                    new KeyValuePair<string,string>("the-key", "the-value"),
                                });
 و نحوه‌ی کار با آن نیز همانند قبل است:
 var theValue = Configuration["the-key"];


امکان بازنویسی تنظیمات انجام شده، بسته به شرایط محیطی

در اینجا محدود به یک فایل JSON و یک فایل تنظیمات برنامه، نیستیم. برای کار با ConfigurationBuilder می‌توان از Fluent interface آن استفاده کرد و به هر تعدادی که نیاز بود، متدهای خواندن از فایل‌های کانفیگ دیگر را اضافه کرد:
public class Startup
{
    public IConfigurationRoot Configuration { set; get; }
 
    public Startup(IHostingEnvironment env)
    {
        var builder = new ConfigurationBuilder()
                            .SetBasePath(env.ContentRootPath)
                            .AddInMemoryCollection(new[]
                                {
                                    new KeyValuePair<string,string>("the-key", "the-value"),
                                })
                            .AddJsonFile("appsettings.json", reloadOnChange: true, optional: false)
                            .AddJsonFile($"appsettings.{env}.json", optional: true);
        Configuration = builder.Build();
    }
و نکته‌ی مهم اینجا است که تنظیمات فایل دوم، تنظیمات مشابه فایل اول را بازنویسی می‌کند.
برای مثال در اینجا آخرین AddJsonFile تعریف شده، بنابر متغیر محیطی فعلی به appsettings.development.json تفسیر شده و در صورت وجود این فایل (با توجه به optional بودن آن) اطلاعات آن دریافت گردیده و اطلاعات مشابه فایل appsettings.json قبلی را بازنویسی می‌کند.


امکان دسترسی به متغیرهای محیطی سیستم عامل

در انتهای زنجیره‌ی ConfigurationBuilder می‌توان متد AddEnvironmentVariables را نیز ذکر کرد:
 var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
این متد سبب می‌شود تا تمام اطلاعات قسمت Environment سیستم عامل، به مجموعه‌ی تنظیمات جاری اضافه شوند (در صورت نیاز) که نمونه‌ای از آن‌را در تصویر ذیل مشاهده می‌کنید:



امکان نگاشت تنظیمات برنامه به کلاس‌‌های متناظر

کار کردن با key/valueهای رشته‌ای، هرچند روش پایه‌ای استفاده‌ی از تنظیمات برنامه است، اما آنچنان refactoring friendly نیست. در ASP.NET Core امکان تعریف تنظیمات strongly typed نیز پیش بینی شده‌است. برای این منظور باید مراحل زیر طی شوند:
به عنوان نمونه تنظیمات فرضی smtp ذیل را به انتهای فایل appsettings.json اضافه کنید:
{
    "Key1": "Value1",
    "Auth": {
        "Users": [ "Test1", "Test2", "Test3" ]
    },
    "Logging": {
        "IncludeScopes": false,
        "LogLevel": {
            "Default": "Debug",
            "System": "Information",
            "Microsoft": "Information"
        }
    },
    "Smtp": {
        "Server": "0.0.0.1",
        "User": "user@company.com",
        "Pass": "123456789",
        "Port": "25"
    }
}
مثال جاری که بر اساس ASP.NET Core Web Application و با قالب خالی آن ایجاد شده‌است، دارای نام فرضی Core1RtmEmptyTest است. در همین پروژه بر روی پوشه‌ی src کلیک راست کرده و گزینه‌ی Add new project را انتخاب کنید و سپس یک پروژه‌ی جدید از نوع NET Core -> Class library. را به آن با نام Core1RtmEmptyTest.ViewModels اضافه کنید (تصویر ذیل).


در این کتابخانه‌ی جدید که محل نگهداری ViewModelهای برنامه خواهد بود، کلاس معادل قسمت smtp فایل config فوق را اضافه کنید:
namespace Core1RtmEmptyTest.ViewModels
{
    public class SmtpConfig
    {
        public string Server { get; set; }
        public string User { get; set; }
        public string Pass { get; set; }
        public int Port { get; set; }
    }
}
از این جهت این کلاس را در یک library جداگانه قرار داده‌ایم تا بتوان از آن در لایه‌ی سرویس و همچنین خود برنامه استفاده کرد. اگر این کلاس را در برنامه‌ی اصلی قرار می‌دادیم، امکان دسترسی به آن در لایه‌ی سرویس میسر نمی‌شد.
سپس به پروژه‌ی Core1RtmEmptyTest مراجعه کرده و بر روی گره references آن کلیک راست کنید. در اینجا گزینه‌ی add reference را انتخاب کرده و سپس Core1RtmEmptyTest.ViewModels را انتخاب کنید، تا اسمبلی آن‌را بتوان در پروژه‌ی جاری استفاده کرد.
انجام اینکار معادل است با افزودن یک سطر ذیل به فایل project.json پروژه:
{
    "dependencies": {
        // same as before        
        "Core1RtmEmptyTest.ViewModels": "1.0.0-*"
    },
اکنون با فرض وجود تنظیمات خواندن فایل appsettings.json در سازنده‌ی کلاس آغازین برنامه، نیاز است بسته‌ی نیوگت Microsoft.Extensions.Configuration.Binder را نصب کنید:


و سپس در کلاس آغازین برنامه و متد ConfigureServices آن، نحوه‌ی نگاشت قسمت Smtp فایل کانفیگ را مشخص کنید:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
   services.Configure<SmtpConfig>(options => Configuration.GetSection("Smtp").Bind(options));
در اینجا مشخص شده‌است که کار وهله سازی کلاس SmtpConfig بر اساس اطلاعات قسمت smtp فایل کانفیگ تامین می‌شود. متغیر Configuration ایی که در اینجا استفاده شده‌است همان خاصیت عمومی public IConfigurationRoot Configuration کلاس آغازین برنامه است.

سپس برای استفاده از این تنظیمات strongly typed (برای نمونه در لایه سرویس برنامه)، ابتدا ارجاعی را به پروژه‌ی Core1RtmEmptyTest.ViewModels به لایه‌ی سرویس برنامه اضافه می‌کنیم (بر روی گره references آن کلیک راست کنید. در اینجا گزینه‌ی add reference را انتخاب کرده و سپس Core1RtmEmptyTest.ViewModels را انتخاب کنید).
در ادامه نیاز است بسته‌ی نیوگت جدیدی را به نام Microsoft.Extensions.Options به لایه‌ی سرویس برنامه اضافه کنیم. به این ترتیب قسمت وابستگی‌های فایل project.json این لایه چنین شکلی را پیدا می‌کند:
    "dependencies": {
        "Core1RtmEmptyTest.ViewModels": "1.0.0-*",
        "Microsoft.Extensions.Configuration.Abstractions": "1.0.0",
        "Microsoft.Extensions.Options": "1.0.0",
        "NETStandard.Library": "1.6.0"
    }
پس از ذخیره سازی این کلاس و بازیابی خودکار وابستگی‌های آن، اکنون برای دسترسی به این تنظیم باید از اینترفیس ویژه‌ی IOptions استفاده کرد (به همین جهت بسته‌ی جدید نیوگت Microsoft.Extensions.Options را نصب کردیم):
public interface IMessagesService
{
    string GetSiteName();
}
 
public class MessagesService : IMessagesService
{
    private readonly IConfigurationRoot _configurationRoot;
    private readonly IOptions<SmtpConfig> _settings;
 
    public MessagesService(IConfigurationRoot configurationRoot, IOptions<SmtpConfig> settings)
    {
        _configurationRoot = configurationRoot;
        _settings = settings;
    }
 
    public string GetSiteName()
    {
        var key1 = _configurationRoot["Key1"];
        var server = _settings.Value.Server;
        return $"DNT {key1} - {server}";
    }
}
همانطور که ملاحظه می‌کنید <IOptions<SmtpConfig به سازنده‌ی کلاس تزریق شده‌است و سپس از طریق خاصیت Value آن می‌توان به تمام اطلاعات کلاس SmtpConfig به شکل strongly typed دسترسی یافت.

اکنون اگر برنامه را جرا کنید، این خروجی را می‌توان مشاهده کرد (که در آن آدرس Server دریافت شده‌ی از فایل کانفیگ نیز مشخص است):


البته همانطور که در قسمت قبل نیز عنوان شد، این تزریق وابستگی‌ها در تمام قسمت‌های برنامه کار می‌کند. برای مثال در کنترلرها هم می‌توان <IOptions<SmtpConfig را به همین نحو تزریق کرد.


نحوه‌ی واکنش به تغییرات فایل‌های کانفیگ

در نگارش‌های قبلی ASP.NET، هر تغییری در فایل web.config، سبب ری‌استارت شدن کل برنامه می‌شد که این مساله نیز خود سبب بروز مشکلات زیادی مانند از دست رفتن سشن تمام کاربران می‌شد.
در ASP.NET Core، برنامه‌ی وب ما دیگر متکی به فایل web.config نبوده و همچنین می‌توان چندین و چند نوع فایل config داشت. به علاوه در اینجا متدهای مرتبط معرفی فایل‌های کانفیگ دارای پارامتر مخصوص reloadOnChange نیز هستند:
 .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
این پارامتر در صورت true بودن، به صورت خودکار سبب بارگذاری مجدد اطلاعات فایل کانفیگ می‌شود (بدون ری‌استارت کل برنامه).
مطالب
معرفی کتاب NHibernate 3 Beginners Guide, Aug 2011

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

NHibernate 3 Beginners Guide, Aug 2011 

در این کتاب بصورتی بسیار جامع از ابتدایی‌ترین مسئله تا فنی‌ترین مسائلی که  در هر پروژه‌ی‌ عملی هر توسعه دهنده ای با هر سطحی امکان مواجه شدن با این مشکلات را دارد به تفصیل بررسی شده. این کتاب شامل 12 فصل بوده که مطالب آن به شرح زیر ارائه شده است:
1- فصل اول – نگاه اولیه:
  • NHibernate چیست
  • موارد تازه در آخرین نسخه NHibernate
  • چرا ما استفاده کنیم و چه کسانی دیگری استفاده میکنند
  • زمانیکه به مشکل برخوردیم از کجا کمک بگیریم یا حتی نسخه ای تجاری تهیه کنیم
2- فصل دوم – اولین مثال کامل:
  • آماده سازی سیستم برای توسعه برنامه‌ها با استفاده از NHibernate
  • ایجاد یک مدل ساده از مشکل موجود
  • ایجاد بانک و برپایی یک نگاشت (Map) بین مدل و بانک
  • نوشتن و خواندن داده از و به بانک
  • بحث در مورد بدست آوردن نتیجه معادل بدون استفاده از NHibernate و یا ORM دیگر
3- فصل سوم - ایجاد یک مدل:
  • مدل چیست؟
  • عوامل اصلی موجود در ایجاد یک مدل چیست؟
  • چطور میتوان مدل ساخت؟
4- فصل چهارم – ایجاد شمای بانک:
  • یادگیری جدول چیست؟
  • یادگیری چطور جدولها به هم مرتبط شود؟
  • بحث در مورد  استراتژی‌های تحمیلی ای که کدام داده میتواند ذخیره شود
  • نمایش امکانات موجود برای بهبود کارایی دسترسی به داده
  • ایجاد بانک داده برای سیستم سفارش (Ordering System)
5- فصل پنجم - نگاشت مدل به بانک داده:
  • بدست آوردن یک درک درست درباره نگاشت و پیش نیازهای آن
  • بحث در مورد ریزه کاری‌های چهار تکنیک پر استفاده معمول نگاشت
  • توصیف و توسعه قراردادها برای کاهش تقلا در کدنویسی
  • ایجاد خودکار اسکریپت برای ایجاد شمای بانک دیتا از روی نگاشت مان
  • توصیف و نگاشت مدل دامنه سیستم سفارش مان
6- فصل ششم – وهله‌ها و تراکنش ها
  • بحث در مورد اشیاء وهله و تراکنش
  • معرفی شیء session factory
  • پیاده سازی برنامه ای که دیتا ذخیره و بازخوانی میکند
  • تجزیه و تحلیل متدهای گوناگون برای مدیریت وهله‌ها در پر استفاده‌ترین انواع برنامه
7- فصل هفتم - آزمایش کردن، نمایه سازی، نظارت، واقع نگاری
  • پیاده سازی یک بستر پایه برای ساخت آزمایش ساده کد دسترسی به بانک داده
  • ایجاد آزمایش‌ها برای تایید کد دسترسی به بانک داده مان
  • تجزیه و تحلیل ارتباط بین NHibernate و بانک داده
  • پیکربندی NHibernate برای واقع نگاری داده‌های مورد توجه
8- فصل هشتم - پیکر بندی
  • بحث در مورد پیکربندی قبل از شروع
  • آشنا شدن با لیست مولفه‌های NHibernate که میتوان پیکربندی کرد
  • یادگیری چهار روش متفاوت پیکربندی که چگونه میتوان در برنامه هایمان استفاده کرد
9- فصل نهم – نوشتن پرس و جو
  • یادگیری چگونگی استفاه از (LINQ (Language Integrated Query در NHibernate  برای دریافت داده
  • پرس و جو با استفاده از criteria query API
  • استفاده از گویش object-oriented اصلی SQL بنام Hibernate Query Language HQL
  • بحث در مورد موجودیت هایی با خواصی که توان lazy load دارند
  • مقابله با بارگذاری حریصانه با lazy loading بطوریکه بصورت دسته ای از پرس و جو بنظر آید
10- فصل دهم – اعتبار سنجی داده برای نگهداری (ذخیره)
11- فصل یازدهم – اشتباهات متداول – چیزهایی برای جلوگیری
 

 

 
مطالب
Angular CLI - قسمت پنجم - ساخت و توزیع برنامه
ساخت و توزیع برنامه‌های Angular یکی از مهم‌ترین و بحث برانگیزترین قسمت‌های نگارش‌های جدید آن است و به ازای هر پروژه و قالبی که برای آن توسط گروه‌های مختلف ارائه شده‌است، روش‌های متفاوتی را شاهد خواهید بود. در ادامه روش توصیه شده‌ی توسط تیم Angular را که مبتنی است بر webpack و به صورت خودکار توسط Angular CLI مدیریت می‌شود، بررسی خواهیم کرد.


ساخت (Build) برنامه‌های Angular

Angular CLI کار ساخت و کامپایل برنامه را به صورت خودکار انجام داده و خروجی را در مسیری مشخص درج می‌کند. در اینجا می‌توان گزینه‌هایی را بر اساس نوع کامپایل مدنظر مانند کامپایل برای حالت توسعه و یا کامپایل برای حالت توزیع نهایی، انتخاب کرد. همچنین مباحث bundling و یکی کردن تعداد بالای ماژول‌های برنامه در آن لحاظ می‌شوند تا برنامه در حالت توزیع نهایی، سبب 100ها رفت و برگشت به سرور برای دریافت ماژول‌های مختلف آن نشود. به علاوه مباحث uglification (به نوعی obfuscation کدهای جاوا اسکریپتی نهایی) و tree-shaking (حذف کدهایی که در برنامه استفاده نشده‌اند؛ یا کدهای مرده) نیز پیاده سازی می‌شوند. با انجام tree-shaking‌، نه تنها اندازه‌ی توزیع نهایی به کاربر کاهش پیدا می‌کند، بلکه مرورگر نیز حجم کمتری از کدهای جاوااسکریپتی را باید تفسیر کند.
برای شروع می‌توان از دستور ذیل برای مشاهده‌ی تمام گزینه‌های مهیای ساخت برنامه استفاده کرد:
> ng build --help
ذکر تنهای دستور ng build‌، بدون هیچ گزینه‌ای، برای حالت «توسعه‌ی» برنامه بسیار ایده‌آل است (و دقیقا به معنای صدور دستور ng build --dev است). در این حالت خروجی کامپایل شده‌ی برنامه در پوشه‌ی dist تولید می‌شود. اگر از قسمت دوم این سری به خاطر داشته باشید، نام این پوشه‌ی خروجی، جزئی از تنظیمات فایل angular-cli.json. است:
"apps": [
{
   "outDir": "dist",
زمانیکه دستور ng build‌  صادر شود، این فایل‌ها را در پوشه‌ی dist خواهید یافت:

فایل 
توضیح 
 inline.bundle.js   WebPack runtime
از آن برای بارگذاری ماژول‌های برنامه و چسباندن قسمت‌های مختلف به یکدیگر استفاده می‌شود. 
 main.bundle.js   شامل تمام کدهای ما است. 
 polyfills.bundle.js   Polyfills - جهت پشتیبانی از مرورگرهای مختلف.
 styles.bundle.js    شامل بسته بندی تمام شیوه نامه‌های برنامه است 
vendor.bundle.js  کدهای کتابخانه‌های ثالث مورد استفاده و همچنین خود Angular، در اینجا بسته بندی می‌شوند. 
 

روشی برای بررسی محتوای bundleهای تولید شده

تولید bundleها در جهت کاهش رفت و برگشت‌های به سرور و بالا بردن کارآیی برنامه ضروری هستند؛ اما دقیقا این بسته بندی‌ها شامل چه اطلاعاتی می‌شوند؟ این اطلاعات را می‌توان از فایل‌های source map تولیدی استخراج کرد و برای این منظور می‌توان از برنامه‌ی source-map-explorer استفاده کرد.

روش نصب عمومی آن:
 > npm install -g source-map-explorer
روش اجرا:
 > source-map-explorer dist/main.bundle.js
پس از آن یک گزارش HTML ایی از محتوای bundle مدنظر تولید می‌شود.


یک مثال: ساخت برنامه‌ی مثال قسمت چهارم - تنظیمات مسیریابی در حالت dev

در ادامه، کار Build همان مثالی را که در قسمت قبل توضیح داده شد، بررسی می‌کنیم. برای این منظور از طریق خط فرمان به ریشه‌ی پوشه‌ی اصلی پروژه وارد شده و دستور ng build را صادر کنید. یک چنین خروجی را مشاهده خواهید کرد:
 D:\Prog\angular-routing>ng build
Hash: 123cae8bd8e571f44c31
Time: 33862ms
chunk {0} polyfills.bundle.js, polyfills.bundle.js.map (polyfills) 158 kB {4} [initial] [rendered]
chunk {1} main.bundle.js, main.bundle.js.map (main) 14.7 kB {3} [initial] [rendered]
chunk {2} styles.bundle.js, styles.bundle.js.map (styles) 9.77 kB {4} [initial] [rendered]
chunk {3} vendor.bundle.js, vendor.bundle.js.map (vendor) 2.34 MB [initial] [rendered]
chunk {4} inline.bundle.js, inline.bundle.js.map (inline) 0 bytes [entry] [rendered]
و اگر فایل index.html تولیدی آن‌را بررسی کنید، تنها الحاق همین 4 فایل js تولیدی را مشاهده می‌نمائید:
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>AngularRouting</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <app-root>Loading...</app-root>
<script type="text/javascript" src="inline.bundle.js">
</script><script type="text/javascript" src="polyfills.bundle.js">
</script><script type="text/javascript" src="styles.bundle.js">
</script><script type="text/javascript" src="vendor.bundle.js">
</script><script type="text/javascript" src="main.bundle.js"></script>
</body>
</html>

یک نکته: زمانیکه دستور ng serve -o صادر می‌شود، در پشت صحنه دقیقا همین دستور ng build صادر شده و اطلاعات را درون حافظه تشکیل می‌دهد. اما اگر کار ng build را دستی انجام دهیم، اینبار ng serve -o اطلاعات را از پوشه‌ی dist دریافت می‌کند. بنابراین در حین کار با ng serve -o نیازی به build دستی پروژه نیست.

سؤال: چرا حجم فایل endor.bundle.js اینقدر بالا است و شامل چه اجزایی می‌شود؟
نکته‌ای که در اینجا وجود دارد، حجم بالای فایل vendor.bundle.js آن است که 2.34 MB می‌باشد:


چون دستور ng build بدون پارامتری ذکر شده‌است، برنامه را برای حالت توسعه Build می‌کند و به همین جهت هیچگونه بهینه سازی در این مرحله صورت نخواهد گرفت. برای بررسی محتوای این فایل می‌توان دستور ذیل را در ریشه‌ی اصلی پروژه صادر کرد:
 > source-map-explorer dist/vendor.bundle.js
پس از اجرای این دستور، بلافاصله مرورگر پیش فرض سیستم اجرا شده و گزارشی را ارائه می‌دهد.


همانطور که مشاهده می‌کنید، در حالت بهینه سازی نشده و Build برای توسعه، کامپایلر Angular حدود 41 درصد حجم فایل vendor.bundle.js را تشکیل می‌دهد. به علاوه ماژول‌ها و قسمت‌هایی را ملاحظه می‌کنید که اساسا برنامه‌ی فعلی مثال ما از آن‌ها استفاده نمی‌کند؛ مانند http، فرم‌ها و غیره.


سفارشی سازی Build برای محیط‌های مختلف

اگر به پروژه‌ی تولید شده‌ی توسط Angular CLI دقت کنید، حاوی پوشه‌ای است به نام src\environments


هدف از فایل‌های environment برای نمونه تغییر آدرس توزیع برنامه در حالت توسعه و ارائه نهایی است.
همچنین در اینجا می‌توان نحوه‌ی بهینه سازی فایل‌های تولیدی را توسط Build Targets مشخص کرد و اینکار توسط ذکر پرچم prod-- (مخفف production) صورت می‌گیرد.
در ادامه، تفاوت‌های دستورهای ng build و ng build --prod را ملاحظه می‌کنید:
- با اجرای ng build، از فایل environment.ts استفاده می‌شود؛ برخلاف حالت اجرای ng build --prod که از فایل environment.prod.ts استفاده می‌کند.
- Cache-busting در حالت ارائه‌ی نهایی، به تمام اجزای پروژه اعمال می‌شود؛ اما در حالت توسعه فقط برای تصاویر قید شده‌ی در فایل‌های css.
- فایل‌های source map فقط برای حالت توسعه تولید می‌شوند.
- در حالت توسعه، cssها داخل فایل‌های js تولیدی قرار می‌گیرند؛ اما در حالت ارائه‌ی نهایی به صورت فایل‌های css بسته بندی می‌شوند.
- در حالت توسعه برخلاف حالت ارائه‌ی نهایی، کار uglification انجام نمی‌شود.
- در حالت توسعه برخلاف حالت ارائه‌ی نهایی، کار tree-shaking یا حذف کدهای مرده و بدون ارجاع، انجام نمی‌شود.
- در حالت توسعه برخلاف حالت ارائه‌ی نهایی، کار AOT انجام نمی‌شود. در اینجا AOT به معنای Ahead of time compilation است.
- در هر دو حالت توسعه و ارائه‌ی نهایی کار bundling و دسته بندی فایل‌ها انجام خواهد شد.

به همین جهت است که ng build سریع است؛ اما حجم بالاتری را هم تولید می‌کند. چون بسیاری از بهینه سازی‌های حالت ارائه‌ی نهایی را به همراه ندارد.


دستورات build برای حالت توسعه و ارائه‌ی نهایی

برای حالت توسعه، هر 4 دستور ذیل یک مفهوم را دارند و به همین جهت مورد ng build متداول‌تر است:
>ng build --target=development --environment=dev
>ng build --dev -e=dev
>ng build --dev
>ng build

برای حالت ارائه‌ی نهایی، هر 3 دستور ذیل یک مفهوم را دارند و به همین جهت مورد ng build --prod متداول‌تر است:
>ng build --target=production --environment=prod
>ng build --prod -e=prod
>ng build --prod

همچنین هر کدام از این دستورات را توسط پرچم‌های ذیل نیز می‌توان سفارشی سازی کرد:

 پرچم  مخفف  توضیح
 sourcemap--  sm-  تولید سورس‌مپ
aot--    Ahead of Time compilation 
watch--  w-  تحت نظر قرار دادن فایل‌ها و ساخت مجدد
environment--  e-  محیط ساخت
 target--  t-  نوع ساخت
 dev--    مخفف نوع ساخت جهت توسعه
 prod--     مخفف نوع ساخت جهت ارائه نهایی

برای مثال در حالت prod، سورس‌مپ‌ها تولید نخواهند شد. اگر علاقمندید تا این فایل‌ها نیز تولید شوند، پرچم souremap را نیز ذکر کنید.
و یا اگر برای حالت dev می‌خواهید AOT را فعالسازی کنید، پرچم aot-- را در آنجا قید کنید.


یک مثال: ساخت برنامه‌ی مثال قسمت چهارم - تنظیمات مسیریابی در حالت prod

تا اینجا خروجی حالت dev ساخت برنامه‌ی قسمت چهارم را بررسی کردیم. در ادامه دستور ng build --prod را در ریشه‌ی پروژه صادر می‌کنیم:
 D:\Prog\angular-routing>ng build --prod
Hash: f5bd7fd555a85af8a86f
Time: 39932ms
chunk {0} polyfills.18173234f9641113b9fe.bundle.js (polyfills) 158 kB {4} [initial] [rendered]
chunk {1} main.c6958def7c5f51c45261.bundle.js (main) 50.3 kB {3} [initial] [rendered]
chunk {2} styles.d41d8cd98f00b204e980.bundle.css (styles) 69 bytes {4} [initial] [rendered]
chunk {3} vendor.b426ba6883193375121e.bundle.js (vendor) 1.37 MB [initial] [rendered]
chunk {4} inline.8cec210370dd3af5f1a0.bundle.js (inline) 0 bytes [entry] [rendered]


همانطور که ملاحظه می‌کنید، اینبار نه تنها حجم فایل‌ها به میزان قابل ملاحظه‌ای کاهش پیدا کرده‌اند، بلکه این نام‌ها به همراه یک سری hash هم هستند که کار cache-busting (منقضی کردن کش مرورگر، با ارائه‌ی نگارشی جدید) را انجام می‌دهند.

در ادامه اگر بخواهیم مجددا برنامه‌ی source-map-explorer را جهت بررسی محتوای فایل‌های js اجرا کنیم، به خطای عدم وجود sourcemapها خواهیم رسید (چون در حالت prod، به صورت پیش فرض غیرفعال هستند). به همین‌جهت برای این مقصود خاص نیاز است از پرچم فعالسازی موقت آن استفاده کرد:
> ng build --prod --sourcemap
> source-map-explorer dist/vendor.b426ba6883193375121e.bundle.js


همانطور که در تصویر نیز مشخص است، اینبار کامپایلر Angular به همراه تمام ماژول‌هایی که در برنامه ارجاعی به آن‌ها وجود نداشته‌است، حذف شده‌اند و کل حجم بسته‌ی Angular به 366 KB کاهش یافته‌است.


بررسی دستور ng serve

تا اینجا برای اجرای برنامه در حالت dev از دستور ng serve -o استفاده کرده‌ایم. کار ارائه‌ی برنامه توسط این دستور، از محتوای کامپایل شده‌ی درون حافظه با مدیریت webpack انجام می‌شود. به همین جهت بسیار سریع بوده و قابلیت live reload را ارائه می‌دهد (نمایش آنی تغییرات در مرورگر، با تغییر فایل‌ها).
همانند تمام دستورات دیگر، اطلاعات بیشتری را در مورد این دستور، از طریق راهنمای آن می‌توان به دست آورد:
 > ng serve --help

که شامل این موارد هستند (علاوه بر تمام مواردی را که در حالت ng build می‌توان مشخص کرد؛ مثلا ng serve --prod -o):

 پرچم مخفف
توضیح
 open-- o-
بازکردن خودکار مرورگر پیش فرض.
حالت پیش فرض آن گشودن مرورگر توسط خودتان است و سپس مراجعه‌ی دستی به آدرس برنامه. 
 port--  p-  تغییر پورت پیش فرض مانند ng server -p 8626 
 live-reload--  lr-   فعال است مگر اینکه آن‌را با false مقدار دهی کنید.
 ssl--    ارائه به صورت HTTPS
 proxy-config--  pc-  Proxy configuration file 


استخراج فایل تنظیمات webpack از Angular CLI

Angular CLI برای مدیریت build، در پشت صحنه از webpack استفاده می‌کند. فایل تنظیمات آن نیز جزئی از فایل‌های توکار این ابزار است و قرار نیست به صورت پیش فرض و مستقیم توسط پروژه‌ی جاری ویرایش شود. به همین جهت آن‌را در ساختار پروژه‌ی تولید شده، مشاهده نمی‌کنید.
اگر علاقمند به سفارشی سازی بیشتر این تنظیمات پیش فرض باشید، ابتدا باید آن‌را اصطلاحا eject کنید و سپس می‌توان آن‌را ویرایش کرد:
 > ng eject
Ejection was successful.

To run your builds, you now need to do the following commands:
- "npm run build" to build.
- "npm run test" to run unit tests.
- "npm start" to serve the app using webpack-dev-server.
- "npm run e2e" to run protractor.

Running the equivalent CLI commands will result in an error.
============================================
Some packages were added. Please run "npm install".
همانطور که مشاهده می‌کنید عنوان کرده‌است که از این پس خودتان باید بسیاری از مسایل را به صورت دستی مدیریت کنید و Angular CLI دیگر آن‌ها را به صورت خودکار مدیریت نمی‌کند و دیگر دستورات ng build و ng serve کار نخواهند کرد (این تغییرات در فایل package.json درج می‌شوند).
در این حالت است که فایل webpack.config.js به ریشه‌ی پروژه جهت سفارشی سازی شما اضافه خواهد شد. همچنین فایل‌های .angular-cli.json، package.json نیز جهت درج این تغییرات ویرایش می‌شوند.

و اگر در این لحظه پشیمان شده‌اید (!) فقط کافی است تا این مرحله‌ی جدید commit شده‌ی به مخزن کد را لغو کنید و باز هم به همان Angular CLI قبلی می‌رسید.
مطالب
انقیاد داده‌ها در WPF (بخش اول)
در این مقاله مفاهیم مختلفی را در ارتباط با DataBinding بررسی خواهیم کرد:
• One Way Binding بخش اول
• INPC  بخش اول
• Tow Way Binding بخش اول
• List Binding  بخش دوم
• Element Binding بخش دوم
• Data Conversion بخش دوم
در ابتدا مفهوم انقیاد داده‌ها یا همان DataBinding  را مرور می‌کنیم. به فرآیند مرتبط سازی منابع اطلاعاتی به کنترل‌ها در برنامه‌ها یا به بیان امروزی‌تر، به View‌ها و نمایش اطلاعات در آنها، انقیاد (Databinding) گویند.

One Way Data Binding (انقیاد یک طرفه)
در این حالت اطلاعات را صرفا در یک View و یا یک کنترل نمایش می‌دهیم و تغییر اطلاعات در View، تاثیری بر روی منبع اطلاعاتی نخواهد داشت.
مثال: یک پروژه‌ی WPF ساده را ایجاد و سپس کلاس Employee را با خصوصیات زیر، تعریف می‌کنیم:
public class Employee
    {
        public string Name { get; set; }
        public string Title { get; set; }
        public static Employee GetEmployee()
        {
            var emp = new Employee
            {
                Name = "Mani",
                Title = "CEO"
            };
            return emp;
        }
    }
در بخش markup فایل MainWindow.xaml کد‌های زیر را ایجاد می‌کنیم:
<Grid>
        <StackPanel Name="Display">
            <StackPanel Orientation="Horizontal">
                <TextBlock>First Name :</TextBlock>
                <TextBlock Margin="5,0,0,0" Text="{Binding Name}"/>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock>Title :</TextBlock>
                <TextBlock Margin="5,0,0,0" Text="{Binding Title}"></TextBlock>
            </StackPanel>
        </StackPanel>
    </Grid>
در markup  فوق برای بایند کردن اطلاعات شیء Employee به خصوصیت Text در Textblock، از روش خاصی استفاده می‌کنیم. ابتدا یک {} ایجاد می‌کنیم و درون آن عبارت Binding و خصوصیت مورد نظر جهت عملیات انقیاد داده‌ها را می‌نویسیم.
برای تکمیل و انجام عملیات Binding کافی است خصوصیت DataContext را با استفاده از متد استاتیک تعریف شده‌ی در کلاس Employee پر کنیم.
 public MainWindow()
        {
            InitializeComponent();        
            DataContext = Employee.GetEmployee();
        }
در ادامه‌ی بحث لازم است کمی مفهوم DataContext را بررسی کنیم. منبع داده پیش فرض در WPF شیء DataContext است؛ مگر اینکه منبع داده را خودمان تغییر دهیم. DataContext یک خصوصیت از کلاس FrameworkElement است که بیشتر کنترل‌های بخش UI در WPF از این کلاس مشتق می‌شوند.
 
INPC یا INotifyPropertyChanged Interface 
پس از بایند کردن اطلاعات به View  مورد نظر، ممکن است منبع داده در طول زمان استفاده تغییر کند. از این رو لازم است که این تغییرات، به View اعمال شوند. بدین منظور می‌بایست ابتدا Interface  ، INotifyPropertyChanged    را برای کلاس Employee پیاده سازی کنیم:
   public class Employee :INotifyPropertyChanged
    {
        public string Name { get; set; }
        public string Title { get; set; }
        public static Employee GetEmployee()
        {
            var emp = new Employee
            {
                Name = "Mani",
                Title = "CEO"
            };
            return emp;
        }
        public event PropertyChangedEventHandler PropertyChanged;
        [NotifyPropertyChangedInvocator]
        protected virtual void OnPropertyChanged
            ([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
پس از پیاده سازی Interface، خصوصیات کلاس Employee  نبایستی بصورت  AutoProperty  باشند. پس پیاده سازی خصوصیات را با تعریف فیلد‌های مورد نیاز تغییر می‌دهیم.
علت تغییر پیاده سازی، لزوم فراخوانی رویداد dOnPropertyChange در بلاک Set خصوصیات کلاس Employee  می‌باشد:
 private string _name;
        private string _title;
        public string Name
        {
            get { return _name; }
            set
            {
                _name = value;
                OnPropertyChanged();
            }
        }
        public string Title
        {
            get { return _title; }
            set
            {
                _title = value;
                OnPropertyChanged();
            }
        }

در ادامه ، در بخش CodeBehind در سازنده کلاس کد‌های زیر را جایگزین کد‌های قبلی  می کنیم :
        private Employee emp;
        public MainWindow()
        {
            InitializeComponent();
            emp = new Employee()
            {
                Name = "Mani",
                Title = "CEO"
            };
            DataContext = emp;
        }
سپس در بخش Markup، یک یک دکمه را ایجاد و رویداد کلیک آن را تعیین می‌کنیم:
     <StackPanel Orientation="Horizontal">
                <Button Click="btnClick" Width="70" Height="30" Content="Change"/>
     </StackPanel>
در بخش CodeBehind، رویداد Click را به شکل زیر پیاده سازی می‌کنیم:
private void btnClick(object sender, RoutedEventArgs e)
        {
            emp.Name = "Amir";
            emp.Title = "Manager";
        }
در برنامه‌ی فوق، در ابتدا View با اطلاعات ارسالی در بخش سازنده، پر می‌شود و با کلیک بر روی دکمه‌، منبع داده به‌روز شده (در اینجا شیء emp) و به‌صورت اتوماتیک View ما به‌روز خواهد شد.

Tow Way Data binding  (انقیاد دو طرفه)
در این حالت از Data binding، با تغییر View، تغییرات بر روی منبع داده نیز اعمال می‌شوند.
ابتدا بخش markup مثال فوق را با اضافه کردن ویژگی Mode در کنار ویژگی Binding به شکل زیر تغییر می‌دهیم:
<Grid>
        <StackPanel Name="Display" >
            <StackPanel Orientation="Horizontal">
                <TextBlock  Margin="5,0,0,0">Name :</TextBlock>
                <TextBox Margin="5,0,0,0" Text="{Binding Name, Mode=TwoWay}"/>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Margin="5,0,0,0">Title :</TextBlock>
                <TextBox Margin="5,0,0,0" Text="{Binding Title,Mode=TwoWay}"/>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock  Margin="5,0,0,0">Name :</TextBlock>
                <TextBlock Margin="5,0,0,0" Text="{Binding Name}"/>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Margin="5,0,0,0">Title :</TextBlock>
                <TextBlock Margin="5,0,0,0" Text="{Binding Title}"/>
            </StackPanel>
        </StackPanel>
    </Grid>
اگر ویژگی Mode نوشته نشود بصورت پیش فرض به‌صورت OneWay تعبیر می‌شود. حالت قبل. همچنین در کد بالا دو Textbox در صفحه قرار داده شده‌اند تا با تغییر محتوای آن بتوانیم تاثیر عملیات دوطرفه‌ی انقیاد را بر روی Textblockهای بعدی مشاهده کنیم.
پس از اجرای برنامه (بخش CodeBehind نیازی به اصلاح ندارد) مقداری جدید در Textbox موجود در صفحه تایپ کنید. کافی است Focus از روی کنترلی که محتوای آن را تغییر داده‌اید، عوض شود، بلافاصله Textblock متناظر با آن، با محتوای جدیدی که در منبع داده اعمال شده است به‌روز می‌شود.
مطالب
ویژگی های کمتر استفاده شده در NET. - بخش هفتم

DebuggerStepThroughAttribute

ویژگی DebuggerStepThroughAttribute باعث می‌شود که در زمان دیباگ کردن کد، با کلید F11، متدهایی که این ویژگی را دارند، بدون رفتن به داخل متد (همانند دیباگ با کلید F10 عمل می‌کند، به جز زمانی که در داخل متد break point گذاشته باشید) ، تنها اجرا می‌شوند.
به مثال زیر توجه کنید:
class Program
{
    public static void Main(string[] args)
    {
        DebuggerStepThroughMethod1();
    }

    [DebuggerStepThrough]
    public static void DebuggerStepThroughMethod1()
    {
        Console.WriteLine( "Method 1" );
        DebuggerStepThroughMethod2();
    }

    [DebuggerStepThrough]
    public static void DebuggerStepThroughMethod2()
    {
        Console.WriteLine( "Method 2" );
    }
}
و نتیجه دیباگ با استفاده از F11 به صورت زیر می‌شود:

همانطور که مشاهده می‌کنید برنامه را با کلید F11 اجرا کردم. بعد از ورود به Method1، با زدن کلید F11 دستور بعدی، break point درون Method2 است.

ConditionalAttribute

شما با استفاده از Conditional می توانید اجرای یک متد را به شناساننده پیش پردازشی ( pre-processing identifier ) وابسته کنید. ConditionalAttribute می‌تواند بر روی یک کلاس یا یک متد بکار برده شود.
class Program
{
    public static void Main(string[] args)
    {
        DebugMode();
    }

    [Conditional("DEBUG")]
    public static void DebugMode()
    {
        Console.WriteLine( "Debug mode" );
    }
}
در صورتی که مثال بالا را در حالت Debug اجرا کنید، خروجی کنسول پیام Debug mode است و در صورتی که در حالت Release اجرا کنید، متد DebugMode اجرا نخواهد شد.
نکته: شما می‌توانید با استفاده از دستور define# (در بیرون از فضای نام) مقدار سفارشی خود را تعریف کنید.
#define ReleaseMode


Flags Enum Attribute

ویژگی Flags برای پوشش فیلدهای بیتی و انجام مقایسه بیتی استفاده می‌شود. از این ویژگی باید برای زمانیکه یک داده شمارشی می‌تواند چندین مقدار را به صورت همزمان داشته باشد، استفاده کرد.
[System.Flags]
public enum Permission
{
    View = 1,
    Insert = 2,
    Update = 4,
    Delete = 8
}
این نکته خیلی مهم است که Flags به صورت خودکار، مقادیر enum را به توان دو نمی‌رساند و شما باید به صورت دستی این مقادیر را تعیین کنید. در صورتیکه مقادیر عددی را تعیین نکنید، enum در عملیات بیتی به درستی کار نخواهد کرد، چرا که مقدار enum از 0 شروع می‌شود و افزایش پیدا می‌کند.  
public static void Main( string[] args )
{
    var permission = ( Permission.View | Permission.Insert ).ToString();
    Console.WriteLine( permission ); // Displays ‘View, Insert’

    var userPermission = Permission.View | Permission.Insert | Permission.Update | Permission.Delete;
    // To retrieve the value from property you can do this
    if ( ( userPermission & Permission.Delete ) == Permission.Delete )
    {
        Console.WriteLine( "کاربر دارای مجوز دسترسی به عملیات حذف می‌باشد" );
    }

    // In .NET 4 and later
    Console.WriteLine( userPermission.HasFlag( Permission.Delete )
                            ? "کاربر دارای مجوز دسترسی به عملیات حذف می‌باشد"
                            : "کاربر مجوز دسترسی به عملیات حذف را ندارد");
}

نکته: در صورتیکه مقداری را برای enum تعریف کرده باشید، نمی‌توانید آن را با مقدار 0 مشخص کنید (در زمانی که ویژگی flags را بر روی enum اضافه کرده باشید)، چرا که با استفاده از عملیات بیتی AND نمی‌توانید دارا بودن آن مقدار را تست کنید و همیشه نتیجه صفر خواهد بود.


Dynamically Compile and Execute C# Code

CodeDOM

با استفاده از CodeDOM می‌توانید یک سورس کد را به صورت یک فایل اسمبلی کامپایل و ذخیره کنید.
public static void Main( string[] args )
{
    var sourceCode = @"class DotNetTips
                        {
                            public void Print()
                            {
                                System.Console.WriteLine("".Net Tips"");
                            }
                        }";
    var compiledAssembly = CompileSourceCodeDom( sourceCode );
    ExecuteFromAssembly( compiledAssembly );
}

static Assembly CompileSourceCodeDom( string sourceCode )
{
    CodeDomProvider csharpCodeProvider = new CSharpCodeProvider();
    var cp = new CompilerParameters
                {
                    GenerateExecutable = false
                };
    cp.ReferencedAssemblies.Add( "System.dll" );
    var cr = csharpCodeProvider.CompileAssemblyFromSource( cp,
                                                            sourceCode );
    return cr.CompiledAssembly;
}
همانطور که در مثال بالا مشاهده می‌کنید، متغیر sourceCode حاوی کد مربوط به یک کلاس #C می‌باشد که یک متد Print در آن تعریف شده است.


Roslyn

سکوی کامپایلر دات نت " Roslyn "،  کامپایلرهای متن باز #C و  VB.NET را به همراه APIهای تجزیه و تحلیل کد ارائه کرده است که با استفاده از این APIها می‌توان ابزارهای آنالیز کد جهت استفاده در ویژوال استودیو را ایجاد کرد.

برای استفاده از Roslyn باید این کتابخانه را نصب کنید

Install-Package Microsoft.CodeAnalysis

حال مثال قبل را با استفاده از Roslyn بازنویسی می‌کنیم:

public static void Main(string[] args)
{
    var sourceCode = @"class DotNetTips
                        {
                            public void Print()
                            {
                                System.Console.WriteLine("".Net Tips"");
                            }
                        }";
    var compiledAssembly = CompileSourceRoslyn( sourceCode );
    ExecuteFromAssembly( compiledAssembly );
}

private static Assembly CompileSourceRoslyn(string sourceCode)
{
    using ( var memoryStream = new MemoryStream() )
    {
        var assemblyFileName = string.Concat( Guid.NewGuid().ToString(),
                                                ".dll" );
        var compilation = CSharpCompilation.Create( assemblyFileName,
                                                    new[]
                                                    {
                                                        CSharpSyntaxTree.ParseText( sourceCode )
                                                    },
                                                    new[]
                                                    {
                                                        MetadataReference.CreateFromFile( typeof( object ).Assembly.Location )
                                                    },
                                                    new CSharpCompilationOptions( OutputKind.DynamicallyLinkedLibrary ) );
        compilation.Emit( memoryStream );
        var assembly = Assembly.Load( memoryStream.GetBuffer() );
        return assembly;
    }
}

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

static void ExecuteFromAssembly( Assembly assembly )
{
    var helloKittyPrinterType = assembly.GetType( "DotNetTips" );
    var printMethod = helloKittyPrinterType.GetMethod( "Print" );
    var kitty = assembly.CreateInstance( "DotNetTips" );
    printMethod.Invoke( kitty,
                        BindingFlags.InvokeMethod,
                        null,
                        null,
                        CultureInfo.CurrentCulture );
}


مطالب دوره‌ها
Lazy loading در تزریق وابستگی‌ها به کمک StructureMap
پیشنیاز این بحث، مطلب «استفاده از StructureMap به عنوان یک IoC Container» می‌باشد که پیشتر در این سری مطالعه کردید (در حد نحوه نصب StructureMap و آشنایی با تنظیمات اولیه آن)

ابتدا ساختار بحث جاری را به نحو زیر درنظر بگیرید:
namespace DI04.Services
{
    public interface IAccounting
    {
        void CreateInvoice(int orderId, int count);
    }
}

namespace DI04.Services
{
    public interface ISales
    {
        bool ShippingAllowed(int orderId);
    }
}

namespace DI04.Services
{
    public interface IOrderHandler
    {
        void Handle(int orderId, int count);
    }
}

using System;

namespace DI04.Services
{
    public class Accounting : IAccounting
    {
        public Accounting()
        {
            Console.WriteLine("Accounting ctor.");
        }

        public void CreateInvoice(int orderId, int count)
        {
            // ...
        }
    }
}

using System;

namespace DI04.Services
{
    public class Sales : ISales
    {
        public Sales()
        {
            Console.WriteLine("Sales ctor.");
        }

        public bool ShippingAllowed(int orderId)
        {
            // فقط جهت آزمایش سیستم
            return false;
        }
    }
}

using System;

namespace DI04.Services
{
    public class OrderHandler : IOrderHandler
    {
        private readonly IAccounting _accounting;
        private readonly ISales _sales;
        public OrderHandler(IAccounting accounting, ISales sales)
        {
            Console.WriteLine("OrderHandler ctor.");
            _accounting = accounting;
            _sales = sales;
        }

        public void Handle(int orderId, int count)
        {
            if (_sales.ShippingAllowed(orderId))
            {
                _accounting.CreateInvoice(orderId, count);
            }
        }
    }
}
در اینجا کار مدیریت سفارشات در کلاس OrderHandler انجام می‌شود. این کلاس دارای دو وابستگی تزریق شده در سازنده کلاس می‌باشد.
در متد Handle، اگر مجوز کار توسط متد ShippingAllowed صادر شد، آنگاه کار نهایی توسط متد CreateInvoice باید صورت گیرد. با توجه به اینکه تزریق وابستگی‌ها در سازنده کلاس صورت می‌گیرد، نیاز است پیش از وهله سازی کلاس OrderHandler، هر دو وابستگی آن وهله سازی و تزریق شوند. در حالیکه در مثال جاری هیچگاه به وهله‌ای از نوع IAccounting نیاز نخواهد شد؛ زیرا متد ShippingAllowed در این مثال، فقط false بر می‌گرداند.
و از این نمونه‌ها زیاد هستند. کلاس‌هایی با تعداد متدهای بالا و تعداد وابستگی‌های قابل توجه که ممکن است در طول عمر شیء وهله سازی شده این کلاس، تنها به یکی از این وابستگی‌ها نیاز شود و نه به تمام آن‌ها.
راه حلی برای این مساله در دات نت 4 با معرفی کلاس Lazy ارائه شده است؛ به این نحو که اگر برای مثال در اینجا accounting را Lazy تعریف کنیم، تنها زمانی وهله سازی خواهد شد که به آن نیاز باشد و نه پیش از آن.
 private readonly Lazy<IAccounting> _accounting;

سؤال: Lazy loading تزریق وابستگی‌ها را چگونه می‌توان توسط StructureMap فعال ساخت؟

ابتدا تعاریف کلاس  OrderHandlerرا به نحو زیر بازنویسی می‌کنیم:
using System;

namespace DI04.Services
{
    public class OrderHandlerLazy : IOrderHandler
    {
        private readonly Lazy<IAccounting> _accounting;
        private readonly Lazy<ISales> _sales;
        public OrderHandlerLazy(Lazy<IAccounting> accounting, Lazy<ISales> sales)
        {
            Console.WriteLine("OrderHandlerLazy ctor.");
            _accounting = accounting;
            _sales = sales;
        }

        public void Handle(int orderId, int count)
        {
            if (_sales.Value.ShippingAllowed(orderId))
            {
                _accounting.Value.CreateInvoice(orderId, count);
            }
        }
    }
}
در اینجا سازنده‌های کلاس، به صورت Lazy معرفی شده‌اند. دسترسی به فیلدهای sales و accounting نیز اندکی تغییر کرده‌اند و اینبار از طریق خاصیت Value آن‌ها باید انجام شود.
مرحله نهایی هم اندکی تغییر در نحوه معرفی تنظیمات اولیه StructureMap است:
using System;
using DI04.Services;
using StructureMap;

namespace DI04
{
    class Program
    {
        static void Main(string[] args)
        {
            // تنظیمات اولیه برنامه که فقط یکبار باید در طول عمر برنامه انجام شود
            ObjectFactory.Initialize(x =>
            {
                x.For<IOrderHandler>().Use<OrderHandlerLazy>();

                // Lazy loading
                x.For<Lazy<IAccounting>>().Use(c => new Lazy<IAccounting>(c.GetInstance<Accounting>));
                x.For<Lazy<ISales>>().Use(c => new Lazy<ISales>(c.GetInstance<Sales>));
            });

            var orderHandler = ObjectFactory.GetInstance<IOrderHandler>();
            orderHandler.Handle(orderId: 1, count: 10);
        }
    }
}
به این ترتیب زمانیکه برنامه به sales.Value می‌رسد آنگاه نیاز به وهله سازی شیء متناظر با آن‌را خواهد داشت که در اینجا از طریق متد GetInstance به آن ارسال خواهد گردید.

خروجی برنامه در این حالت:
OrderHandlerLazy ctor.
Sales ctor.
همانطور که مشاهده می‌کنید، هرچند کلاس OrderHandlerLazy دارای دو وابستگی تعریف شده در سازنده کلاس است، اما تنها وابستگی Sales آن زمانیکه به آن نیاز شده، وهله سازی گردیده است و خبری از وهله سازی کلاس Accounting نیست (چون مطابق تعاریف کلاس‌های برنامه هیچگاه به مسیر accounting.Value نخواهیم رسید؛ بنابراین نیازی هم به وهله سازی آن نخواهد بود).


دریافت مثال این قسمت
DI04.zip