نظرات مطالب
طراحی افزونه پذیر با ASP.NET MVC 4.x/5.x - قسمت اول
با تشکر به خاطر مطلب مفیدی که منتشر کردید.
مشکلی که به آن برخوردم این است که افزونه به خوبی در پروژه اصلی بار گذاری میشود ولی متد RegisterArea مربوط به Area موجود در افزونه اجرا نمیشود .
مطالب
افزودن تصدیق ایمیل به ASP.NET Identity در MVC 5
در پست قبلی نحوه سفارشی کردن پروفایل کاربران در ASP.NET Identity را مرور کردیم. اگر بیاد داشته باشید یک فیلد آدرس ایمیل به کلاس کاربر اضافه کردیم. در این پست از این فیلد استفاده میکنیم تا در پروسه ثبت نام ایمیل‌ها را تصدیق کنیم. بدین منظور پس از ثبت نام کاربران یک ایمیل فعالسازی برای آنها ارسال می‌کنیم که حاوی یک لینک است. کاربران با کلیک کردن روی این لینک پروسه ثبت نام خود را تایید می‌کنند و می‌توانند به سایت وارد شوند. پیش از تایید پروسه ثبت نام، کاربران قادر به ورود نیستند.


در ابتدا باید اطلاعات کلاس کاربر را تغییر دهید تا دو فیلد جدید را در بر گیرد. یک فیلد شناسه تایید (confirmation token) را ذخیره می‌کند، و دیگری فیلدی منطقی است که مشخص می‌کند پروسه ثبت نام تایید شده است یا خیر. پس کلاس ApplicationUser  حالا باید بدین شکل باشد.
public class ApplicationUser : IdentityUser
{
    public string Email { get; set; }
    public string ConfirmationToken { get; set; }
    public bool IsConfirmed { get; set; }
}
اگر پیش از این کلاس ApplicationUser را تغییر داده اید، باید مهاجرت‌ها را فعال کنید و دیتابیس را بروز رسانی کنید. حالا می‌توانیم از این اطلاعات جدید در پروسه ثبت نام  استفاده کنیم و برای کاربران ایمیل‌های تاییدیه را بفرستیم.
private string CreateConfirmationToken()
{
    return ShortGuid.NewGuid();
}
 
private void SendEmailConfirmation(string to, string username, string confirmationToken)
{
    dynamic email = new Email("RegEmail");
    email.To = to;
    email.UserName = username;
    email.ConfirmationToken = confirmationToken;
    email.Send();
}
 
//
// POST: /Account/Register
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
    if (ModelState.IsValid)
    {
        string confirmationToken = CreateConfirmationToken();
        var user = new ApplicationUser()
        {
            UserName = model.UserName,
            Email = model.Email,
            ConfirmationToken = confirmationToken, 
                IsConfirmed = false };
        var result = await UserManager.CreateAsync(user, model.Password);
        if (result.Succeeded)
        {
            SendEmailConfirmation(model.Email, model.UserName, confirmationToken);
            return RedirectToAction("RegisterStepTwo", "Account");
        }
        else
        {
            AddErrors(result);
        }
    }
 
    // If we got this far, something failed, redisplay form
    return View(model);
}
برای تولید شناسه‌های تایید (tokens) از کلاسی بنام ShortGuid استفاده شده است. این کلاس یک مقدار GUID را encode می‌کند که در نتیجه آن مقدار خروجی کوتاه‌تر بوده و برای استفاده در URL‌ها ایمن است. کد این کلاس را از این وبلاگ گرفته ام. پس از ایجاد حساب کاربری باید شناسه تولید شده را به آن اضافه کنیم و مقدار فیلد IsConfirmed را به false تنظیم کنیم. برای تولید ایمیل‌ها من از Postal استفاده می‌کنم. Postal برای ساختن ایمیل‌های دینامیک شما از موتور Razor استفاده می‌کند. می‌توانید ایمیل‌های ساده (plain text) یا HTML بسازید، عکس و فایل در آن درج و ضمیمه کنید و امکانات بسیار خوب دیگر. اکشن متد RegisterStepTwo تنها کاربر را به یک View هدایت می‌کند که پیامی به او نشان داده می‌شود.
بعد از اینکه کاربر ایمیل را دریافت کرد و روی لینک تایید کلیک کرد به اکشن متد RegisterConfirmation باز می‌گردیم.
private bool ConfirmAccount(string confirmationToken)
{
    ApplicationDbContext context = new ApplicationDbContext();
    ApplicationUser user =  context.Users.SingleOrDefault(u => u.ConfirmationToken == confirmationToken);
    if (user != null)
    {
        user.IsConfirmed = true;
        DbSet<ApplicationUser> dbSet = context.Set<ApplicationUser>();
        dbSet.Attach(user);
        context.Entry(user).State = EntityState.Modified;
        context.SaveChanges();
 
        return true;
    }
    return false;
}
 
[AllowAnonymous]
public ActionResult RegisterConfirmation(string Id)
{
    if (ConfirmAccount(Id))
    {
        return RedirectToAction("ConfirmationSuccess");
    }
    return RedirectToAction("ConfirmationFailure");
}
متد ConfirmAccount سعی می‌کند کاربری را در دیتابیس پیدا کند که شناسه تاییدش با مقدار دریافت شده از URL برابر است. اگر این کاربر پیدا شود، مقدار خاصیت IsConfirmed را به true تغییر می‌دهیم و همین مقدار را به تابع باز می‌گردانیم. در غیر اینصورت false بر می‌گردانیم. اگر کاربر تایید شده است، می‌تواند به سایت وارد شود. برای اینکه مطمئن شویم کاربران پیش از تایید ایمیل شان نمی‌توانند وارد سایت شوند، باید اکشن متد Login را کمی تغییر دهیم.
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
    if (ModelState.IsValid)
    {
        var user = await UserManager.FindAsync(model.UserName, model.Password);
        if (user != null && user.IsConfirmed)
        {
            await SignInAsync(user, model.RememberMe);
            return RedirectToLocal(returnUrl);
        }
        else
        {
            ModelState.AddModelError("", "Invalid username or password.");
        }
    }
 
    // If we got this far, something failed, redisplay form
    return View(model);
}
تنها کاری که می‌کنیم این است که به دنبال کاربری می‌گردیم که فیلد IsConfirmed آن true باشد. اگر مقدار این فیلد false باشد کاربر را به سایت وارد نمی‌کنیم و پیغام خطایی نمایش می‌دهیم.
همین. این تمام چیزی بود که برای اضافه کردن تصدیق ایمیل به اپلیکیشن خود نیاز دارید. از آنجا که سیستم ASP.NET Identity با Entity Framework مدیریت می‌شود و با مدل Code First ساخته شده، سفارشی کردن اطلاعات کاربران و سیستم عضویت ساده‌تر از همیشه است.



توضیحاتی درباره کار با Postal

اگر به متد SendEmailConfirmation دقت کنید خواهید دید که آبجکتی از نوع Email می‌سازیم (که در اسمبلی‌های Postal وجود دارد) و از آن برای ارسال ایمیل استفاده می‌کنیم. عبارت "RegEmail" نام نمایی است که باید برای ساخت ایمیل استفاده شود. این متغیر از نوع dynamic است، مانند خاصیت ViewBag. بدین معنا که می‌توانید مقادیر مورد نظر خود را بصورت خواص دینامیک روی این آبجکت تعریف کنید. از آنجا که Postal از موتور Razor استفاده می‌کند، بعدا در View ایمیل خود می‌توانید به این مقادیر دسترسی داشته باشید.
در پوشه Views پوشه جدیدی بنام Emails بسازید. سپس یک فایل جدید با نام RegEmail.cshtml در آن ایجاد کنید. کد این فایل را با لیست زیر جایگزین کنید.
To: @ViewBag.To
From: YOURNAME@gmail.com
Subject: Confirm your registration

Hello @ViewBag.UserName,
Please confirm your registration by following the link bellow.

@Html.ActionLink(Url.Action("RegisterConfirmation", "Account", new { id = @ViewBag.ConfirmationToken }), "RegisterConfirmation", "Account", new { id = @ViewBag.ConfirmationToken }, null)
این فایل، قالب ایمیل‌های شما خواهد بود. ایمیل‌ها در حال حاظر بصورت plain text ارسال می‌شوند. برای اطلاعات بیشتر درباره ایمیل‌های HTML و امکانات پیشرفته‌تر به سایت پروژه Postal  مراجعه کنید.

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

  • ViewBag.To آدرس ایمیل گیرنده را نشان می‌دهد.
  • ViewBag.UserName نام کاربر جاری را نمایش می‌دهد.
  • ViewBag.ConfirmationToken شناسه تولید شده برای تایید کاربر است.
در این قالب لینکی به متد RegisterConfirmation در کنترلر Account وجود دارد که شناسه تایید را نیز با پارامتری بنام id انتقال می‌دهد.

یک فایل ViewStart.cshtml_ هم در این پوشه بسازید و کد آن را با لیست زیر جایگزین کنید.
@{ Layout = null; /* Overrides the Layout set for regular page views. */ }
مطالب
فراخوانی متدهای Controllerها در Viewهای ASP.NET MVC

شاید در ابتدا فراخوانی متدی از یک کنترلر در یک View کار سختی به نظر برسد، ولی در واقع با استفاده از مفاهیم Lambda expressions و Delegate‌ها این کار بسیار راحت خواهد بود. 

برای این کار میتوانیم متد مورد نظر را به صورت یک delegate تعریف کرده و به view ارسال کنیم. فرض کنیم متدی داریم برای برگرداندن مجموع 2 عدد به صورت string: 

public string Sum(int a,int b)
{
    return (a + b).ToString();
}

حال برای اینکه بتوانیم این متد را بصورت یک delegate به view ارسال کنیم لازم است تا یک delegate را بصورت public و در خارج از تعریف کلاسها و درون یک namespace مشخصی تعریف کنیم. در اینجا برای راحتی در همان MvcTest.Controllers namespace  یک delegate را بصورت زیر (MvcTest  نام پروژه است) تعریف می‌کنیم: 

public delegate string SumOf2Number(int a, int b);

حال میتوانیم بصورت زیر این متد را از طریق ViewBag به View ارسال کنیم: 

SumOf2Number sum2numbers = Sum;
ViewBag.SumFunc3 = sum2numbers;

در روش دوم، می‌توانیم متد مورد نظر را بصورت Func  به View ارسال کنیم. این کار را میتوانیم به دو صورت انجام دهیم، که هر دو را در تکه کد زیر خواهید دید: 

ViewBag.SumFunc =(Func<int,int,string>) Sum;//way 1
ViewBag.SumFunc2 = (Func<int, int, string>)((int a, int b) => { return (a + b).ToString(); });//way 2
همانطور که متوجه شدید، در روش اول تنها کاری که کردیم متد Sum را از طریق TypeCasting به یک Func تبدیل کردیم و در روش دوم هم یک Lambda expression را بصورت مستقیم به Func تبدیل کرده و استفاده کردیم.

میتوانیم یک Lambda expression را به یک متغیر delegate نیز ربط دهیم؛ به این صورت:

SumOf2Number sum2numbers2 = (int a, int b) => { return (a + b).ToString(); };

در نهایت کد بخش کنترلر کلاً به اینصورت خواهد بود: 

namespace MvcTest.Controllers
{
    public  delegate string SumOf2Number(int a, int b);
    public class HomeController : Controller
    {        
        public ActionResult Index()
        {            
            SumOf2Number sum2numbers = Sum;
            SumOf2Number sum2numbers2 = (int a, int b) => { return (a + b).ToString(); };
 
            ViewBag.SumFunc =(Func<int,int,string>) Sum;
            ViewBag.SumFunc2 = (Func<int, int, string>)((int a, int b) => { return (a + b).ToString(); });
            ViewBag.SumFunc3 = sum2numbers;
            ViewBag.SumFunc4 = sum2numbers2;
            return View();
        }
        public string Sum(int a,int b)
        {
            return (a + b).ToString();
        }
        public ActionResult About()
        {
            ViewBag.Message = "Your application description page.";
 
            return View();
        }
 
        public ActionResult Contact()
        {
            ViewBag.Message = "Your contact page.";
 
            return View();
        }
    }
}

و در Index View خواهیم داشت: (البته اصولاً استفاده از controller namespace در سمت view کار درستی نیست، منتها اینجا فقط یک مثال کاربردی ساده است)  

@using MvcTest.Controllers;
@{
    ViewBag.Title = "Home Page";
}
   
<h1>
    @ViewBag.SumFunc(7,8)
</h1>
<h1>
    @ViewBag.SumFunc2(9, 10)
</h1>
<h1>
    @ViewBag.SumFunc3(5, 1)
</h1>
<h1>
    @ViewBag.SumFunc4(2, 3)
</h1>
مطالب
الگوی Composite
الگوی Composite یکی دیگر از الگوهای ساختاری می‌باشد که قصد داریم در این مقاله آن را بررسی نماییم.

الگوی Composite در عمل یک Collection Pattern (الگوی مجموعه ای) است. که می‌توان در درون آن ترکیبی از زیر مجموعه‌های مختلف را قرار داد و سپس هر زیر مجموعه را به نوبه خود فراخوانی نمود.به بیان دیگر الگوی Composite به ما کمک می‌کند که در یک ساختار درختی بتوانیم مجموعه ای (Collection ی)،از بخشی از آبجکتهای سلسله مراتبی را نمایش دهیم. این الگو به Client اجازه می‌دهد، که رفتار یکسانی نسبت به یک Collection ی از آبجکتها یا یک آبجکت تنها داشته باشد.

مثالهای متعددی می‌توان از الگوی Composite زد، که در ذیل به چند نمونه از آنها می‌پردازیم:

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

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

نمونه سوم: یک File System را در نظر بگیرید،که ساختارش از File و Folder تشکیل شده است. و می‌تواند یک ساختار سلسله مراتبی داشته باشد.بطوریکه درون هر Folder می‌تواند یک یا چند File یا Folder قرار گیرد. و در درون Folder‌های زیر مجموعه می‌توان چندین File یا Folder دیگر قرار داد.اگر بخواهیم به عنوان نمونه شکل ساختار درختی File و فولدر را نمایش دهیم بصورت زیر خواهد بود:

در ساختار درختی به Folder شاخه یا Branch گویند، چون می‌تواند زیر شاخه‌های دیگری نیز در خود داشته باشد. و به File برگ یا Leaf گویند.برگ نمی‌تواند زیر مجموعه ای داشته باشد. در واقع برگ (Leaf) بیانگر انتهای یک شاخه می‌باشد.

نمونه آخر:می توان به ساختار منوها در برنامه‌ها اشاره نمود.هر منو می‌تواند شامل چندین زیر منو باشد. و همان زیر منوها می‌توانند از چندین زیر منوی دیگر تشکیل شوند. این ساختار نیز یک ساختار سلسله مراتبی می‌باشد، و برای نگهداری آبجکتهای یک مجموعه می‌توان از الگوی Composite استفاده نمود.

الگوی Composite از سه Component اصلی تشکیل شده است،که یکایک آنها را بررسی می‌کنیم:

  • Component: کلاس پایه ای است که در آن متدها یا Functionality‌های مشترک تعریف می‌گردد. Component می‌تواند یک Abstract Class یا Interface باشد.
  • Leaf : به آبجکتهای گفته می‌شود که هیچ Child ی ندارند. و فقط یک آبجکت مستقل تنها می‌باشد. کلاس Leaf متدهای مشترک تعریف شده در Component  را پیاده سازی می‌کند.اگر مثال File و Folder را بخاطر آورید،File یک آبجکت از نوع Leaf است چون نمی‌تواند هیچ فرزندی داشته باشد و یک آبجکت تنها می‌باشد.
  • Composite: کلاس فوق Collection ی از آبجکتها را در خود نگهداری می‌کند، به عبارتی در Composite می‌توان بخشی از ساختار درختی را قرار داد، که این ساختار می‌تواند ترکیبی از آبجکتهای Leaf و Composite باشد. در مثال File و Folder، یک Folder را می‌توان به عنوان Composite در نظر گرفت،زیرا که یک Folder می‌تواند چندین File یا Folder را در خود جای دهد. در کلاس Composite معمولا متدهایی همچون Add (افزودن Remove،( Child (حذف یک Child) و غیرو... وجود دارد. 
کلاس Leaf و کلاس Composite از کلاس Component ارث بری (Inherit) می‌شوند.
شکل زیر بیانگر الگوی Composite می‌باشد:


توصیف شکل: طبق تعاریف گفته شده، دو کلاس Leaf و Composite از Inherit ،Component شده اند. و Client نیز فقط متد‌های مشترک تعریف شده در Component را مشاهده می‌کند، به عبارتی Client رفتار یکسانی نسبت به Leaf و Composite خواهد داشت.
برای درک بیشتر الگوی Composite مثالی را بررسی می‌کنیم، فرض کنید در کلاس Component متدی به نام Display را تعریف می‌کنیم،بطوریکه نام آبجکت را نمایش دهد.بنابراین خواهیم داشت:
اینترفیسی را برای Component در نظر می‌گیریم، و متد Display را در آن تعریف می‌کنیم:
public interface Icomponent
    {
        void Display(int depth);
    }
در کلاس Leaf، اینترفیس IComponent را پیاده سازی می‌نماییم:
 public class Leaf:Icomponent
    {
        private String name = string.Empty;
        public Leaf(string name)
        {
            this.name = name;
        }

        public void Display(int depth)
        {

            Console.WriteLine(new String('-', depth) + ' ' + name);

        }
    }
در کلاس Composite نیز اینترفیس IComponent را پیاده سازی می‌نماییم، با این تفاوت که متد‌های Add و Remove را نیز در کلاس Composite اضافه می‌کنیم، چون قبلا هم گفته بودیم، Composite در حکم یک Collection می‌باشد، بنابراین می‌بایست قابلیت حذف و اضافه نمود آبجکت در خود را داشته باشد. پیاده سازی متد Display در آن بصورت Recursive (بازگشتی) میباشد. و علتش این است که بتوانیم ساختار سلسله مراتبی را بازیابی نماییم.
 public class Composite:Icomponent
    {
        private List<Icomponent> _children = new List<Icomponent>();
        private String name = String.Empty;

        public Composite(String sname)
        {
            this.name = sname;
        }

        public void Add(Icomponent component)
        {

            _children.Add(component);

        }


        public void Remove(Icomponent component)
        {

            _children.Remove(component);

        }


        public void Display(int depth)
        {

            Console.WriteLine(new String('-', depth) + ' ' + name);



            // Recursively display child nodes

            foreach (Icomponent component in _children)
            {

                component.Display(depth + 2);

            }

        }
    }
در ادامه بوسیله چندین آبجکت Leaf و Composite یک ساختار درختی را ایجاد می‌کنیم.
class Program
    {
        static void Main(string[] args)
        {
            // Create a tree structure

            Composite root = new Composite("root");

            root.Add(new Leaf("Leaf A"));

            root.Add(new Leaf("Leaf B"));



            Composite comp = new Composite("Composite X");

            comp.Add(new Leaf("Leaf XA"));

            comp.Add(new Leaf("Leaf XB"));



            root.Add(comp);

            root.Add(new Leaf("Leaf C"));



            // Add and remove a leaf

            Leaf leaf = new Leaf("Leaf D");

            root.Add(leaf);

            root.Remove(leaf);



            // Recursively display tree

            root.Display(1);

            Console.ReadKey();
        }
    }
در ابتدا یک آبجکت Composite ایجاد می‌کنیم و آن را به عنوان ریشه در نظر گرفته و نام آن را Root قرار می‌دهیم. سپس دو آبجکت LeafA و LeafB را به آن می‌افزاییم، در ادامه آبجکت Composite دیگری به نام Comp ایجاد می‌کنیم، که خود دارای دو فرزند به نامهای LeafXA و LeafXB  می باشد. و سر آخر هم یک آبجکت LeafC ایجاد می‌کنیم.
آبجکت LeafD صرفا جهت نمایش افزودن و حذف کردن آن در یک آبجکت Composite نوشته شده است. برای این که بتوانیم ساختار سلسله مراتبی کد بالا را مشاهده نماییم، متد Root.Display را اجرا می‌کنیم و خروجی آن بصورت زیر خواهد بود:

اگر بخواهیم،شکل درختی آن را تصور کنیم بصورت زیر خواهد بود:


درپایان باید بگویم،که نمونه کد بالا را می‌توان به ساختار File و Folder نیز تعمیم داد، بطوریکه متدهای مشترک بین File و Folder را در اینترفیس IComponent تعریف می‌کنیم و بطور جداگانه در کلاسهای Composite و Leaf پیاده سازی می‌کنیم.
امیدوارم توضیحات داده شده در مورد الگوی Composite مفید واقع شود.

نظرات مطالب
پشتیبانی توکار از انجام کارهای پس‌زمینه در ASP.NET Core 2x
یک نکته‌ی تکمیلی: روش اجرای خودکار کدها در ابتدای کار برنامه

حتی اگر نخواهیم از IHostedService‌ها استفاده کنیم، می‌توان از یک قابلیت جالب آن‌ها استفاده کرد: اجرای خودکار کدها در زمان آغاز برنامه.
//Define your hosted service with startup logic
public class MyHostedService : IHostedService
{
    public async Task StartAsync(CancellationToken cancellationToken)
    {
        //Startup logic here
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        //Cleanup logic here
    }
}

//Register hosted service 
public void ConfigureServices(IServiceCollection services)
{
    services.AddHostedService<MyHostedService>();
}
برای مثال بجای اینکه سرویسی را مستقیما در انتهای public static void Main فراخوانی کنیم:
//"Main" method
public static void Main(string[] args)
{
    var host = CreateHostBuilder(args).Build();
    //Startup logic here
    host.Run();
}
 می‌توان اجرای خودکار آن‌را به متد StartAsync فوق منتقل کرد. این روش خصوصا جهت ساده سازی توزیع کتابخانه‌ها مفید است؛ چون تنظیمات کمتری را به همراه خواهد داشت.
مطالب
ارسال ایمیل در ASP.NET Core
فضای نام System.Net.Mail در NET Core 1.2. که پیاده سازی netstandard2.0 است، ارائه خواهد شد. بنابراین فعلا (در زمان NET Core 1.1.) راه حل توکار و رسمی برای ارسال ایمیل در برنامه‌های مبتنی بر NET Core. وجود ندارد. اما می‌توان کتابخانه‌ی ثالثی را به نام MailKit، به عنوان راه‌حلی که .NET 4.0, .NET 4.5, .NET Core, Xamarin.Android, و Xamarin.iOS را پشتیبانی می‌کند، درنظر گرفت و توانمندی‌ها و پروتکل‌های پشتیبانی شده‌ی توسط آن، از System.Net.Mail توکار نیز بسیار بیشتر است.


افزودن وابستگی‌های MailKit به برنامه

برای شروع به استفاده‌ی از MailKit، می‌توان بسته‌ی نیوگت آن‌را به فایل project.json برنامه معرفی کرد:
{
    "dependencies": {
        "MailKit": "1.10.0"
    }
}


استفاده از MailKit جهت تکمیل وابستگی‌های ASP.NET Core Identity

قسمتی از ASP.NET Core Identity، شامل ارسال ایمیل‌های «ایمیل خود را تائید کنید» است که آن‌را می‌توان توسط MailKit به نحو ذیل تکمیل کرد:
using System.Threading.Tasks;
using ASPNETCoreIdentitySample.Services.Contracts.Identity;
using MailKit.Net.Smtp;
using MailKit.Security;
using MimeKit;
 
namespace ASPNETCoreIdentitySample.Services.Identity
{
    public class AuthMessageSender : IEmailSender, ISmsSender
    {
        public async Task SendEmailAsync(string email, string subject, string message)
        {
            var emailMessage = new MimeMessage();
 
            emailMessage.From.Add(new MailboxAddress("DNT", "do-not-reply@dotnettips.info"));
            emailMessage.To.Add(new MailboxAddress("", email));
            emailMessage.Subject = subject;
            emailMessage.Body = new TextPart(TextFormat.Html)
            {
                Text = message
            };
 
            using (var client = new SmtpClient())
            {
                client.LocalDomain = "dotnettips.info";
                await client.ConnectAsync("smtp.relay.uri", 25, SecureSocketOptions.None).ConfigureAwait(false);
                await client.SendAsync(emailMessage).ConfigureAwait(false);
                await client.DisconnectAsync(true).ConfigureAwait(false);
            }
        }
 
        public Task SendSmsAsync(string number, string message)
        {
            // Plug in your SMS service here to send a text message.
            return Task.FromResult(0);
        }
    }
}
در اینجا MimeMessage بیانگر محتوا و تنظیمات ایمیلی است که قرار است ارسال شود. ابتدا قسمت‌های From و To آن تنظیم می‌شوند تا مشخص باشد که ایمیل ارسالی از کجا ارسال شده و قرار است به چه آدرسی ارسال شود. در ادامه موضوع و عنوان ایمیل تنظیم شده‌است. سپس متن ایمیل، به خاصیت Body شیء MimeMessage انتساب داده خواهد شد. فرمت ایمیل ارسالی را نیز می‌توان در اینجا تنظیم کرد. برای مثال TextFormat.Html جهت ارسال پیام‌هایی حاوی تگ‌های HTML مناسب است و اگر قرار است صرفا متن ارسال شود، می‌توان TextFormat.Plain را انتخاب کرد.
در آخر، این پیام به SmtpClient جهت ارسال نهایی، فرستاده می‌شود. این SmtpClient هرچند هم نام مشابه آن در System.Net.Mail است اما با آن یکی نیست و متعلق است به MailKit. در اینجا ابتدا LocalDomain تنظیم شده‌است. تنظیم این مورد اختیاری بوده و صرفا به SMTP سرور دریافت کننده‌ی ایمیل‌ها، مرتبط است که آیا قید آن‌را اجباری کرده‌است یا خیر. تنظیمات اصلی SMTP Server در متد ConnectAsync ذکر می‌شوند که شامل مقادیر host ،port و پروتکل ارسالی هستند.


ارسال ایمیل به SMTP pickup folder

روشی که تا به اینجا بررسی شد، جهت ارسال ایمیل‌ها به یک SMTP Server واقعی کاربرد دارد. اما در حین توسعه‌ی محلی برنامه می‌توان ایمیل‌ها را در داخل یک پوشه‌ی موقتی ذخیره و آن‌ها را توسط برنامه‌ی Outlook (و یا حتی مرورگر Firefox) بررسی و بازبینی کامل کرد.
در این حالت تنها کاری را که باید انجام داد، جایگزین کردن قسمت ارسال ایمیل واقعی توسط SmtpClient در کدهای فوق، با قطعه کد ذیل است:
using (var stream = new FileStream($@"c:\smtppickup\email-{Guid.NewGuid().ToString("N")}.eml", FileMode.CreateNew))
{
   emailMessage.WriteTo(stream);
}
تولید فایل‌های eml جهت «شبیه سازی ارسال ایمیل در ASP.Net» بسیار مفید هستند.


FAQ و منبع تکمیلی
مطالب
Postable

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



دریافت برنامه (برای اجرا نیاز به دات نت فریم ورک 2 دارد)

این برنامه‌ی کمکی، انجام چند کار زیر را در بلاگر برای شما ساده‌تر خواهد کرد:
الف) escape خودکار کاراکترهای غیرمجاز xml هنگام ارسال سورس کدهای خود و همچنین قرار دادن آن‌ها داخل تگ‌های div و pre مناسب.

روش برنامه نویسی آن:

public static string EscapeXml(string s)
{
var xml = s;
if (!string.IsNullOrEmpty(xml))
{
// replace literal values with entities
xml = xml.Replace("&", "&amp;");
xml = xml.Replace("<", "&lt;");
xml = xml.Replace(">", "&gt;");
xml = xml.Replace("\"", "&quot;");
xml = xml.Replace("'", "&apos;");
}
return xml;
}
ب) حذف خطوط بین تگ‌های html . این مورد هنگام ارسال یک table استاندارد html در بلاگر لازم است. برای مثال در بلاگر ارسال کد زیر
<table>
<tr>
<td>data
</td>
</tr>
سبب ایجاد فواصل عجیبی در حین نمایش ردیف‌های جدول در سایت خواهد شد، زیرا بلاگر به ازای هر خط جدید یک br را به صورت خودکار نمایش خواهد داد. برای رفع این مشکل از دکمه remove html new lines استفاده کنید. به این صورت اطلاعات فوق به صورت خودکار به شکل زیر تبدیل می‌شوند:

<table> <tr> <td>data</td> </tr>

روش برنامه نویسی آن :

private static readonly Regex REGEX_BETWEEN_TAGS = new Regex(@">\s+<", RegexOptions.Compiled);
private static readonly Regex REGEX_LINE_BREAKS = new Regex(@"\n\s+", RegexOptions.Compiled);
public static string RemoveSpaces(string html)
{
html = REGEX_BETWEEN_TAGS.Replace(html, "> <");
return REGEX_LINE_BREAKS.Replace(html, string.Empty);

}

ج) حذف کاراکتر 0xA0 . البته این مورد ارتباطی به بلاگر پیدا نمی‌کند ولی اگر با CPP کار کرده باشید، حتما به مورد کپی سورس از اینترنت به داخل ادیتور و عدم کامپایل آن، برخورده‌اید. در سورس کدهای CPP مجاز به استفاده از کاراکتر No-Break Space نیستید (0xA0) و باید حذف شود. حال فرض کنید با بیش از 200 سطر سر و کار دارید. بنابراین نیاز به یک تمیز کننده سریع وجود خواهد داشت. (این مورد در ادیتور برنامه management studio اس کیوال سرور هم صادق است)

txtMod.Text = txtOrig.Text.Replace((char)160, ' ');
ساده است ولی انجام دستی آن مشکل خواهد بود.

مطالب
غیرمعتبر کردن توکن و یا کوکی سرقت شده در برنامه‌های مبتنی بر ASP.NET Core
چند روز قبل، یکی از کانال‌های فنی معروف یوتیوب با بیش از 15 میلیون مشترک، هک و پاک شد! که داستان آن‌را در اینجا می‌توانید پیگیری کنید. در این هک، مهاجم در سعی اول، پیشنهاد پشتیبانی مالی از شبکه را داده و در ایمیل دوم، پس از جلب اعتماد اولیه، یک فایل به ظاهر PDF مفاد قرارداد را ارسال کرده که با کلیک بر روی آن، تمام کوکی‌های یوتیوب مالک کانال، سرقت و مورد سوء استفاده قرار گرفته! در یک چنین حالتی، مهم نیست که شما اعتبارسنجی دو مرحله‌ای را فعال کرده‌اید و یا از بهترین روش‌های رمزنگاری برای امن کردن اطلاعات کوکی و یا توکن خود استفاده کرده‌اید، همینقدر که اصل محتوای کوکی و یا توکن شما در اختیار شخص دیگری قرار گیرد، می‌تواند بدون نیاز به لاگین و دانستن کلمه‌ی عبور شما، بجای شما وارد سیستم شده و تغییرات دلخواهی را اعمال کند!
بنابراین سؤال اینجاست که ما (توسعه دهندگان) چگونه می‌توانیم یک چنین حملاتی را مشکل‌تر کنیم؟ در این مطلب روشی را در جهت سعی در غیرمعتبر کردن توکن‌ها و یا کوکی‌های سرقت شده، در برنامه‌های مبتنی بر ASP.NET Core بررسی خواهیم کرد.


توسعه‌ی یک سرویس تشخیص مرورگر و سیستم عامل شخص وارد شده‌ی به سیستم

یکی از روش‌های غیرممکن کردن یک چنین حملاتی، درج مشخصات سیستم عامل و مرورگر شخص وارد شده‌ی به سیستم، در کوکی و همچنین توکن صادر شده‌ی حاصل از اعتبارسنجی موفق است. سپس زمانیکه قرار است از اطلاعات این کوکی و یا توکن در برنامه استفاده شود، این اطلاعات را با اطلاعات درخواست جاری کاربر مقایسه کرده و در صورت عدم تطابق، درخواست او را برگشت می‌زنیم. برای مثال اگر عملیات لاگین، در ویندوز انجام شده و اکنون توکن و یا کوکی حاصل، در سیستم عامل اندروید در حاصل استفاده‌است، یعنی ... این عملیات مشکوک است و باید خاتمه یابد و کاربر باید مجبور به لاگین مجدد شود و نه اعتبارسنجی خودکار بدون زحمت!
برای این منظور می‌توان از کتابخانه‌ی UA-Parser استفاده کرد و توسط آن سرویس زیر را توسعه داد:
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
using UAParser;

namespace ASPNETCore2JwtAuthentication.Services;

/// <summary>
///     To invalidate an old user's token from a new device
/// </summary>
public class DeviceDetectionService : IDeviceDetectionService
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly ISecurityService _securityService;

    public DeviceDetectionService(ISecurityService securityService, IHttpContextAccessor httpContextAccessor)
    {
        _securityService = securityService ?? throw new ArgumentNullException(nameof(securityService));
        _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
    }

    public string GetCurrentRequestDeviceDetails() => GetDeviceDetails(_httpContextAccessor.HttpContext);

    public string GetDeviceDetails(HttpContext context)
    {
        var ua = GetUserAgent(context);
        if (ua is null)
        {
            return "unknown";
        }

        var client = Parser.GetDefault().Parse(ua);
        var deviceInfo = client.Device.Family;
        var browserInfo = $"{client.UA.Family}, {client.UA.Major}.{client.UA.Minor}";
        var osInfo = $"{client.OS.Family}, {client.OS.Major}.{client.OS.Minor}";
        //TODO: Add the user's IP address here, if it's a banking system.
        return $"{deviceInfo}, {browserInfo}, {osInfo}";
    }

    public string GetDeviceDetailsHash(HttpContext context) =>
        _securityService.GetSha256Hash(GetDeviceDetails(context));

    public string GetCurrentRequestDeviceDetailsHash() => GetDeviceDetailsHash(_httpContextAccessor.HttpContext);

    public string GetCurrentUserTokenDeviceDetailsHash() =>
        GetUserTokenDeviceDetailsHash(_httpContextAccessor.HttpContext?.User.Identity as ClaimsIdentity);

    public string GetUserTokenDeviceDetailsHash(ClaimsIdentity claimsIdentity)
    {
        if (claimsIdentity?.Claims == null || !claimsIdentity.Claims.Any())
        {
            return null;
        }

        return claimsIdentity.FindFirst(ClaimTypes.System)?.Value;
    }

    public bool HasCurrentUserTokenValidDeviceDetails() =>
        HasUserTokenValidDeviceDetails(_httpContextAccessor.HttpContext?.User.Identity as ClaimsIdentity);

    public bool HasUserTokenValidDeviceDetails(ClaimsIdentity claimsIdentity) =>
        string.Equals(GetCurrentRequestDeviceDetailsHash(), GetUserTokenDeviceDetailsHash(claimsIdentity),
                      StringComparison.Ordinal);

    private static string GetUserAgent(HttpContext context)
    {
        if (context is null)
        {
            return null;
        }

        return context.Request.Headers.TryGetValue(HeaderNames.UserAgent, out var userAgent)
                   ? userAgent.ToString()
                   : null;
    }
}
توضیحات:
اصل کار این سرویس در متد زیر رخ می‌دهد:
    public string GetDeviceDetails(HttpContext context)
    {
        var ua = GetUserAgent(context);
        if (ua is null)
        {
            return "unknown";
        }

        var client = Parser.GetDefault().Parse(ua);
        var deviceInfo = client.Device.Family;
        var browserInfo = $"{client.UA.Family}, {client.UA.Major}.{client.UA.Minor}";
        var osInfo = $"{client.OS.Family}, {client.OS.Major}.{client.OS.Minor}";
        //TODO: Add the user's IP address here, if it's a banking system.
        return $"{deviceInfo}, {browserInfo}, {osInfo}";
    }
در اینجا با استفاده از کتابخانه‌ی UA-Parser، سعی می‌کنیم تا جزئیات مرورگر و سیستم عامل شخص را تهیه کنیم. سپس در قسمت دیگری از این سرویس، این اطلاعات را هش می‌کنیم. از این جهت که هم حجم آن کاهش یابد و بی‌جهت کوکی و یا توکن ما را حجیم نکند و هم بررسی محتوای آن جهت شبیه سازی آن، غیرممکن شود. هر مشخصات دریافتی در حین لاگین، همواره یک هش مشخص و یکتا را دارد. به همین جهت متدهای هش کردن اطلاعات را هم در اینجا مشاهده می‌کنید. به علاوه‌ی متد HasUserTokenValidDeviceDetails که کار آن، دریافت Claim مرتبط با این اطلاعات، از کوکی و یا توکن جاری و مقایسه‌ی آن با اطلاعات Http Request جاری است. اگر این دو یکی نبودند، یعنی احتمال سوء استفاده‌ی از اطلاعات شخص، وجود دارد.


اضافه کردن اطلاعات مشخصات دستگاه کاربر به کوکی و یا توکن او

همانطور که عنوان شد، در متد HasUserTokenValidDeviceDetails، ابتدا مشخصات دستگاه موجود در کوکی و یا توکن دریافتی، استخراج می‌شود. به همین جهت نیاز است این مشخصات را دقیقا در حین لاگین موفق، به صورت یک Claim جدید، برای مثال از نوع ClaimTypes.System به مجموعه‌ی Claims کاربر اضافه کرد:
new(ClaimTypes.System, _deviceDetectionService.GetCurrentRequestDeviceDetailsHash(),
ClaimValueTypes.String, _configuration.Value.Issuer),


یکپارچه کردن DeviceDetectionService با اعتبارسنج‌های کوکی‌ها و توکن‌ها

پس از افزودن مشخصات سیستم کاربر وارد شده‌ی به سیستم، به صورت یک Claim جدید به توکن‌ها، روش اعتبارسنجی اطلاعات موجود در توکن رسیده، در رخ‌داد گردان OnTokenValidated است که امکان دسترسی به HttpContext و محتوای توکن را میسر می‌کند:
.AddJwtBearer(cfg =>
{
      cfg.Events = new JwtBearerEvents
      {
           OnTokenValidated = context =>
           {
               var tokenValidatorService = context.HttpContext.RequestServices.GetRequiredService<ITokenValidatorService>();
              return tokenValidatorService.ValidateAsync(context);
           },
       };
  });
و یا اگر از کوکی‌ها استفاده می‌کنید، معادل آن به صورت زیر است:
.AddCookie(options =>
{
    options.Events = new CookieAuthenticationEvents
    {
       OnValidatePrincipal = context =>
       {
         var cookieValidatorService = context.HttpContext.RequestServices.GetRequiredService<ICookieValidatorService>();
         return cookieValidatorService.ValidateAsync(context);
       }
    };
});

در کل تمام تغییرات مورد نیاز مرتبط را جهت یک برنامه‌ی تولید کننده‌ی JWT در اینجا و برای یک برنامه‌ی مبتنی بر کوکی‌ها در اینجا می‌توانید مشاهده کنید.
مطالب
محدود سازی نرخ دسترسی به منابع در برنامه‌های ASP.NET Core - قسمت دوم - پیاده سازی
در قسمت قبل با مفاهیم، اصطلاحات و الگوریتم‌های مرتبط با میان‌افزار جدید Rate limiting مخصوص ASP.NET Core 7 آشنا شدیم که در پشت صحنه از امکانات موجود در فضای نام System.Threading.RateLimiting استفاده می‌کند. در این قسمت نحوه‌ی استفاده‌ی از آن‌را مرور خواهیم کرد.


روش افزودن میان‌افزار RateLimiter به برنامه‌های ASP.NET Core

شبیه به سایر میان‌افزارها، جهت فعالسازی میان‌افزار RateLimiter، ابتدا باید سرویس‌های متناظر با آن‌را به برنامه معرفی کرد و پس از فعالسازی میان‌افزار مسیریابی، آن‌‌را به زنجیره‌ی مدیریت یک درخواست معرفی نمود. برای نمونه در مثال زیر، امکان دسترسی به تمام درخواست‌ها، به 10 درخواست در دقیقه، محدود می‌شود که پارتیشن بندی آن (در مورد پارتیشن بندی در قسمت قبل بیشتر بحث شد)، بر اساس username کاربر اعتبارسنجی شده و یا hostname یک کاربر غیراعتبارسنجی شده‌است:
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRateLimiter(options =>
{
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
        RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: httpContext.User.Identity?.Name ?? httpContext.Request.Headers.Host.ToString(),
            factory: partition => new FixedWindowRateLimiterOptions
            {
                AutoReplenishment = true,
                PermitLimit = 10,
                QueueLimit = 0,
                Window = TimeSpan.FromMinutes(1)
            }));
});

// ...

var app = builder.Build();

// ...

app.UseRouting();
app.UseRateLimiter();

app.MapGet("/", () => "Hello World!");

app.Run();
توضیحات:
- فراخوانی builder.Services.AddRateLimiter، سبب معرفی سرویس‌های میان‌افزار rate limiter به سیستم تزریق وابستگی‌های ASP.NET Core می‌شود.
- در اینجا می‌توان برای مثال خاصیت options.GlobalLimiter تنظیمات آن‌را نیز مقدار دهی کرد. GlobalLimiter، سبب تنظیم یک محدود کننده‌ی سراسری نرخ، برای تمام درخواست‌های رسیده‌ی به برنامه می‌شود.
- GlobalLimiter را می‌توان با هر نوع PartitionedRateLimiter مقدار دهی کرد که در اینجا از نوع FixedWindowLimiter انتخاب شده‌است تا بتوان «الگوریتم‌های بازه‌ی زمانی مشخص» را به برنامه اعمال نمود تا برای مثال فقط امکان پردازش 10 درخواست در هر دقیقه برای هر کاربر، وجود داشته باشد.
- در پایان کار، فراخوانی app.UseRateLimiter را نیز مشاهده می‌‌کنید که سبب فعالسازی میان‌افزار، بر اساس تنظیمات صورت گرفته می‌شود.

برای آزمایش برنامه، آن‌را  اجرا کرده و سپس به سرعت شروع به refresh کردن صفحه‌ی اصلی آن کنید. پس از 10 بار ریفرش، پیام  503 Service Unavailable را مشاهده خواهید کرد که به معنای مسدود شدن دسترسی به برنامه توسط میان‌افزار rate limiter است.


بررسی تنظیمات رد درخواست‌ها توسط میان‌افزار rate limiter

اگر پس از محدود شدن دسترسی به برنامه توسط میان افزار rate limiter از status code = 503 دریافتی راضی نیستید، می‌توان آن‌را هم تغییر داد:
builder.Services.AddRateLimiter(options =>
{
    options.RejectionStatusCode = 429;

    // ...
});
برای مثال بسیاری از سرویس‌ها بجای 503، از status code دیگری مانند 429 Too Many Requests استفاده می‌کنند که نحوه‌ی تنظیم آن‌را در مثال فوق مشاهده می‌کنید.
علاوه بر آن در اینجا گزینه‌ی OnRejected نیز پیش بینی شده‌است تا بتوان response ارائه شده را در حالت رد درخواست، سفارشی سازی کرد تا بتوان پیام بهتری را به کاربری که هم اکنون دسترسی او محدود شده‌است، ارائه داد:
builder.Services.AddRateLimiter(options =>
{
    options.OnRejected = async (context, token) =>
    {
        context.HttpContext.Response.StatusCode = 429;
        if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
        {
            await context.HttpContext.Response.WriteAsync(
                $"Too many requests. Please try again after {retryAfter.TotalMinutes} minute(s). " +
                $"Read more about our rate limits at https://example.org/docs/ratelimiting.", cancellationToken: token);
        }
        else
        {
            await context.HttpContext.Response.WriteAsync(
                "Too many requests. Please try again later. " +
                "Read more about our rate limits at https://example.org/docs/ratelimiting.", cancellationToken: token);
        }
    };

    // ...
});
برای نمونه در مثال فوق ابتدا status code، به 429 تنظیم می‌شود و سپس یک response با معنا به سمت کاربر ارسال می‌گردد که دقیقا مشخص می‌کند آن کاربر چه زمانی می‌تواند مجددا سعی کند و همچنین لینکی را به مستندات محدود سازی برنامه جهت توضیحات بیشتر ارائه می‌دهد.

یک نکته: باتوجه به اینکه در اینجا به HttpContext دسترسی داریم، یعنی به context.HttpContext.RequestServices نیز دسترسی خواهیم داشت که توسط آن می‌توان برای مثال سرویس ILogger را از آن درخواست کرد و رخ‌داد واقع شده را برای بررسی بیشتر لاگ نمود؛ برای مثال چه کاربری مشکل پیدا کرده‌است؟
context.HttpContext.RequestServices.GetService<ILoggerFactory>()?
                .CreateLogger("Microsoft.AspNetCore.RateLimitingMiddleware")
                .LogWarning("OnRejected: {RequestPath}", context.HttpContext.Request.Path);
همچنین باید دقت داشت که اگر در اینجا از بانک اطلاعاتی استفاده کرده‌اید، تعداد کوئری‌های آن‌را محدود کنید؛ وگرنه واقعا rate limiter از لحاظ محدود کردن دسترسی به منابع، کمک زیادی را به شما نخواهد کرد.

طراحی فعلی میان‌افزار rate limiter، کمی محدود است. برای مثال «retry after»، تنها metadata مفیدی است که جهت بازگشت ارائه می‌دهد و همچنین مانند GitHub مشخص نمی‌کند که در لحظه‌ی جاری چند درخواست دیگر را می‌توان ارسال کرد و امکان دسترسی به اطلاعات آماری درونی آن وجود ندارد. اگر نیاز به یک چنین اطلاعاتی دارید شاید استفاده از میان‌افزار ثالث دیگری به نام AspNetCoreRateLimit برای شما مفیدتر باشد!


الگوریتم‌های پشتیبانی شده‌ی توسط میان‌افزار rate limiter

در قسمت قبل با چند الگوریتم استاندارد طراحی میان‌افزارهای rate limiter آشنا شدیم که میان‌افزار توکار rate limiter موجود در ASP.NET Core 7x، اکثر آن‌ها را پشتیبانی می‌کند:
- Concurrency limit: ساده‌ترین نوع محدود سازی نرخ درخواست‌ها است و کاری به زمان ندارد و فقط برای آن، تعداد درخواست‌های همزمان مهم است. برای مثال پیاده سازی «مجاز بودن تنها 10 درخواست همزمان».
- Fixed window limit: توسط آن می‌توان محدودیت‌هایی مانند «مجاز بودن تنها 60 درخواست در دقیقه» را اعمال کرد که به معنای امکان ارسال یک درخواست در هر ثانیه در هر دقیقه و یا حتی ارسال یکجای 60 درخواست در یک ثانیه است.
- Sliding window limit: این محدودیت بسیار شبیه به حالت قبل است اما به همراه قطعاتی که کنترل بیشتری را بر روی محدودیت‌ها میسر می‌کند؛ مانند مجاز بودن 60 درخواست در هر دقیقه که فقط در این حالت یک درخواست در هر ثانیه مجاز باشد.
- Token bucket limit: امکان کنترل نرخ سیلان را میسر کرده و همچنین از درخواست‌های انفجاری نیز پشتیبانی می‌کند (این مفاهیم در قسمت قبل بررسی شدند).

علاوه بر این‌ها امکان ترکیب گزینه‌های فوق توسط متد کمکی PartitionedRateLimiter.CreateChained نیز میسر است:
builder.Services.AddRateLimiter(options =>
{
    options.GlobalLimiter = PartitionedRateLimiter.CreateChained(
        PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
            RateLimitPartition.GetFixedWindowLimiter(httpContext.ResolveClientIpAddress(), partition =>
                new FixedWindowRateLimiterOptions
                {
                    AutoReplenishment = true,
                    PermitLimit = 600,
                    Window = TimeSpan.FromMinutes(1)
                })),
        PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
            RateLimitPartition.GetFixedWindowLimiter(httpContext.ResolveClientIpAddress(), partition =>
                new FixedWindowRateLimiterOptions
                {
                    AutoReplenishment = true,
                    PermitLimit = 6000,
                    Window = TimeSpan.FromHours(1)
                })));

    // ...
});
برای نمونه در مثال فوق به ازای یک آدرس IP مشخص، تنها می‌توان 600 درخواست را در دقیقه ارسال کرد؛ با این محدودیت که جمع آن‌ها در ساعت، بیشتر از 6000 مورد نباشد.
در این مثال فرضی، متد الحاقی ResolveClientIpAddress اهمیتی ندارد. بهتر است برای برنامه‌ی خود از کلید پارتیشن بندی بهتر و معقول‌تری استفاده کنید.


امکان در صف قرار دادن درخواست‌ها بجای رد کردن آن‌ها

در تنظیمات مثال‌های فوق، در کنار PermitLimit، می‌توان QueueLimit را نیز مشخص کرد. به این ترتیب با رسیدن به PermitLimit، به تعداد QueueLimit، درخواست‌ها در صف قرار می‌گیرند، بجای اینکه کاملا رد شوند:
PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
    RateLimitPartition.GetFixedWindowLimiter(httpContext.ResolveClientIpAddress(), partition =>
        new FixedWindowRateLimiterOptions
        {
            AutoReplenishment = true,
            PermitLimit = 10,
            QueueLimit = 6,
            QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
            Window = TimeSpan.FromSeconds(1)
        })));
در این مثال هر کلاینت می‌تواند 10 درخواست در ثانیه را ارسال کند. در صورت رسیدن به این محدودیت، تا 6 عدد از درخواست‌های جدید رسیده، بجای رد شدن، در صف قرار می‌گیرند تا در ثانیه‌ی بعدی که این بازه‌ی مشخص به پایان می‌رسد، پردازش شوند.
این تنظیم، تجربه‌ی کاربری بهتری را برای استفاده کنندگان از برنامه‌ی شما به همراه خواهد داشت؛ بجای رد قاطع درخواست‌های ارسالی توسط آن‌ها.

یک نکته: بهتر است QueueLimitهای بزرگی را انتخاب نکنید؛ خصوصا برای بازه‌های زمانی طولانی. چون یک مصرف کننده نیاز دارد تا سریع، پاسخی را دریافت کند و اگر این‌طور نباشد، دوباره سعی خواهد کرد. تنها چند ثانیه‌ی کوتاه در صف بودن برای کاربران معنا دارد.


امکان ایجاد سیاست‌های محدود سازی سفارشی

اگر الگوریتم‌های توکار میان‌افزار rate limiter برای کار شما مناسب نیستند، می‌توانید با پیاده سازی <IRateLimiterPolicy<TPartitionKey، یک نمونه‌ی سفارشی را ایجاد کنید. پیاده سازی این اینترفیس، نیاز به دو متد را دارد:
الف) متد GetPartition که بر اساس HttpContext جاری، یک rate limiter مخصوص را باز می‌گرداند.
ب) متد OnRejected که امکان سفارشی سازی response رد درخواست‌ها را میسر می‌کند.

در مثال زیر پیاده سازی یک rate limiter سفارشی را مشاهده می‌کنید که نحوه‌ی پارتیشن بندی آن بر اساس user-name کاربر اعتبارسنجی شده و یا host-name کاربر وارد نشده‌ی به سیستم است. در اینجا کاربر وارد شده‌ی به سیستم، محدودیت بیشتری دارد:
public class ExampleRateLimiterPolicy : IRateLimiterPolicy<string>
{
    public RateLimitPartition<string> GetPartition(HttpContext httpContext)
    {
        if (httpContext.User.Identity?.IsAuthenticated == true)
        {
            return RateLimitPartition.GetFixedWindowLimiter(httpContext.User.Identity.Name!,
                partition => new FixedWindowRateLimiterOptions
                {
                    AutoReplenishment = true,
                    PermitLimit = 1_000,
                    Window = TimeSpan.FromMinutes(1),
                });
        }

        return RateLimitPartition.GetFixedWindowLimiter(httpContext.Request.Headers.Host.ToString(),
            partition => new FixedWindowRateLimiterOptions
            {
                AutoReplenishment = true,
                PermitLimit = 100,
                Window = TimeSpan.FromMinutes(1),
            });
    }

    public Func<OnRejectedContext, CancellationToken, ValueTask>? OnRejected { get; } =
        (context, _) =>
        {
            context.HttpContext.Response.StatusCode = 418; // I'm a 🫖
            return new ValueTask();
        };
}
و نحوه‌ی معرفی آن به سیستم به صورت زیر است:
options.AddPolicy<string, ExampleRateLimiterPolicy>("myPolicy");


امکان تعریف سیاست‌های محدود سازی نرخ دسترسی به گروهی از endpoints

تا اینجا روش‌های سراسری محدود سازی دسترسی به منابع برنامه را بررسی کردیم؛ اما ممکن است در برنامه‌ای بخواهیم محدودیت‌های متفاوتی را به گروه‌های خاصی از endpoints اعمال کنیم و یا شاید اصلا نخواهیم تعدادی از آن‌ها را محدود کنیم:
builder.Services.AddRateLimiter(options =>
{
    options.AddFixedWindowLimiter("Api", options =>
    {
        options.AutoReplenishment = true;
        options.PermitLimit = 10;
        options.Window = TimeSpan.FromMinutes(1);
    });

    options.AddFixedWindowLimiter("Web", options =>
    {
        options.AutoReplenishment = true;
        options.PermitLimit = 10;
        options.Window = TimeSpan.FromMinutes(1);
    });

    // ...
});
در این مثال روش تعریف دو سیاست مختلف محدودسازی را مشاهده می‌کنید که اینبار «نامدار» هستند؛ نام یکی Api است و نام دیگری Web.
البته باید درنظر داشت که متدهای الحاقی Add داری را که در اینجا ملاحظه می‌کنید، محدود سازی را بر اساس نام درنظر گرفته شده انجام می‌دهند. یعنی درحقیقت یک محدودسازی سراسری بر اساس گروهی از endpoints هستند و امکان تعریف پارتیشنی را به ازای یک کاربر یا آدرس IP خاص، ندارند. اگر نیاز به اعمال این نوع پارتیشن بندی را دارید، باید از متدهای AddPolicy استفاده کنید:
options.AddPolicy("Api", httpContext =>
        RateLimitPartition.GetFixedWindowLimiter(httpContext.ResolveClientIpAddress(),
        partition => new FixedWindowRateLimiterOptions
        {
            AutoReplenishment = true,
            PermitLimit = 10,
            Window = TimeSpan.FromSeconds(1)
        }));
متدهای AddPolicy دار، هم امکان دسترسی به httpContext جاری را میسر می‌کنند و هم نامدار هستند که قابلیت اعمال آن‌ها را به گروهی از endpoints ممکن می‌کند.


محدود سازی نرخ دسترسی به منابع در ASP.NET Core Minimal API

پس از تعریف نامی برای سیاست‌های دسترسی، اکنون می‌توان از آن‌ها به صورت زیر جهت محدود سازی یک endpoint و یا گروهی از آن‌ها استفاده کرد:
// Endpoint
app.MapGet("/api/hello", () => "Hello World!").RequireRateLimiting("Api");

// Group
app.MapGroup("/api/orders").RequireRateLimiting("Api");
و یا حتی می‌توان بطور کامل محدود سازی نرخ دسترسی را برای یک endpoint و یا گروهی از آن‌ها غیرفعال کرد:
// Endpoint
app.MapGet("/api/hello", () => "Hello World!").DisableRateLimiting();

// Group
app.MapGroup("/api/orders").DisableRateLimiting();


محدود سازی نرخ دسترسی به منابع در ASP.NET Core MVC

می‌توان سیاست‌های نرخ دسترسی تعریف شده را بر اساس نام آن‌ها به کنترلرها و یا اکشن متدها اعمال نمود:
[EnableRateLimiting("Api")]
public class Orders : Controller
{
    [DisableRateLimiting]
    public IActionResult Index()
    {
        return View();
    }

    [EnableRateLimiting("ApiListing")]
    public IActionResult List()
    {
        return View();
    }
}
در اینجا سیاست نرخ دسترسی با نام Api، به کل کنترلر و اکشن متدهای آن اعمال شده، اما اکشن متد Index آن با بکارگیری ویژگی DisableRateLimiting، از این محدودیت خارج و اکشن متد List، از سیاست نام دار دیگری استفاده کرده‌است.
و یا حتی می‌توان این سیاست‌های محدود سازی نرخ دسترسی را به تمام کنترلرها و صفحات razor نیز به صورت زیر اعمال کرد:
app.UseConfiguredEndpoints(endpoints =>
{
    endpoints.MapRazorPages()
        .DisableRateLimiting();

    endpoints.MapControllers()
        .RequireRateLimiting("UserBasedRateLimiting");
});