مطالب
پَرباد - راهنمای اتصال و پیاده‌سازی درگاه‌های پرداخت اینترنتی (شبکه شتاب)

پَرباد چیست؟

همانطور که همه ما میدانیم، اتصال و راه اندازی درگاه‌های پرداخت اینترنتی (شبکه شتاب)، از همان ابتدا کاری مشکل و  پر دردسر برای برنامه نویسان بود. هر بانک، سیستم متفاوت و مخصوص به خود را دارد و این بدان معنا است که برنامه نویسان باید کدهای کاملا متفاوت و همچنین پیاده سازی‌های متفاوتی را از روی فایل‌های PDF راهنمای بانکی، که در نهایت منجر به بی نظمی در پروژه‌ها می‌شود، بنویسند و البته مشکل بزرگتر آن است که پس از پیاده سازی هم اطمینان کاملی از صحت کدهای نوشته شده وجود ندارد؛ چه بسا که واحد‌های پشتیبانی درگاه‌های پرداخت هم افراد حرفه‌ای و آشنا با توسعه نرم افزار نیستند و اکثر اوقات نمی‌توان به آنها تکیه کرد.
برای راحتی کار برنامه نویسان حوضه فریم ورک دات نت، سیستمی جامع، اوپن سورس و کاملا رایگان، بدون نیاز به اضافه کردن هیچ گونه وب سرویسی تهیه شده است که به برنامه نویسان اجازه می‌دهد تنها با نوشتن چند خط کد، وب سایت خود را به پرداخت اینترنتی مجهز کنند. لطفا پیشنهادات، بحث‌ها و نظرات خود را در صفحه مخصوص این پروژه ارسال کنید.  
این سیستم در حال حاضر متشکل از درگاه‌های پرداخت اینترنتی بانک‌های ملت، سامان، پارسیان، تجارت و پاسارگاد است.
همچنین این سیستم در قالب یک Nuget Package برای نصب راحت در اپلیکیشن آماده شده است.


آنچه که شما در این مطلب یاد خواهید گرفت:

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


طریقه نصب

PM> Install-Package Parbad

برای وب سایت‌های بر پایه فریم ورک MVC

PM> Install-Package Parbad.MVC5


ایجاد صورتحساب و ارسال کاربر به درگاه پرداخت

ابتدا یک شیٔ صورتحساب را به صورت زیر ایجاد کنید
var invoice = new Invoice( [Order Number], [Amount], [Verify URL]);

- Order Number شماره صورتحساب است و باید همیشه یک عدد یکتا باشد (تکراری نباشد).
- Amount مبلغ قابل پرداخت به ریال است.
- Verify URL یک آدرس در وب سایت شما، برای بازگشت مشتری پس از پرداخت و تأیید صورتحساب است.
برای مثال:
var invoice = new Invoice(1, 30000, "http://www.mywebsite.com/payment/verify" );
سپس صورتحساب را به درگاه مورد نظر ارسال میکنیم.
var result = Payment.Request(Gateways.Mellat, invoice);

شیٔ result حاوی شماره یکتا رجوع و وضعیت درخواست (موفقیت یا عدم موفقیت درخواست) است.
if (result.Status == RequestResultStatus.Success)
{
    // این متد، کاربر را به سمت وب سایت درگاه پرداخت هدایت میکند
    result.Process(Context);
}
else
{
    // در صورت تمایل می‌توانید پیغام مورد نظر از درگاه پرداخت را نمایش دهید
    var msg = result.Message;
}

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

if (result.Status == RequestResultStatus.Success)
{
   // کاربر را به سمت وب سایت درگاه پرداخت هدایت میکند 
   return new RequestActionResult(result);
}
else
{
   return View("Error");
}


تأیید صورتحساب

پس از بازگشت کاربر از وب سایت بانک، باید از پرداخت صورتحساب توسط کاربر اطمینان حاصل کنید. کد زیر را باید در آدرسی که هنگام ساخت صورتحساب ذکر کرده بودید، قرار دهید.
var result = Payment.Verify(System.Web.HttpContext.Current);

شیٔ result در اینجا حاوی اطلاعاتی مانند: درگاه بانکی (که کاربر در آن صورتحساب را پرداخت کرده)، شماره رجوع، شماره تراکنش یکتای بانکی، وضعیت پرداخت و پیام درگاه است.
شما می‌توانید با بررسی این شیٔ، تصمیمات لازم را بگیرید.
if(result.Status == VerifyResultStatus.Success)
{
    // کاربر، صورتحساب را پرداخت کرده است و شما میتوانید ادامه عملیات خرید را انجام دهید
}
else
{
    // کاربر بنا به دلایلی صورتحساب را پرداخت نکرده است
    // شما همچنین میتوانید علت را در قالب یک پیام از پراپرتی پیام مشاهده کنید

    // بنابراین شما میتوانید این صورتحساب را در پایگاه داده خود مردود اعلام کنید
}


مردود کردن صورتحساب قبل از انتقال وجه از مشتری به فروشنده

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



همانطور که در تصویر می‌بینید، در هنگام بازگشت مشتری به وب سایت شما و تأیید کردن صورتحساب، شما می‌توانید اطلاعات تراکنش مورد نظر را که شامل، درگاه پرداخت بانکی، شماره سفارش و شماره رجوع است را دریافت کنید و سپس با استفاده از این اطلاعات، پایگاه داده خود را بررسی کرده و در صورت لزوم، متد Cancel را فراخوانی کنید. به این ترتیب به درگاه بانکی، هیچگونه تأییدیه ای اعلام نمی‌شود و این بدان معناست که اگر وجهی به حساب فروشگاه واریز شده باشد، پس از چند دقیقه (معمولا ۱۵ دقیقه) به حساب مشتری برگشت داده خواهد شد.


برگشت وجه به حساب مشتری پس از تأیید صورتحساب

var refundResult = Payment.Refund(new RefundInvoice([Order Number], [Amount]));
در اینجا، Order Number همان شماره سفارش صورتحساب و Amount مقداری از وجه و یا کل وجه برای برگشت به حساب مشتری است.
پس از آن شما می‌توانید نتیجه این عملیات را در شیٔ refundResult بررسی کنید.


درگاه مجازی پرداخت

درصورتیکه شما نیاز به تست عملکرد اپلیکیشن خود داشته باشید، نیازی به داشتن یک حساب واقعی در بانک‌های اینترنتی ندارید و می‌توانید اپلیکیشن خود را با یک درگاه مجازی بسیار ساده تست کنید. برای انجام این کار در هنگام ارسال صورتحساب، از میان درگاه‌های بانکی، درگاه مجازی پَرباد را انتخاب کنید.
var result = Payment.Request(Gateways.ParbadVirtualGateway, invoice);


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

اما قبل از کار با درگاه مجازی باید در فایل web.config وب اپلیکیشن خود، تنظیمات زیر را قرار دهید:
<system.webServer>
  <handlers>
   <add name="ParbadGatewayPage" verb="*" path="Parbad.axd" type="Parbad.Web.Gateway.ParbadVirtualGatewayHandler" />
  </handlers>
</system.webServer>
در اینجا، درگاه مجازی به عنوان یک HttpHandler معرفی شده است. مقداری که در مشخصه path ذکر شده، در واقع آدرس درگاه مجازی است که شما می‌توانید به دلخواه خود آن را وارد کنید. ما در این مثال از آدرس parbad.axd استفاده کرده ایم.
و در نهایت در وب اپلیکیشن خود، مسیر ذکر شده را به صورت زیر معرفی کنید:
ParbadConfiguration.Gateways.ConfigureParbadVirtualGateway(new ParbadVirtualGatewayConfiguration("Parbad.axd"));
در نتیجه در هنگام هدایت کاربر به درگاه مجازی، شما باید در نوار آدرس مرورگر خود، مقداری را که تنظیم کرده اید مشاهده کنید.


نکته مهم: فراموش نکنید، قبل از انتشار نهایی وب سایت بر روی سرور (نمایش عمومی)، تنظیمات HttpHandler مربوط به این درگاه مجازی را از درون فایل web.config حذف کنید. بدین صورت، این درگاه از دسترس عموم خارج خواهد بود.

تنظیمات پَرباد

بهترین مکان برای درج این تنظیمات در اپلیکیشن‌های ASP.NET WebForms فایل Global.asax.cs و در اپلیکیشن‌های ASP.NET MVC فایل Startup.cs است.
ASP.NET Web Forms
public class Global : HttpApplication
{
    void Application_Start(object sender, EventArgs e)
    {
        // configurations
    }
}

ASP.NET MVC
public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        // configurations
    }
}

تنظیمات درگاه‌های پرداخت

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


تنظیمات ذخیره سازی اطلاعات پرداخت

پَرباد برای ذخیره و بازیابی اطلاعات پرداخت، نیاز به یک منبع ذخیره سازی دارد.
منبع پیش فرض پَرباد، کلاس TemporaryMemoryStorage است که همانطور که از نام آن پیداست، اطلاعات را به صورت موقت در حافظه رَم سرور ذخیره میکند. اگر شما خودتان اطلاعات پرداخت را در پایگاه داده ذخیره میکنید، این منبع، گزینه مناسبی است به دلیل سرعت بسیار بالای حافظه رَم.
توجه: در نظر داشته باشید که اگر به هر دلیلی سرور و یا وب سایت شما، ری‌استارت شود، کلیه اطلاعات موجود در این منبع هم از بین خواهد رفت.
ذخیره و بازیابی توسط SQL Server
برای این منظور در قسمت تنظیمات، کد زیر را قرار داده و رشته اتصال و نام جدول پرداخت را معرفی کنید.
ParbadConfiguration.Storage = new SqlServerStorage("Connection String", "MyPaymentTableName");

فیلد‌های مورد نیاز در این جدول:

ذخیره و بازیابی اطلاعات توسط روش مورد نظر شما:
در صورتیکه مایلید ذخیره و بازیابی را به روش خود انجام دهید، کلاس Storage را پیاده سازی کنید
public class MyStorage : Storage
{
    // Implement methods here...
}

و کلاس مورد نظر را در تنظیمات به عنوان منبع، معرفی کنید.
ParbadConfiguration.Storage = new MyStorage();

لازم به ذکر است که این کلاس شامل متد‌های synchronous و همچنین asynchronous است. بنابراین در صورتیکه برای مثال در هنگام ارسال درخواست به بانک، از متد‌های async استفاده می‌کنید، نیازی به پیاده سازی کردن متد‌های synchronous نیست.
در صورتیکه هر گونه پیشنهاد یا انتقاد نسبت به کارکرد این سیستم دارید، صمیمانه منتظر شنیدن آن در راستای توسعه این سیستم هستم.
همچنین در صورت تمایل به توسعه آن، می‌توانید آن را در گیت هاب دنبال کنید و یا لطفا پیشنهادات، بحث‌ها و نظرات خود را در صفحه مخصوص این پروژه ارسال کنید. 
با تشکر.
مطالب
مراحل تنظیم Let's Encrypt در IIS
روزگاری دریافت مجوزهای SSL، گران و سخت بود. برای رفع این مشکلات مؤسسه‌هایی مانند Let's Encrypt پدیدار شده‌اند که مجوزهای SSL رایگانی را برای سایت‌های اینترنتی صادر می‌کنند. دسترسی به سرویس آن‌ها از طریق API ارائه شده‌ی آن، بسیار ساده بوده، کار با آن رایگان است و نیاز به مجوز خاصی ندارد. فقط باید دقت داشت که گواهینامه‌های Let's Encrypt دو ماهه هستند و وب‌سرور سایت شما باید اجازه‌ی دسترسی به محل ویژه‌ای را که جهت تعیین اعتبار دومین درخواستی ایجاد می‌شود، صادر کند. البته درخواست گواهی مجدد و تمدید آن در هر زمانی، حتی اگر پیش از انقضای آن باشد، مسیر است و از این لحاظ محدودیتی ندارد. در ادامه نحوه‌ی کار با این سرویس را در ویندوزهای سرور بررسی خواهیم کرد.


دریافت برنامه‌ی win-acme

برنامه‌ی win-acme کار دریافت، نصب و تنظیم به روز رسانی خودکار مجوزهای Let’s Encrypt را در ویندوز بسیار ساده کرده‌است و تقریبا به برنامه‌ی استاندارد انجام اینکار تبدیل شده‌است. این برنامه، انجام مراحل زیر را خودکار کرده‌است:
- اسکن IIS برای یافتن bindings و نام سایت‌ها
-  اتصال به Let’s Encrypt certificate authority و دریافت مجوزهای لازم
- درج خودکار مجوزهای دریافتی در Windows certificate store
- ایجاد HTTPS binding خودکار در IIS
- استفاده از Windows Task Scheduler‌، جهت ایجاد وظیفه‌ی به روز رسانی خودکار مجوزهای درخواست شده

به همین جهت پیش از هر کاری نیاز است این برنامه را دریافت کنید:
https://github.com/PKISharp/win-acme/releases

این برنامه از دات نت نگارش 4.6.2 استفاده می‌کند. بنابراین نیاز است این نگارش و یا ترجیحا آخرین نگارش دات نت فریم ورک را بر روی سرور نصب کنید.


آماده سازی برنامه‌ی ASP.NET جهت دریافت مجوزهای Let’s Encrypt

سرور Let’s Encrypt، در حین صدور مجوز برای سایت شما نیاز دارد بررسی کند که آیا شما واقعا صاحب همان دومین هستید یا خیر. به همین جهت مسیر
/.well-known/acme-challenge/id
را بر روی سرور شما بررسی خواهد کرد (بنابراین سرور شما باید به اینترنت متصل بوده و همچنین مجوز دسترسی به این مسیر را عمومی کند). برنامه‌ی win-acme این id را از سرور Let’s Encrypt به صورت خودکار دریافت کرده و فایلی را در مسیر یاد شده ایجاد می‌کند. سپس سرور Let’s Encrypt یکبار این مسیر را خواهد خواند. مشکل اینجا است که دسترسی به این فایل بدون پسوند در برنامه‌های MVC به صورت پیش‌فرض مسیر نیست و نیازی به تنظیمات خاصی دارد:
روش انجام اینکار در ASP.NET Core به صورت زیر است:
[HttpGet("/.well-known/acme-challenge/{id}")]
public IActionResult LetsEncrypt(string id, [FromServices] IHostingEnvironment env)
{
   id = Path.GetFileName(id); // security cleaning
   var file = Path.Combine(env.ContentRootPath, ".well-known", "acme-challenge", id);
   return PhysicalFile(file, "text/plain");
}
این اکشن متد را در هر کنترلری قرار دهید، تفاوتی نمی‌کند و کار خواهد کرد؛ چون attribute routing آن مستقل از محل قرارگیری آن است.
در MVC 5x پارامتر env را حذف و بجای آن از Server.MapPath و در آخر از return File استفاده کنید.
[Route(".well-known/acme-challenge/{id}")]
public ActionResult LetsEncrypt(string id)
{
   id = Path.GetFileName(id); // security cleaning
   var file = Path.Combine(Server.MapPath("~/.well-known/acme-challenge"), id);
   return File(file, "text/plain", id);
}
اگر این مرحله را تنظیم نکنید، در وسط کار دریافت مجوز توسط برنامه‌ی win-acme، به علت اینکه مشخص نیست آیا شما صاحب دامنه هستید یا خیر، خطایی را دریافت کرده و برنامه متوقف می‌شود.


آماده سازی IIS برای دریافت مجوزهای Let’s Encrypt

ابتدا به قسمت Edit bindings وب سایتی که قرار است مجوز را دریافت کند مراجعه کرده:


و سپس bindings مناسبی را ایجاد کنید (از نوع HTTP و نه HTTPS):


برای مثال اگر سایت شما قرار است توسط آدرس‌های www.dotnettips.info و dotnettips.info در دسترس باشد، نیاز است دو binding را در اینجا ثبت کنید. برای تمام موارد ثبت شده هم این تنظیمات را مدنظر داشته باشید:
Type:http
Port:80
IP address:All Unassigned
برنامه‌ی win-acme بر اساس این HTTP Bindings است که معادل‌های متناظر HTTPS آن‌ها را به صورت خودکار ثبت و تنظیم می‌کند.


اجرای برنامه‌ی win-acme بر روی ویندوز سرور 2008

IISهای 8 به بعد (و یا ویندوز سرور 2012 به بعد) دارای ویژگی هستند به نام Server Name Indication و یا SNI که اجاز می‌دهند بتوان چندین مجوز SSL را بر روی یک IP تنظیم کرد.


در اینجا به ازای هر Bindings تعریف شده‌ی در قسمت قبل، یک مجوز Let’s Encrypt دریافت خواهد شد. اما چون ویندوز سرور 2008 به همراه IIS 7.5 است، فاقد ویژگی SNI است. به همین جهت در حالت عادی برای مثال فقط برای www.dotnettips.info مجوزی را دریافت می‌کنید و اگر کاربر به آدرس dotnettips.info مراجعه کند، دیگر نمی‌تواند به سایت وارد شود و پیام غیرمعتبر بودن مجوز SSL را مشاهده خواهد کرد.
برنامه‌ی win-acme برای رفع این مشکل، از ویژگی خاصی به نام «SAN certificate» پشتیبانی می‌کند.
به این ترتیب با ویندوز سرور 2008 هم می‌توان دامنه‌ی اصلی و زیر دامنه‌های تعریف شده را نیز پوشش داد و سایت به این ترتیب بدون مشکل کار خواهد کرد. مراحل تنظیم SAN توسط برنامه‌ی win-acme به این صورت است:

ابتدا که برنامه‌ی win-acme را با دسترسی admin اجرا می‌کنید، چنین منویی نمایش داده می‌شود:
 N: Create new certificate
 M: Create new certificate with advanced options
 L: List scheduled renewals
 R: Renew scheduled
 S: Renew specific
 A: Renew *all*
 V: Revoke certificate
 C: Cancel scheduled renewal
 X: Cancel *all* scheduled renewals
 Q: Quit
گزینه‌ی N یا ایجاد مجوز جدید را انتخاب کنید.
سپس منوی بعدی را نمایش می‌دهد:
 [INFO] Running in Simple mode

 1: Single binding of an IIS site
 2: SAN certificate for all bindings of an IIS site
 3: SAN certificate for all bindings of multiple IIS sites
 4: Manually input host names
 C: Cancel
در این حالت برای ویندوز سرور 2008، فقط و فقط گزینه‌ی 2 را انتخاب کنید.
سپس لیست سایت‌های نصب شده‌ی در IIS را نمایش می‌دهد:
 1: Default Web Site
 C: Cancel

 Choose site: 1
در اینجا برای مثال شماره‌ی 1 یا هر شماره‌ی دیگر متناظر با وب سایت مدنظر را انتخاب کنید.
در ادامه منوی زیر را نمایش می‌دهد:
 * www.dotnettips.info
 * dotnettips.info

 Press enter to include all listed hosts, or type a comma-separated lists of exclusions:
لیستی را که در اینجا مشاهده می‌کنید، همان Bindings است که پیشتر ایجاد کردیم. عنوان می‌کند که برای کدامیک از این‌ها نیاز است مجوز دریافت و نصب شود. کلید enter را فشار دهید تا برای تمام آن‌ها اینکار صورت گیرد.
و ... همین! پس از آن کار دریافت، نصب و به روز رسانی Bindings در IIS به صورت خودکار انجام خواهد شد. اکنون اگر به قسمت Binding سایت مراجعه کنید، این تنظیمات خودکار جدید را مشاهده خواهید کرد:


اگر به لاگ نصب مجوزها دقت کنید این دو سطر نیز در انتهای آن ذکر می‌شوند:
 [INFO] Adding renewal for Default Web Site
 [INFO] Next renewal scheduled at 2018/7/21 4:19:20 AM
علت اینجا است که مجوزهای Let’s Encrypt طول عمر کمی دارند و در صورت به روز نشدن مداوم، کاربران دیگر قادر به مرور سایت نخواهند بود. به همین جهت این برنامه یک Scheduled Task ویندوز را نیز جهت به روز رسانی خودکار این مجوزها ایجاد و تنظیم می‌کند.


اجرای برنامه‌ی win-acme بر روی ویندوزهای سرور 2012 به بعد

چون IIS ویندوزهای سرور 2012 به بعد، از نصب و فعالسازی بیش از یک مجوز SSL به ازای یک IP به صورت توکار تحت عنوان ویژگی SNI پشتیبانی می‌کنند، مراحل انجام آن ساده‌تر هستند.
ابتدا که برنامه‌ی win-acme را با دسترسی admin اجرا می‌کنید، چنین منویی نمایش داده می‌شود:
 N: Create new certificate
 M: Create new certificate with advanced options
 L: List scheduled renewals
 R: Renew scheduled
 S: Renew specific
 A: Renew *all*
 V: Revoke certificate
 C: Cancel scheduled renewal
 X: Cancel *all* scheduled renewals
 Q: Quit
گزینه‌ی N یا ایجاد مجوز جدید را انتخاب کنید.
سپس منوی بعدی را نمایش می‌دهد:
 [INFO] Running in Simple mode

 1: Single binding of an IIS site
 2: SAN certificate for all bindings of an IIS site
 3: SAN certificate for all bindings of multiple IIS sites
 4: Manually input host names
 C: Cancel
در این حالت گزینه‌ی 4 را انتخاب کنید (با فرض اینکه از IIS 8.0 به بعد استفاده می‌کنید).
سپس از شما درخواست می‌کند که لیست دامنه و زیر دامنه‌هایی را که قرار است برای آن‌ها مجوز SSL صادر شوند، به صورت لیست جدا شده‌ی توسط کاما، وارد کنید:
 Enter comma-separated list of host names, starting with the primary one: dotnettips.info, www.dotnettips.info
در ادامه لیست وب سایت‌های ثبت شده‌ی در IIS را نمایش می‌دهد:
1: Default Web Site
2: mysiteName
Choose site to create new bindings: 1
در اینجا شماره‌ی سایتی را که می‌خواهید برای آن مجوز صادر شود، انتخاب کنید.
و ... همین! پس از آن مجوزهای SSL درخواستی، دریافت، نصب و تنظیم خواهند شد. همچنین یک Scheduled Task هم برای به روز رسانی خودکار آن تنظیم می‌شود.
مطالب
روش استفاده‌ی صحیح از HttpClient در برنامه‌های دات نت
اگر در کدهای خود قطعه کد ذیل را دارید:
using(var client = new HttpClient())
{
   // do something with http client
}
استفاده‌ی از using در اینجا، نه‌تنها غیرضروری و اشتباه است، بلکه سبب از کار افتادن زود هنگام برنامه‌ی شما با صدور استثنای ذیل خواهد شد:
 Unable to connect to the remote server
System.Net.Sockets.SocketException: Only one usage of each socket address (protocol/network address/port) is normally permitted.


HttpClient خود را Dispose نکنید

کلاس HttpClient اینترفیس IDisposable را پیاده سازی می‌کند. بنابراین روش استفاده‌ی اصولی آن باید به صورت ذیل و با پیاده سازی خودکار رهاسازی منابع مرتبط با آن باشد:
using (var client = new HttpClient())
{
       var result = await client.GetAsync("http://example.com/");
}
اما در این حال فرض کنید به همین روش تعدادی درخواست را ارسال کرده‌اید:
for (int i = 0; i < 10; i++)
{
      using (var client = new HttpClient())
      {
            var result = await client.GetAsync("http://example.com/");
            Console.WriteLine(result.StatusCode);
      }
}
مشکل این روش، در ایجاد سوکت‌های متعددی است که حتی پس از بسته شدن برنامه نیز باز، باقی خواهند ماند:
  TCP    192.168.1.6:13996      93.184.216.34:http     TIME_WAIT
  TCP    192.168.1.6:13997      93.184.216.34:http     TIME_WAIT
  TCP    192.168.1.6:13998      93.184.216.34:http     TIME_WAIT
  TCP    192.168.1.6:13999      93.184.216.34:http     TIME_WAIT
  TCP    192.168.1.6:14000      93.184.216.34:http     TIME_WAIT
  TCP    192.168.1.6:14001      93.184.216.34:http     TIME_WAIT
  TCP    192.168.1.6:14002      93.184.216.34:http     TIME_WAIT
  TCP    192.168.1.6:14003      93.184.216.34:http     TIME_WAIT
  TCP    192.168.1.6:14004      93.184.216.34:http     TIME_WAIT
  TCP    192.168.1.6:14005      93.184.216.34:http     TIME_WAIT
این یک نمونه‌ی خروجی برنامه‌ی فوق، توسط دستور netstat «پس از بسته شدن کامل برنامه» است.

بنابراین اگر برنامه‌ی شما تعداد زیادی کاربر دارد و یا تعداد زیادی درخواست را به روش فوق ارسال می‌کند، سیستم عامل به حد اشباع ایجاد سوکت‌های جدید خواهد رسید.
این مشکل نیز ارتباطی به طراحی این کلاس و یا زبان #C و حتی استفاده‌ی از using نیز ندارد. این رفتار، رفتار معمول سیستم عامل، با سوکت‌های ایجاد شده‌است. TIME_WAIT ایی را که در اینجا ملاحظه می‌کنید، به معنای بسته شدن اتصال از طرف برنامه‌ی ما است؛ اما سیستم عامل هنوز منتظر نتیجه‌ی نهایی، از طرف دیگر اتصال است که آیا قرار است بسته‌ی TCP ایی را دریافت کند یا خیر و یا شاید در بین راه تاخیری وجود داشته‌است. برای نمونه ویندوز به مدت 240 ثانیه یک اتصال را در این حالت حفظ خواهد کرد، که مقدار آن نیز در اینجا تنظیم می‌شود:
 [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\TcpTimedWaitDelay]

بنابراین روش توصیه شده‌ی کار با HttpClient، داشتن یک وهله‌ی سراسری از آن در برنامه و عدم Dispose آن است. HttpClient نیز thread-safe طراحی شده‌است و دسترسی به یک شیء سراسری آن در برنامه‌های چند ریسمانی مشکلی را ایجاد نمی‌کند. همچنین Dispose آن نیز غیرضروری است و پس از پایان برنامه به صورت خودکار توسط سیستم عامل انجام خواهد شد.


تمام اجزای HttpClient به صورت Thread-safe طراحی نشده‌اند

تا اینجا به این نتیجه رسیدیم که روش صحیح کار کردن با HttpClient، نیاز به داشتن یک وهله‌ی Singleton از آن‌را در سراسر برنامه دارد و Dispose صریح آن، بجز اشباع سوکت‌های سیستم عامل و ناپایدار کردن تمام برنامه‌هایی که از آن سرویس می‌گیرند، حاصلی را به همراه نخواهد داشت. در این بین مطابق مستندات HttpClient، استفاده‌ی از متدهای ذیل این کلاس thread-safe هستند:
CancelPendingRequests
DeleteAsync
GetAsync
GetByteArrayAsync
GetStreamAsync
GetStringAsync
PostAsync
PutAsync
SendAsync
اما تغییر این خواص در کلاس HttpClient به هیچ عنوان thread-safe نبوده و در برنامه‌های چند ریسمانی و چند کاربری، مشکل ساز می‌شوند:
BaseAddress
DefaultRequestHeaders
MaxResponseContentBufferSize
Timeout
بنابراین در طراحی کلاس مدیریت کننده‌ی HttpClient برنامه‌ی خود نیاز است به ازای هر BaseAddress‌، یک HttpClient خاص آن‌را ایجاد کرد و HttpClientهای سراسری نمی‌توانند BaseAddress‌های خود را نیز به اشتراک گذاشته و تغییری را در آن ایجاد کنند.


استفاده‌ی سراسری و مجدد از HttpClient، تغییرات DNS را متوجه نمی‌شود

با طراحی یک کلاس مدیریت کننده‌ی سراسری HttpClient با طول عمر Singelton، به یک مشکل دیگر نیز برخواهیم خورد: چون در اینجا از اتصالات، استفاده‌ی مجدد می‌شوند، دیگر تغییرات DNS را لحاظ نخواهند کرد.
برای حل این مشکل، در زمان ایجاد یک HttpClient سراسری، به ازای یک BaseAddress مشخص، باید از ServicePointManager کوئری گرفته و زمان اجاره‌ی اتصال آن‌را دقیقا مشخص کنیم:
var sp = ServicePointManager.FindServicePoint(new Uri("http://thisisasample.com"));
sp.ConnectionLeaseTimeout = 60*1000; //In milliseconds
با این‌کار هرچند هنوز هم از اتصالات استفاده‌ی مجدد می‌شود، اما این استفاده‌ی مجدد، نامحدود نبوده و مدت معینی را پیدا می‌کند.


طراحی یک کلاس، برای مدیریت سراسری وهله‌های HttpClient‌

تا اینجا به صورت خلاصه به نکات ذیل رسیدیم:
- HttpClient باید به صورت یک وهله‌ی سراسری Singleton مورد استفاده قرار گیرد. هر وهله سازی مجدد آن 35ms زمان می‌برد.
- Dispose یک HttpClient غیرضروری است.
- HttpClient تقریبا thread safe طراحی شده‌است؛ اما تعدادی از خواص آن مانند BaseAddress‌  اینگونه نیستند.
- برای رفع مشکل اتصالات چسبنده (اتصالاتی که هیچگاه پایان نمی‌یابند)، نیاز است timeout آن‌را تنظیم کرد.

بنابراین بهتر است این نکات را در یک کلاس به صورت ذیل کپسوله کنیم:
using System;
using System.Collections.Generic;
using System.Net.Http;

namespace HttpClientTips
{
    public interface IHttpClientFactory : IDisposable
    {
        HttpClient GetOrCreate(
            Uri baseAddress,
            IDictionary<string, string> defaultRequestHeaders = null,
            TimeSpan? timeout = null,
            long? maxResponseContentBufferSize = null,
            HttpMessageHandler handler = null);
    }
}

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading;

namespace HttpClientTips
{
    /// <summary>
    /// Lifetime of this class should be set to `Singleton`.
    /// </summary>
    public class HttpClientFactory : IHttpClientFactory
    {
        // 'GetOrAdd' call on the dictionary is not thread safe and we might end up creating the HttpClient more than
        // once. To prevent this Lazy<> is used. In the worst case multiple Lazy<> objects are created for multiple
        // threads but only one of the objects succeeds in creating the HttpClient.
        private readonly ConcurrentDictionary<Uri, Lazy<HttpClient>> _httpClients =
                         new ConcurrentDictionary<Uri, Lazy<HttpClient>>();
        private const int ConnectionLeaseTimeout = 60 * 1000; // 1 minute

        public HttpClientFactory()
        {
            // Default is 2 minutes: https://msdn.microsoft.com/en-us/library/system.net.servicepointmanager.dnsrefreshtimeout(v=vs.110).aspx
            ServicePointManager.DnsRefreshTimeout = (int)TimeSpan.FromMinutes(1).TotalMilliseconds;
            // Increases the concurrent outbound connections
            ServicePointManager.DefaultConnectionLimit = 1024;
        }

        public HttpClient GetOrCreate(
           Uri baseAddress,
           IDictionary<string, string> defaultRequestHeaders = null,
           TimeSpan? timeout = null,
           long? maxResponseContentBufferSize = null,
           HttpMessageHandler handler = null)
        {
            return _httpClients.GetOrAdd(baseAddress,
                             uri => new Lazy<HttpClient>(() =>
                             {
                                 // Reusing a single HttpClient instance across a multi-threaded application means
                                 // you can't change the values of the stateful properties (which are not thread safe),
                                 // like BaseAddress, DefaultRequestHeaders, MaxResponseContentBufferSize and Timeout.
                                 // So you can only use them if they are constant across your application and need their own instance if being varied.
                                 var client = handler == null ? new HttpClient { BaseAddress = baseAddress } :
                                               new HttpClient(handler, disposeHandler: false) { BaseAddress = baseAddress };
                                 setRequestTimeout(timeout, client);
                                 setMaxResponseBufferSize(maxResponseContentBufferSize, client);
                                 setDefaultHeaders(defaultRequestHeaders, client);
                                 setConnectionLeaseTimeout(baseAddress, client);
                                 return client;
                             },
                             LazyThreadSafetyMode.ExecutionAndPublication)).Value;
        }

        public void Dispose()
        {
            foreach (var httpClient in _httpClients.Values)
            {
                httpClient.Value.Dispose();
            }
        }

        private static void setConnectionLeaseTimeout(Uri baseAddress, HttpClient client)
        {
            // This ensures connections are used efficiently but not indefinitely.
            client.DefaultRequestHeaders.ConnectionClose = false; // keeps the connection open -> more efficient use of the client
            ServicePointManager.FindServicePoint(baseAddress).ConnectionLeaseTimeout = ConnectionLeaseTimeout; // ensures connections are not used indefinitely.
        }

        private static void setDefaultHeaders(IDictionary<string, string> defaultRequestHeaders, HttpClient client)
        {
            if (defaultRequestHeaders == null)
            {
                return;
            }
            foreach (var item in defaultRequestHeaders)
            {
                client.DefaultRequestHeaders.Add(item.Key, item.Value);
            }
        }

        private static void setMaxResponseBufferSize(long? maxResponseContentBufferSize, HttpClient client)
        {
            if (maxResponseContentBufferSize.HasValue)
            {
                client.MaxResponseContentBufferSize = maxResponseContentBufferSize.Value;
            }
        }

        private static void setRequestTimeout(TimeSpan? timeout, HttpClient client)
        {
            if (timeout.HasValue)
            {
                client.Timeout = timeout.Value;
            }
        }
    }
}
در اینجا به ازای هر baseAddress جدید، یک HttpClient خاص آن ایجاد می‌شود تا در کل برنامه مورد استفاده‌ی مجدد قرار گیرد. برای مدیریت thread-safe ایجاد HttpClientها نیز از نکته‌ی مطلب «الگویی برای مدیریت دسترسی همزمان به ConcurrentDictionary» استفاده شده‌است. همچنین نکات تنظیم ConnectionLeaseTimeout و سایر خواص غیر thread-safe کلاس HttpClient نیز در اینجا لحاظ شده‌اند.

پس از تدارک این کلاس، نحوه‌ی معرفی آن به سیستم باید به صورت Singleton باشد. برای مثال اگر از ASP.NET Core استفاده می‌کنید، آن‌را به صورت ذیل ثبت کنید:
namespace HttpClientTips.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<IHttpClientFactory, HttpClientFactory>();
            services.AddMvc();
        }

اکنون، یک نمونه، نحوه‌ی استفاده‌ی از اینترفیس IHttpClientFactory تزریقی به صورت ذیل می‌باشد:
namespace HttpClientTips.Web.Controllers
{
    public class HomeController : Controller
    {
        private readonly IHttpClientFactory _httpClientFactory;
        public HomeController(IHttpClientFactory httpClientFactory)
        {
            _httpClientFactory = httpClientFactory;
        }

        public async Task<IActionResult> Index()
        {
            var host = new Uri("http://localhost:5000");
            var httpClient = _httpClientFactory.GetOrCreate(host);
            var responseMessage = await httpClient.GetAsync("home/about").ConfigureAwait(false);
            var responseContent = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
            return Content(responseContent);
        }
سرویس IHttpClientFactory یک HttpClient را به ازای host درخواستی ایجاد کرده و در طول عمر برنامه از آن استفاده‌ی مجدد می‌کند. به همین جهت دیگر مشکل اشباع سوکت‌ها در این سیستم رخ نخواهند داد.


برای مطالعه‌ی بیشتر

You're using HttpClient wrong and it is destabilizing your software
Disposable, Finalizers, and HttpClient
Using HttpClient as it was intended (because you’re not)
Singleton HttpClient? Beware of this serious behaviour and how to fix it
Beware of the .NET HttpClient
Effectively Using HttpClient
مطالب
احراز هویت و اعتبارسنجی کاربران در برنامه‌های Angular - قسمت ششم - کار با منابع محافظت شده‌ی سمت سرور
پس از تکمیل کنترل دسترسی‌ها به قسمت‌های مختلف برنامه بر اساس نقش‌های انتسابی به کاربر وارد شده‌ی به سیستم، اکنون نوبت به کار با سرور و دریافت اطلاعات از کنترلرهای محافظت شده‌ی آن است.



افزودن کامپوننت دسترسی به منابع محافظت شده، به ماژول Dashboard

در اینجا قصد داریم صفحه‌ای را به برنامه اضافه کنیم تا در آن بتوان اطلاعات کنترلرهای محافظت شده‌ی سمت سرور، مانند MyProtectedAdminApiController (تنها قابل دسترسی توسط کاربرانی دارای نقش Admin) و MyProtectedApiController (قابل دسترسی برای عموم کاربران وارد شده‌ی به سیستم) را دریافت و نمایش دهیم. به همین جهت کامپوننت جدیدی را به ماژول Dashboard اضافه می‌کنیم:
 >ng g c Dashboard/CallProtectedApi
سپس به فایل dashboard-routing.module.ts ایجاد شده مراجعه کرده و مسیریابی کامپوننت جدید ProtectedPage را اضافه می‌کنیم:
import { CallProtectedApiComponent } from "./call-protected-api/call-protected-api.component";

const routes: Routes = [
  {
    path: "callProtectedApi",
    component: CallProtectedApiComponent,
    data: {
      permission: {
        permittedRoles: ["Admin", "User"],
        deniedRoles: null
      } as AuthGuardPermission
    },
    canActivate: [AuthGuard]
  }
];
توضیحات AuthGuard و AuthGuardPermission را در قسمت قبل مطالعه کردید. در اینجا هدف این است که تنها کاربران دارای نقش‌های Admin و یا User قادر به دسترسی به این مسیر باشند.
لینکی را به این صفحه نیز در فایل header.component.html به صورت ذیل اضافه خواهیم کرد تا فقط توسط کاربران وارد شده‌ی به سیستم (isLoggedIn) قابل مشاهده باشد:
<li *ngIf="isLoggedIn" routerLinkActive="active">
        <a [routerLink]="['/callProtectedApi']">‍‍Call Protected Api</a>
</li>


نمایش و یا مخفی کردن قسمت‌های مختلف صفحه بر اساس نقش‌های کاربر وارد شده‌ی به سیستم

در ادامه می‌خواهیم دو دکمه را بر روی صفحه قرار دهیم تا اطلاعات کنترلرهای محافظت شده‌ی سمت سرور را بازگشت دهند. دکمه‌ی اول قرار است تنها برای کاربر Admin قابل مشاهده باشد و دکمه‌ی دوم توسط کاربری با نقش‌های Admin و یا User.
به همین جهت call-protected-api.component.ts را به صورت ذیل تغییر می‌دهیم:
import { Component, OnInit } from "@angular/core";
import { AuthService } from "../../core/services/auth.service";

@Component({
  selector: "app-call-protected-api",
  templateUrl: "./call-protected-api.component.html",
  styleUrls: ["./call-protected-api.component.css"]
})
export class CallProtectedApiComponent implements OnInit {

  isAdmin = false;
  isUser = false;
  result: any;

  constructor(private authService: AuthService) { }

  ngOnInit() {
    this.isAdmin = this.authService.isAuthUserInRole("Admin");
    this.isUser = this.authService.isAuthUserInRole("User");
  }

  callMyProtectedAdminApiController() {
  }

  callMyProtectedApiController() {
  }
}
در اینجا دو خاصیت عمومی isAdmin و isUser، در اختیار قالب این کامپوننت قرار گرفته‌اند. مقدار دهی آن‌ها نیز توسط متد isAuthUserInRole که در قسمت قبل توسعه دادیم، انجام می‌شود. اکنون که این دو خاصیت مقدار دهی شده‌اند، می‌توان از آن‌ها به کمک یک ngIf، به صورت ذیل در قالب call-protected-api.component.html جهت مخفی کردن و یا نمایش قسمت‌های مختلف صفحه استفاده کرد:
<button *ngIf="isAdmin" (click)="callMyProtectedAdminApiController()">
  Call Protected Admin API [Authorize(Roles = "Admin")]
</button>

<button *ngIf="isAdmin || isUser" (click)="callMyProtectedApiController()">
  Call Protected API ([Authorize])
</button>

<div *ngIf="result">
  <pre>{{result | json}}</pre>
</div>


دریافت اطلاعات از کنترلرهای محافظت شده‌ی سمت سرور

برای دریافت اطلاعات از کنترلرهای محافظت شده، باید در قسمتی که HttpClient درخواست خود را به سرور ارسال می‌کند، هدر مخصوص Authorization را که شامل توکن دسترسی است، به سمت سرور ارسال کرد. این هدر ویژه را به صورت ذیل می‌توان در AuthService تولید نمود:
  getBearerAuthHeader(): HttpHeaders {
    return new HttpHeaders({
      "Content-Type": "application/json",
      "Authorization": `Bearer ${this.getRawAuthToken(AuthTokenType.AccessToken)}`
    });
  }

روش دوم انجام اینکار که مرسوم‌تر است، اضافه کردن خودکار این هدر به تمام درخواست‌های ارسالی به سمت سرور است. برای اینکار باید یک HttpInterceptor را تهیه کرد. به همین منظور فایل جدید app\core\services\auth.interceptor.ts را به برنامه اضافه کرده و به صورت ذیل تکمیل می‌کنیم:
import { Injectable } from "@angular/core";
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from "@angular/common/http";
import { Observable } from "rxjs/Observable";

import { AuthService, AuthTokenType } from "./auth.service";

@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  constructor(private authService: AuthService) { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

    const accessToken = this.authService.getRawAuthToken(AuthTokenType.AccessToken);
    if (accessToken) {
      request = request.clone({
        headers: request.headers.set("Authorization", `Bearer ${accessToken}`)
      });
    }

    return next.handle(request);
  }
}
در اینجا یک clone از درخواست جاری ایجاد شده و سپس به headers آن، یک هدر جدید Authorization که به همراه توکن دسترسی است، اضافه خواهد شد.
به این ترتیب دیگری نیازی نیست تا به ازای هر درخواست و هر قسمتی از برنامه، این هدر را به صورت دستی تنظیم کرد و اضافه شدن آن پس از تنظیم ذیل، به صورت خودکار انجام می‌شود:
import { HTTP_INTERCEPTORS } from "@angular/common/http";

import { AuthInterceptor } from "./services/auth.interceptor";

@NgModule({
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthInterceptor,
      multi: true
    }
  ]
})
export class CoreModule {}
در اینجا نحوه‌ی معرفی این HttpInterceptor جدید را به قسمت providers مخصوص CoreModule مشاهده می‌کنید.

در این حالت اگر برنامه را اجرا کنید، خطای ذیل را در کنسول توسعه‌دهنده‌های مرورگر مشاهده خواهید کرد:
compiler.js:19514 Uncaught Error: Provider parse errors:
Cannot instantiate cyclic dependency! InjectionToken_HTTP_INTERCEPTORS ("[ERROR ->]"): in NgModule AppModule in ./AppModule@-1:-1
در سازنده‌ی کلاس سرویس AuthInterceptor، سرویس Auth تزریق شده‌است که این سرویس نیز دارای HttpClient تزریق شده‌ی در سازنده‌ی آن است. به همین جهت Angular تصور می‌کند که ممکن است در اینجا یک بازگشت بی‌نهایت بین این interceptor و سرویس Auth رخ‌دهد. اما از آنجائیکه ما هیچکدام از متدهایی را که با HttpClient کار می‌کنند، در اینجا فراخوانی نمی‌کنیم و تنها کاربرد سرویس Auth، دریافت توکن دسترسی است، این مشکل را می‌توان به صورت ذیل برطرف کرد:
import { Injector } from "@angular/core";

  constructor(private injector: Injector) { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const authService = this.injector.get(AuthService);
ابتدا سرویس Injector را به سازنده‌ی کلاس AuthInterceptor تزریق می‌کنیم و سپس توسط متد get آن، سرویس Auth را درخواست خواهیم کرد (بجای تزریق مستقیم آن در سازنده‌ی کلاس):
@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  constructor(private injector: Injector) { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const authService = this.injector.get(AuthService);
    const accessToken = authService.getRawAuthToken(AuthTokenType.AccessToken);
    if (accessToken) {
      request = request.clone({
        headers: request.headers.set("Authorization", `Bearer ${accessToken}`)
      });
    }

    return next.handle(request);
  }
}


تکمیل متدهای دریافت اطلاعات از کنترلرهای محافظت شده‌ی سمت سرور

اکنون پس از افزودن AuthInterceptor، می‌توان متدهای CallProtectedApiComponent را به صورت ذیل تکمیل کرد. ابتدا سرویس‌های Auth ،HttpClient و همچنین تنظیمات آغازین برنامه را به سازنده‌ی CallProtectedApiComponent تزریق می‌کنیم:
  constructor(
    private authService: AuthService,
    private httpClient: HttpClient,
    @Inject(APP_CONFIG) private appConfig: IAppConfig,
  ) { }
سپس متدهای httpClient.get و یا هر نوع متد مشابه دیگری را به صورت معمولی فراخوانی خواهیم کرد:
  callMyProtectedAdminApiController() {
    this.httpClient
      .get(`${this.appConfig.apiEndpoint}/MyProtectedAdminApi`)
      .map(response => response || {})
      .catch((error: HttpErrorResponse) => Observable.throw(error))
      .subscribe(result => {
        this.result = result;
      });
  }

  callMyProtectedApiController() {
    this.httpClient
      .get(`${this.appConfig.apiEndpoint}/MyProtectedApi`)
      .map(response => response || {})
      .catch((error: HttpErrorResponse) => Observable.throw(error))
      .subscribe(result => {
        this.result = result;
      });
  }

در این حالت اگر برنامه را اجرا کنید، افزوده شدن خودکار هدر مخصوص Authorization:Bearer را در درخواست ارسالی به سمت سرور، مشاهده خواهید کرد:



مدیریت خودکار خطاهای عدم دسترسی ارسال شده‌ی از سمت سرور

ممکن است کاربری درخواستی را به منبع محافظت شده‌ای ارسال کند که به آن دسترسی ندارد. در AuthInterceptor تعریف شده می‌توان به وضعیت این خطا، دسترسی یافت و سپس کاربر را به صفحه‌ی accessDenied که در قسمت قبل ایجاد کردیم، به صورت خودکار هدایت کرد:
    return next.handle(request)
      .catch((error: any, caught: Observable<HttpEvent<any>>) => {
        if (error.status === 401 || error.status === 403) {
          this.router.navigate(["/accessDenied"]);
        }
        return Observable.throw(error);
      });
در اینجا ابتدا نیاز است سرویس Router، به سازنده‌ی کلاس تزریق شود و سپس متد catch درخواست پردازش شده، به صورت فوق جهت عکس العمل نشان دادن به وضعیت‌های 401 و یا 403 و هدایت کاربر به مسیر accessDenied تغییر کند:
@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  constructor(
    private injector: Injector,
    private router: Router) { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const authService = this.injector.get(AuthService);
    const accessToken = authService.getRawAuthToken(AuthTokenType.AccessToken);
    if (accessToken) {
      request = request.clone({
        headers: request.headers.set("Authorization", `Bearer ${accessToken}`)
      });
      return next.handle(request)
        .catch((error: any, caught: Observable<HttpEvent<any>>) => {
          if (error.status === 401 || error.status === 403) {
            this.router.navigate(["/accessDenied"]);
          }
          return Observable.throw(error);
        });
    } else {
      // login page
      return next.handle(request);
    }
  }
}
برای آزمایش آن، یک کنترلر سمت سرور جدید را با نقش Editor اضافه می‌کنیم:
using ASPNETCore2JwtAuthentication.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;

namespace ASPNETCore2JwtAuthentication.WebApp.Controllers
{
    [Route("api/[controller]")]
    [EnableCors("CorsPolicy")]
    [Authorize(Policy = CustomRoles.Editor)]
    public class MyProtectedEditorsApiController : Controller
    {
        public IActionResult Get()
        {
            return Ok(new
            {
                Id = 1,
                Title = "Hello from My Protected Editors Controller! [Authorize(Policy = CustomRoles.Editor)]",
                Username = this.User.Identity.Name
            });
        }
    }
}
و برای فراخوانی سمت کلاینت آن در CallProtectedApiComponent خواهیم داشت:
  callMyProtectedEditorsApiController() {
    this.httpClient
      .get(`${this.appConfig.apiEndpoint}/MyProtectedEditorsApi`)
      .map(response => response || {})
      .catch((error: HttpErrorResponse) => Observable.throw(error))
      .subscribe(result => {
        this.result = result;
      });
  }
چون این نقش جدید به کاربر جاری انتساب داده نشده‌است (جزو اطلاعات سمت سرور او نیست)، اگر آن‌را توسط متد فوق فراخوانی کند، خطای 403 را دریافت کرده و به صورت خودکار به مسیر accessDenied هدایت می‌شود:



نکته‌ی مهم: نیاز به دائمی کردن کلیدهای رمزنگاری سمت سرور

اگر برنامه‌ی سمت سرور ما که توکن‌ها را اعتبارسنجی می‌کند، ری‌استارت شود، چون قسمتی از کلیدهای رمزگشایی اطلاعات آن با اینکار مجددا تولید خواهند شد، حتی با فرض لاگین بودن شخص در سمت کلاینت، توکن‌های فعلی او برگشت خواهند خورد و از مرحله‌ی تعیین اعتبار رد نمی‌شوند. در این حالت کاربر خطای 401 را دریافت می‌کند. بنابراین پیاده سازی مطلب «غیرمعتبر شدن کوکی‌های برنامه‌های ASP.NET Core هاست شده‌ی در IIS پس از ری‌استارت آن» را فراموش نکنید.



کدهای کامل این سری را از اینجا می‌توانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه‌ی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشه‌ی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود. 
مطالب
ویژگی Batching در EF Core
در EF 6.x به ازای هر عبارت insert/update/delete یکبار رفت و برگشت به بانک اطلاعاتی صورت می‌گیرد. به همین جهت کارآیی تعداد بالای ثبت، به روز رسانی و حذف رکوردها توسط آن پایین است. برای رفع این مشکل ویژگی Batching به EF Core اضافه شده‌است که توسط آن اینبار دسته‌ای از عبارات را به صورت یکجا و در طی یک رفت و برگشت، به سمت بانک اطلاعاتی ارسال می‌کند. به این ترتیب کارآیی و سرعت insert/update/delete آن به شدت افزایش خواهد یافت.


نحوه‌ی فعالسازی Batching در EF Core

Batching به صورت پیش فرض در EF Core بدون نیاز به هیچگونه تنظیم اضافه‌تری فعال است. اما اگر خواستید برای مثال، حالت پیش فرض EF 6.x را توسط آن شبیه سازی کنید، می‌توانید مقدار MaxBatchSize را به عدد 1 تنظیم نمائید (تا غیرفعال شود):
optionsBuilder.UseSqlServer(
   @"Server=(localdb)\mssqllocaldb;Database=Demo.Batching;Trusted_Connection=True;",
   options => options.MaxBatchSize(1)
);

مقدار پیش فرض MaxBatchSize را در کلاس SqlServerModificationCommandBatch می‌توانید مشاهده کنید:
public class SqlServerModificationCommandBatch : AffectedCountModificationCommandBatch
    {
        private const int DefaultNetworkPacketSizeBytes = 4096;
        private const int MaxScriptLength = 65536 * DefaultNetworkPacketSizeBytes / 2;
        private const int MaxParameterCount = 2100;
        private const int MaxRowCount = 1000;
در اینجا MaxRowCount همان MaxBatchSize پیش فرض است که به عدد 1000 تنظیم شده‌است. بنابراین اگر تنظیم options => options.MaxBatchSize(1) را ذکر نکنید، به معنای ارسال 1000 تایی دستورات insert/update/delete در طی یک درخواست به سمت سرور است.


آیا محدودیتی هم در مورد عملیات Batching وجود دارد؟

SQL Server به ازای هر batch تنها 2100 پارامتر را پشتیبانی می‌کند. در این حالت EF Core به صورت خودکار یک چنین کوئری‌های حجیمی را به چند Batch جهت تنظیم این محدودیت تقسیم خواهد کرد و در نهایت برنامه به مشکلی بر نمی‌خورد.


یک آزمایش: Batching پیش فرض به چه صورتی کار می‌کند و چه اثری را دارد؟

کدهای کامل این آزمایش را از اینجا می‌توانید دریافت کنید: Batching.zip
در اینجا کلاس Blog را به همراه Context متناظر با آن مشاهده می‌کنید:
    public class Blog
    {
        public int BlogId { get; set; }
        public string Name { get; set; }
        public string Url { get; set; }
    }

    public class BloggingContext : DbContext
    {
        public DbSet<Blog> Blogs { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(
                @"Server=(localdb)\mssqllocaldb;Database=Demo.Batching;Trusted_Connection=True;"/*,
                options => options.MaxBatchSize(2)*/
                );
            optionsBuilder.EnableSensitiveDataLogging();
        }
    }
در ابتدا MaxBatchSize را تنظیم نخواهیم کرد. یعنی از همان عدد 1000 پیش فرض استفاده می‌شود. تنظیم EnableSensitiveDataLogging نیز سبب می‌شود تا لاگ نهایی تهیه شده جهت نمایش، پرمحتواتر شود.
در این حالت اگر به روز رسانی‌ها (2 مورد) و ثبت‌های ذیل (6 مورد) را انجام دهیم:
            using (var db = new BloggingContext())
            {
                db.GetService<ILoggerFactory>().AddProvider(new MyLoggerProvider());

                // Modify some existing blogs
                var existing = db.Blogs.ToArray();
                existing[0].Url = "http://sample.com/blogs/dogs";
                existing[1].Url = "http://sample.com/blogs/cats";

                // Insert some new blogs
                db.Blogs.Add(new Blog { Name = "The Horse Blog", Url = "http://sample.com/blogs/horses" });
                db.Blogs.Add(new Blog { Name = "The Snake Blog", Url = "http://sample.com/blogs/snakes" });
                db.Blogs.Add(new Blog { Name = "The Fish Blog", Url = "http://sample.com/blogs/fish" });
                db.Blogs.Add(new Blog { Name = "The Koala Blog", Url = "http://sample.com/blogs/koalas" });
                db.Blogs.Add(new Blog { Name = "The Parrot Blog", Url = "http://sample.com/blogs/parrots" });
                db.Blogs.Add(new Blog { Name = "The Kangaroo Blog", Url = "http://sample.com/blogs/kangaroos" });

                db.SaveChanges();
            }
یک چنین خروجی SQL ایی تولید می‌شود:
Executed DbCommand (41ms) [Parameters=[@p1='57', @p0='http://sample.com/blogs/dogs' (Size = 4000), @p3='58', @p2='http://sample.com/blogs/cats' (Size = 4000), @p4='The Horse Blog' (Size = 4000), @p5='http://sample.com/blogs/horses' (Size = 4000), @p6='The Snake Blog' (Size = 4000), @p7='http://sample.com/blogs/snakes' (Size = 4000), @p8='The Fish Blog' (Size = 4000), @p9='http://sample.com/blogs/fish' (Size = 4000), @p10='The Koala Blog' (Size = 4000), @p11='http://sample.com/blogs/koalas' (Size = 4000), @p12='The Parrot Blog' (Size = 4000), @p13='http://sample.com/blogs/parrots' (Size = 4000), @p14='The Kangaroo Blog' (Size = 4000), @p15='http://sample.com/blogs/kangaroos' (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
UPDATE [Blogs] SET [Url] = @p0
WHERE [BlogId] = @p1;
SELECT @@ROWCOUNT;

UPDATE [Blogs] SET [Url] = @p2
WHERE [BlogId] = @p3;
SELECT @@ROWCOUNT;

DECLARE @inserted2 TABLE ([BlogId] int, [_Position] [int]);
MERGE [Blogs] USING (
VALUES (@p4, @p5, 0),
(@p6, @p7, 1),
(@p8, @p9, 2),
(@p10, @p11, 3),
(@p12, @p13, 4),
(@p14, @p15, 5)) AS i ([Name], [Url], _Position) ON 1=0
WHEN NOT MATCHED THEN
INSERT ([Name], [Url])
VALUES (i.[Name], i.[Url])
OUTPUT INSERTED.[BlogId], i._Position
INTO @inserted2;

SELECT [t].[BlogId] FROM [Blogs] t
INNER JOIN @inserted2 i ON ([t].[BlogId] = [i].[BlogId])
ORDER BY [i].[_Position];
در این دستورات موارد ذیل قابل توجه هستند:
- فقط یکبار Executed DbCommand مشاهده می‌شود.
- کل دستورات update و insert در طی یک درخواست و یک تراکنش به سمت بانک اطلاعاتی ارسال شده‌اند.
- ثبت دسته‌ای توسط merge using انجام شده‌است.
- در آخر نیز طبق معمول کار EF، شماره Idهای رکوردهای ثبت شده به سمت کلاینت بازگشت داده می‌شود.

در ادامه MaxBatchSize را به عدد 2 تنظیم می‌کنیم:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
   optionsBuilder.UseSqlServer(
     @"Server=(localdb)\mssqllocaldb;Database=Demo.Batching;Trusted_Connection=True;",
     options => options.MaxBatchSize(2)
    );
    optionsBuilder.EnableSensitiveDataLogging();
}
در این حالت اگر برنامه را اجرا کنیم، یک چنین خروجی قابل مشاهده خواهد بود:
Executed DbCommand (17ms) [Parameters=[@p1='65', @p0='http://sample.com/blogs/dogs' (Size = 4000), @p3='66', @p2='http://sample.com/blogs/cats' (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
UPDATE [Blogs] SET [Url] = @p0
WHERE [BlogId] = @p1;
SELECT @@ROWCOUNT;

UPDATE [Blogs] SET [Url] = @p2
WHERE [BlogId] = @p3;
SELECT @@ROWCOUNT;

Executed DbCommand (18ms) [Parameters=[@p0='The Horse Blog' (Size = 4000), @p1='http://sample.com/blogs/horses' (Size = 4000), @p2='The Snake Blog' (Size = 4000), @p3='http://sample.com/blogs/snakes' (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
DECLARE @inserted0 TABLE ([BlogId] int, [_Position] [int]);
MERGE [Blogs] USING (
VALUES (@p0, @p1, 0),
(@p2, @p3, 1)) AS i ([Name], [Url], _Position) ON 1=0
WHEN NOT MATCHED THEN
INSERT ([Name], [Url])
VALUES (i.[Name], i.[Url])
OUTPUT INSERTED.[BlogId], i._Position
INTO @inserted0;

SELECT [t].[BlogId] FROM [Blogs] t
INNER JOIN @inserted0 i ON ([t].[BlogId] = [i].[BlogId])
ORDER BY [i].[_Position];

Executed DbCommand (34ms) [Parameters=[@p0='The Fish Blog' (Size = 4000), @p1='http://sample.com/blogs/fish' (Size = 4000), @p2='The Koala Blog' (Size = 4000), @p3='http://sample.com/blogs/koalas' (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
DECLARE @inserted0 TABLE ([BlogId] int, [_Position] [int]);
MERGE [Blogs] USING (
VALUES (@p0, @p1, 0),
(@p2, @p3, 1)) AS i ([Name], [Url], _Position) ON 1=0
WHEN NOT MATCHED THEN
INSERT ([Name], [Url])
VALUES (i.[Name], i.[Url])
OUTPUT INSERTED.[BlogId], i._Position
INTO @inserted0;

SELECT [t].[BlogId] FROM [Blogs] t
INNER JOIN @inserted0 i ON ([t].[BlogId] = [i].[BlogId])
ORDER BY [i].[_Position];

Executed DbCommand (15ms) [Parameters=[@p0='The Parrot Blog' (Size = 4000), @p1='http://sample.com/blogs/parrots' (Size = 4000), @p2='The Kangaroo Blog' (Size = 4000), @p3='http://sample.com/blogs/kangaroos' (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
DECLARE @inserted0 TABLE ([BlogId] int, [_Position] [int]);
MERGE [Blogs] USING (
VALUES (@p0, @p1, 0),
(@p2, @p3, 1)) AS i ([Name], [Url], _Position) ON 1=0
WHEN NOT MATCHED THEN
INSERT ([Name], [Url])
VALUES (i.[Name], i.[Url])
OUTPUT INSERTED.[BlogId], i._Position
INTO @inserted0;

SELECT [t].[BlogId] FROM [Blogs] t
INNER JOIN @inserted0 i ON ([t].[BlogId] = [i].[BlogId])
ORDER BY [i].[_Position];
در این دستورات موارد ذیل قابل توجه هستند:
- اینبار تعداد 4 دستور Executed DbCommand مشاهده می‌شود ( برای انجام 2 به روز رسانی و 6 ثبت).
- هر batch بر اساس تنظیم MaxBatchSize به 2 دستور T-SQL محدود شده‌است که البته در انتها در حالت‌های insert، یک select هم برای بازگشت Idها به سمت کلاینت وجود دارد.
بنابراین اینبار بجای یکبار رفت و برگشت حالت قبل (استفاده از مقدار پیش فرض 1000 برای MaxBatchSize)، 4 بار رفت و برگشت به سمت بانک اطلاعاتی صورت گرفته‌است.

زمان کل انجام عملیات در حالت اول 41 میلی ثانیه و در حالت دوم 84 میلی ثانیه است که سرعت آن 51 درصد نسبت به حالت اول کاهش یافته‌است.
مطالب
طراحی شیء گرا: OO Design Heuristics - قسمت چهارم

Dynamic Semantics

Objectها علاوه بر داده و رفتار به عنوان توصیفات ثابت، در زمان اجرا دارای یک Local State (‏‏a snapshot) از مقادیر داینامیک مربوط به اعضای داده‌ای خود، می‌باشند. مجموعه تمام حالاتی که وهله‌های یک کلاس می‌توانند بین آنها گذر (transition) داشته باشد، dynamic semantics مربوط به کلاس نامیده می‌شود و به وهله‌های کلاس این امکان را می‌دهند تا به یک پیغام مشابه رسیده و در زمان‌های مختلف از چرخه زندگی خود، به اشکال مختلف پاسخ دهند.

Method junk for the class X 
if (local state #1) then
do something
else if (local state #2) then
do something different
End Method

بخش اصلی هر طراحی شیء گرا، dynamic semantics وهله‌ها می‌باشد. dynamic semantics هر کلاسی باید در قالب یک دیاگرام state-transition مستند شود. شکل زیر dynamic semantics پروسه‌های موجود در یک سیستم عامل را در قابل یک دیاگرام حالت نمایش می‌دهد. این پروسه‌ها توانایی این را دارند که در هر کدام از حالات: runnable، current process، blocked، sleeping و یا در حالت exited، قرار داشته باشند. همچنین به عنوان مثال، یک پروسه زمانی می‌تواند در حالت current process قرار گیرد که حتما قبلا در حالت runnable قرار داشته باشد. این اطلاعات برای ایجاد تست برای کلاس‌ها و وهله‌های آنها می‌تواند مفید واقع شود.

شکل 2.8 State-transition diagram notation 

برخی از طراحان به طور تصادفی، dynamic semantics یک کلاس را به عنوان static semantics آن کلاس مدل می‌کنند. به عنوان مثال اگر color یکی از اعضای داده ای (data member) کلاس توپ باشد و بعد از وهله سازی از کلاس توپ، color آن بازهم قابل تغییر باشد، منظور اینکه توپ آبی به عنوان یک وهله از کلاس توپ در زمان حیات خود تغییر رنگ دهد، اصطلاحا می‌گویند: color جزء dynamic semantics کلاس توپ می‌باشد. با توجه به توضحیاتی که داده شد، حال اگر طراحی برای هر رنگ توپ یک کلاس جدا در نظر گرفته باشد، dynamic semantics را به عنوان static semantics مدل کرده و به احتمال زیاد ما را به سمت ایجاد مشکل Class Proliferation (ازدیاد کلاس ها) سوق خواهد داد.

Abstract Classes

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

  • آیا هرگز میوه خورده‌اید؟
  • آیا هرگز پیش غذا خورده‌اید؟ 
  • آیا هرگز دسر خورده‌اید؟ 
اکثر مردم به این سوالات جواب «بله» را خواهند داد.
حال با توجه به سوالات «مزه غذا چطور بود؟ دسری که خوردید، چه تعداد کالری داشت؟ هزینه پیش غذایی که خوردید چقدر بود» پاسخ چه خواهد بود؟
من (نویسنده) ادعا میکنم که هیچ کسی تا به حال میوه نخورده است. بیشتر مردم، سیب، موز و پرتقال خورده‌اند؛ میوه‌ی قرمز رنگی به ارزش 3 پوند را نخورده‌اند.

شبیه به این مسئله برای زمانی است که گارسون رستوران از شما سوال می‌کند: «برای شام چه چیزی میل دارید» و شما جواب می‌دهید: «یک پیش غذا، یک غذای اصلی و یک دسر». در این حالت چون شما دقیقا مشخص نکرده‌اید چه نوعی می‌خواهید، گارسون، مات و مبهوت خواهد ماند. همه می‌دانیم که چیزی تحت عنوان میوه، پیش غذا و یا وهله دسر در واقعیت وجود ندارد؛ بله این عبارات اطلاعات مفیدی را تسخیر می‌کنند. اگر من در دستم یک ساعت زنگی گرفته و از شما می‌پرسیدم: «نظرتان در مورد میوه من چیست؟»؛ بدون شک فکر می‌کردید من دیوانه شده‌ام. حال اگر در دستم سیبی گرفته و سوال قبلی را می‌پرسیدم، این بار از نظر شما من یک شخص عاقل بودم.
با وجود اینکه نمی‌توان از میوه وهله سازی کرد، اما اطلاعات مفیدی را تسخیر می‌کند. در واقع میوه، یک کلاسی (concept) است که دانشی از نحوه وهله سازی وهله هایش به وسیله Type پیاده ساز خود، ندارد.

کلاسی که دانشی از نحوه وهله سازی وهله‌های خود ندارد، abstract class (کلاس مجرد یا انتزاعی) نامیده می‌شود.
کلاسی که دانش نحوه وهله سازی وهله‌های خود دارد، concrete class نامیده می‌شود.

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

Roles Versus Classes

قاعده شهودی 2.11
مطمئن باشید انتزاع هایی را که مدل می‌کنید کلاس بوده و نه نقش‌هایی که وهله‌های آنها بازی می‌کنند. (Be sure the abstractions that you model are classes and not simply the roles objects play)
آیا مادر و پدر به عنوان یک کلاس هستند یا نقش‌هایی هستند که وهله‌های کلاس شخص، بازی می‌کند؟ پاسخ این سوال وابسته به دامینی (domain) است که طراح در حال مدل سازی آن می‌باشد. اگر در دامین مورد نظر، مادر و پدر رفتارهای مختلفی دارند، احتمالا باید به عنوان کلاس‌های جدا مدل شوند. اگر رفتارهای یکسانی دارند، در نتیجه نقش‌های مختلفی هستند که وهله‌های کلاس شخص بازی می‌کنند. به عنوان مثال، می‌توان کلاس خانواده را متشکل از وهله‌ای از کلاس پدر، وهله‌ای از کلاس مادر و مجموعه‌ای از وهله‌های کلاس فرزند در نظر گرفت. در مقابل ممکن است کلاس خانواده را متشکل از وهله‌ای از کلاس شخص به عنوان پدر، وهله‌ای از کلاس شخص به عنوان مادر و آرایه‌ای از وهله‌های شخص به عنوان فرزندان، مدل کنید. قرار گرفتن در وضیعتی که هر نقش، بخشی از رفتاری‌های شخص را مورد استفاده قرار می‌دهد، کافی نیست و باید مطمئن شوید که رفتار‌ها واقعا متفاوت می‌باشند. همچنین باید به یاد داشته باشید که زمانیکه وهله‌ای از بخشی از رفتارهای کلاس خود استفاده می‌کند، نیز مشکلی وجود ندارد و لازم نیست کلاس‌های دیگری را به خاطر این موضوع در طراحی خود در نظر بگیرید.

شکل 2.9 Two views of a family   

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

قواعد شهودی فصل دوم

قاعده شهودی 2.1 
همه داده‌ها باید در داخل کلاس خود پنهان شده باشند. (All data should be hidden within its class) 
قاعده شهودی 2.2
استفاده کنندگان از کلاس باید به واسط عمومی آن وابسته باشند، اما یک کلاس نباید به استفاده کنندگان خود، وابسته باشد. (Users of a class must be dependent on its public interface, but a class should not be dependent on its users)
قاعده شهودی 2.3
تعداد پیغام‌های موجود در قرارداد یک کلاس را کمینه سازید. (Minimize the number of messages in the protocol of a class) 
قاعده شهودی 2.4
پیاده سازی یک واسط عمومی یکسان کمینه برای همه کلاس‌ها  (Implement a minimal public interface that all classes understand [e.g., operations such as copy (deep versus shallow), equality testing, pretty printing, parsing from an ASCII description, etc.].) 
قاعده شهودی 2.5 
جزئیات پیاده سازی، مانند توابع خصوصی common-code  ( توابعی که کد مشترک سایر متدهای کلاس را در بدنه خود دارند) را در واسط عمومی یک کلاس قرار ندهید.  (Do not put implementation details such as common-code private functions into the public interface of a class)
قاعده شهودی 2.6 
واسط عمومی کلاس را با اقلامی که یا استفاده کنندگان از کلاس توانایی استفاده از آن را نداشته و یا تمایلی به استفاده از آنها ندارند، آمیخته نکنید.  (Do not clutter the public interface of a class with items that users of that class are not able to use or are not interested in using )
قاعده شهودی 2.7
اتصال و پیوستگی مابین کلاس‌ها باید از نوع Nil یا Export باشد؛ به این معنی که یک کلاس فقط از واسط عمومی کلاس دیگر استفاده کند یا کاری با آن نداشته باشد. (Classes should only exhibit nil or export coupling with other classes, that is, a class should only use operations in the public interface of another class or have nothing to do with that class.)
قاعده شهودی 2.8 
یک کلاس باید یک و تنها یک Key Abstraction را تسخیر نماید. (A class should capture one and only one key abstraction) 
قاعده شهودی 2.9 
داده و رفتار مرتبط را در یک جا (کلاس) نگه دارید. (Keep related data and behavior in one place)
قاعده شهودی 2.10 
اطلاعات نامرتبط به هم را در کلاس‌های جدا از هم قرار دهید. ((Spin off nonrelated information into another class (i.e., noncommunicating behavior)
قاعده شهودی 2.11
مطمئن باشید انتزاع هایی را که مدل می‌کنید کلاس بوده و نه نقش‌هایی که وهله‌های آنها بازی می‌کنند. (Be sure the abstractions that you model are classes and not simply the roles objects play)  
مطالب
طراحی شیء گرا: OO Design Heuristics - قسمت دوم

در قسمت اول با مفاهیم اولیه Class و Object آشنا شدیم.

Messages and Methods

Objectها باید مانند ماشین‌هایی تلقی شوند که عملیات موجود در واسط عمومی خود را برای افرادی که درخواست مناسبی ارسال کنند، اجرا خواهند کرد. با توجه به اینکه یک object از استفاده کننده خود مستقل است و وابستگی به او ندارد و همچنین توجه به ساختار نحوی (syntax) برخی از زبان‌های شیء گرای جدید، عبارت «sending a message» برای توصیف اجرای رفتاری از مجموعه رفتارهای object، استفاده میشود.
به محض اینکه پیغامی (Message) به سمت object ارسال شود، ابتدا باید تصمیم بگیرد که این پیغام ارسالی را درک می‌کند. فرض کنیم این پیغام قابل درک است. در این صورت object مورد نظر، همزمان با نگاشت پیغام به یک فراخوانی تابع (function call)، خود را به صورت ضمنی به عنوان اولین آرگومان ارسال می‌کند. تصمیم گرفتن در رابطه با قابل درک بودن یک پیغام، در زبان‌های مفسری در زمان اجرا و در زبان‌های کامپایلری در زمان کامپایل، انجام میگرد. 
نام (یا prototype) رفتار یک وهله، Message (پیغام) نامیده می‌شود. بسیاری از زبان‌های شیء گرا مفهموم Overloaded Functions Or Operators را پشتیبانی می‌کنند. در این صورت می‌توان در سیستم دو تابعی داشت که با نام یکسان، یا انواع مختلف آرگومان (intraclass overloading) داشته باشند یا در کلاس‌های مختلفی (interclass overloading) قرار گیرند. 
ممکن است کلاس ساعت زنگدار، دو پیغام set_time که یکی از آنها با دو آرگومان از نوع عدد صحیح و دیگری یک آرگومان رشته‌ای است داشته باشد.

void AlarmClock::set_time(int hours, int minutes); 

void AlarmClock::set_time(String time);

در مقابل، کلاس ساعت زنگدار و کلاس ساعت مچی هر دو messageای به نام set_time با دو آرگومان از نوع عدد صحیح دارند.

void AlarmClock::set_time(int hours, int minutes); 

void Watch::set_time(int hours, int minutes);

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

در برخی از زبان‌ها و یا سیستم‌ها، اطلاعات دیگری مانند: انواع استثناءهایی که از سمت پیغام پرتاب می‌شوند تا اطلاعات همزمانی (پیغام به صورت همزمان است یا ناهمزمان) را برای استفاده کننده مهیا کنند. از طرفی پیاده سازی کنندگان یک کلاس باید از پیاده سازی پیغام آگاه باشند. جزئیات پیاده سازی یک پیغام -کدی که پیغام را پیاده سازی می‌کند- Method (متد) نامیده میشود. آنگاه که نخ (thread) کنترل درون متد باشد، برای مشخص کردن اینکه پیغام رسیده برای کدام وهله ارسال شده‌است، ارجاعی به وهله مورد نظر و به عنوان اولین آرگومان، به صورت ضمنی ارسال می‌شود. این آرگومان ضمنی، در بیشتر زبان‌ها Self Object نامیده می‌شود (در سی پلاس پلاس this object نام دارد). در نهایت، مجموعه پیغام‌هایی که یک وهله می‌تواند به آنها پاسخ دهد، Protocol (قرارداد) نام دارد.

دو پیغام خاصی که کلاس‌ها یا وهله‌ها می‌توانند به آنها پاسخ دهند، اولی که استفاده کنندگان کلاس برای ساخت وهله‌ها از آن استفاده می‌کنند، Constructor (سازنده) نام دارد. هر کلاسی می‌تواند سازنده‌های متعددی داشته باشد که هر کدام مجموعه پارامترهای مختلفی را برای مقدار دهی اولیه می‌پذیرند. دومین پیغام، عملیاتی است که وهله را قبل از حذف از سیستم، پاک سازی می‌کند. این عملیات، Destructor (تخریب کننده) نام دارد. بیشتر زبان‌های شیء گرا، برای هر کلاس تنها یک تخریب کننده دارند. این پیغام‌ها را به عنوان مکانیزم مقدار دهی اولیه و پاک سازی در پارادایم شیء گرا در نظر بگیرید.

قاعده شهودی 2.2

استفاده کنندگان از کلاس باید به واسط عمومی آن وابسته باشند، اما یک کلاس نباید به استفاده کنندگان خود، وابسته باشد. (Users of a class must be dependent on its public interface, but a class should not be dependent on its users)

منطق پشت این قاعده، یکی از شکل‌های قابلیت استفاده مجدد (resuability) می‌باشد. یک ساعت زنگدار ممکن است توسط شخصی در اتاق خواب او استفاده شود. واضح است که شخص مورد نظر به واسط عمومی ساعت زنگدار وابسته می‌باشد. به هر حال، ساعت زنگدار نباید به شخصی وابسته باشد. اگر ساعت زنگدار به شخصی وابسته باشد، بدون مهیا کردن یک شخص، نمی‌توان از آن برای ساخت یک TimeLockSafe استفاده کرد. این وابستگی‌ها برای مواقعیکه می‌خواهیم امکان این را داشته باشیم تا کلاس ساعت زنگدار را از دامین (domain) خود خارج کرده و در دامین دیگری، بدون وابستگی هایش مورد استفاده قرار دهیم، نامطلوب هستند.

شکل 2.4 The Use Of Alarm Clocks

 The Use Of Alarm Clocks قاعده شهودی 2.3

تعداد پیغام‌های موجود در قرارداد یک کلاس را کمینه سازید. (Minimize the number of messages in the protocol of a class)

چندین سال قبل، مطلبی منتشر شد که کاملا متضاد این قاعده شهودی می‌باشد. طبق آن، پیاده سازی کننده یک کلاس می‌تواند یکسری عملیات را با فرض اینکه در آینده مورد استفاده قرار گیرند، برای آن در نظر بگیرد. ایراد این کار چیست؟ اگر شما از این قاعده پیروی کنید، حتما کلاس LinkedList من، توجه شما را جلب خواهد کرد؛ این کلاس در واسط عمومی خود 4000 عملیات را دارد. فرض کنید قصد ادغام دو وهله از این کلاس را داشته باشید. در این صورت حتما فرض شما این است که عملیاتی تحت عنوان merge در این کلاس تعبیه شده است. بعد از جستجوی بین این تعداد عملیات، در نهایت این عملیات خاص را پیدا نخواهید کرد. چراکه این عملیات متأسفانه به صورت یک overloaded plus operator پیاده سازی شده است. مشکل اصلی واسط عمومی با تعداد زیادی عملیات این است که فرآیند یافتن عملیات مورد نظرمان را خیلی سخت یا حتی ناممکن خواهد کرد و مشکلی جدی برای قابلیت استفاده مجدد تلقی می‌شود.

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

قاعده شهودی 2.4

پیاده سازی یک واسط عمومی یکسان کمینه برای همه کلاس‌ها  (Implement a minimal public interface that all classes understand [e.g., operations such as copy (deep versus shallow), equality testing, pretty printing, parsing from an ASCII description, etc.].)

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

به عنوان مثال کلاس Object در دات نت به عنوان کلاس پایه ضمنی با یکسری از متدهای عمومی (برای مثال ToString)، نشان دهنده تعریف یک واسط عمومی مشترک برای همه کلاس‌ها در این فریمورک، می‌باشد.

public class Object
    {
        public Object();
        public static bool Equals(Object objA, Object objB){...}
        public static bool ReferenceEquals(Object objA, Object objB){...}
        public virtual bool Equals(Object obj){...}
        public virtual int GetHashCode(){...}
        public Type GetType(){...}
        public virtual string ToString(){...}
        protected Object MemberwiseClone(){...}
    }


قاعده شهودی 2.5 

جزئیات پیاده سازی، مانند توابع خصوصی common-code  ( توابعی که کد مشترک سایر متدهای کلاس را در بدنه خود دارند) را در واسط عمومی یک کلاس قرار ندهید.  (Do not put implementation details such as common-code private functions into the public interface of a class)

این قاعده برای کاهش پیچیدگی واسط عمومی کلاس برای استفاده کنندگان آن، طراحی شده است. ایده اصلی این است که استفاده کنندگان کلاس تمایلی ندارند به اعضایی دسترسی داشته باشند که از آنها استفاده نخواهند کرد؛ این اعضا باید به صورت خصوصی در کلاس قرار داده شوند. این توابع خصوصی common-code، زمانیکه متدهای یک کلاس، کد مشترکی را داشته باشند، ایجاد خواهند شد. قرار دادن این کد مشترک در یک متد، معمولا روش مناسبی می‌باشد. نکته قابل توجه این است که این متد، عملیات جدیدی نمی‌باشد؛ بله جزئیات پیاده سازی دو عملیات دیگر از کلاس را ساده کرده است.

شکل 2.5  Example of a common-code private function

Example of a common-code private function

مثال واقعی

فرض کنید در شکل بالا، کلاس X معادل یک LinkedList کلاس، f1و f2 به عنوان توابع insert و remove و تابع f به عنوان تابع common-code که عملیات یافتن آدرس را برای درج و حذف انجام می‌دهد، می‌باشند.

قاعده شهودی 2.6

واسط عمومی کلاس را با اقلامی که یا استفاده کنندگان از کلاس توانایی استفاده از آن را نداشته و یا تمایلی به استفاده از آنها ندارند، آمیخته نکنید.  (Do not clutter the public interface of a class with items that users of that class are not able to use or are not interested in using )

 این قاعده شهودی با قاعده قبلی که با قرار دادن تابع common-code در واسط عمومی کلاس، فقط باعث در هم ریختن واسط عمومی شده بود، مرتبط می‌باشد. در برخی از زبان‌ها مانند C++‎، برای مثال این امکان وجود دارد که سازنده یک کلاس انتزاعی (abstract) را در واسط عمومی آن قرار دهید؛ حتی با وجود اینکه در زمان استفاده از آن سازنده با خطای نحوی روبرو خواهید شد. این قاعده شهودی کلی، برای کاهش این مشکلات در نظر گرفته شده است. 

مطالب
سفارشی سازی ASP.NET Core Identity - قسمت چهارم - User Claims
از نگارش‌های پیشین ASP.NET، هنوز هم اطلاعات شیء User مانند User.Identity.Name در ASP.NET Core نیز در دسترس هستند. به این ترتیب زمانیکه کاربری به سیستم وارد شد، دیگر نیازی نیست تا جهت یافتن Name او، از بانک اطلاعاتی کوئری گرفت. خاصیت Name یاد شده به صورت خودکار از کوکی رمزنگاری شده‌ی او دریافت شده و در اختیار برنامه قرار می‌گیرد. این Name در ASP.NET Core Identity، اصطلاحا یک User Claim پیش‌فرض نام دارد و به صورت خودکار ایجاد و مقدار دهی می‌شود. اکنون این سؤال مطرح می‌شود که آیا می‌توان خاصیت دیگری را به شیء User.Identity اضافه کرد؟


جدول AppUserClaims


جدول AppUserClaims، جزو جداول اصلی ASP.NET Core Identity است و هدف آن ذخیره‌ی اطلاعات ویژه‌ی کاربران و بازیابی ساده‌تر آن‌ها از طریق کوکی‌های آن‌ها است (همانند User.Identity.Name). زمانیکه کاربری به سیستم وارد می‌شود، بر اساس UserId او، تمام رکوردهای User Claims متعلق به او از این جدول واکشی شده و به صورت خودکار به کوکی او اضافه می‌شوند.

در پروژه‌ی DNT Identity از این جدول استفاده نمی‌شود. چون اطلاعات User Claims مورد نیاز آن، هم اکنون در جدول AppUsers موجود هستند. به همین جهت افزودن این نوع User Claimها به جدول AppUserClaims، به ازای هر کاربر، کاری بیهوده است. سناریویی که استفاده‌ی از این جدول را با مفهوم می‌کند، ذخیره سازی تنظیمات ویژه‌ی هرکاربر است (خارج از فیلدهای جدول کاربران). برای مثال اگر سایتی را چندزبانه طراحی کردید، می‌توانید یک User Claim سفارشی جدید را برای این منظور ایجاد و زبان انتخابی کاربر را به عنوان یک رکورد جدید مخصوص آن در این جدول ویژه ثبت کنید. مزیت آن این است که واکشی و افزوده شدن اطلاعات آن به کوکی شخص، به صورت خودکار توسط فریم ورک صورت گرفته و در حین مرور صفحات توسط کاربر، دیگر نیازی نیست تا اطلاعات زبان انتخابی او را از بانک اطلاعاتی واکشی کرد.
بنابراین برای ذخیره سازی تنظیمات با کارآیی بالای ویژه‌ی هرکاربر، جدول جدیدی را ایجاد نکنید. جدول User Claim برای همین منظور درنظر گرفته شده‌است و پردازش اطلاعات آن توسط فریم ورک صورت می‌گیرد.


ASP.NET Core Identity چگونه اطلاعات جدول AppUserClaims را پردازش می‌کند؟

ASP.NET Identity Core در حین لاگین کاربر به سیستم، از سرویس SignInManager خودش استفاده می‌کند که با نحوه‌ی سفارشی سازی آن پیشتر در قسمت دوم این سری آشنا شدیم. سرویس SignInManager پس از لاگین شخص، از یک سرویس توکار دیگر این فریم ورک به نام UserClaimsPrincipalFactory جهت واکشی اطلاعات User Claims و همچنین Role Claims و افزودن آن‌ها به کوکی رمزنگاری شده‌ی شخص، استفاده می‌کند.
بنابراین اگر قصد افزودن User Claim سفارشی دیگری را داشته باشیم، می‌توان همین سرویس توکار UserClaimsPrincipalFactory را سفارشی سازی کرد (بجای اینکه الزاما رکوردی را به جدول AppUserClaims اضافه کنیم).

اطلاعات جالبی را هم می‌توان از پیاده سازی متد CreateAsync آن استخراج کرد:
  public virtual async Task<ClaimsPrincipal> CreateAsync(TUser user)
1) userId شخص پس از لاگین از طریق User Claims ایی با نوع Options.ClaimsIdentity.UserIdClaimType به کوکی او اضافه می‌شود.
2) userName شخص پس از لاگین از طریق User Claims ایی با نوع Options.ClaimsIdentity.UserNameClaimType به کوکی او اضافه می‌شود.
3) security stamp او (آخرین بار تغییر اطلاعات اکانت کاربر) نیز یک Claim پیش‌فرض است.
4) اگر نقش‌هایی به کاربر انتساب داده شده باشند، تمام این نقش‌ها واکشی شده و به عنوان یک Claim جدید به کوکی او اضافه می‌شوند.
5) اگر یک نقش منتسب به کاربر دارای Role Claim باشد، این موارد نیز واکشی شده و به کوکی او به عنوان یک Claim جدید اضافه می‌شوند. در ASP.NET Identity Core نقش‌ها نیز می‌توانند Claim داشته باشند (امکان پیاده سازی سطوح دسترسی پویا).

بنابراین حداقل مدیریت Claims این 5 مورد خودکار است و اگر برای مثال نیاز به Id کاربر لاگین شده را داشتید، نیازی نیست تا آن‌را از بانک اطلاعاتی واکشی کنید. چون این اطلاعات هم اکنون در کوکی او موجود هستند.


سفارشی سازی کلاس UserClaimsPrincipalFactory جهت افزودن User Claims سفارشی

تا اینجا دریافتیم که کلاس UserClaimsPrincipalFactory کار مدیریت Claims پیش‌فرض این فریم ورک را برعهده دارد. در ادامه از این کلاس ارث بری کرده و متد CreateAsync آن‌را جهت افزودن Claims سفارشی خود بازنویسی می‌کنیم. این پیاده سازی سفارشی را در کلاس ApplicationClaimsPrincipalFactory می‌توانید مشاهده کنید:
        public override async Task<ClaimsPrincipal> CreateAsync(User user)
        {
            var principal = await base.CreateAsync(user).ConfigureAwait(false); 
            addCustomClaims(user, principal);
            return principal;
        }

        private static void addCustomClaims(User user, IPrincipal principal)
        {
            ((ClaimsIdentity) principal.Identity).AddClaims(new[]
            {
                new Claim(ClaimTypes.NameIdentifier, user.Id.ToString(), ClaimValueTypes.Integer),
                new Claim(ClaimTypes.GivenName, user.FirstName ?? string.Empty),
                new Claim(ClaimTypes.Surname, user.LastName ?? string.Empty),
                new Claim(PhotoFileName, user.PhotoFileName ?? string.Empty, ClaimValueTypes.String),
            });
        }
در حین بازنویسی متد CreateAsync، ابتدا base.CreateAsync را فراخوانی کرده‌ایم، تا اخلالی در عملکرد این فریم ورک رخ ندهد و هنوز هم همان مواردی که در قسمت قبل توضیح داده شد، به صورت پیش فرض به کوکی شخص اضافه شوند. سپس در متد addCustomClaims، تعدادی Claim سفارشی خاص خودمان را اضافه کرده‌ایم.
برای مثال نام، نام خانوادگی و نام تصویر شخص به صورت Claimهایی جدید به کوکی او اضافه می‌شوند. در این حالت دیگر نیازی نیست تا به ازای هر کاربر، جدول AppUserClaims را ویرایش کرد و اطلاعات جدیدی را افزود و یا تغییر داد. همینقدر که کاربر به سیستم لاگین کند، شیء User او به متد Create کلاس UserClaimsPrincipalFactory ارسال می‌شود. به این ترتیب می‌توان به تمام خواص این کاربر دسترسی یافت و در صورت نیاز آن‌ها را به صورت Claimهایی به کوکی او افزود.

پس از تدارک کلاس ApplicationClaimsPrincipalFactory‌، تنها کاری را که باید در جهت معرفی و جایگرینی آن انجام داد، تغییر ذیل در کلاس IdentityServicesRegistry است:
services.AddScoped<IUserClaimsPrincipalFactory<User>, ApplicationClaimsPrincipalFactory>();
services.AddScoped<UserClaimsPrincipalFactory<User, Role>, ApplicationClaimsPrincipalFactory>();
یکبار ApplicationClaimsPrincipalFactory را به عنوان پیاده سازی کننده‌ی IUserClaimsPrincipalFactory معرفی کرده‌ایم. همچنین یکبار هم سرویس توکار UserClaimsPrincipalFactory را به سرویس سفارشی خودمان هدایت کرده‌ایم. به این ترتیب مطمئن خواهیم شد که همواره از ApplicationClaimsPrincipalFactory ما استفاده خواهد شد (حتی اگر UserClaimsPrincipalFactory اصلی از سیستم تزریق وابستگی‌ها درخواست شود).
 

چگونه به اطلاعات User Claims در سرویس‌های برنامه دسترسی پیدا کنیم؟

برای دسترسی به اطلاعات User Claims نیاز به دسترسی به HttpContext جاری را داریم. در این مورد و تزریق سرویس IHttpContextAccessor جهت تامین آن، در مطلب «بررسی روش دسترسی به HttpContext در ASP.NET Core» پیشتر بحث شده‌است.
به علاوه در کلاس IdentityServicesRegistry، تزریق وابستگی‌های سفارشی‌تری نیز صورت گرفته‌است:
services.AddScoped<IPrincipal>(provider =>
    provider.GetService<IHttpContextAccessor>()?.HttpContext?.User ?? ClaimsPrincipal.Current);
در اینجا اگر نیاز به اطلاعات Claims شیء User را داشتید، می‌توانید اینترفیس IPrincipal را هم بجای IHttpContextAccessor، به سازنده‌ی کلاس مدنظر خود تزریق کنید.


چگونه اطلاعات User Claims سفارشی را دریافت کنیم؟

برای کار ساده‌تر با Claims یک کلاس کمکی به نام IdentityExtensions به پروژه اضافه شده‌است و متدهایی مانند دو متد ذیل را می‌توانید در آن مشاهده کنید:
        public static string FindFirstValue(this ClaimsIdentity identity, string claimType)
        {
            return identity?.FindFirst(claimType)?.Value;
        }

        public static string GetUserClaimValue(this IIdentity identity, string claimType)
        {
            var identity1 = identity as ClaimsIdentity;
            return identity1?.FindFirstValue(claimType);
        }
در اینجا نحوه‌ی استخراج اطلاعات را از شیء User و یا User.Identity مشاهده می‌کنید. تنها کافی است claimType ایی را درخواست کرده و سپس مقدار آن‌را از کوکی شخص به نحو فوق واکشی کنیم.
برای نمونه متد GetUserDisplayName این کلاس کمکی، از همان Claims سفارشی که در کلاس ApplicationClaimsPrincipalFactory تعریف کردیم، اطلاعات خود را استخراج می‌کند و اگر در View ایی خواستید این اطلاعات را نمایش دهید، می‌توانید بنویسید:
 @User.Identity.GetUserDisplayName()


چگونه پس از ویرایش اطلاعات کاربر، اطلاعات کوکی او را نیز به روز کنیم؟

در پروژه قسمتی وجود دارد جهت ویرایش اطلاعات کاربران (UserProfileController). اگر کاربری برای مثال نام و نام خانوادگی خود را ویرایش کرد، می‌خواهیم بلافاصله متد GetUserDisplayName اطلاعات صحیح و به روزی را از کوکی او دریافت کند. برای اینکار یا می‌توان او را وادار به لاگین مجدد کرد (تا پروسه‌ی رسیدن به متد CreateAsync کلاس ApplicationClaimsPrincipalFactory طی شود) و یا روش بهتری نیز وجود دارد:
 // reflect the changes, in the current user's Identity cookie
await _signInManager.RefreshSignInAsync(user).ConfigureAwait(false);
در اینجا تنها کافی است متد RefreshSignInAsync را مجددا بر اساس اطلاعات ویرایش شده‌ی کاربر، فراخوانی کنیم تا کوکی او را بلافاصله به روز کند و این روش نیازی به اجبار به لاگین مجدد کاربر را ندارد.


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


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

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

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


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

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

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

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

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

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


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

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

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

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

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

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

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


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

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

تعیین اجباری بودن یا نبودن ستون‌ها در EF Core

به صورت پیش فرض در EF Core، هر نوع CLR ایی که نال پذیر باشد، به صورت یک ستون اختیاری در نظر گرفته می‌شود؛ مانند:
 string, int?, byte[]
و هر ستونی که نوع CLR آن نال پذیر نباشد، مقدار دهی آن در EF Core اجباری است؛ مانند:
 int, decimal, bool, DateTime
همچنین باید دقت داشت که حتی اگر در تنظیمات نگاشت‌های برنامه به صورت اختیاری تعریف شوند، باز هم EF Core آن‌ها را اجباری درنظر می‌گیرد.

برای لغو اختیاری بودن یک خاصیت نال پذیر می‌توان از ویژگی Required استفاده کرد:
 [Required]
public string Url { get; set; }
نوع string نال پذیر است. برای لغو این وضعیت می‌توان از ویژگی Required استفاده کرد که در سمت بانک اطلاعاتی نیز به not null ترجمه می‌شود.
و یا معادل Fluent API آن با استفاده از ذکر متد IsRequired است:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
   modelBuilder.Entity<Blog>()
              .Property(b => b.Url)
              .IsRequired();
}
با توجه به این توضیحات، نیازی نیست در بالای یک خاصیت از نوع int، ویژگی Required را ذکر کرد. چون int نال پذیر نیست، مقدار دهی آن اجباری است.


کار با رشته‌ها در EF Core

ذکر یک خاصیت رشته‌ای به این صورت:
public string FirstName { get; set; }
به معنای نال پذیر بودن این ستون است (چون Required تعریف نشده‌است) و همچنین نوع و طول آن در SQL Server به nvarchar max تنظیم می‌شود. این تنظیم طول هرچند در مورد SQL Server صادق است، اما ممکن است در SQL Server CE به nvarchar 4000 تفسیر شود (و این مشکل را به همراه داشته باشد که چرا نمی‌توان متون طولانی را در آن ثبت کرد). به عبارتی عدم ذکر دقیق طول یک خاصیت رشته‌ای، در پروایدرهای مختلف، ممکن است معانی مختلفی را به همراه داشته باشد. بنابراین نیاز است طول خواص رشته‌ای حتما ذکر شوند تا در تمام بانک‌های اطلاعاتی با دقت کامل و بدون حدس و گمان تنظیم گردند.
  [StringLength(450)]
  public string FirstName { get; set; }

  [MaxLength(450)]
  public string LastName { get; set; }

  [MaxLength]
  public string Address { get; set; }
برای تعیین طول دقیق یک فیلد رشته‌ای، می‌توان از ویژگی‌های StringLength و یا MaxLength با ذکر اندازه‌ای استفاده کرد.
برای تعیین صریح یک فیلد رشته‌ای به حداکثر مقدار آن بهتر است ویژگی MaxLength را بدون ذکر پارامتری قید کرد. این مورد جهت سازگاری با بانک‌های اطلاعاتی مختلف ضروری است.
معادل این تنظیمات با روش Fluent API به صورت زیر است:
برای تعیین صریح طول یک فیلد رشته‌ای:
modelBuilder.Entity<Person>()
   .Property(x => x.Address)
   .HasMaxLength(450);
و برای تعیین صریح nvarchar max بودن آن فیلد:
modelBuilder.Entity<Person>()
   .Property(x => x.Address)
   .HasColumnType("nvarchar(max)");
حالت پیش فرض EF Core، کار با رشته‌های یونیکد است. یعنی تمام فیلدهای فوق به nvarchar تفسیر می‌شوند و این n ایی که در ابتدا ذکر شده‌است به معنای یونیکد بودن آن است. اگر می‌خواهید این پیش‌فرض تعیین نوع یونیکد را تغییر دهید، می‌توان از ویژگی Column استفاده کرد:
   [Column(TypeName = "varchar")]
  [MaxLength]
  public string Address { get; set; }
البته اگر اطلاعاتی را که با آن کار می‌کنید چندزبانی و یونیکد هستند، بهتر است این مورد را تغییر ندهید.

نکته‌ای در مورد تغییر نوع خواص: اگر از متد HasColumnType و یا ویژگی Column به نحو فوق استفاده کردید، نیاز است طول رشته را صریحا مشخص کنید. در غیر اینصورت در حین migration خطای ذیل را دریافت خواهید کرد:
 Data type 'varchar' is not supported in this form. Either specify the length explicitly in the type name, for example as 'varchar(16)',
or remove the data type and use APIs such as HasMaxLength to allow EF choose the data type.
در اینجا عنوان می‌کند که اگر مقصود شما varchar max است، ویژگی MaxLength را حذف کرده و تنها بنویسید:
   [Column(TypeName = "varchar(max)")]

نکته‌ای در مورد ایندکس‌ها: در قسمت قبل عنوان شد که می‌توان بر روی خواص، ایندکس منحصربفرد اعمال کرد. در مورد رشته‌ها در SQL Server، اگر طول فیلد مدنظر حداکثر تا 900 بایت باشد، یک چنین کاری را می‌توان انجام داد. البته این محدودیت 900 بایتی تا SQL Server 2014 وجود دارد. این سقف در SQL Server 2016 به 1700 بایت افزایش یافته‌است (900bytes for a clustered index. 1,700 for a nonclustered index). بنابراین چون نوع پیش فرض ستون‌های رشته‌ای، یونیکد و nvarchar درنظر گرفته می‌شود، حداکثر طول امنی را که می‌توان برای آن تعریف کرد، مساوی 450 است (نصف 900 بایت). به همین جهت ذکر ایندکس منحصربفرد بر روی یک ستون رشته‌ای، باید به همراه ذکر اجباری حداکثر طول مساوی 450 آن باشد.


کار با اعداد در EF Core

کلاس نمونه‌ای را با ساختار ذیل درنظر بگیرید:
    public class Person 
    {
        public int Id { set; get; }

        public DateTime? DateAdded { set; get; }

        public DateTime? DateUpdated { set; get; }

        [StringLength(450)]
        public string FirstName { get; set; }

        [MaxLength(450)]
        public string LastName { get; set; }

        //[Column(TypeName = "varchar")]
        [MaxLength]
        public string Address { get; set; }


        //bit
        public bool IsActive { get; set; }

        //tiny Int
        public byte Age { get; set; }

        //small Int
        public short Short { get; set; }

        //int
        public int Int32 { get; set; }

        //Big int
        public long Long { get; set; }
    }
پس از اعمال مهاجرت‌ها و به روز رسانی ساختار بانک اطلاعاتی، به ساختار ذیل خواهیم رسید:


همانطور که ملاحظه می‌کنید، نوع bool دات نت به نوع bit در SQL Server، نوع long به bigint، نوع short به smallint، نوع int به int و نوع byte به tinyint نگاشت شده‌اند.


نکته‌ای در مورد اعداد اعشاری: توصیه شده‌است در تعاریف موجودیت‌های خود بهتر است از نوع‌های float و یا double استفاده نکنید. برای کار با اعداد اعشاری از نوع decimal استفاده نمائید تا بتوانید از قابلیت مقایسه‌ی دقیق آن‌ها استفاده کنید. اطلاعات بیشتر: «روش صحیح مقایسه دو عدد اعشاری با هم»


کار با تاریخ در EF Core

اگر به تصویر فوق دقت کنید، نوع DateTime دات نت به datetime2 در سمت SQL Server ترجمه شده‌است:
 CREATE TABLE [dbo].[Persons](
 [DateAdded] [datetime2](7) NULL,
 [DateUpdated] [datetime2](7) NULL,
اگر در داده‌های خود نیازی به زمان ندارید، می‌توان این نوع پیش فرض را با ویژگی Column که پیشتر بحث شد، به date تغییر داد.
اطلاعات بیشتر: «کنترل نوع‌های داده با استفاده از EF در SQL Server»

به علاوه در دات نت نوع DateTime از نوع value type است. بنابراین همانطور که در ابتدای بحث نیز عنوان شد، مقدار دهی آن اجباری است؛ مگر آنکه آن‌را نال پذیر تعریف کنید.


کار با مباحث همزمانی در EF Core

EF Core به صورت پیش فرض، فرض می‌کند رکوردی را که با آن در حال کار هستید، توسط هیچ کاربر دیگری در شبکه تغییر نیافته‌است و تغییرات شما را در حین فراخوانی متد SaveChanges ذخیره می‌کند. اگر علاقمند هستید که EF Core در صورت تغییر مقدار خاصیت خاصی توسط سایر کاربران، این مساله را با صدور استثنایی به شما اطلاع رسانی کند، از ویژگی ConcurrencyCheck
 [ConcurrencyCheck]
public string Name { set; get; }
و یا متد IsConcurrencyToken حالت Fluent API استفاده نمائید:
modelBuilder.Entity<Person>()
    .Property(p => p.Name)
    .IsConcurrencyToken();
در این حالت کوئری به روز رسانی، علاوه بر فیلد Id رکورد، حاوی فیلد Name نیز خواهد بود (در حین تشکیل شرط یافتن رکورد) و اگر در بین فاصله‌ی یافتن شخص و به روز رسانی نام او، شخص دیگری این‌کار را انجام داده باشد، این به روز رسانی موفقیت آمیز نبوده و استثنایی صادر می‌شود.

اگر علاقمند هستید که تمام فیلدهای جدول تحت نظر قرارگیرند، می‌توان از روش ویژه‌ای به نام Timestamp/row version استفاده کرد:
 [Timestamp]
 public byte[] Timestamp { get; set; }
با معادل Fluent API ذیل:
modelBuilder.Entity<Blog>()
   .Property(p => p.Timestamp)
   .ValueGeneratedOnAddOrUpdate()
   .IsConcurrencyToken();
در مورد ValueGeneratedOnAddOrUpdate در قسمت قبل بحث کردیم. فیلد TimeStamp نیز جزو فیلدهای ویژه‌ای است که SQL Server به صورت خودکار قادر است آن‌را مقدار دهی کند و زمانیکه ValueGeneratedOnAddOrUpdate قید می‌شود، یعنی این فیلد همواره با فراخوانی متد SaveChanges، به صورت خودکار مقدار دهی خواهد شد (و نیازی نیست تا توسط برنامه مقدار دهی شود).
در این حالت در حین به روز رسانی یک چنین رکوردی، اگر از زمان کوئری آن (یافتن رکورد) و ذخیره سازی آن، شخص دیگری آن‌را تغییر داده باشد، به علت عدم تطابق Timestamp ها، عملیات به روز رسانی باشکست روبرو شده و یک استثناء صادر می‌شود.