مطالب دوره‌ها
استفاده از async و await در برنامه‌های کنسول و سرویس‌های ویندوز NT
فرض کنید می‌خواهید از await در متد Main یک برنامه‌ی کنسول به نحو ذیل استفاده کنید:
using System;
using System.Net;

namespace Async15
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var webClient = new WebClient { })
            {
                webClient.Headers.Add("User-Agent", "AsyncContext 1.0");
                var data = await webClient.DownloadStringTaskAsync("https://www.dntips.ir");
                Console.WriteLine(data);
            }
        }
    }
}
کامپایلر چنین اجازه‌ای را نمی‌دهد. زیرا از await جایی می‌توان استفاده کرد که متد فراخوان آن با async مزین شده باشد و همچنین دارای یک Context باشد تا نتیجه را بتواند دریافت کند. اگر در اینجا سعی کنید async را به امضای متد Main اضافه نمائید، کامپایلر مجددا خطای an entry point cannot be marked with the 'async' modifier را صادر می‌کند.
اضافه کردن واژه‌ی کلیدی async به روال‌های رخدادگردان void برنامه‌های دسکتاپ مجاز است؛ با توجه به اینکه متد async پیش از پایان کار به فراخوان بازگشت داده می‌شوند (ذات متدهای async به این نحو است). در برنامه‌های دسکتاپ، این بازگشت به UI event loop است؛ بنابراین برنامه بدون مشکل به کار خود ادامه خواهد داد. اما در اینجا، بازگشت متد Main، به معنای بازگشت به OS است و خاتمه‌ی برنامه. به همین جهت کامپایلر از async کردن آن ممانعت می‌کند.
برای حل این مشکل در برنامه‌های کنسول و همچنین برنامه‌های سرویس ویندوز NT که دارای یک async-compatible context نیستند، می‌توان از یک کتابخانه‌ی کمکی سورس باز به نام Nito AsyncEx استفاده کرد. برای نصب آن دستور ذیل را در کنسول پاورشل نیوگت وارد کنید:
 PM> Install-Package Nito.AsyncEx
پس از نصب برای استفاده از آن خواهیم داشت:
using System;
using System.Net;
using Nito.AsyncEx;

namespace Async15
{
    class Program
    {
        static void Main(string[] args)
        {
            AsyncContext.Run(async () =>
            {
                using (var webClient = new WebClient())
                {
                    webClient.Headers.Add("User-Agent", "AsyncContext 1.0");
                    var data = await webClient.DownloadStringTaskAsync("https://www.dntips.ir");
                    Console.WriteLine(data);
                }
            });
        }
    }
}
Context ارائه شده در اینجا برخلاف مثال‌های قسمت‌های قبل، نیازی به فراخوانی متد همزمان Wait و یا خاصیت Result که هر دو از نوع blocking هستند ندارد و یک فراخوانی async واقعی است. همچنین می‌شد یک متد async void را نیز در اینجا برای استفاده از DownloadStringTaskAsync تعریف کرد (تا برنامه کامپایل شود). اما پیشتر عنوان شد که هدف از این نوع متدهای خاص async void صرفا استفاده از آن‌ها در روال‌های رخدادگردان UI هستند. زیرا ماهیت آن‌ها fire and forget است و برای دریافت نتیجه‌ی نهایی به نحوی باید ترد اصلی را قفل کرد. برای مثال در یک برنامه‌ی کنسول متد Console.ReadLine را در انتهای کار فراخوانی کرد. اما با استفاده از AsyncContext.Run نیازی به این کارها نیست.


async lambda
در مثال فوق از یک async lambda، برای فراخوانی استفاده شده است که به همراه دات نت 4.5 ارائه شده‌اند:
Action, () => { }
Func<Task>, async () => { await Task.Yield(); }

Func<TResult>, () => { return 13; }
Func<Task<TResult>>, async () => { await Task.Yield(); return 13; }
آرگومان متد AsyncContext.Run از نوع Func of Task است. بنابراین برای مقدار دهی inline آن توسط lambda expressions مطابق مثال‌های فوق می‌توان از async lambda استفاده کرد.
روش دوم استفاده از AsyncContext.Run و مقدار دهی Func of Task، تعریف یک متد مستقل async Task دار، به نحو ذیل است:
class Program
{
  static async Task<int> AsyncMain()
  {
    ..
  }

  static int Main(string[] args)
  {
    return AsyncContext.Run(AsyncMain);
  }
}


رخ‌دادهای مرتبط با طول عمر برنامه را async تعریف نکنید

همانند متد Main که async تعریف کردن آن سبب بازگشت آنی روال کار به OS می‌شود و برنامه خاتمه می‌یابد، روال‌های رخدادگردانی که با طول عمر یک برنامه‌ی UI سر و کار دارند مانند Application_Launching، Application_Closing، Application_Activated و Application_Deactivated (خصوصا در برنامه‌های ویندوز 8) نیز نباید async void تعریف شوند (چون مطابق ذات متدهای async، بلافاصله به برنامه اعلام می‌کنند که کار تمام شد). در این موارد خاص نیز می‌توان از متد AsyncContext.Run برای انجام اعمال async استفاده کرد.
مطالب
مدیریت طول عمر DbContext در برنامه‌های Blazor SSR

فرض کنید یک صفحه‌ی Blazor SSR، از سه کامپوننت منوی سمت راست، محتوای اصلی صفحه و فوتر سایت که به همراه متنی است، تشکیل شده‌است. منوی سمت راست، به همراه لینک‌هایی‌است که آمار آن‌ها را نیز نمایش می‌دهد و این اطلاعات را از بانک اطلاعاتی، به کمک EF-Core دریافت می‌کند. فوتر صفحه، سال شروع به کار و نام برنامه را از بانک اطلاعاتی دریافت می‌کند و محتوای اصلی صفحه نیز از بانک اطلاعاتی دریافت می‌شود. پس از تکمیل این سه کامپوننت مجزا، اگر برنامه را اجرا کنید، بلافاصله با خطای زیر مواجه می‌شوید:

A second operation started on this context before a previous operation completed

مشکل کجاست؟! مشکل اینجاست که تنها یک نمونه از DbContext، در طول درخواست جاری رسیده، بین سه کامپوننت جاری برنامه به اشتراک گذاشته می‌شود (به سازنده‌ی سرویس‌های مرتبط تزریق می‌شود) و ... در Blazor SSR، پردازش کامپوننت‌های یک صفحه، به صورت موازی و همزمان انجام می‌شوند؛ یعنی ترتیبی نیست. اگر ابتدا کامپوننت منو، بعد محتوای صفحه و در آخر فوتر، رندر می‌شدند، هیچگاه پیام فوق را مشاهده نمی‌کردیم؛ اما ... هر سه کامپوننت، با هم و همزمان رندر می‌شوند و سپس نتیجه‌ی نهایی در Response درج خواهد شد. یعنی یک DbContext بین چندین ترد به اشتراک گذاشته می‌شود که چنین حالتی توسط EF-Core پشتیبانی نمی‌شود و مجاز نیست.

روش مواجه شدن با یک چنین حالت‌هایی، نمونه سازی مجزای DbContext به ازای هر کامپوننت است که نمونه‌ای از آن‌را پیشتر در مطلب «نکات ویژه‌ی کار با EF-Core در برنامه‌های Blazor Server» مشاهده کرده‌اید. در این مطلب، راه‌حل دیگری برای اینکار ارائه می‌شود که ساده‌تر است و نیازی به تغییرات آنچنانی در کدهای کامپوننت‌ها و کل برنامه ندارد.

استفاده از کلاس پایه‌ی OwningComponentBase برای نمونه سازی مجدد DbContext به‌ازای هر کامپوننت

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

@inject IHotelRoomService HotelRoomService

طول عمر دریافتی سرویس، دقیقا بر اساس طول عمر اصلی تعریف شده‌ی آن عمل می‌کند (شبیه به برنامه‌های ASP.NET Core). یعنی برای مثال اگر Scoped باشد، DbContext تزریق شده‌ی در آن هم Scoped است و این DbContext، بین تمام کامپوننت‌های در حال پردازش موازی در طول یک درخواست، به‌اشتراک گذاشته می‌شود که مطلوب ما نیست. ما می‌خواهیم بتوانیم به ازای هر کامپوننت مجزای صفحه، یک DbContext جدید داشته باشیم. یعنی باید بتوانیم خودمان این سرویس Scoped را نمونه سازی کنیم و نه اینکه آن‌را مستقیما از سیستم تزریق وابستگی‌ها دریافت کنیم.

بنابراین اگر بخواهیم قسمت‌های مختلف برنامه را تغییر ندهیم و همان تعاریف ابتدایی services.AddDbContext و Scoped تعریف کردن سرویس‌های برنامه بدون تغییر باقی بمانند (و از IDbContextFactory و موارد مشابه دیگر مطلب «نکات ویژه‌ی کار با EF-Core در برنامه‌های Blazor Server» هم استفاده نکنیم)، باید جایگزینی را برای نمونه سازی سرویس‌ها ارائه دهیم. به همین جهت در ابتدا، یک ویژگی جدیدی را به صورت زیر تعریف می‌کنیم:

[AttributeUsage(AttributeTargets.Property)]
public sealed class InjectComponentScopedAttribute : Attribute
{
}

تا بتوانیم بجای:

@inject IHotelRoomService HotelRoomService

بنویسیم:

[InjectComponentScoped] internal IHotelRoomService HotelRoomService { set; get; } = null!;

مرحله‌ی بعد، نوبت به نمونه سازی خودکار این سرویس‌های درخواستی علامتگذاری شده‌ی با InjectComponentScoped است. برای این منظور، تمام کامپوننت‌های برنامه را از کلاس پایه و استاندارد OwningComponentBase ارث‌بری می‌کنیم. مزیت اینکار، امکان دسترسی به خاصیتی به نام ScopedServices در تمام کامپوننت‌های برنامه است که توسط آن می‌توان به متد ScopedServices.GetRequiredService آن دسترسی یافت. یعنی با ارث‌بری از کلاس پایه‌ی OwningComponentBase به ازای هر کامپوننت، به صورت خودکار Scope جدیدی شروع می‌شود که توسط آن می‌توان به نمونه‌ی جدیدی از سرویس مدنظر دسترسی یافت و نه به نمونه‌ی اشتراکی در طی درخواست جاری.

اکنون اگر از این مزیت به صورت زیر استفاده کنیم، می‌توان تمام سرویس‌های درخواستی مزین به InjectComponentScopedAttribute یک کامپوننت را به صورت خودکار یافته و با استفاده از ScopedServices.GetRequiredService، مقدار دهی کرد:

public class BlazorScopedComponentBase : OwningComponentBase
{
    private static readonly ConcurrentDictionary<Type, Lazy<List<PropertyInfo>>> CachedProperties = new();

    private List<PropertyInfo> InjectComponentScopedPropertiesList => CachedProperties.GetOrAdd(GetType(),
            type => new Lazy<List<PropertyInfo>>(
                () => type.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Instance |
                                         BindingFlags.NonPublic | BindingFlags.Public)
                    .Where(p => p.GetCustomAttribute<InjectComponentScopedAttribute>() is not null)
                    .ToList(), LazyThreadSafetyMode.ExecutionAndPublication)).Value;

    protected override void OnInitialized()
    {
        foreach (var propertyInfo in InjectComponentScopedPropertiesList)
        {
            propertyInfo.SetValue(this, ScopedServices.GetRequiredService(propertyInfo.PropertyType));
        }
    }
}

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

فعالسازی ارث‌بری خودکار در تمام کامپوننت‌های برنامه

مرحله‌ی بعد، ارث‌بری خودکار تمام کامپوننت‌های برنامه از OwningComponentBase سفارشی فوق است و در اینجا قصد نداریم تمام کامپوننت‌ها را جهت معرفی آن، به صورت دستی تغییر دهیم. برای اینکار فقط کافی است به فایل Imports.razor_ مراجعه و یک سطر زیر را در آن درج کنیم:

@inherits BlazorScopedComponentBase

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

[InjectComponentScoped] internal IHotelRoomService HotelRoomService { set; get; } = null!;

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

یک نکته: اگر کامپوننت شما متد OnInitialized را بازنویسی می‌کند، ‌فراموش نکنید که در ابتدای آن باید ()base.OnInitialized را هم فراخوانی کنید تا متد OnInitialized کامپوننت پایه‌ی BlazorScopedComponentBase نیز فراخوانی شود. البته این مورد در حین بازنویسی نمونه‌ی async آن مهم نیست؛ چون همیشه OnInitialized غیر async در ابتدا فراخوانی می‌شود و سپس نمونه‌ی async آن اجرا خواهد شد.

مطالب
روش تولید خودکار URLها در برنامه‌های ASP.NET Core
پیشتر مطلب «نحوه صحیح تولید Url در ASP.NET MVC» را در این سایت مطالعه کرده‌اید و خلاصه‌ی آن به این صورت است که اگر در جائی از برنامه‌ی خود، مسیر Home/Details/1 را به صورت دستی وارد کرده‌اید، با تغییر الگوی مسیریابی برنامه برای مثال به صورت "uni/{controller=Home}/{action=Index}/{id?}" در آینده، مسیر یاد شده دیگر معتبر و قابل دسترسی نبوده و نیاز خواهید داشت تمام مسیرهای دستی وارد شده‌ی اینگونه را در سراسر برنامه اصلاح کنید. به همین جهت در این مطلب روش جدید تولید خودکار URL‌ها را در برنامه‌های ASP.NET Core بررسی می‌کنیم.


تولید URL‌های منتهی به اکشن متدها در کنترلرها

هنوز هم در اکشن متدهای ASP.NET Core می‌توان از متد Url.Action برای تولید لینکی به سایر اکشن متدهای کنترلرهای دیگر استفاده کرد. البته اینبار مقادیر مسیریابی آن به عنوان پارامتر سوم باید وارد شوند و همچنین می‌توان بر اساس Scheme جاری، به صورت خودکار http و یا https را در ابتدای URL درج کرد (البته ذکر Scheme سبب تولید URLهای مطلق می‌شود؛ اگر نیاز به مسیرهای نسبی است، آن‌را ذکر نکنید):
var url = this.Url.Action("About", "Home", new { id = 1 }, this.Request.Scheme);

اما اگر در اکشن متدهای کنترلرها قرار نداشتیم چطور؟
در هر قسمتی از برنامه که دسترسی به httpContext وجود دارد، می‌توان به سرویس IUrlHelper آن نیز دسترسی یافت (this.Url در یک اکشن متد، وهله‌ای از IUrlHelper است):
var urlHelper = httpContext.RequestServices.GetRequiredService<IUrlHelper>()
و پس از آن برای نمونه متد urlHelper.Action یاد شده (معادل this.Url.Action) در دسترس می‌باشد.


تولید URL‌های منتهی به اکشن متدها در خارج از کنترلرها

فرض کنید می‌خواهید در متد Configure فایل آغازین برنامه، آدرس منتهی به یک اکشن متد خاصی را تولید کنید و یا در یک میان‌افزار که عملکرد آن باتوجه به محل قرارگیری آن، پیش از رخ‌دادن و اجرای میان‌افزار MVC است. در این حالت دیگر روش IUrlHelper یاد شده کار نمی‌کند؛ چون در این مکان‌ها دسترسی به Action Context میان‌افزار MVC وجود ندارد و هنوز این میان‌افزار اجرا نشده‌است.
برای رفع این مشکل، از زمان ASP.NET Core 2.2 به بعد، سرویس توکار جدیدی به نام LinkGenerator اضافه شده‌است که الزاما برای کار کردن، نیازی به Http Context و همچنین Action Context را ندارد. برای مثال اگر در متد void Configure(IApplicationBuilder app, IWebHostEnvironment env)، دسترسی به app وجود دارد، توسط آن می‌توان سرویس LinkGenerator را دریافت کرد و سپس با کمک متد GetPathByAction آن، مسیر منتهی به یک اکشن متد خاص را به صورت خودکار تولید کرد:
var generator = app.ApplicationServices.GetRequiredService<LinkGenerator>();
var controllerName = nameof(HomeController).Replace("Controller", "");
var url = generator.GetPathByAction(nameof(HomeController.Index), controllerName)
بدیهی است اگر در قسمتی از برنامه امکان تزریق وابستگی‌ها در سازنده‌ی کلاس مدنظر وجود داشته باشد، می‌توان LinkGenerator را به آن تزریق کرد (بدون نیاز به تنظیم خاصی) و از امکانات آن استفاده کرد. طول عمر LinkGenerator به صورت پیش‌فرض به Singleton تنظیم شده‌است. بنابراین می‌توان آن‌را به سازنده‌ی یک میان‌افزار نیز تزریق کرد (چون طول عمر میان‌افزاها نیز Singleton است):
public class MyMiddleware
{
    private readonly LinkGenerator _linkGenerator;

    public MyMiddleware(RequestDelegate next, LinkGenerator linkGenerator) 
   {
       _linkGenerator = linkGenerator;
   }   

    public async Task Invoke(HttpContext httpContext)
    {
        var url = _linkGenerator.GenerateLink(new { controller = "Store", action = "ListProducts" });
    
        httpContext.Response.ContentType = "text/plain";
        return httpContext.Response.WriteAsync($"Go to {url} to see the list of products.");
    }
}
و یا حتی روش ()<var linkGenerator = httpContext.RequestServices.GetService<LinkGenerator نیز در اینجا برای دسترسی به سرویس LinkGenerator کار می‌کند. به علاوه امکان تزریق مستقیم آن به Viewها و صفحات Razor نیز وجود دارد

یک نکته: متد GetUriByAction امکان دریافت HttpContext را نیز دارد:
public static string GetPathByAction(this LinkGenerator generator,
 HttpContext httpContext, string action = null, string controller = null,
 object values = null, PathString? pathBase = null,
 FragmentString fragment = default, LinkOptions options = null);
مانند:
var url = _linkGenerator.GetUriByAction(_accessor.HttpContext,
action: "GetContentByFileId",
values: new { FileId = 1 }
);
در این مثال accessor همان IHttpContextAccessor تزریق شده‌ی به یک سرویس خاص است. مزیت این روش، عدم نیاز به تکمیل سایر پارامترهای متد GetUriByAction است. اگر متد GetUriByAction را بدون HttpContext استفاده کنید، نیاز خواهید داشت تعداد پارامترهای بیشتری از آن‌را برای راهنمایی به آن تکمیل کنید:
public static string GetPathByAction(this LinkGenerator generator, string action, string controller,
 object values = null, PathString pathBase = default,
FragmentString fragment = default, LinkOptions options = null);
مطالب
مستند سازی ASP.NET Core 2x API توسط OpenAPI Swagger - قسمت هفتم - سفارشی سازی ظاهر مستندات API
امکان کنترل کامل و سفارشی سازی ظاهر نهایی Swagger-UI وجود دارد که جزئیات آن‌را در این قسمت بررسی خواهیم کرد.


بهبود ظاهر کامنت‌ها با بکارگیری Markdown

Markdown زبان سبکی است برای تعیین شیوه‌نامه‌ی نمایش متون ساده. اگر پیشتر با سیستم ارسال نظرات Github و یا Stackoverflow کار کرده باشید، قطعا با آن آشنایی دارید. توضیحات کامل و جزئیات آن‌را می‌توانید در آدرس markdownguide.org مطالعه کنید. خوشبختانه امکان استفاده‌ی از Markdown در OpenAPI spec نیز وجود دارد که سبب بهبود ظاهر مستندات نهایی حاصل از آن خواهد شد.
در قسمت سوم، سعی کردیم مثالی را توسط remarks، به قسمت Patch اضافه کنیم که ظاهر آن، آنچنان مطلوب به نظر نمی‌رسد و بهتر است آن‌را به صورت یک قطعه کد نمایش داد:


برای بهبود این ظاهر می‌توان از Markdown استفاده کرد. بنابراین ابتدا تمام backslash‌های اضافه شده را که جهت نمایش خطوط جدید اضافه شده بودند، حذف می‌کنیم. در Markdown خطوط جدید با درج حداقل 2 فاصله (space) و یک سطر جدید مشخص می‌شوند. همچنین استفاده‌ی از ** سبب ضخیم شدن نمایش عبارت‌ها می‌شود. برای اینکه قطعه کد نوشته شده را در Markdown به صورت کدی با پس زمینه‌ای مشخص نمایش دهیم، پیش از شروع هر سطر آن نیاز است یک tab و یا 4 فاصله (space) درج شوند:
/// <remarks>
/// Sample request (this request updates the author's **first name**)
///
///     PATCH /authors/id
///     [
///         {
///           "op": "replace",
///           "path": "/firstname",
///           "value": "new first name"
///           }
///     ]
/// </remarks>
پس از این تغییرات خواهیم داشت:

که نسبت به حالت قبلی بسیار بهتر به نظر می‌رسد.
در نگارش فعلی، استفاده‌ی از Markdown برای توضیحات remarks، پارامترها و response codes پشتیبانی می‌شود؛ اما نه برای قسمت summary که برای نگارش بعدی درنظر گرفته شده‌است. همچنین از قابلیت‌های پیشترفته‌ی Markdown هم استفاده نکنید (در کل نیاز به مقداری سعی و خطا دارد تا مشخص شود چه قابلیت‌هایی را پشتیبانی می‌کند).


سفارشی سازی مقدماتی UI به کمک تنظیمات API آن

جائیکه تنظیمات میان‌افزار Swashbuckle.AspNetCore در کلاس Starup تعریف می‌شوند، می‌توان تغییراتی را نیز در UI آن اعمال کرد:
namespace OpenAPISwaggerDoc.Web
{
    public class Startup
    {
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
    // ...
            app.UseSwaggerUI(setupAction =>
            {
                setupAction.SwaggerEndpoint(
                    url: "/swagger/LibraryOpenAPISpecification/swagger.json",
                    name: "Library API");
                setupAction.RoutePrefix = "";

                setupAction.DefaultModelExpandDepth(2);
                setupAction.DefaultModelRendering(Swashbuckle.AspNetCore.SwaggerUI.ModelRendering.Model);
                setupAction.DocExpansion(Swashbuckle.AspNetCore.SwaggerUI.DocExpansion.None);
                setupAction.EnableDeepLinking();
                setupAction.DisplayOperationId();
            });
    // ...
        }
    }
}
با این خروجی که به علت تنظیم DocExpansion آن به None، اینبار لیست قابلیت‌ها را به صورت باز شده نمایش نمی‌دهد:


همچنین چون DefaultModelRendering به Model تنظیم شده‌است، اینبار بجای مثال، مشخصات مدل را به صورت پیش‌فرض نمایش می‌دهد:


کار DisplayOperationId نمایش Id هر Operation است؛ مانند get_api_authors. در اینجا Operation همان مداخل API ما هستند و به عنوان هر قسمت، Tag گفته می‌شود؛ مانند Authors و یا Books:


با فعالسازی EnableDeepLinking، آدرس‌هایی مانند tagName/# و یا tagName/OperationId/# قابلیت مرور و بازشدن خودکار را پیدا می‌کنند (tagName همان نام کنترلر است و OperationId همان اکشن متدی که عمومی شده‌است). برای مثال اگر آدرس https://localhost:5001/index.html#/Books/get_api_authors__authorId__books را در یک برگه‌ی جدید مرورگر باز کنیم، بلافاصله گروه Books، باز شده و سپس به اکشن متد یا مدخلی که Id آن ذکر شده‌است، هدایت می‌شویم:



اعمال تغییرات پیشرفته در UI

Swagger-UI در اصل از یک سری فایل html، css، js و فونت تشکیل شده‌است که آن‌ها را می‌توانید در آدرس src/Swashbuckle.AspNetCore.SwaggerUI مشاهده کنید. برای مثال فایل index.html آن‌را در اینجا می‌توانید مشاهده کنید. اصل آن در div ای با id مساوی swagger-ui رخ می‌دهد و در این قسمت است که رابط کاربری آن به صورت پویا تولید شده و رندر خواهد شد. بررسی فایل‌های js و css آن در این مخزن کد مشکل است؛ چون نگارش فشرده شده‌ی آن هستند. به همین جهت می‌توان به مخزن کد اصلی Swagger-UI که نگارش جایگذاری شده‌ی آن (embedded) توسط Swashbuckle.AspNetCore ارائه می‌شود، رجوع کرد. برای مثال در پوشه‌ی src/styles آن، اصل فایل‌های css آن برای سفارشی سازی وجود دارند.

پس از اعمال تغییرات خود، می‌توانید css و یا js سفارشی خود را به نحو زیر به تنظیمات app.UseSwaggerUI سیستم معرفی کنید:
setupAction.InjectStylesheet("/Assets/custom-ui.css");
setupAction.InjectJavaScript("/Assets/custom-js.js");
باید دقت داشت که این فایل‌ها باید در پوشه‌ی wwwroot قرار گرفته و قابل دسترسی باشند.
برای اعمال تغییرات اساسی و تزریق صفحه‌ی index.html جدیدی، می‌توان به صورت زیر عمل کرد:
setupAction.IndexStream = () => 
    GetType().Assembly.GetManifestResourceStream(
  "OpenAPISwaggerDoc.Web.EmbeddedAssets.index.html");
نکته‌ی مهم: اینبار این فایل باید به صورت embedded ارائه شود. به همین جهت در مثال فوق، عبارت OpenAPISwaggerDoc.Web به فضای نام اصلی اسمبلی جاری اشاره می‌کند. سپس EmbeddedAssets، نام پوشه‌ای است که فایل index.html سفارشی سازی شده، در آن قرار خواهد گرفت. اکنون برای اینکه این فایل را به صورت EmbeddedResource معرفی کنیم، نیاز است فایل csproj را به نحو زیر ویرایش کرد:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <ItemGroup>
    <None Remove="EmbeddedAssets\index.html" />
  </ItemGroup>

  <ItemGroup>
    <EmbeddedResource Include="EmbeddedAssets\index.html" />
  </ItemGroup>
</Project>

کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: OpenAPISwaggerDoc-07.zip
مطالب
پشتیبانی توکار از GDPR در ASP.NET Core 2.1
دیروز (25 ماه May سال 2018) اولین روز فعالسازی GDPR یا General Data Protection Regulation بود و به همین خاطر است که اگر به سرویس‌های مهم اینترنتی دقت کرده باشید، پر شده‌است از پیام‌هایی مانند «ما از کوکی استفاده می‌کنیم»، «ما اطلاعات شما را به این صورت ذخیره می‌کنیم» و امثال آن. همچنین تعداد زیادی از سرویس‌های اینترنتی نیز به کاربران خود پیام‌هایی را جهت تائید قوانین جدید رعایت حریم خصوصی آن‌ها ارسال کرده‌اند. برای مثال اگر این قوانین جدید را تائید نکنید، از دریافت بسیاری از خبرنامه‌ها محروم خواهید شد. این مورد نیز از بخش‌نامه‌ی اتحادیه‌ی اروپا نشات می‌گیرد که از روز جمعه ۲۵ می‌(۴ خرداد) تمامی شرکت‌ها، افراد، وب‌سایت‌ها و ارائه‌دهندگان خدمات آنلاین، موظف به رعایت آن هستند. موضوع بخش‌نامه قبل از هرچیزی، حفاظت از اطلاعات خصوصی کاربران است. نهادها و شرکت‌ها و وب‌سایت‌هایی که تا ۲۵ می‌۲۰۱۸ زمینه اجرای این بخش‌نامه را فراهم نکرده باشند، در خطر جریمه‌های سنگین هستند. بخش‌نامه جدید حریم خصوصی اطلاعات، تعیین می‌کند که چه میزان اطلاعاتی درباره‌ی هرکسی می‌تواند جمع‌آوری و بررسی شود، مورد پردازش قرار گیرد و البته تبدیل به پول شود. این بخش‌نامه حق تک‌تک کاربران، بر اطلاعات‌شان را تقویت می‌کند. کاربران حالا حق بیشتری بر اطلاعات‌شان، برای «پاک کردن» آن‌ها و «پس‌گرفتن» آن‌ها دارند. البته آن‌چه که احتمالا برای همه قابل رؤیت خواهد بود توضیحات مربوط به حفاظت از اطلاعات در وبسایت‌ها است. این توضیحات باید جزئی‌تر و دقیق‌تر و برای مخاطبان قابل فهم‌تر باشند و این به این معنا است که این توضیحات به‌مراتب طولانی‌تر خواهند شد.
در این بین اگر به قالب پیش‌فرض پروژه‌های MVC تولید شده‌ی توسط ASP.NET Core 2.1 نیز دقت کنید، پشتیبانی توکار از پیشنیازهای GDPR در آن لحاظ شده‌است؛ چه از لحاظ گوشزد کردن شرایط حریم خصوصی و پذیرش آن و چه از لحاظ «پاک کردن» و «پس گرفتن» اطلاعات شخصی.


قالب و کوکی پذیرش شرایط حریم خصوصی سایت (Cookie Consent)


اگر قالب پیش‌فرض یک پروژه‌ی ASP.NET Core 2.1 را اجرا کنید، تصویر فوق را که در آن نوار پذیرش شرایط حریم خصوصی سایت در بالای صفحه درج شده‌است، مشاهده خواهید کرد.
قالب جدید نوار پذیرش شرایط حریم خصوصی در مسیر Views\Shared\_CookieConsentPartial.cshtml واقع شده‌است و در فایل layout برنامه توسط tag helper جدید Partial، رندر و نمایش داده می‌شود:
<partial name="_CookieConsentPartial" />
در ابتدای این partial view، یک چنین کدهایی درج شده‌اند:
@using Microsoft.AspNetCore.Http.Features
@{
  var consentFeature = Context.Features.Get<ITrackingConsentFeature>();
  var showBanner = !consentFeature?.CanTrack ?? false;
  var cookieString = consentFeature?.CreateConsentCookie();
}
بنابراین پذیرش شخص را در یک کوکی درج می‌کند و در دفعات بعدی بازدید او بر اساس این کوکی است که در مورد نمایش یا عدم نمایش این نوار پذیرش شرایط، تصمیم گیری خواهد شد. این کوکی نیز که تحت عنوان میان‌افزار CookiePolicy در سیستم مدیریت و پردازش می‌شود، به صورت زیر در فایل آغازین برنامه مدیریت می‌گردد:
الف) تنظیم نیاز به دریافت پذیرش
public void ConfigureServices(IServiceCollection services)
{
   services.Configure<CookiePolicyOptions>(options =>
   {
     // This lambda determines whether user consent for non-essential cookies is needed for a given request.
     options.CheckConsentNeeded = context => true;
     options.MinimumSameSitePolicy = SameSiteMode.None;
   });
ب) فعالسازی میان‌افزار مدیریت کوکی پذیرش شرایط حریم خصوصی
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
   // ...
   app.UseCookiePolicy();


دادن حق فراموش شدن به کاربران

علاوه بر Cookie Consent فوق که در یک قالب ابتدایی MVC نیز درج شده‌است، در قالب پروژه‌های ASP.NET Core Identity، دو گزینه‌ی جدید دریافت اطلاعات شخصی و همچنین حذف اکانت (دادن حق فراموشی به کاربران) نیز پیش‌بینی شده‌است: PersonalData.cshtml


البته این صفحه جزو بسته‌ی جدید Microsoft.AspNetCore.Identity.UI است که به همراه ASP.NET Core 2.1 ارائه می‌شود:
 dotnet add package Microsoft.AspNetCore.Identity.UI --version 2.1.0-rc1-final
در این بسته تمام کدها و صفحات مخصوص Identity به داخل یک Class library جدید منتقل شده‌اند و دیگر جزو قالب پروژه‌ی «dotnet new mvc --auth Individual» یا همان تنظیم اعتبارسنجی به individual user accounts نیستند و باید به صورت جداگانه دریافت و تنظیم شوند (اختیاری است).
مطالب
سفارشی سازی ASP.NET Core Identity - قسمت ششم - فارسی سازی پیام‌ها
هرچند ASP.NET Core Identity تمام پیام‌های خطایی را که ارائه می‌دهد از یک فایل resx دریافت می‌کند، اما این فایل در نگارش 1.1 آن حداقل قابلیت چندزبانی شدن را ندارد و اگر فایل resx فارسی آن‌را تهیه کنیم، توسط این فریم ورک استفاده نخواهد شد. در ادامه ابتدا نگاهی خواهیم داشت به زیرساخت استفاده شده‌ی در این فریم ورک برای بومی سازی پیام‌های داخلی آن و سپس نحوه‌ی فارسی کردن آن‌را بررسی می‌کنیم.


ASP.NET Core Identity 1.1 چگونه پیام‌های خطای خود را تامین می‌کند؟

نگارش 1.1 این فریم ورک به همراه یک فایل Resources.resx است که تمام پیام‌های خطاهای ارائه شده‌ی توسط متدهای مختلف آن‌را به همراه دارد. این فایل توسط کلاس IdentityErrorDescriber به نحو ذیل استفاده می‌شود:
    public class IdentityErrorDescriber
    {
        public virtual IdentityError DefaultError()
        {
            return new IdentityError
            {
                Code = nameof(DefaultError),
                Description = Resources.DefaultError
            };
        }
برای مثال برای نمایش یک پیام خطای عمومی، به کلاس Resources معادل فایل resx یاد شده مراجعه کرده و خاصیت DefaultError آن‌را ارائه می‌دهد و به همین نحو برای سایر خطاها و اخطارها.
سپس کلاس IdentityErrorDescriber به سیستم تزریق وابستگی‌های آن اضافه شده و هرجائیکه نیاز به نمایش پیامی را داشته، از آن استفاده می‌کند.
بنابراین همانطور که ملاحظه می‌کنید کلاس Resources آن ثابت است و قابل تغییر نیست. به همین جهت اگر معادل فارسی این فایل را تهیه کنیم، توسط این فریم ورک به صورت خودکار استفاده نخواهد شد.


فارسی سازی IdentityErrorDescriber

بهترین راه فارسی سازی کلاس IdentityErrorDescriber، ارث بری از آن و بازنویسی متدهای virtual آن است که اینکار در کلاس CustomIdentityErrorDescriber انجام شده‌است:
    public class CustomIdentityErrorDescriber : IdentityErrorDescriber
    {
        public override IdentityError DefaultError()
        {
            return new IdentityError
            {
                Code = nameof(DefaultError),
                Description = "خطایی رخ داده‌است."
            };
        }
پس از بازنویسی کامل این کلاس، اکنون نوبت به جایگزینی آن با IdentityErrorDescriber پیش‌فرض است:
services.AddScoped<IdentityErrorDescriber, CustomIdentityErrorDescriber>();

services.AddIdentity<User, Role>(identityOptions =>
            {
            }).AddUserStore<ApplicationUserStore>()
               // the rest of the setting …
              .AddErrorDescriber<CustomIdentityErrorDescriber>()
               // the rest of the setting …
معرفی CustomIdentityErrorDescriber، در دو قسمت معرفی عمومی آن به سیستم تزریق وابستگی‌ها و همچنین متد AddErrorDescriber زنجیره‌ی AddIdentity کلاس IdentityServicesRegistry انجام شده‌است.
به این ترتیب این فریم ورک هرزمانیکه نیاز به وهله‌ای از نوع IdentityErrorDescriber را داشته باشد، از وهله‌ی فارسی سازی شده‌ی ما استفاده می‌کند.


مشکل! هنوز پس از جایگزینی سرویس IdentityServicesRegistry اصلی، تعدادی از خطاها فارسی نیستند!

اگر به کلاس PasswordValidator آن مراجعه کنید، در سازنده‌ی کلاس یک چنین تعریفی را می‌توان مشاهده کرد:
    public class PasswordValidator<TUser> : IPasswordValidator<TUser> where TUser : class
    {
        public PasswordValidator(IdentityErrorDescriber errors = null)
        {
            Describer = errors ?? new IdentityErrorDescriber();
        }
یعنی اگر ما این کلاس PasswordValidator را سفارشی سازی کردیم و فراموش کردیم که سازنده‌ی آن‌را هم بازنویسی کنیم، پارامتر errors آن نال خواهد بود (چون از مقدار پیش‌فرض پارامترها استفاده کرده‌اند). یعنی از new IdentityErrorDescriber اصلی (بدون مراجعه‌ی به سیستم تزریق وابستگی‌ها و استفاده‌ی از نسخه‌ی سفارشی سازی شده‌ی ما) استفاده می‌کند. بنابراین در هر دو کلاس سفارشی سازی شده‌ی اعتبارسنجی کاربران و کلمات عبور آن‌ها، ذکر سازنده‌ی پیش‌فرض این کلاس‌ها و ذکر پارامتر IdentityErrorDescriber آن، اجباری است:
    public class CustomPasswordValidator : PasswordValidator<User>
    {
        private readonly IUsedPasswordsService _usedPasswordsService;
        private readonly ISet<string> _passwordsBanList;

        public CustomPasswordValidator(
            IdentityErrorDescriber errors,// How to use CustomIdentityErrorDescriber
            IOptionsSnapshot<SiteSettings> configurationRoot,
            IUsedPasswordsService usedPasswordsService) : base(errors)



    public class CustomUserValidator : UserValidator<User>
    {
        private readonly ISet<string> _emailsBanList;

        public CustomUserValidator(
            IdentityErrorDescriber errors,// How to use CustomIdentityErrorDescriber
            IOptionsSnapshot<SiteSettings> configurationRoot
            ) : base(errors)
به این ترتیب، زمانیکه این کلاس‌ها توسط سیستم تزریق وابستگی‌ها وهله سازی می‌شوند، IdentityErrorDescriber آن دقیقا همان کلاس فارسی سازی شده‌ی ما خواهد بود و دیگر شرط ()errors ?? new IdentityErrorDescriber در کلاس پایه محقق نمی‌شود تا بازهم به همان تامین کننده‌ی پیش فرض خطاها مراجعه کند؛ چون  base(errors) آن با کلاس جدید ما مقدار دهی شده‌است.

یک نکته: اگر کلاس‌های زیر را سفارشی سازی کردید، تمامشان از حالت ()errors ?? new IdentityErrorDescriber در سازنده‌ی کلاس خود استفاده می‌کنند. بنابراین ذکر مجدد و بازنویسی سازنده‌ی آن‌ها را فراموش نکنید (در حد ذکر مجدد سازنده‌ی کلاس پایه کفایت می‌کند و مابقی آن توسط سیستم تزریق وابستگی‌ها مدیریت خواهد شد):
- PasswordValidator
- RoleManager
- RoleStore
- UserStore
- UserValidator
- RoleValidator


کدهای کامل این سری را در مخزن کد DNT Identity می‌توانید ملاحظه کنید.
مطالب
بررسی فرمت کوکی‌های ASP.NET Identity
فرمت کوکی‌های ASP.NET Identity از پروژه‌ی سورس باز Katana دریافت شده‌است و تولید آن پس از لاگین کاربر، شامل مراحل زیر می‌باشد:
1- با استفاده از کلاس ApplicationUser، شیء ClaimsPrincipal را تولید می‌کند.
2- به این ClaimsPrincipal اطلاعاتی مانند ApplicationUser.Id و SecurityStamp اضافه می‌شوند.
3- در ادامه، ClaimsPrincipal به OWIN و کلاس CookieAuthenticationHandler آن ارسال می‌شود.
4- کار کلاس CookieAuthenticationHandler، تولید و تنظیم اطلاعاتی مانند تاریخ صدور کوکی، تاریخ انقضای آن، نوع کوکی، مانند ماندگار بودن یا امن بودن (HTTPS) و امثال آن است. حاصل این مراحله، تولید یک AuthenticationTicket است.
5- در آخر، AuthenticationTicket و  ClaimsPrincipal به کلاس SecureDataFormat، برای ابتدا، serialize شدن اشیاء، رمزنگاری و در نهایت تبدیل آن‌ها به فرمت base64، ارسال می‌شوند.

جزئیات تکمیلی مرحله‌ی آخر آن نیز به این ترتیب است:
AuthenticationTicket با استفاده از کلاس TicketSerializer سریالایز می‌شود. پس از آن یک memory stream تشکیل شده و اطلاعات ClaimsIdentity و AuthenticationTicket سریالایز شده به آن ارسال می‌شوند. این memory stream با استفاده از الگوریتم GZip فشرده شده و برای پردازش بیشتر بازگشت داده می‌شود. مرحله‌ی بعد، رمزنگاری اطلاعات فشرده سازی شده‌است. برای این منظور از کلاس DpapiDataProtector دات نت استفاده می‌کنند. پس از رمزنگاری، استریم نهایی با فرمت base64 برای درج در HTTP Response آماده خواهد شد.

سؤال: چرا کوکی‌‌های یک کاربر معین لاگین شده‌ی توسط ASP.NET Identity، در مرورگرهای مختلف متفاوت است؟
هرچند اطلاعاتی مانند ApplicationUser.Id و SecurityStamp برای یک کاربر، در مرورگرهای مختلف یکسان هستند، اما در مرحله‌ی چهارم، ذکر شد که AuthenticationTicket دارای اطلاعات بیشتری مانند زمان تولید کوکی نیز هست. بنابراین اطلاعات نهایی رمزنگاری شده‌ی در این حالت که در زمان‌های مختلفی تولید شده‌اند، یکسان نخواهند بود.

سؤال: در ساب دومین‌های مختلف دومین مشخصی، چندین برنامه‌ی مختلف نصب شده‌اند. چگونه می‌توان از یک سیستم لاگین ASP.NET Identity برای تمام آن‌ها استفاده کرد؟
برای این منظور نیاز هست خاصیت CookieDomain را به صورت صریح مقدار دهی کرد. برای اینکار فایل Startup.Auth.cs را گشوده و  CookieAuthenticationOptions را تنظیم کنید:
var cookieAuthenticationOptions = new CookieAuthenticationOptions
{
    AuthenticationType  = DefaultAuthenticationTypes.ApplicationCookie,
    LoginPath           = new PathString("/Account/Login"),
    CookieDomain        = ".mydomain.com"
};
البته کار به همینجا ختم نمی‌شود. پس از آن نیاز است به ازای تمام دومین‌های موجود، یک machine key مشخص تنظیم شود. از این جهت که در مرحله‌ی پنجم تولید کوکی، کلاس DpapiDataProtector دات نت، از machine key موجود، برای رمزنگاری اطلاعات استفاده می‌کند و اگر این machine key، به ازای برنامه‌های مختلف متفاوت باشد، کوکی تولید شده، قابل رمزگشایی و استفاده نخواهد بود.
برای اینکار به کنسول IIS مراجعه کرده و گزینه‌ی machine key آن‌را بیابید. در این قسمت بر روی generate keys کلیک کرده و اطلاعات تولیدی را باید به تمام web.config‌های موجود کپی کنید:
 <machineKey
  validationKey="DAD9E2B0F9..."
  decryptionKey="ADD1C39C02..."
  validation="SHA1"
  decryption="AES"
/>

سؤال: برنامه‌های مختلفی بر روی یک دومین نصب هستند، اما قصد نداریم از سیستم اعتبارسنجی یکپارچه‌ای برای تمام آن‌ها استفاده کنیم. اما اگر در یکی لاگین کنیم، بلافاصله لاگین در برنامه‌ی دوم منقضی می‌شود، چرا؟
شبیه به همین مساله با Forms Authentication هم وجود دارد. برای رفع آن باید نام کوکی‌های هر برنامه را منحصربفرد کنید و از نام پیش فرض کوکی‌ها استفاده نکنید تا بر روی یکدیگر بازنویسی نشوند. برای اینکار خاصیت CookieName شیء CookieAuthenticationOptions را جداگانه مقدار دهی کنید:
 CookieName = "my-very-own-cookie-name"

سؤال: لاگین انجام شده‌ی در برنامه‌ای که از ASP.NET Identity استفاده می‌کند، زود منقضی می‌شود؛ چرا؟
برای تنظیم صریح زمان انقضای کوکی ASP.NET Identity نیاز است خاصیت ExpireTimeSpan آن‌را مقدار دهی کنید:
 app.UseCookieAuthentication(new CookieAuthenticationOptions
{
     ExpireTimeSpan = TimeSpan.FromHours(24.0),
});

سؤال: کاربر سیستم ASP.NET Identity از سیستم خارج شده‌است (log off کرده) ولی هنوز می‌توان از کوکی پیشین او برای اعتبارسنجی مجدد استفاده کرد. چطور می‌توان این نقیصه‌ی امنیتی را برطرف کرد؟
مشکل از اینجا است:
   public ActionResult LogOff()
  {
      AuthenticationManager.SignOut();
      return RedirectToAction("Index", "Home");
  }
در مثال رسمی ASP.NET Identity یک چنین کدی برای خروج از سیستم ارائه شده‌است. نمونه‌ی امن‌تر آن به صورت زیر است:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> LogOff()
{
    var user = await UserManager.FindByNameAsync(User.Identity.Name);
    AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
    await UserManager.UpdateSecurityStampAsync(user.Id);
    return RedirectToAction("Login", "Account");
}
در اینجا علاوه بر عدم استفاده‌ی از متد بدون پارامتر SignOut (با توجه به خاصیت AuthenticationType ذکر شده‌ی در CookieAuthenticationOptions)، کار به روز رسانی مجدد SecurityStamp کوکی نیز انجام شده‌است. با این تغییر، کوکی موجود بلا استفاده خواهد شد؛ چون دیگر قابل رمزگشایی نیست.
همچنین بهتر است مقدار validateInterval مربوط به SecurityStampValidator.OnValidateIdentity که به صورت پیش فرض 30 دقیقه است را به مقدار کمتری مانند 5 دقیقه تغییر دهید (تنظیمات OnValidateIdentity مربوط به CookieAuthenticationOptions فایل آغارین برنامه). کار این تنظیم، بررسی اعتبار کوکی، در بازه‌های زمانی مشخص شده‌است.
مطالب
CoffeeScript #15

قسمت‌های اصلاح نشده

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

استفاده از parseInt

تابع ()parseInt در جاوااسکریپت، در صورتیکه یک مقدار رشته‌ای را به آن ارسال کنید و پایه‌ی مناسب آن را تعیین نکنید، نتایج غیره منتظره‌ای (unexpected) را باز می‌گرداند . برای مثال:

# Returns 8, not 10!
parseInt('010') is 8
البته ممکن است شما این کد را در مرورگر خود تست کنید و مقدار 10 را باز گرداند؛ اما این برای همه‌ی مرورگرها یکسان نیست. برای اطمینان از مقدار بازگشتی صحیح، همیشه پایه‌ی آن را تعیین کنید.
# Use base 10 for the correct result
parseInt('010', 10) is 10
دقت کنید این چیزی نیست که CoffeeScript بتواند برای شما انجام دهد؛ شما فقط یادتان باشد که همیشه پایه‌ی صحیح را در موقع استفاده‌ی از ()parseInt تعریف کنید.

Strict mode


Strict mode یکی از قابلیت‌های ECMAScript 5 است که به شما اجازه می‌دهد تا یک برنامه یا تابع جاوااسکریپت را در محیطی محدود اجرا کنید. این محدودیت موجب نمایش بیشتر خطاها و هشدارها نسبت به حالت نرمال می‌شود و به توسعه دهندگان این امکان را می‌دهد تا از نوشتن کدهای غیر قابل بهینه سازی برای اشتباهات رایج جلوگیری کنند.
به عبارت دیگر Strict mode باعث کاهش اشکالات، افزایش امنیت، بهبود عملکرد و حذف برخی از سختی‌های استفاده از ویژگی‌های زبان می‌شود.
در حال حاضر Strict mode، در مرورگرهای زیر پشتیبانی می‌شود:

  • Chrome >= 13.0
  • Safari >= 5.0
  • Opera >= 12.0
  • Firefox >= 4.0
  • IE >= 10.0

با این حال، Strict mode به طور کامل با مرورگرهای قدیمی سازگار است.


تغییرات Strict mode

بیشتر تغییرات Strict mode مربوط به syntax جاوااسکریپت بوده است:

  • خطا در پروپرتی‌ها و نام آرگومان‌های تابع تکراری
  • خطا در عدم استفاده‌ی صحیح از delete
  • خطا در زمان دسترسی به arguments.caller و arguments.callee (به دلایل عملکرد)
  • استفاده از عمگر with سبب بروز خطای نحوی می‌شود
  • متغیرهای خاص مانند undefined که قابل نوشتن نیستند
  • معرفی کلمات کلیدی رزرو شده مانند implements, interface, let, package, private, protected, public, static و yield.

با این حال، برخی از رفتارهای زمان اجرای Strict mode نیز تغییر کرده است:

  • متغییرهای سراسری به صورت صریح و روشن هستند (کلمه کلیدی var نیاز است). مقدار سراسری this نیز به صورت undefined است.
  • eval نمی‌تواند متغیر جدیدی را در حوزه‌ی محلی خود تعریف کند.
  • بدنه‌ی هر تابع باید قبل از استفاده تعریف شده باشد (قبلا گفتم که در جاوااسکریپت شما می‌توانید قبل از تعریف تابع آن را فراخوانی کنید).
  • آرگومان‌ها تغییر ناپذیر هستند.

CoffeeScript در حال حاضر بسیاری از الزامات Strict mode را پیاده سازی کرده‌است مانند: همیشه از کلمه کلیدی var برای تعریف متغیر استفاده می‌کند؛ اما فعال کردن Strict mode در برنامه‌های CoffeeScript نیز بسیار مفید خواهد بود. در واقع CoffeeScript بر روی انطباق برنامه‌ها با Strict mode در زمان کامپایل را، در برنامه‌های آینده خود دارد.


استفاده از Strict mode

برای فعال کردن بررسی محدودیت، کد و توابع خود را با این رشته شروع کنید:
->
  "use strict"

  # ... your code ...
فقط با استفاده از رشته "use strict". به مثال زیر توجه کنید:
do ->
  "use strict"
  console.log(arguments.callee)
اجرای قطعه کد بالا درحالت strict mode، سبب بروز خطای syntax می‌شود؛ در حالیکه در حالت معمول این کد به خوبی اجرا می‌شود.
Strict mode دسترسی به arguments.callee و arguments.caller، که تاثیر بدی را بر روی عملکرد کد شما دارند، حذف می‌کند و استفاده‌ی از آنها سبب بروز خطا می‌شود.

در مثال زیر در حالت strict mode سبب بروز خطای TypeError می‌شود، اما در حالت نرمال به خوبی اجرا شده و یک متغیر سراسری را ایجاد می‌کند.
do ->
  "use strict"
  class @Spine
دلیل این رفتار این است که در Strict mode متغیر this به صورت undefined است؛ در حالیکه در حالت نرمال، this به شیء window اشاره می‌کند. راه حل این مشکل تعریف متغیرهای سراسری به صورت صریح به شیء window است.
do ->
  "use strict"
  class window.Spine
در حالیکه توصیه می‌شود که همیشه Strict mode فعال باشد، اما Strict mode هیچ یک از ویژگی‌های جدید جاوااسکریپت را که هنوز آماده نیست، فعال نمی‌کند و در واقع به علت بررسی بیشتر کدهای شما در زمان اجرا، باعث کاهش سرعت می‌شود.
شما می‌توانید در زمان توسعه برنامه جاوااسکریپت خود Strict mode را فعال کنید و در زمان انتشار، بدون Strict mode برنامه‌ی خود را منتشر کنید.

JavaScript Lint 

JavaScript Lint یک ابزار بررسی کیفیت کدهای جاوااسکریپت است و اجرای برنامه‌ی شما از طریق این راه عالی باعث بهبود کیفیت و بهترین شیوه‌ی کد نویسی می‌شود. این پروژه براساس ابزار JSLint است. شما می‌توانید چک لیست سایت JSLint را که شامل موضوعاتی است که باید آن‌ها در نظر داشته باشید، مانند متغیرهای سراسری، فراموش کردن نوشتن سمی کالن، کیفیت ضعیف عمل مقایسه را نام برد.

خبر خوب این است که CoffeeScript تمام موارد گفته شده‌ی در چک لیست را انجام می‌دهد. بنابراین کد تولیدی CoffeeScript با JavaScript Lint کاملا سازگار است. در واقع ابزار coffee از lint ،option پشتیبانی می‌کند.

coffee --lint index.coffee
  index.coffee: 0 error(s), 0 warning(s)

مطالب
CoffeeScript #12

بخش‌های بد

جاوااسکریپت یک زبان پیچیده است که شما برای کار با آن، نیاز است قسمت‌هایی را که باید از آن‌ها دوری کنید و قسمت‌های مهمی را که باید استفاده کنید، بشناسید. همانطور که Sun Tzu گفته "دشمن خود را بشناس"، ما نیز در این قسمت می‌خواهیم برای شناخت بیشتر قسمت‌های تاریک و روشن جاوااسکریپت به آن بپردازیم.

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

اول از قسمت‌هایی که توسط CoffeeScript حل شده‌اند شروع می‌کنیم.

A JavaScript Subset

with یک دستور بسیار زمانبر است و مضر شناخته شده است و نباید از آن استفاده کنید. with با ایجاد یک ساختار خلاصه نویسی، برای جستجو بر روی خصوصیات اشیاء در نظر گرفته شده بود. برای نمونه به جای نوشتن:

dataObj.users.vahid.email = "info@vmt.ir";
می‌توانید به این صورت این کار را انجام دهید:
with(dataObj.users.vahid) {
  email = "info@vmt.ir";
}
مفسر جاوااسکریپت دقیقا نمی‌داند که شما می‌خواهید چه کاری را با with انجام دهید، و به شیء مشخص شده فشار می‌آورد تا اول اسم همه مراجعه شده‌ها را جستجو کند. این عمل واقعا به عملکرد و کارآیی لطمه می‌زند. یعنی مترجم، تمام انواع بهینه سازی‌های JIT را خاموش می‌کند. همچنین پیشنهادهایی مبنی بر حذف کامل آن از نسخه‌های بعدی جاوااسکریپت نیز مطرح شده است.
همه چیز برای عدم استفاده از with در نظر گرفته شده است. CoffeeScript یک قدم جلوتر از همه برداشته و with را از syntax خود حذف کرده است. به عبارت دیگر در صورتیکه شما از آن استفاده کنید، کامپایلر CoffeeScript خطا صادر می‌کند.

Global variables

به طور پیش فرض تمامی برنامه‌های جاوااسکریپت در دامنه global اجرا می‌شوند و تمامی متغیرهایی که ساخته می‌شوند به طور پیش فرض در ناحیه‌ی global قرار می‌گیرند. اگر شما بخواهید متغیری را در ناحیه‌ی local ایجاد کنید، باید از کلمه کلیدی var استفاده کنید.

usersCount = 1;        // Global
var groupsCount = 2;   // Global

(function(){              
  pagesCount = 3;      // Global
  var postsCount = 4;  // Local
})()
اکثر اوقات شما می‌خواهید متغیر local ایی را ایجاد کنید و نه global. توسعه دهندگان باید همیشه به یاد داشته باشند که قبل از مقداردهی اولیه‌ی هر متغیری، کلمه‌ی کلیدی var را قرار دهند یا با انواع و اقسام مشکلات، هنگامی که متغیرها به طور تصادفی با یکدیگر برخورد و یا بازنویسی بر روی یکدیگر انجام می‌دهند، روبرو شوند.
خوشبختانه CoffeeScript به کمک شما می‌آید و به طور کامل انتساب متغیرهای global را به طور ضمنی از بین می‌برد. به عبارت دیگر کلمه کلیدی var در CoffeeScript رزرو شده است و در صورت استفاده خطا صادر می‌شود.
به صورت پیش فرض به طور ضمنی متغیرها local ایجاد می‌شوند و خیلی سخت می‌شود متغیر global ایی را بدون انتساب آن به عنوان خصوصیتی از شیء window ایجاد کرد.
outerScope = true
do ->
  innerScope = true
نتیجه‌ی کامپایل آن می‌شود:
var outerScope;
outerScope = true;
(function() {
  var innerScope;
  return innerScope = true;
})();
همانطور که مشاهده می‌کنید CoffeeScript مقداردهی اولیه متغیر را (با استفاده از var) به صورت خودکار در context ایی که برای اولین بار استفاده شده است انجام می‌دهد. باید مواظب باشید تا از نام متغیر خارجی مجددا استفاده نکنید که این اتفاق ممکن است در کلاس یا تابع با عمق زیاد ایجاد شود. برای مثال، در اینجا به صورت تصادفی متغیر package در یک تابع کلاس بازنویسی شده است:
package = require('./package')

class Test
  build: ->
    # Overwrites outer variable!
    package = @testPackage.compile()

  testPackage: ->
    package.create()
برای ایجاد متغیرهای global باید از انتساب آنها به عنوان خصوصیتی از شیء window استفاده کرد.
  class window.Asset
    constructor: ->
با تضمین متغیرهای global به صورت صریح و روشن به جای به طور ضمنی بودن آنها، CoffeeScript یکی از منابع اصلی ایجاد مشکلات در جاوااسکریپت را حذف کرد‌ه‌است.

Semicolons

جاوااسکریپت اجباری برای نوشتن ";" ندارد، بنابراین ممکن است یک سری از دستورات از قلم بیافتند. با این حال در پشت صحنه‌ی کامپایلر جاوااسکریپت به ";" احتیاج دارد. به طوری که parser جاوااسکریپت به صورت خودکار هر زمانی که نتواند ارزیابی از دستورات داشته باشد، یک بار دیگر با ";" این کار را انجام می‌دهد و درصورت موفقیت، پیام خطایی مبنی بر نبود ";" را صادر می‌کند.
متاسفانه این یک ایده بد است. چرا که ممکن است تغییر رفتاری در کد نوشته شده به وجود آید. به مثال زیر توجه کنید. به نظر کد نوشته شده صحیح است؛ درسته؟
function() {}
(window.options || {}).property
اشتباه است، حداقل با توجه به parser، یک خطای syntax صادر می‌شود. در مورد دوم نیز parser، ";" اضافه نمی‌کند و کد نوشته شده به کد یک خطی تبدیل می‌شوند.
function() {}(window.options || {}).property
حالا شما می‌توانید این موضوع را ببینید که چرا parser خطا داده‌است. وقتی شما در حال نوشتن کد جاوااسکریپتی هستید، باید بعد از هر دستور از ";" استفاده کنید. خوشبختانه در تمام زمانیکه درحال نوشتن کد CoffeeScript هستید، نیازی به نوشتن ";" ندارید. در زمانیکه کد CoffeeScript نوشته شده کامپایل می‌شود، به صورت خودکار ";" را در جای مناسبی قرار می‌دهد.

مطالب
تبدیل خودکار استثنای HttpRequestValidationException به یک ModelError در ASP.NET MVC
فرم هایی که اطلاعاتی را از یک کاربر دریافت کرده و به سمت سرور Post می‌کنند، از مهمترین اجزای لاینفک یک وب سایت می‌باشند. بی شک همه‌ی ما از چنین فرمهایی حتی در یک پروژه‌ی هرچند کوچک استفاده کرده‌ایم. ممکن است هنگام ارسال این فرمها، کاربری شیطنت به خرج داده و درون یکی از فیلدهای فرم، از عبارت‌های HTML و یا یک اسکریپت استفاده کرده باشد. در ASP.Net + MVC از مکانیزم ValidationRequest برای مقابله با چنین حملاتی (XSS) استفاده شده است.
یک فرم با یک Textbox در یک صفحه قرار دهید و درون Textbox یک تگ HTML بنویسید و آنرا به سرور ارسال کنید، در حالت پیش فرض اتفاقی که خواهد افتاد اینست که با یک صفحه‌ی خطا روبرو خواهید شد که به شما هشدار می‌دهد داده‌های ارسال شده، دارای مقادیر غیرمجازی می‌باشند. شما می‌توانید این مکانیزم اعتبارسنجی-امنیتی را غیرفعال کنید. برای غیرفعال کردن آن هم در لینکی که در فوق معرفی گردید توضیحات کافی داده شده است. ولی توصیه میشود برای جلوگیری از حملات XSS هیچگاه این مکانیزم را غیرفعال نکنید، مگر اینکه هیچ داده‌ای را بدون Encode کردن در صفحه نمایش ندهید.
راه‌های زیادی برای هندل کردن این خطا و مطلع کردن کاربر وجود دارند و معمولا از صفحه‌ی خطای سفارشی برای اینکار استفاده میشود. اما ایده‌ی بهتری نیز برای مقابله با این خطا وجود دارد!
فرض کنید اطلاعات فرم شما از طریق Ajax به سرور ارسال می‌شود و نتیجه بصورت Json به مروگر برگشت داده می‌شود. در حالت معمول در صورت رخ دادن خطای فوق، سرور کد 500 را برگشت می‌دهد و تنها راه هندل کردن آن در رویداد error شئ Ajax شما می‌باشد؛ آن‌هم با یک پیغام ساده. ولی من ترجیح می‌دهم به جای صادر کردن خطای 500، در صورت وقوع این خطا آن‌را بصورت یک خطای ModelState به کاربر نمایش دهم. به نظر من این‌کار وجهه‌ی بهتری دارد و ضمنا لازم نیست در هر رویدادی این خطا را هندل کنید. برای رسیدن به این هدف هم چندین راه وجود دارد که ساده‌ترین آن‌ها اینست که یک ModelBinder سفارشی ایجاد کنید و با هندل کردن این خطا، یک پیام خطا را به ModelState اضافه کنید و بعد به MVC بگویید که از این ModelBinder برای پروژه‌ی من استفاده کن. با این روش هرگاه کاربر داده‌ای حاوی کدهای HTML درون فیلدهای فرم وارد کرده باشد، بدون هیچ کار اضافه‌ای وضعیت ModelState شما Invalid می‌شود و خطای مدل به کاربر نمایش داده خواهد شد.
ابتدا کلاسی برای ModelBinder سفارشی خود ایجاد کنید و از کلاس DefaultModelBinder ارث بری کنید. سپس با بازنویسی متد BindModel آن منطق خود را پیاده سازی کنید:
using System.Web;
using System.Web.Mvc;
using System.Web.Helpers;
using System.Globalization;

namespace Parsnet
{
    public class ParsnetModelBinder : DefaultModelBinder
    {
        public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            try
            {
                return base.BindModel(controllerContext, bindingContext);
            }
            catch (HttpRequestValidationException ex)
            {
                var modelState = new ModelState();
                modelState.Errors.Add("اطلاعات ارسالی شما دارای کدهای HTML می‌باشند.");
                var key = bindingContext.ModelName;
                var value = controllerContext.RequestContext.HttpContext.Request.Unvalidated().Form[key];
                modelState.Value = new ValueProviderResult(value, value, CultureInfo.InvariantCulture);
                bindingContext.ModelState.Add(key, modelState);
            }

            return null;
        }
    }
}

در این کلاس ابتدا سعی میکنیم بطور عادی کار را به متد BindModel کلاس پایه بسپاریم. اگر داده‌های ارسال شده حاوی کد HTML نباشد، بدون هیچ خطایی Model Binding صورت می‌گیرد. ولی در صورتیکه کاربر در فیلدی، از کدهای HTML استفاده کرده باشد، یک خطای HttpRequestValidationException  رخ خواهد داد که با هندل کردن آن هدف خود را تامین می‌کنیم.
برای اینکار در قسمت catch بلوک مدیریت خطا، ابتدا یک نمونه از کلاس ModelState را میسازیم. بعد پیام خطای مورد نظر خود را به Errors‌های آن اضافه میکنیم. حال باید یک زوج کلید/مقدار برای این ModelState تعریف کنیم و به bindingContext اضافه کنیم. کلید ما در اینجا نام مدل جاری و مقدار آن هم نام فیلدی از فرم است که سبب بروز این خطا شده است.
حالا نهایت کاری که باید انجام دهیم اینست که در رویداد Application_Start این مدل بایندیگ سفارشی را جایگزین مدل بایندیگ پیش فرض کنیم:
    ModelBinders.Binders.DefaultBinder = new ParsnetModelBinder();

کار تمام است. از حالا به بعد در صورتیکه کاربر در فیلدهای هر فرمی از سایت شما از کدهای HTML استفاده کند دیگر خطایی رخ نمی‌دهد و فقط ModelState در وضعیت Invalid قرار میگیرد که میتوانید با قرار دادن یک ValidationMessage یا ValidationSummary به راحتی خطا را به کاربر نشان دهید.