بازخوردهای پروژه‌ها
کاهش رفت و برگشت به دیتابیس در حین ذخیره سازی اطلاعات
در حین لاگین شما SetAuthCookie رو به userName تنظیم کردید. بعد نتیجه‌اش این شده، هرجایی که نیاز به یافتن Id کاربر بوده، مجبور هستیم یکبار کوئری بگیریم و این مورد زیاد در برنامه وجود داره. بهتره در زمان فراخوانی SetAuthCookie، مقدار userId رو که داریم به عنوان پارامتر بهش پاس بدیم. به این ترتیب در خیلی از جاها جلو خواهیم افتاد.
مثلا الان اکثر کلاس‌های موجودیت‌ها چنین تعریفی رو دارند:
 public class SomeName
{
    public virtual User User { get; set; }
}
میشه کمی این تعریف رو اصلاح کرد و بجایش نوشت:
   [ForeignKey("UserId")]
  public virtual User User { set; get; }
  public int? UserId { set; get; }
حالا موقع ست کردن اطلاعات این کلاس، دیگر نیازی نیست تا یکبار id کاربر از دیتابیس Find شود (چون در User.Identity.Name موجود هست؛ مهم نیست که Name اینجا داره، مهم کاربرد آن است در برنامه). فقط کافی هست این مقدار به UserId نسبت داده شود.
تقریبا اکثر کلاس‌های دومین شما نکته کار با کلیدهای خارجی رو جهت کاهش رفت و برگشت به دیتابیس ندارند.
مطالب
کار با Docker بر روی ویندوز - قسمت چهارم - اجرای برنامه‌های خط فرمان درون Containerها
یکی از مزایای مهم کار با Docker، امکان اجرای برنامه‌ای، بدون اینکه بدانیم چگونه باید آن‌را نصب کرد، می‌باشد. در اینجا فقط کافی است دستور docker run را صادر کنیم و ... آن برنامه اجرا می‌شود. در این قسمت سعی خواهیم کرد تعدادی برنامه‌ی خط فرمان را توسط Docker اجرا کنیم تا بتوانیم با جزئیات بیشتری از آن آشنا شویم.


داخل یک Image چیست؟

در قسمت قبل دریافتیم که یک Image، از کل User Space مورد نیاز جهت اجرای یک برنامه تشکیل می‌شود. در ادامه می‌خواهیم بررسی کنیم، این ادعا تا چه حد صحت دارد و یک Image‌، واقعا از چه اجزائی تشکیل می‌شود.
با استفاده از دستور docker images می‌توان لیست imageهای دریافت شده را به همراه tagهای مرتبط با هر کدام، مشاهده کرد. سپس با استفاده از دستور docker save می‌توان image ای خاص را export کرد؛ برای مثال:
docker save microsoft/iis:nanoserver -o iis.tar
این دستور، image مربوط به iis با tag ای به نام nanoserver را در فایل c:\Users\Vahid\iis.tar ذخیره می‌کند.
البته ویندوز در حالت استاندارد آن، قابلیت کار با فایل‌های tar را ندارد. هرچند می‌توان از نرم افزارهای ثالثی مانند win-rar و یا 7zip برای انجام اینکار استفاده کرد، اما تمام Linux Containers، به همراه ابزار کمکی tar، برای استخراج محتوای این نوع فایل‌ها نیز هستند. بنابراین نیاز است از حالت Windows Containers به Linux Containers سوئیچ کرد. برای اینکار فقط کافی بر روی آیکن Docker در قسمت Tray Icons ویندوز کلیک راست نمود و گزینه‌ی Switch to Linux Containers را انتخاب کرد. اکنون اگر دستور docker images را صادر کنیم، تنها لیست imageهای لینوکسی از پیش دریافت شده را می‌توان مشاهده کرد.
در ادامه، image یکی از سبک‌ترین توزیع‌های لینوکسی را به نام alpine، دریافت می‌کنیم که فقط 5 مگابایت حجم دارد؛ چون آنچنان فایلی در user space آن وجود ندارد. به همین جهت برای دریافت آن، دستور docker pull alpine را صادر می‌کنیم. اکنون اگر دستور docker images را صادر کنیم، این image جدید در لیست خروجی آن قابل مشاهده‌است.
سپس چون می‌خواهیم یک shell را در داخل این container اجرا کنیم (مانند اجرای کنسول PowerShell قسمت قبل)، نیاز است دستور docker run آن‌را با سوئیچ it اجرا کنیم:
docker run -it alpine sh
در اینجا sh، همان shell داخل این کانتینر است. پس از اجرای این دستور، به command prompt آن منتقل می‌شویم. برای نمونه در اینجا دستور ps را صادر کنید تا بتوانید لیست پروسه‌های در حال اجرای آن‌را مشاهده کنید و یا دستور tar را صادر کنید؛ مشاهده خواهید کرد که این ابزار در داخل این توزیع لینوکسی وجود دارد.
اکنون می‌خواهیم به فایل iis.tar ای که export کردیم از اینجا دسترسی پیدا کنیم. این فایل نیز بر روی فایل سیستم ویندوز 10 تولید شده‌است. به همین جهت نیاز است بتوان به این فایل خارج از container جاری دسترسی پیدا کرد.


روش به اشتراک گذاری فایل سیستم میزبان با کانتینرها

برای اینکه از داخل یک container به فایلی در خارج از آن دسترسی پیدا کنیم، باید درایو آن فایل خارجی را اصطلاحا mount کرد؛ دقیقا مانند اتصال یک USB Stick به سیستم و اضافه شدن آن به صورت یک درایو جدید. در Docker، اینکار توسط مفهومی به نام Volumes انجام می‌شود. برای این منظور بر روی آیکن Docker در قسمت Tray Icons ویندوز کلیک راست کرده و گزینه‌ی Settings را انتخاب کنید. البته این تصویر را در حالت کار با Linux Containers مشاهده می‌کنید:


در اینجا درایو C را انتخاب و Apply کنید و سپس نام کاربری و کلمه‌ی عبور ویندوز خود را وارد نمائید، تا مجوز دسترسی به این درایو، به این Container داده شود.
سپس دستور زیر را اجرا کنید:
docker run --rm -v c:/Users:/data alpine ls /data
در این دستور:
- سوئیچ rm به این معنا است که Container، پس از پایان کار، به طور کامل حذف می‌شود.
- کار mount، با سوئیچ v که به معنای volume mount است، انجام می‌شود.
- عبارت c:/Users:/data به این معنا است که پوشه‌ی C:\users ویندوز را به مسیر data/ داخل container لینوکسی نگاشت کن. به همین جهت بین این دو قسمت یک : وجود دارد و مسیرهای لینوکسی فاقد نام درایو خاصی هستند.
در این دستور می‌توان پوشه‌ای خاص مانند c:/Users/Vahid:/data و یا فایلی خاص را مانند c:/Users/Vahid/iis.tar:/data نیز نگاشت کرد. مزیت آن کاهش سطح دسترسی Container به فایل‌های سیستم میزبان است.
- این دستور با اجرای دستور ls، محتوای پوشه‌ی C:\users را لیست می‌کند.


کار با فایل‌های سیستم میزبان از داخل یک Container

در ادامه دستور فوق را به صورت زیر اجرا می‌کنیم که در آن فایل iis.tar میزبان، در داخل container در مسیر data/ آن قابل دسترسی شده‌است و همچنین بجای ls، دستور sh صادر شده‌است تا به shell آن دسترسی پیدا کنیم. به همین جهت نیاز به سوئیچ it نیز وجود دارد:
docker run --rm -it -v c:/Users/vahid/iis.tar:/data alpine sh
اکنون که به shell دسترسی پیدا کرده‌ایم، از ابزار tar برای چاپ محتویات داخل فایل iis.tar استفاده می‌کنیم:
tar -tf /data/iis.tar


در اینجا حداقل هفت پوشه را مشاهده می‌کنید که داخل هر کدام فایلی به نام layer.tar قرار دارد؛ لایه‌هایی که این image را ایجاد می‌کنند.
پس از این، اگر دستور استخراج این فایل‌ها را صادر کنیم (t برای نمایش لیست محتویات است و x برای استخراج آن‌ها):
mkdir /data/extract
tar -xf /data/iis.tar -C /data/extract
این فایل‌ها در پوشه‌ی /data/extract/ استخراج خواهند شد.


انتقال فایل‌ها از Container به سیستم میزبان

البته نکته‌ی مهم اینجا است که این پوشه‌ی /data/extract/ صرفا داخل دیسک مجازی این container ایجاد شده‌است و در سمت ویندوز قابل دسترسی و مشاهده نیست.
برای رفع این مشکل، اینبار دستور tar -xf را به صورت زیر اجرا می‌کنیم:
 docker run --rm -it -v c:/Users/vahid/:/data alpine tar -xf /data/iis.tar -C /data/iis
در این دستور بجای صرفا نگاشت تک فایل c:/Users/vahid/iis.tar میزبان، کل پوشه‌ی c:/Users/vahid میزبان، به مسیر /data/ کانتینر نگاشت شده‌است. در ادامه پس از ذکر نام image، اصل فرمان را مستقیما صادر کرده‌ایم. درست مانند اینکه یک فرمان لینوکسی را مستقیما داخل ویندوز صادر کرده باشیم؛ بدون اینکه نیازی باشد به shell آن توزیع لینوکسی مراجعه کنیم. در این حالت اگر در مسیر c:/users/vahid، پوشه‌ی جدید iis را ایجاد کنیم (در سمت میزبان ویندوزی) و سپس دستور فوق را اجرا کنیم، این فایل‌ها در پوشه‌ی c:\users\vahid\iis نیز ظاهر می‌شوند و دقیقا مانند این است که پوشه‌ی /data/ کانتینر، با میزبان ویندوزی به اشتراک گذاشته شده‌است.
در اینجا اگر دستور tar را بر روی این لایه‌ها اجرا کنیم، در نهایت به یک چنین خروجی‌هایی خواهیم رسید:



که بسیار شبیه به درایو C یک سیستم ویندوزی است. بنابراین این لایه، همان OS Apps & Libs را به همراه دارد که در قسمت قبل با آن آشنا شدیم.


یک مثال دیگر: اجرای ffmpeg توسط docker

اگر خاطرتان باشد بحث docker را در قسمت اول با معرفی ffmpeg و سخت بودن اجرای آن آغاز کردیم. در ادامه می‌خواهیم image آن‌را دریافت و سپس با تبدیل آن به یک container، کار تبدیل فرمت‌های ویدیویی را انجام دهیم:
 docker run --rm --volume ${pwd}:/output jrottenberg/ffmpeg -i http://site.com/file.mp4 /output/file.gif
در این دستور:
- سوئیچ rm کار حذف container ایجاد شده را پس از اتمام کار آن انجام می‌دهد.
- سپس مسیرجاری (پوشه‌ی جاری در سیستم میزبان که دستور فوق را اجرا می‌کند) به مسیر output/ کانتینر نگاشت شده‌است.
- در ادامه مخزن dockerhub ای که ffmpeg قرار است از آن دریافت و اجرا شود، مشخص شده‌است.
- در آخر پارامترهای کار با ffmpeg برای دریافت یک فایل mp4 آنلاین و تبدیل آن به یک فایل animated gif محلی، ذکر شده‌اند. این فایل در مسیر output کانتینر ایجاد می‌شود که در نهایت با میزبان، به اشتراک گذاشته خواهد شد.
- خروجی نهایی تولید شده را در سیستم میزبان در همان پوشه‌ای که دستور فوق را صادر کرده‌اید، مشاهده خواهید کرد.
مطالب
فعال‌سازی Multiple Active Result Sets
(Multiple Active Result Sets (MARS یکی از قابلیتهای SQL SERVER است. این قابلیت در واقع این امکان را برای ما فراهم می‌کند تا بر روی یک Connection همزمان چندین کوئری را به صورت موازی ارسال کنیم. در این حالت برای هر کوئری یک سشن مجزا در نظر گرفته می‌شود. 
مدل:
namespace EnablingMARS.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Desc { get; set; }
        public float Price { get; set; }
        public Category Category { get; set; }

    }

    public enum Category
    {
        Cate1,
        Cate2,
        Cate3
    }
}
کلاس Context:
namespace EnablingMARS.Models
{
    public class ProductDbContext : DbContext
    {
        public ProductDbContext() : base("EnablingMARS") {}
        public DbSet<Product> Products { get; set; }

    }
}
ابتدا یک سطر جدید را توسط کد زیر به دیتابیس اضافه می‌کنیم:
MyContext.Products.Add(new Product()
 {
                Title = "title1",
                Desc = "desc",
                Price = 4500f,
                Category = Category.Cate1
   });
MyContext.SaveChanges();
اکنون می‌خواهیم قیمت محصولاتی را که در دسته‌بندی Cate1 قرار دارند، تغییر دهیم:
foreach (var product in _dvContext.Products.Where(category => category.Category == Category.Cate1))
{
     product.Price = 50000;
     MyContext.SaveChanges();
}
خوب؛ اکنون اگر برنامه را اجرا کنیم با خطای زیر مواجه می‌شویم:
There is already an open DataReader associated with this Command which must be closed first.
این استثناء زمانی اتفاق می‌افتد که بر روی نتایج حاصل از یک کوئری، یک کوئری دیگر را ارسال کنیم. البته استثنای صادر شده بستگی به کوئری دوم شما دارد ولی در حالت کلی و با مشاهده Stack Trace، پیام فوق نمایش داده می‌شود. همانطور که در کد بالا ملاحظه می‌کنید درون حلقه‌ی forach ما به پراپرتی Price دسترسی پیدا کرده‌ایم، در حالیکه کوئری اصلی ما هنوز فعال (Active) است. MARS در اینجا به ما کمک می‌کند که بر روی یک Connection، بیشتر از یک کوئری فعال داشته باشیم. در حالت عادی Entity Framework Code First این ویژگی را به صورت پیش‌فرض برای ما فعال نمی‌کند. اما اگر خودمان کانکشن‌استرینگ را اصلاح کنیم، این ویژگی SQL SERVER فعال می‌گردد. برای حل این مشکل کافی است به کانکشن‌استرینگ، MultipleActiveResultSets=true را اضافه کنیم:
"Data Source=(LocalDB)\v11.0;Initial Catalog=EnablingMARS; MultipleActiveResultSets=true"
لازم به ذکر است که این قابلیت از نسخه SQL SERVER 2005 به بالا در دسترس می‌باشد. همچنین در هنگام استفاده از این قابلیت می‌بایستی موارد زیر را در نظر داشته باشید:
  • وقتی کانکشنی در حالت MARS برقرار می‌شود، یک سشن نیز همراه با یکسری اطلاعات اضافی برای آن ایجاد شده که باعث ایجاد Overhead خواهد شد.
  • دستورات مارس  thread-safe  نیستند.
مطالب
‫دریافت کل یک مخزن SVN به کمک برنامه نویسی

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

using SharpSvn;
...
using (var sc = new SvnClient())
{
var target = SvnTarget.FromUri(new Uri("http://someproject.googlecode.com/svn/trunk/"));
var finalSaveToDir = "somepath ..."; //Note: this path should not exist
sc.Export(target, finalSaveToDir);
}

نمونه‌ای از کاربردها:
- راه اندازی یک سایت برای دریافت ساده‌تر مخازن کد برای مثال Google-code یا source forge و امثال آن.

بازخوردهای دوره
ارتباطات بلادرنگ و SignalR
در اینجا
- در سمت کلاینت فایروال مانعی نخواهد بود چون ارتباطات از طریق مرورگر (هم می‌تواند) انجام می‌شود.
- باز هم نهایتا از سوکت‌ها استفاده خواهد شد اما در سطحی بالاتر و بدون درگیری با جزئیات آن‌ها. اینبار یک فریم ورک آماده، تست شده و تهیه شده برفراز سوکت‌های دات نت و ویندوز در اختیار شما است. به علاوه این فریم ورک فراتر است از صرفا برقراری ارتباط و ارسال داده، بلکه حالت امکان اجرای متدهای خاصی در سمت کلاینت یا سرور را هم دارا است (بحث قسمت بعد).
- تنوع کلاینت‌ها. محدود به یک برنامه ویندوزی نخواهید بود. مثلا امکان استفاده از یک کلاینت jQuery، که برای اجرا، نیازی به سطح دسترسی خاصی ندارد، یا حتی یک کلاینت سیلورلایت یا اندروید و غیره هم برای آن تهیه کرده‌اند.
- امکان استفاده از IIS به عنوان سرور. همین مساله یعنی درگیر نشدن با مسایلی مانند مقیاس پذیری، مدیریت تعداد کانکشن‌های بالا و امثال آن.
- امکان یکپارچه کردن یک برنامه سرویس دهنده هاب با یک برنامه ASP.NET در کنار هم در یک پروژه.
و ...
مطالب
Functional Programming - قسمت چهارم - برخورد با Exception ها
چنانچه قسمت‌های قبلی سری آموزش برنامه نویسی تابعی Functional Programming را مطالعه نکرده‌اید، پیشنهاد میکنم قبلا آن‌ها را  (+  و  +  و  +) قبل از شروع بخوانید. در این قسمت قرار است تاثیر استثناءها (exception) را بر روی کدها بررسی کرده و راهکاری را از جنس functional برایش ارائه کنیم. 



Exception و خوانایی کد

تکه کد زیر را در نظر بگیرید: یک Action معمولی در Asp.Net MVC که یک نام را دریافت کرده و یک کارمندرا ایجاد میکند:

public ActionResult CreateEmployee(string name) { 
    try { 
        ValidateName(name);
        // ادامه کد‌ها return View("با موفقیت ثبت شد");
        }
    catch (ValidationException ex) 
    { 
        return View("خطا", ex.Message);
    }
}

private void ValidateName(string name) { 
    if (string.IsNullOrWhiteSpace(name)) 
        throw new ValidationException("نام نمی‌تواند خالی باشد");

    if (name.Length > 100) 
        throw new ValidationException("نام نمی‌تواند طولانی باشد");
}

در این قطعه کد، در متد ValidateName، در صورت معتبر نبودن ورودی، یک Exception رخ میدهد و بلاک کد try/catch، این exception را دریافت کرده و خطای مناسبی را به کاربر نشان خواهد داد. تا اینجا ظاهرا همه چیز مرتب است و مشکلی ندارد! احتمالا کد‌های مشابه به این کد را زیاد دیده‌اید. در اینجا متد ValidateName، صادق نیست. در قسمت اول، در مورد Honesty صحبت کردیم. به عبارت ساده‌تر شما از امضای این متد نمی‌توانید به نوع خروجی و کاری که قرار است انجام دهد، پی ببرید. در واقع شما همیشه باید پیاده سازی متد را گوشه‌ای، در ذهن خود داشته باشید و برای اطمینان از کاری که متد انجام میدهد، همیشه باید به بدنه‌ی متد برگردیم. اگر به‌خاطر داشته باشید، توابع برنامه نویسی را به توابع ریاضی تشبیه کردیم. پس میتوانیم بگوییم: 

به عبارت دیگر وقتی از exception‌ها برای کنترل flow برنامه استفاده میکنید، مشابه کاری را انجام می‌دهید که دستور GOTO انجام می‌داد. این دستور در روش‌های قبل از برنامه نویسی ساخت یافته وجود داشت و توسط یک دانشمند هلندی به نام آقای دیکسترا حذف شد. وقتی از دستور GOTO یا JUMP استفاده میکنیم، فهمیدن flow برنامه پیچیدگی‌های زیادی را خواهد داشت. چراکه فراخوانی قطعه‌های کد و متد‌ها، وابستگی شدیدی خواهند داشت و البته میتوان گفت استفاده از exception‌ها برای کنترل جریان برنامه، می‌توانند از GOTO هم بد‌تر باشند؛ چرا که exception میتواند از لایه‌های مختلف کد نیز عبور کند.

امیدوارم تا اینجا به یک عقیده‌ی مشترک رسیده باشیم. خوب راهکار چیست؟ تصور کنید که تکه کد بالا را به صورت زیر تبدیل کنیم: 

public ActionResult CreateEmployee(string name) { 
    string error = ValidateName(name);

 if (error != string.Empty) 
        return View("خطا", error);
    // ادامه کد‌ها return View("با موفقیت ثبت شد");
}

private string ValidateName(string name) { 

    if (string.IsNullOrWhiteSpace(name)) 
        return "نام نمی‌تواند خالی باشد";

    if (name.Length > 100) 
        return "طول نام نمی‌تواند بیشتر از 100 کاراکتر باشد";

    return string.Empty;
}

با refactor ای که انجام دادیم، متد ValidateName را به یک تابع ریاضی تبدیل کردیم. به این معنا که هر آنچه را که از امضای متد، مشخص است، انجام می‌دهد و در این حالت چیزی مخفی نیست. توجه داشته باشید که این راهکار نهایی ما نیست و لطفا مقاله را تا انتها بخوانید!  



موارد استفاده Exception

با همه‌ی بدی‌هایی که از Exception‌ها گفتیم، با این حساب پس چه زمانی از آن استفاده کنیم؟

  1. Exception‌ها واقعا برای موارد استثنائی هستند.
  2. Exception‌ها برای شرایطی هستند که به معنای واقعی یک باگ باشند.
  3. منتظر رخ دادن Exception نباشیم! 

در توضیح مورد سوم، در اعتبار سنجی داده‌های کاربر (Validation) انتظار داده‌ی نادرستی را می‌توان داشت، پس نمی‌توانیم آن را یک حالت استثنایی بدانیم. معماری زیر را در نظر بگیرید


دیتایی که به API ما ارسال خواهد شد، همیشه شامل عملیات Filter یا به عبارتی Validation است و از آنجایی که می‌توان انتظار استفاده‌ی نادرست یا دیتای نادرست را داشت، نمیتوانیم این را حالتی از استثنائات در نظر بگیریم؛ ولی بر خلاف آن، وقتی در دامین پروژه و ارتباط بین دامین‌های مختلف، دیتایی رد و بدل می‌شود که معتبر نیست، میتوانیم آن را جزء استثناء‌ها در نظر بگیریم. به مثال زیر دقت کنید:

public ActionResult UpdateEmployee(int employeeId, string name) { 
    string error = ValidateName(name);
    
    if (error != string.Empty) 
        return View("Error", error);
    
    Employee employee = GetEmployee(employeeId); 
    employee.UpdateName(name);
}

public class Employee { 

    public void UpdateName(string name){

        if (name == null) 
            throw new ArgumentNullException();
        
        // ادامه کد‌ها }
}

در قطعه کد بالا تصور این است که کلاس Employee و متد UpdateName خارج از دامین می‌باشند. همانطورکه مشاهده میکنید، ما در action controller، از خالی نبودن نام اطمینان حاصل کردیم و سپس آن را به متد UpdateName ارجاع دادیم. ولی اگه به بدنه‌ی متد UpdateName دقت کنید، می‌بینید که مجددا از خالی نبودن نام اطمینان حاصل کرده‌ایم و در صورت خالی بودن، یک Exception را صادر میکنیم! به این مدل چک کردن‌ها در دامین‌های مختلف، معمولا guard clause گفته می‌شود و یک نوع قرارداد بین برنامه نویس هاست. اگر طبق تعریفی که بالاتر ارائه کردیم هم چک کنیم، میتوانیم حدس بزنیم که خالی بودن نام، نشان یک باگ در نرم افزار است! 



مفهوم fail fast

تا اینجا متوجه شدیم که از exception‌ها باید در شرایط استثنائی استفاده کنیم. خوب با توجه به این مساله، چه طور میتوانیم آن‌ها را Handle کنیم؟ این سؤال ما را به مفهومی به نام fail fast می‌رساند. این مفهوم به ما میگوید:

  • کار جاری را به محض یک اتفاق استثنائی باید متوقف کنیم.
  • رعایت این نکته در نهایت ما را به یک نرم افزار پایدار خواهد رساند.


برای درک هر چه بهتر این موضوع، بیایید به عکس این حالت نگاه کنیم؛ اصطلاحا Fail Silently.

متد زیر را ببینید: 

public void ProcessItems(List<Item> items) { 

    foreach (Item item in items) { 
        try { 
            Process(item);
 } 
        catch (Exception ex) 
        { 
            Logger.Log(ex);
 }
 }
 }

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

به صور خلاصه مهم‌ترین مزیت Fail Fast را میتوانیم به صورت زیر خلاصه کنیم:

  • مسیر رسیدن به خطا‌ها سر راست‌تر می‌شود.
  • نرم افزار به پایداری مناسبی خواهد رسید.
  • از اعتبار دیتای ذخیره شده اطمینان خواهیم داشت.


کجا exception‌ها را به دام بیندازیم؟

در یکی از حالت‌های زیر:

  • لاگ کردن
  • متوقف کردن عملیات
  • هیچ گاه در بلاک catch هیچ منطقی را پیاده نکنید.


حالت دیگر در استفاه از کتابخانه‌های دیگران (3rd parties) است. به طور مثال در استفاده از EF ممکن است به دلیل عدم برقراری ارتباط با دیتابیس، خطایی را دریافت کنید. در این حالت با توجه به نکات فوق، با این استثنائات برخورد کنید:

  • جلوی این نوع استثنائات را در پایین‌ترین حد ممکن در کد خود بگیرید.
  • Exception هایی را catch کنید که میدانید در حالت استثناء، چه کاری را می‌توانید انجام دهید.


این به این معنی میباشد که به صورت کلی همه نوع Exception ای را به صورت کلی نگیرید و نوع Exception اختصاصی را در بلاک catch قرار دهید. الان که قرار شد در بعضی از حالت‌ها جلوی استثنائات را بگیریم، خوب است ببینیم چطور باید اینکار را انجام بدیم.

قطعه کد زیر را در نظر بگیرید:

public void CreateCustomer(string name) { 
    Customer customer = new Customer(name); 
    bool result = SaveCustomer(customer);
    if (!result) { 
        MessageBox.Show("Error connecting to the database. Please try again later.");
    }
}

private bool SaveCustomer(Customer customer) { 
    try { 
        using (MyContext context = new MyContext()) { 
            context.Customers.Add(customer);
         context.SaveChanges();
        } 
        return true;
    }
    catch (DbUpdateException ex) { 
        if (ex.Message == "Unable to open the DB connection") 
            return false; 
        else 
            throw;
    }
}

همانطور که مشاهده میکنید، در حالتیکه خطایی از نوع DbUpdateException رخ میدهد، مقدار بازگشتی متد را برابر با false میکنیم. اما مشکلی که وجود دارد این است که این‌کار به اندازه‌ی کافی خوانا نیست. همچنین honest بودن متد را نقض کرده‌ایم. به علاوه مشکل بزرگتر دیگر این است که ما با بازگرداندن یک مقدار bool، میتوانیم به متد بالاتر اطلاع بدهیم که کار مورد نظر انجام شده یا نه، اما در مورد دلیل انجام نشدن آن، هیچ کاری نمیتوانیم بکنیم. پیشنهاد من برای مقدار بازگشتی متد‌هایی که احتمال انجام نشدن کاری در آن‌ها می‌رود، استفاده از یک نوع اختصاصی می‌باشد.

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

  • Honest بودن متد را نگه دارد.
  • خروجی متد را به همراه وضعیت اجرا شدن برگرداند.
  • شکل یکسانی را برای خطا‌ها داشته باشد.
  • فقط جلوی خطا‌های غیر منتظره را بگیرد.


برای مثال کد بالا را به شکل زیر refactor می‌کنیم:

private Result SaveCustomer(Customer customer) { 
    try { 

        using (var context = new MyContext()) { 

            context.Customers.Add(customer); 
            context.SaveChanges();
 } 

        return Result.Ok();
    } 
    catch (DbUpdateException ex) { 
        if (ex.Message == "Unable to open the DB connection") 
            Result.Fail(ErrorType.DatabaseIsOffline);

        if (ex.Message.Contains("IX_Customer_Name")) 
            return Result.Fail(ErrorType.CustomerAlreadyExists);

        throw;
    }
}

به عبارتی با این روش میتوانیم از انجام شدن/نشدن عملیات اطمینان حاصل کنیم و خروجی/دلیل انجام نشدن را نیز میتوانیم برگردانیم.

اگر به امضای متد‌های زیر نگاه کنیم، می‌توانیم آن‌ها را طبق الگوی CQS دسته‌بندی کنیم: 

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

مطالب
Identity 2.0 : تایید حساب های کاربری و احراز هویت دو مرحله ای
در پست قبلی نگاهی اجمالی به انتشار نسخه جدید Identity Framework داشتیم. نسخه جدید تغییرات چشمگیری را در فریم ورک بوجود آورده و قابلیت‌های جدیدی نیز عرضه شده‌اند. دو مورد از این قابلیت‌ها که پیشتر بسیار درخواست شده بود، تایید حساب‌های کاربری (Account Validation) و احراز هویت دو مرحله ای (Two-Factor Authorization) بود. در این پست راه اندازی این دو قابلیت را بررسی می‌کنیم.

تیم ASP.NET Identity پروژه نمونه ای را فراهم کرده است که می‌تواند بعنوان نقطه شروعی برای اپلیکیشن‌های MVC استفاده شود. پیکربندی‌های لازم در این پروژه انجام شده‌اند و برای استفاده از فریم ورک جدید آماده است.


شروع به کار : پروژه نمونه را توسط NuGet ایجاد کنید

برای شروع یک پروژه ASP.NET خالی ایجاد کنید (در دیالوگ قالب‌ها گزینه Empty را انتخاب کنید). سپس کنسول Package Manager را باز کرده و دستور زیر را اجرا کنید.

PM> Install-Package Microsoft.AspNet.Identity.Samples -Pre

پس از اینکه NuGet کارش را به اتمام رساند باید پروژه ای با ساختار متداول پروژه‌های ASP.NET MVC داشته باشید. به تصویر زیر دقت کنید.


همانطور که می‌بینید ساختار پروژه بسیار مشابه پروژه‌های معمول MVC است، اما آیتم‌های جدیدی نیز وجود دارند. فعلا تمرکز اصلی ما روی فایل IdentityConfig.cs است که در پوشه App_Start قرار دارد.

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

public class EmailService : IIdentityMessageService
{
    public Task SendAsync(IdentityMessage message)
    {
        // Plug in your email service here to send an email.
        return Task.FromResult(0);
    }
}

public class SmsService : IIdentityMessageService
{
    public Task SendAsync(IdentityMessage message)
    {
        // Plug in your sms service here to send a text message.
        return Task.FromResult(0);
    }
}

اگر دقت کنید هر دو کلاس قرارداد IIdentityMessageService را پیاده سازی می‌کنند. می‌توانید از این قرارداد برای پیاده سازی سرویس‌های اطلاع رسانی ایمیلی، پیامکی و غیره استفاده کنید. در ادامه خواهیم دید چگونه این دو سرویس را بسط دهیم.


یک حساب کاربری مدیریتی پیش فرض ایجاد کنید

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

public class ApplicationDbInitializer 
    : DropCreateDatabaseIfModelChanges<ApplicationDbContext> 
{
    protected override void Seed(ApplicationDbContext context) {
        InitializeIdentityForEF(context);
        base.Seed(context);
    }

    //Create User=Admin@Admin.com with password=Admin@123456 in the Admin role        
    public static void InitializeIdentityForEF(ApplicationDbContext db) 
    {
        var userManager = 
           HttpContext.Current.GetOwinContext().GetUserManager<ApplicationUserManager>();
        
        var roleManager = 
            HttpContext.Current.GetOwinContext().Get<ApplicationRoleManager>();

        const string name = "admin@admin.com";
        const string password = "Admin@123456";
        const string roleName = "Admin";

        //Create Role Admin if it does not exist
        var role = roleManager.FindByName(roleName);

        if (role == null) {
            role = new IdentityRole(roleName);
            var roleresult = roleManager.Create(role);
        }

        var user = userManager.FindByName(name);

        if (user == null) {
            user = new ApplicationUser { UserName = name, Email = name };
            var result = userManager.Create(user, password);
            result = userManager.SetLockoutEnabled(user.Id, false);
        }

        // Add user admin to Role Admin if not already added
        var rolesForUser = userManager.GetRoles(user.Id);

        if (!rolesForUser.Contains(role.Name)) {
            var result = userManager.AddToRole(user.Id, role.Name);
        }
    }
}
همانطور که می‌بینید این قطعه کد ابتدا نقشی بنام Admin می‌سازد. سپس حساب کاربری ای، با اطلاعاتی پیش فرض ایجاد شده و بدین نقش منتسب می‌گردد. اطلاعات کاربر را به دلخواه تغییر دهید و ترجیحا از یک آدرس ایمیل زنده برای آن استفاده کنید.


تایید حساب‌های کاربری : چگونه کار می‌کند

بدون شک با تایید حساب‌های کاربری توسط ایمیل آشنا هستید. حساب کاربری ای ایجاد می‌کنید و ایمیلی به آدرس شما ارسال می‌شود که حاوی لینک فعالسازی است. با کلیک کردن این لینک حساب کاربری شما تایید شده و می‌توانید به سایت وارد شوید.

اگر به کنترلر AccountController در این پروژه نمونه مراجعه کنید متد Register را مانند لیست زیر می‌یابید.

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
    if (ModelState.IsValid)
    {
        var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
        var result = await UserManager.CreateAsync(user, model.Password);

        if (result.Succeeded)
        {
            var code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);

            var callbackUrl = Url.Action(
                "ConfirmEmail", 
                "Account", 
                new { userId = user.Id, code = code }, 
                protocol: Request.Url.Scheme);

            await UserManager.SendEmailAsync(
                user.Id, 
                "Confirm your account", 
                "Please confirm your account by clicking this link: <a href=\"" 
                + callbackUrl + "\">link</a>");

            ViewBag.Link = callbackUrl;

            return View("DisplayEmail");
        }
        AddErrors(result);
    }

    // If we got this far, something failed, redisplay form
    return View(model);
}
اگر به قطعه کد بالا دقت کنید فراخوانی متد UserManager.SendEmailAsync را می‌یابید که آرگومانهایی را به آن پاس می‌دهیم. در کنترلر جاری، آبجکت UserManager یک خاصیت (Property) است که وهله ای از نوع ApplicationUserManager را باز می‌گرداند. اگر به فایل IdentityConfig.cs مراجعه کنید تعاریف این کلاس را خواهید یافت. در این کلاس، متد استاتیک ()Create وهله ای از ApplicationUserManager را می‌سازد که در همین مرحله سرویس‌های پیام رسانی پیکربندی می‌شوند.

public static ApplicationUserManager Create(
    IdentityFactoryOptions<ApplicationUserManager> options, 
    IOwinContext context)
{
    var manager = new ApplicationUserManager(
        new UserStore<ApplicationUser>(
            context.Get<ApplicationDbContext>()));

    // Configure validation logic for usernames
    manager.UserValidator = new UserValidator<ApplicationUser>(manager)
    {
        AllowOnlyAlphanumericUserNames = false,
        RequireUniqueEmail = true
    };

    // Configure validation logic for passwords
    manager.PasswordValidator = new PasswordValidator
    {
        RequiredLength = 6, 
        RequireNonLetterOrDigit = true,
        RequireDigit = true,
        RequireLowercase = true,
        RequireUppercase = true,
    };

    // Configure user lockout defaults
    manager.UserLockoutEnabledByDefault = true;
    manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5);
    manager.MaxFailedAccessAttemptsBeforeLockout = 5;

    // Register two factor authentication providers. This application 
    // uses Phone and Emails as a step of receiving a code for verifying the user
    // You can write your own provider and plug in here.
    manager.RegisterTwoFactorProvider(
        "PhoneCode", 
        new PhoneNumberTokenProvider<ApplicationUser>
    {
        MessageFormat = "Your security code is: {0}"
    });

    manager.RegisterTwoFactorProvider(
        "EmailCode", 
        new EmailTokenProvider<ApplicationUser>
    {
        Subject = "SecurityCode",
        BodyFormat = "Your security code is {0}"
    });

    manager.EmailService = new EmailService();
    manager.SmsService = new SmsService();

    var dataProtectionProvider = options.DataProtectionProvider;

    if (dataProtectionProvider != null)
    {
        manager.UserTokenProvider = 
            new DataProtectorTokenProvider<ApplicationUser>(
                dataProtectionProvider.Create("ASP.NET Identity"));
    }

    return manager;
}

در قطعه کد بالا کلاس‌های EmailService و SmsService روی وهله ApplicationUserManager تنظیم می‌شوند.

manager.EmailService = new EmailService();
manager.SmsService = new SmsService();

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

// Register two factor authentication providers. This application 
// uses Phone and Emails as a step of receiving a code for verifying the user
// You can write your own provider and plug in here.
manager.RegisterTwoFactorProvider(
    "PhoneCode", 
    new PhoneNumberTokenProvider<ApplicationUser>
{
    MessageFormat = "Your security code is: {0}"
});

manager.RegisterTwoFactorProvider(
    "EmailCode", 
    new EmailTokenProvider<ApplicationUser>
    {
        Subject = "SecurityCode",
        BodyFormat = "Your security code is {0}"
    });

تایید حساب‌های کاربری توسط ایمیل و احراز هویت دو مرحله ای توسط ایمیل و/یا پیامک نیاز به پیاده سازی هایی معتبر از قراردارد IIdentityMessageService دارند.

پیاده سازی سرویس ایمیل توسط ایمیل خودتان

پیاده سازی سرویس ایمیل نسبتا کار ساده ای است. برای ارسال ایمیل‌ها می‌توانید از اکانت ایمیل خود و یا سرویس هایی مانند SendGrid استفاده کنید. بعنوان مثال اگر بخواهیم سرویس ایمیل را طوری پیکربندی کنیم که از یک حساب کاربری Outlook استفاده کند، مانند زیر عمل خواهیم کرد.

public class EmailService : IIdentityMessageService
{
    public Task SendAsync(IdentityMessage message)
    {
        // Credentials:
        var credentialUserName = "yourAccount@outlook.com";
        var sentFrom = "yourAccount@outlook.com";
        var pwd = "yourApssword";

        // Configure the client:
        System.Net.Mail.SmtpClient client = 
            new System.Net.Mail.SmtpClient("smtp-mail.outlook.com");

        client.Port = 587;
        client.DeliveryMethod = System.Net.Mail.SmtpDeliveryMethod.Network;
        client.UseDefaultCredentials = false;

        // Creatte the credentials:
        System.Net.NetworkCredential credentials = 
            new System.Net.NetworkCredential(credentialUserName, pwd);

        client.EnableSsl = true;
        client.Credentials = credentials;

        // Create the message:
        var mail = 
            new System.Net.Mail.MailMessage(sentFrom, message.Destination);

        mail.Subject = message.Subject;
        mail.Body = message.Body;

        // Send:
        return client.SendMailAsync(mail);
    }
}
تنظیمات SMTP میزبان شما ممکن است متفاوت باشد اما مطمئنا می‌توانید مستندات لازم را پیدا کنید. اما در کل رویکرد مشابهی خواهید داشت.


پیاده سازی سرویس ایمیل با استفاده از SendGrid

سرویس‌های ایمیل متعددی وجود دارند اما یکی از گزینه‌های محبوب در جامعه دات نت SendGrid است. این سرویس API قدرتمندی برای زبان‌های برنامه نویسی مختلف فراهم کرده است. همچنین یک Web API مبتنی بر HTTP نیز در دسترس است. قابلیت دیگر اینکه این سرویس مستقیما با Windows Azure یکپارچه می‌شود.

می توانید در سایت SendGrid یک حساب کاربری رایگان بعنوان توسعه دهنده بسازید. پس از آن پیکربندی سرویس ایمیل با مرحله قبل تفاوت چندانی نخواهد داشت. پس از ایجاد حساب کاربری توسط تیم پشتیبانی SendGrid با شما تماس گرفته خواهد شد تا از صحت اطلاعات شما اطمینان حاصل شود. برای اینکار چند گزینه در اختیار دارید که بهترین آنها ایجاد یک اکانت ایمیل در دامنه وب سایتتان است. مثلا اگر هنگام ثبت نام آدرس وب سایت خود را www.yourwebsite.com وارد کرده باشید، باید ایمیلی مانند info@yourwebsite.com ایجاد کنید و توسط ایمیل فعالسازی آن را تایید کند تا تیم پشتیبانی مطمئن شود صاحب امتیاز این دامنه خودتان هستید.

تنها چیزی که در قطعه کد بالا باید تغییر کند اطلاعات حساب کاربری و تنظیمات SMTP است. توجه داشته باشید که نام کاربری و آدرس فرستنده در اینجا متفاوت هستند. در واقع می‌توانید از هر آدرسی بعنوان آدرس فرستنده استفاده کنید.

public class EmailService : IIdentityMessageService
{
    public Task SendAsync(IdentityMessage message)
    {
        // Credentials:
        var sendGridUserName = "yourSendGridUserName";
        var sentFrom = "whateverEmailAdressYouWant";
        var sendGridPassword = "YourSendGridPassword";

        // Configure the client:
        var client = 
            new System.Net.Mail.SmtpClient("smtp.sendgrid.net", Convert.ToInt32(587));

        client.Port = 587;
        client.DeliveryMethod = System.Net.Mail.SmtpDeliveryMethod.Network;
        client.UseDefaultCredentials = false;

        // Creatte the credentials:
        System.Net.NetworkCredential credentials = 
            new System.Net.NetworkCredential(credentialUserName, pwd);

        client.EnableSsl = true;
        client.Credentials = credentials;

        // Create the message:
        var mail = 
            new System.Net.Mail.MailMessage(sentFrom, message.Destination);

        mail.Subject = message.Subject;
        mail.Body = message.Body;

        // Send:
        return client.SendMailAsync(mail);
    }
}
حال می‌توانیم سرویس ایمیل را تست کنیم.


آزمایش تایید حساب‌های کاربری توسط سرویس ایمیل

ابتدا اپلیکیشن را اجرا کنید و سعی کنید یک حساب کاربری جدید ثبت کنید. دقت کنید که از آدرس ایمیلی زنده که به آن دسترسی دارید استفاده کنید. اگر همه چیز بدرستی کار کند باید به صفحه ای مانند تصویر زیر هدایت شوید.

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

پیاده سازی سرویس SMS

برای استفاده از احراز هویت دو مرحله ای پیامکی نیاز به یک فراهم کننده SMS دارید، مانند Twilio . مانند SendGrid این سرویس نیز در جامعه دات نت بسیار محبوب است و یک C# API قدرتمند ارائه می‌کند. می‌توانید حساب کاربری رایگانی بسازید و شروع به کار کنید.

پس از ایجاد حساب کاربری یک شماره SMS، یک شناسه SID و یک شناسه Auth Token به شما داده می‌شود. شماره پیامکی خود را می‌توانید پس از ورود به سایت و پیمایش به صفحه Numbers مشاهده کنید.

شناسه‌های SID و Auth Token نیز در صفحه Dashboard قابل مشاهده هستند.

اگر دقت کنید کنار شناسه Auth Token یک آیکون قفل وجود دارد که با کلیک کردن روی آن شناسه مورد نظر نمایان می‌شود.

حال می‌توانید از سرویس Twilio در اپلیکیشن خود استفاده کنید. ابتدا بسته NuGet مورد نیاز را نصب کنید.

PM> Install-Package Twilio
پس از آن فضای نام Twilio را به بالای فایل IdentityConfig.cs اضافه کنید و سرویس پیامک را پیاده سازی کنید.

public class SmsService : IIdentityMessageService
{
    public Task SendAsync(IdentityMessage message)
    {
        string AccountSid = "YourTwilioAccountSID";
        string AuthToken = "YourTwilioAuthToken";
        string twilioPhoneNumber = "YourTwilioPhoneNumber";

        var twilio = new TwilioRestClient(AccountSid, AuthToken);

        twilio.SendSmsMessage(twilioPhoneNumber, message.Destination, message.Body); 

        // Twilio does not return an async Task, so we need this:
        return Task.FromResult(0);
    }
}

حال که سرویس‌های ایمیل و پیامک را در اختیار داریم می‌توانیم احراز هویت دو مرحله ای را تست کنیم.


آزمایش احراز هویت دو مرحله ای

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

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

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

پس از اینکه گزینه خود را انتخاب کردید، کد احراز هویت دو مرحله ای برای شما ارسال می‌شود که توسط آن می‌توانید پروسه ورود به سایت را تکمیل کنید.

حذف میانبرهای آزمایشی

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

بدین منظور باید نماها و کدهای مربوطه را ویرایش کنیم تا اینگونه اطلاعات به کلاینت ارسال نشوند. اگر کنترلر AccountController را باز کنید و به متد ()Register بروید با کد زیر مواجه خواهید شد.

if (result.Succeeded)
{
    var code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
    var callbackUrl = 
        Url.Action("ConfirmEmail", "Account", 
            new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);

    await UserManager.SendEmailAsync(user.Id, "Confirm your account", 
        "Please confirm your account by clicking this link: <a href=\"" + callbackUrl + "\">link</a>");

    // This should not be deployed in production:
    ViewBag.Link = callbackUrl;

    return View("DisplayEmail");
}

AddErrors(result);
همانطور که می‌بینید پیش از بازگشت از این متد، متغیر callbackUrl به ViewBag اضافه می‌شود. این خط را Comment کنید یا به کلی حذف نمایید.

نمایی که این متد باز می‌گرداند یعنی DisplayEmail.cshtml نیز باید ویرایش شود.

@{
    ViewBag.Title = "DEMO purpose Email Link";
}

<h2>@ViewBag.Title.</h2>

<p class="text-info">
    Please check your email and confirm your email address.
</p>

<p class="text-danger">
    For DEMO only: You can click this link to confirm the email: <a href="@ViewBag.Link">link</a>
    Please change this code to register an email service in IdentityConfig to send an email.
</p>

متد دیگری که در این کنترلر باید ویرایش شود ()VerifyCode است که کد احراز هویت دو مرحله ای را به صفحه مربوطه پاس می‌دهد.

[AllowAnonymous]
public async Task<ActionResult> VerifyCode(string provider, string returnUrl)
{
    // Require that the user has already logged in via username/password or external login
    if (!await SignInHelper.HasBeenVerified())
    {
        return View("Error");
    }

    var user = 
        await UserManager.FindByIdAsync(await SignInHelper.GetVerifiedUserIdAsync());

    if (user != null)
    {
        ViewBag.Status = 
            "For DEMO purposes the current " 
            + provider 
            + " code is: " 
            + await UserManager.GenerateTwoFactorTokenAsync(user.Id, provider);
    }

    return View(new VerifyCodeViewModel { Provider = provider, ReturnUrl = returnUrl });
}

همانطور که می‌بینید متغیری بنام Status به ViewBag اضافه می‌شود که باید حذف شود.

نمای این متد یعنی VerifyCode.cshtml نیز باید ویرایش شود.

@model IdentitySample.Models.VerifyCodeViewModel

@{
    ViewBag.Title = "Enter Verification Code";
}

<h2>@ViewBag.Title.</h2>

@using (Html.BeginForm("VerifyCode", "Account", new { ReturnUrl = Model.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" })) {
    @Html.AntiForgeryToken()
    @Html.ValidationSummary("", new { @class = "text-danger" })
    @Html.Hidden("provider", @Model.Provider)
    <h4>@ViewBag.Status</h4>
    <hr />

    <div class="form-group">
        @Html.LabelFor(m => m.Code, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.TextBoxFor(m => m.Code, new { @class = "form-control" })
        </div>
    </div>

    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <div class="checkbox">
                @Html.CheckBoxFor(m => m.RememberBrowser)
                @Html.LabelFor(m => m.RememberBrowser)
            </div>
        </div>
    </div>

    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <input type="submit" class="btn btn-default" value="Submit" />
        </div>
    </div>
}

در این فایل کافی است ViewBag.Status را حذف کنید.


از تنظیمات ایمیل و SMS محافظت کنید

در مثال جاری اطلاعاتی مانند نام کاربری و کلمه عبور، شناسه‌های SID و Auth Token همگی در کد برنامه نوشته شده اند. بهتر است چنین مقادیری را بیرون از کد اپلیکیشن نگاه دارید، مخصوصا هنگامی که پروژه را به سرویس کنترل ارسال می‌کند (مثلا مخازن عمومی مثل GitHub). بدین منظور می‌توانید یکی از پست‌های اخیر را مطالعه کنید.

مطالب
ارتقاء به HTTP Client در Angular 4.3
عموما در برنامه‌های SPA، اطلاعات از طریق HTTP و از طرف سرور دریافت می‌شوند. از نگارش‌های ابتدایی Angular، اینکار از طریق HTTP Module آن مسیر بود و هست. در Angular 4.3 روش بهبودیافته‌ای نسبت به این روش متداول ارائه شده‌است که در ادامه تعدادی از ویژگی‌های مقدماتی آن‌را بررسی می‌کنیم.
هرچند ارتقاء به HttpClient الزامی نیست و کدهای پیشین، هنوز هم به خوبی کار می‌کنند؛ اما طراحی جدید آن شامل ویژگی‌های توکاری است که به سختی توسط HTTP Module پیشین قابل پیاده سازی هستند.


به روز رسانی وابستگی‌های پروژه

پیش از هرکاری نیاز است وابستگی‌های پروژه را به روز رسانی کرد و یکی از روش‌های ساده‌ی یافتن شماره نگارش‌های جدید بسته‌های تعریف شده‌ی در فایل package.json برنامه، استفاده از بسته‌ی npm-check-updates است:
npm install npm-check-updates -g
ncu
دستور اول، این بسته را به صورت عمومی نصب کرده و صدور دستور دوم در ریشه‌ی پروژه، سبب می‌شود تا گزارشی از آخرین به روز رسانی‌ها، نمایش داده شود (بدون هیچگونه تغییری در پروژه):


در اینجا شماره نگارش‌های جدید مشخص شده‌اند و همچنین روش سریع ارتقاء به آن‌ها نیز ذکر شده‌است. فقط کافی است دستورات ذیل را صادر کنیم تا این به روز رسانی‌ها توسط ncu انجام شوند:
ncu -a
npm update
دستور اول صرفا شماره نگارش‌های جدید را در فایل package.json، به صورت خودکار اصلاح می‌کند و دستور دوم سبب دریافت، نصب و اعمال آن‌ها خواهد گردید.


تغییرات مورد نیاز جهت معرفی ماژول HttpClient

این ماژول جدید از طریق اینترفیس HttpClientModule ارائه می‌شود. بنابراین اولین تغییر در جهت ارتقاء به نگارش 4.3، اصلاح importهای لازم است:
از:
 import { HttpModule } from '@angular/http';
به:
 import { HttpClientModule } from '@angular/common/http';

پس از آن، این HttpClientModule را به لیست imports ماژول اصلی برنامه اضافه می‌کنیم؛ تا در کل برنامه قابل دسترسی شود:
@NgModule({
    imports: [
        // ...
        HttpClientModule,
        // ...
    ],
declarations: [ ... ],
providers: [ ... ],
exports: [ ... ]
})
export class AppModule { }


تغییرات مورد نیاز در سازنده‌ها و تزریق وابستگی‌ها

پس از تغییرات فوق، دیگر دسترسی به HttpModule پیشین را نداریم. بنابراین نیاز است هر جائی را که سرویس Http به سازنده‌ی کلاسی تزریق شده‌است، یافته و به صورت ذیل تغییر دهیم:
از:
 constructor(private http: Http) { }
به:
import { HttpClient } from '@angular/common/http';
// ...
constructor(private http: HttpClient) { }


تغییرات مورد نیاز در کدهای سرویس‌ها جهت کار با HTTP Verbs

یکی از اهداف HTTP Client جدید، سادگی کار با اطلاعات دریافتی از سرور است. برای مثال در HTTP Module پیشین، روش دریافت اطلاعات از سرور به صورت ذیل است:
public get(): Observable<MyType> => {
    return this.http.get(url)
        .map((response: Response) => <MyType>response.json());
}
ابتدا درخواستی ارسال شده و سپس نتیجه‌ی آن به JSON تبدیل گشته و در آخر به نوع بازگشتی متد تبدیل می‌شود.
در HTTP Client جدید دیگر نیازی نیست تا متد ()json. فراخوانی شود. در اینجا به صورت پیش‌فرض نوع بازگشتی از سرور JSON فرض می‌شود. همچنین اکنون متدهای get/put/post و امثال آن برخلاف HTTP Client قبلی، جنریک هستند. یعنی در همینجا می‌توان نوع بازگشتی را هم مشخص کرد. به این ترتیب، قطعه کد قدیمی فوق، به کد ساده‌ی ذیل تبدیل می‌شود که در آن خبری از map و همچنین یک cast اضافی نیست:
get<T>(url: string): Observable<T> {
    return this.http.get<T>(url);
}
برای نمونه شبیه به همین نکته برای post نیز صادق است:
post<T>(url: string, body: string): Observable<T> {
   return this.http.post<T>(url, body);
}

نکته 1: در اینجا اگر خروجی از سرور، نوع دیگری را داشت، نیاز است responseType را به صورت صریحی به شکل ذیل مشخص کرد:
 getData() {
  this.http.get(this.url, { responseType: 'text' }).subscribe(res => {
       this.data = res;
  });
}
در این‌حالت خروجی متنی <Observable<string را دریافت می‌کنیم و نیازی به ذکر <get<string نیست.

نکته 2: ممکن است اطلاعات بازگشتی از سمت سرور، داخل یک فیلد محصور شده باشند:
{
  "results": [
    "Item 1",
    "Item 2",
  ]
}
در این حالت برای دسترسی به اطلاعات این فیلد می‌توان از حالت key/value بودن اشیاء جاوا اسکریپتی به شکل زیر برای دسترسی به خاصیت results استفاده کرد:
this.http.get('/api/items').subscribe(data => {
   this.results = data['results'];
});


نکاتی را که باید حین کار با یک RxJS Observable-based API در نظر داشت

این API جدید نیز همانند قبل مبتنی بر RxJS Observables است. بنابراین نکات ذیل در مورد آن نیز صادق است:
- اگر متد subscribe بر روی این observables فراخوانی نشود، اتفاقی رخ نخواهد داد.
- اگر چندین بار مشترک این observables شویم، چندین درخواست HTTP صادر می‌شوند.
- این نوع خاص از observables، تنها یک مقدار را بازگشت می‌دهند. اگر درخواست HTTP موفقیت آمیز باشد، این observables یک نتیجه را بازگشت داده و سپس خاتمه پیدا می‌کنند.
- این observables اگر در حین درخواست HTTP با خطایی مواجه شوند، سبب صدور استثنایی می‌شوند.


تغییرات مورد نیاز در کدهای سرویس‌ها جهت کار با HTTP Headers

در اینجا برای تعریف headers می‌توان به صورت ذیل عمل کرد:
import { HttpHeaders } from "@angular/common/http";

const headers = new HttpHeaders({ "Content-Type": "application/json" });
و یا به صورت fluent به شکل زیر:
 const headers = new HttpHeaders().set("Accept", "application/json").set('Content-Type', 'application/json');

سپس آن‌را به عنوان پارامتر سوم، به متدهای http ارسال می‌کنیم. یک مثال:
  updateAppProduct(id: number, item: AppProduct): Observable<AppProduct> {
    const header = new HttpHeaders({ "Content-Type": "application/json" });
    return this.http
      .put<AppProduct>(
        `${this.baseUrl}/UpdateProduct/${id}`,
        JSON.stringify(item),
        { headers: header }
      )
      .map(response => response || {});
  }

تعریف پارامتر options اینبار به صورت یک شیء دارای چندین خاصیت درآمده‌است. به همین جهت است که در اینجا یک {} را نیز مشاهده می‌کنید:
(method) HttpClient.post(url: string, body: any, options?: {
          headers?: HttpHeaders;
          observe?: "body";
          params?: HttpParams;
          reportProgress?: boolean;
          responseType?: "json";
          withCredentials?: boolean;
}): Observable<Object>

یک نکته: شیء HttpHeaders به صورت immutable طراحی شده‌است. یعنی اگر آن‌را به صورت ذیل فراخوانی کنیم:
const headers = new HttpHeaders();
headers = headers.set('Content-Type', 'application/json');
headers = headers.set('Accept', 'application/json');
headers تولیدی ... خالی خواهد بود. به همین جهت روش صحیح تشکیل آن به صورت ذیل و زنجیروار است:
 const headers = new HttpHeaders()
.set('Content-Type', 'application/json')
.set('Accept', 'application/json')
;


امکان تعریف HttpParams

اگر به شیء options در تعریف فوق دقت کنید، دارای خاصیت اختیاری params نیز هست. از آن می‌توان جهت تعریف کوئری استرینگ‌ها استفاده کرد. برای مثال درخواست ذیل:
http
  .post('/api/items/add', body, {
      params: new HttpParams().set('id', '3'),
  })
  .subscribe();
سبب تولید یک چنین URL ایی می‌گردد:
  /api/items/add?id=3

یک نکته: شیء HttpParams به صورت immutable طراحی شده‌است. یعنی اگر آن‌را به صورت ذیل فراخوانی کنیم:
const params = new HttpParams();
params.set('orderBy', '"$key"')
params.set('limitToFirst', "1");
params تولیدی ... خالی خواهد بود. به همین جهت روش صحیح تشکیل آن به صورت ذیل و زنجیروار است:
const params = new HttpParams()
.set('orderBy', '"$key"')
.set('limitToFirst', "1");
به علاوه روش تعریف ذیل نیز برای کار با HttpParams مجاز است:
 const params = new HttpParams({fromString: 'orderBy="$key"&limitToFirst=1'});


تغییرات مورد نیاز در کدهای سرویس‌ها جهت مدیریت خطاها

در اینجا اینبار خطای بازگشتی، از نوع ویژه‌ی HttpErrorResponse است که شامل اطلاعات شماره کد و متن خطای حاصل می‌باشد:
import { HttpClient, HttpHeaders, HttpErrorResponse } from "@angular/common/http";

postData() { 
  this.http.post(this.url, this.payload).subscribe( 
    res => { 
      console.log(res); 
    }, 
    (err: HttpErrorResponse) => { 
      console.log(err.error); 
      console.log(err.name); 
      console.log(err.message); 
      console.log(err.status); 

        if (err.error instanceof Error) { 
          console.log("Client-side error occured."); 
        } else { 
          console.log("Server-side error occured."); 
        }
    } 
  ); 
}


امکان سعی مجدد در اتصال توسط HTTP Client

ممکن است در اولین سعی در اتصال به سرور، خطایی رخ دهد و یا سرور در دسترس نباشد. در اینجا توسط متد retry می‌توان درخواست سعی مجدد در اتصال را صادر کرد.
برای این منظور ابتدا عملگر retry مربوط به RxJS را import می‌کنیم:
 import 'rxjs/add/operator/retry';
سپس:
http
  .get<ItemsResponse>('/api/items')
  .retry(3)
  .subscribe(...);
این کد در صورت بروز خطایی، این عملیات را سه بار تکرار می‌کند. در انتها اگر بازهم خطایی دریافت شد، این خطا را به برنامه بازگشت می‌دهد.


امکان درخواست کل Response بجای Body

اگر به امضای پارامتر اختیاری options دقت کنید، خاصیت observe آن به صورت پیش فرض به body تنظیم شده‌است. به این معنا که تنها body یک response را تبدیل به یک شیء JSON می‌کند:
(method) HttpClient.post(url: string, body: any, options?: {
          headers?: HttpHeaders;
          observe?: "body";
          params?: HttpParams;
          reportProgress?: boolean;
          responseType?: "json";
          withCredentials?: boolean;
}): Observable<Object>
اما گاهی از اوقات نیاز است تا به کل Response دسترسی داشته باشیم. در این حالت باید نوع observe را به response تنظیم کرد:
http
  .get<MyJsonData>('/data.json', {observe: 'response'})
  .subscribe(resp => {
    console.log(resp.headers.get('X-Custom-Header'));
    console.log(resp.body.someField);
  });
به این ترتیب اینبار resp از نوع <HttpResponse<MyJsonData خواهد بود که توسط آن می‌توان به خواص headers و یا body، به صورت جداگانه‌ای دسترسی یافت.


یک نکته‌ی تکمیلی: کدهای سری کار با فرم‌ها در Angular را اگر به HttpClient ارتقاء دهیم، خلاصه‌ی تغییرات آن‌ها به این صورت خواهند بود.
مطالب
شروع به کار با EF Core 1.0 - قسمت 14 - لایه بندی و تزریق وابستگی‌ها
در مورد «امکانات توکار تزریق وابستگی‌ها در ASP.NET Core» پیشتر بحث شد. همچنین «نحوه‌ی تعریف Context، تزریق سرویس‌های EF Core و تنظیمات رشته‌ی اتصالی آن» را نیز بررسی کردیم. به علاوه مباحث «به روز رسانی ساختار بانک اطلاعاتی» و «انتقال مهاجرت‌ها به یک اسمبلی دیگر» نیز مرور شدند. بنابراین در این قسمت برای لایه بندی برنامه‌های EF Core، صرفا یک مثال را مرور خواهیم کرد که این قسمت‌ها را در کنار هم قرار می‌دهد و عملا نکته‌ی اضافه‌تری را ندارد.


تزریق مستقیم کلاس Context برنامه، تزریق وابستگی‌ها نام ندارد!

در همان قسمت اول سری شروع به کار با EF Core 1.0، مشاهده کردیم که پس از انجام تنظیمات اولیه‌ی آن در کلاس آغازین برنامه:
public void ConfigureServices(IServiceCollection services)
{    
   services.AddDbContext<ApplicationDbContext>(ServiceLifetime.Scoped);
Context برنامه را در تمام قسمت‌های آن می‌توان تزریق کرد و کار می‌کند:
    public class TestDBController : Controller
    {
        private readonly ApplicationDbContext _ctx;

        public TestDBController(ApplicationDbContext ctx)
        {
            _ctx = ctx;
        }

        public IActionResult Index()
        {
            var name = _ctx.Persons.First().FirstName;
            return Json(new { firstName = name });
        }
    }
این روشی است که در بسیاری از مثال‌های گوشه و کنار اینترنت قابل مشاهده‌است. یا کلاس Context را مستقیما در سازنده‌ی کنترلرها تزریق می‌کنند و از آن استفاده می‌کنند (روش فوق) و یا لایه‌ی سرویسی را ایجاد کرده و مجددا همین تزریق مستقیم را در آنجا انجام می‌دهند و سپس اینترفیس‌های آن سرویس را در کنترلرهای برنامه تزریق کرده و استفاده می‌کنند. به این نوع تزریق وابستگی‌ها، تزریق concrete types و یا concrete classes می‌گویند.
مشکلاتی را که تزریق مستقیم کلاس‌ها و نوع‌ها به همراه دارند به شرح زیر است:
- اگر نام این کلاس تغییر کند، باید این نام، در تمام کلاس‌هایی که به صورت مستقیم از آن استفاده می‌کنند نیز تغییر داده شود.
- اگر سازنده‌ای به آن اضافه شد و یا امضای سازنده‌ی موجود آن، تغییر کرد، باید نحوه‌ی وهله سازی این کلاس را در تمام کلاس‌های وابسته نیز اصلاح کرد.
- یکی از مهم‌ترین دلایل استفاده‌ی از تزریق وابستگی‌ها، بالابردن قابلیت تست پذیری برنامه است. زمانیکه از اینترفیس‌ها استفاده می‌شود، می‌توان در مورد نحوه‌ی تقلید (mocking) رفتار کلاسی خاص، مستقلا تصمیم گیری کرد. اما هنگامیکه یک کلاس را به همان شکل اولیه‌ی آن تزریق می‌کنیم، به این معنا است که همواره دقیقا همین پیاده سازی خاص مدنظر ما است و این مساله، نوشتن آزمون‌های واحد را با مشکل کردن mocking آن‌ها، گاهی از اوقات غیرممکن می‌کند. هرچند تعدادی از فریم ورک‌های پیشرفته‌ی mocking گاهی از اوقات امکان تقلید رفتار کلاس‌ها و نوع‌ها را نیز فراهم می‌کنند، اما با این شرط که تمام خواص و متدهای آن‌ها را virtual تعریف کنید؛ تا بتوانند متدهای اصلی را با نمونه‌های مدنظر شما بازنویسی (override) کنند.

به همین جهت در ادامه، به همان طراحی EF Code First #12 با نوشتن اینترفیس IUnitOfWork خواهیم رسید. یعنی کلاس Context برنامه را با این اینترفیس نشانه گذاری می‌کنیم (در انتهای لیست تمام اینترفیس‌های دیگری که ممکن است در اینجا ذکر شده باشند):
 public class ApplicationDbContext :  IUnitOfWork
و سپس اینترفیس IUnitOfWork را به لایه سرویس برنامه و یا هر لایه‌ی دیگری که به Context آن نیاز دارد، تزریق خواهیم کرد.


طراحی اینترفیس IUnitOfWork

برای اینکه دیگر با کلاس ApplicationDbContext مستقیما کار نکرده و وابستگی به آن‌را در تمام قسمت‌های برنامه پخش نکنیم، اینترفیسی را ایجاد می‌کنیم که تنها قسمت‌های مشخصی از DbContext را عمومی کند:
public interface IUnitOfWork : IDisposable
{
    DbSet<TEntity> Set<TEntity>() where TEntity : class;
 
    void AddRange<TEntity>(IEnumerable<TEntity> entities) where TEntity : class;
    void RemoveRange<TEntity>(IEnumerable<TEntity> entities) where TEntity : class;
 
    EntityEntry<TEntity> Entry<TEntity>(TEntity entity) where TEntity : class;
    void MarkAsChanged<TEntity>(TEntity entity) where TEntity : class;
 
    void ExecuteSqlCommand(string query);
    void ExecuteSqlCommand(string query, params object[] parameters);
 
    int SaveAllChanges();
    Task<int> SaveAllChangesAsync();
}
توضیحات
- در این طراحی شاید عنوان کنید که DbSet، اینترفیس نیست. تعریف DbSet در EF Core به صورت زیر است و در حقیقت همانند اینترفیس‌ها یک abstraction به حساب می‌آید:
 public abstract class DbSet<TEntity> : IQueryable<TEntity>, IEnumerable<TEntity>, IEnumerable, IQueryable, IAsyncEnumerableAccessor<TEntity>, IInfrastructure<IServiceProvider> where TEntity : class
علت اینکه در پروژه‌های بزرگی مانند EF، تمایل زیادی به استفاده‌ی از کلاس‌های abstract وجود دارد (بجای اینترفیس‌ها) این است که اگر این نوع پرکاربرد را به صورت اینترفیس تعریف کنند، با تغییر متدی در آن، باید تمام کدهای خود را به اجبار بازنویسی کنید. اما در حالت استفاده‌ی از کلاس‌های abstract، می‌توان پیاده سازی پیش فرضی را برای متدهایی که قرار است در آینده اضافه شوند، ارائه داد (یکی از تفاوت‌های مهم آن‌ها با اینترفیس‌ها)، بدون اینکه تمام استفاده کنندگان از این کتابخانه، با ارتقاء نگارش EF خود، دیگر نتوانند برنامه‌ی خود را کامپایل کنند.
- این اینترفیس به عمد به صورت IDisposable تعریف شده‌است. این مساله به IoC Containers کمک خواهد کرد که بتوانند پاکسازی خودکار نوع‌های IDisposable را در انتهای هر درخواست انجام دهند و برنامه مشکلی نشتی حافظه را پیدا نکند.
- اصل کار این اینترفیس، تعریف DbSet و متدهای SaveChanges است. سایر متدهایی را که مشاهده می‌کنید، صرفا جهت بیان اینکه چگونه می‌توان قابلیتی از DbContext را بدون عمومی کردن خود کلاس DbContext، در کلاس‌هایی که از اینترفیس IUnitOfWork استفاده می‌کنند، میسر کرد.

پس از اینکه این اینترفیس تعریف شد، اعمال آن به کلاس Context برنامه به صورت ذیل خواهد بود:
public class ApplicationDbContext : DbContext, IUnitOfWork
{
    private readonly IConfigurationRoot _configuration;
 
    public ApplicationDbContext(IConfigurationRoot configuration)
    {
        _configuration = configuration;
    }
 
    //public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
    //{
    //}
 
    public virtual DbSet<Blog> Blog { get; set; }

 
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(
            _configuration["ConnectionStrings:ApplicationDbContextConnection"]
            , serverDbContextOptionsBuilder =>
             {
                 var minutes = (int)TimeSpan.FromMinutes(3).TotalSeconds;
                 serverDbContextOptionsBuilder.CommandTimeout(minutes);
             }
            );
    }
 
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
 
        base.OnModelCreating(modelBuilder);
    }
 
    public void AddRange<TEntity>(IEnumerable<TEntity> entities) where TEntity : class
    {
        base.Set<TEntity>().AddRange(entities);
    }
 
    public void RemoveRange<TEntity>(IEnumerable<TEntity> entities) where TEntity : class
    {
        base.Set<TEntity>().RemoveRange(entities);
    }
 
    public void MarkAsChanged<TEntity>(TEntity entity) where TEntity : class
    {
        base.Entry(entity).State = EntityState.Modified; // Or use ---> this.Update(entity);
    }
 
    public void ExecuteSqlCommand(string query)
    {
        base.Database.ExecuteSqlCommand(query);
    }
 
    public void ExecuteSqlCommand(string query, params object[] parameters)
    {
        base.Database.ExecuteSqlCommand(query, parameters);
    }
 
    public int SaveAllChanges()
    {
        return base.SaveChanges();
    }
 
    public Task<int> SaveAllChangesAsync()
    {
        return base.SaveChangesAsync();
    }
}
در ابتدا اینترفیس IUnitOfWork به کلاس Context برنامه اعمال شده‌است:
 public class ApplicationDbContext : DbContext, IUnitOfWork
و سپس متدهای آن منهای پیاده سازی اینترفیس IDisposable اعمالی به IUnitOfWork :
 public interface IUnitOfWork : IDisposable
پیاده سازی شده‌اند. علت اینجا است که چون کلاس پایه DbContext از همین اینترفیس مشتق می‌شود، دیگر نیاز به پیاده سازی اینترفیس IDisposable نیست.
در مورد تزریق IConfigurationRoot به سازنده‌ی کلاس Context برنامه، در مطلب اول این سری در قسمت «یک نکته: امکان تزریق IConfigurationRoot به کلاس Context برنامه» پیشتر بحث شده‌است.


ثبت تنظیمات تزریق وابستگی‌های IUnitOfWork

پس از تعریف و پیاده سازی اینترفیس IUnitOfWork، اکنون نوبت به معرفی آن به سیستم تزریق وابستگی‌های ASP.NET Core است:
public void ConfigureServices(IServiceCollection services)
{
  services.AddSingleton<IConfigurationRoot>(provider => { return Configuration; });
  services.AddDbContext<ApplicationDbContext>(ServiceLifetime.Scoped);
  services.AddScoped<IUnitOfWork, ApplicationDbContext>();
در اینجا هم ApplicationDbContext و هم IUnitOfWork با طول عمر Scoped به تنظیمات IoC Container مربوط به ASP.NET Core اضافه شده‌اند. به این ترتیب هر زمانیکه وهله‌ای از نوع IUnitOfWork درخواست شود، تنها یک وهله از ApplicationDbContext در طول درخواست وب جاری، در اختیار مصرف کننده قرار می‌گیرد و همچنین مدیریت Dispose این وهله‌ها نیز خودکار است. به همین جهت اینترفیس IUnitOfWork را با IDisposable علامتگذاری کردیم.


استفاده از IUnitOfWork در لایه سرویس‌های برنامه

اکنون لایه سرویس برنامه و فایل project.json آن چنین شکلی را پیدا می‌کند:
{
  "version": "1.0.0-*",
 
    "dependencies": {
        "Core1RtmEmptyTest.DataLayer": "1.0.0-*",
        "Core1RtmEmptyTest.Entities": "1.0.0-*",
        "Core1RtmEmptyTest.ViewModels": "1.0.0-*",
        "Microsoft.Extensions.Configuration.Abstractions": "1.0.0",
        "Microsoft.Extensions.Options": "1.0.0",
        "NETStandard.Library": "1.6.0"
    },
 
  "frameworks": {
    "netstandard1.6": {
      "imports": "dnxcore50"
    }
  }
}
در اینجا ارجاعاتی را به اسمبلی‌های موجودیت‌ها و DataLayer برنامه مشاهده می‌کنید. در مورد این اسمبلی‌ها در مطلب «شروع به کار با EF Core 1.0 - قسمت 3 - انتقال مهاجرت‌ها به یک اسمبلی دیگر» پیشتر بحث شد.
پس از تنظیم وابستگی‌های این اسمبلی، اکنون یک کلاس نمونه از لایه سرویس برنامه، به شکل زیر خواهد بود: 
namespace Core1RtmEmptyTest.Services
{
    public interface IBlogService
    {
        IReadOnlyList<Blog> GetPagedBlogsAsNoTracking(int pageNumber, int recordsPerPage);
    }
 
    public class BlogService : IBlogService
    {
        private readonly IUnitOfWork _uow;
        private readonly DbSet<Blog> _blogs;
 
        public BlogService(IUnitOfWork uow)
        {
            _uow = uow;
            _blogs = _uow.Set<Blog>();
        }
 
        public IReadOnlyList<Blog> GetPagedBlogsAsNoTracking(int pageNumber, int recordsPerPage)
        {
            var skipRecords = pageNumber * recordsPerPage;
            return _blogs
                        .AsNoTracking()
                        .Skip(skipRecords)
                        .Take(recordsPerPage)
                        .ToList();
        }
    }
}
در اینجا اکنون می‌توان IUnitOfWork را به سازنده‌ی کلاس سرویس Blog تنظیم کرد و سپس به نحو متداولی از امکانات EF Core استفاده نمود.


استفاده از امکانات لایه سرویس برنامه، در دیگر لایه‌های آن

خروجی لایه سرویس، توسط اینترفیس‌هایی مانند IBlogService در قسمت‌های دیگر برنامه قابل استفاده و دسترسی می‌شود.
به همین جهت نیاز است مشخص کنیم، این اینترفیس را کدام کلاس ویژه قرار است پیاده سازی کند. برای این منظور همانند قبل در متد ConfigureServices کلاس آغازین برنامه این تنظیم را اضافه خواهیم کرد:
public void ConfigureServices(IServiceCollection services)
{
  services.AddSingleton<IConfigurationRoot>(provider => { return Configuration; });
  services.AddDbContext<ApplicationDbContext>(ServiceLifetime.Scoped);
  services.AddScoped<IUnitOfWork, ApplicationDbContext>();
  services.AddScoped<IBlogService, BlogService>();
پس از آن، امضای سازنده‌ی کلاس کنترلری که در ابتدای بحث عنوان شد، به شکل زیر تغییر پیدا می‌کند:
public class TestDBController : Controller
{
    private readonly IBlogService _blogService;
    private readonly IUnitOfWork _uow;
 
    public TestDBController(IBlogService blogService, IUnitOfWork uow)
    {
        _blogService = blogService;
        _uow = uow;
    }
در اینجا کنترلر برنامه تنها با اینترفیس‌های IUnitOfWork و IBlogService کار می‌کند و دیگر ارجاع مستقیمی را به کلاس ApplicationDbContext ندارد.