اشتراک‌ها
کتاب C# 11 and .NET 7 – Modern Cross-Platform Development, 7th Edition

کتاب C# 11 and .NET 7 – Modern Cross-Platform Development, 7th Edition (سی شارپ 11 و دات نت 7، مبانی توسعه چند سکویی مدرن، ویرایش هفتم)، راهنمایی قابل دسترس برای برنامه نویسان مبتدی تا متوسط برای مفاهیم، کاربرد‌های دنیای واقعی و جدید‌ترین ویژگی‌های C# 11 و NET 7. به همراه تمرینات عملی با استفاده از Visual Studio 2022 و Visual Studio Code است. جدید‌ترین نسخه این کتاب به طور گسترده ای بازنگری شده است تا تمامی ویژگی‌های جدید ارائه شده با سی شارپ 11 و دات نت 7 را در خود جای دهد. 

کتاب C# 11 and .NET 7 – Modern Cross-Platform Development, 7th Edition
مطالب
معماری لایه بندی نرم افزار #3

Service Layer

نقش لایه‌ی سرویس این است که به عنوان یک مدخل ورودی به برنامه کاربردی عمل کند. در برخی مواقع این لایه را به عنوان لایه‌ی Facade نیز می‌شناسند. این لایه، داده‌ها را در قالب یک نوع داده ای قوی (Strongly Typed) به نام View Model، برای لایه‌ی Presentation فراهم می‌کند. کلاس View Model یک Strongly Typed محسوب می‌شود که نماهای خاصی از داده‌ها را که متفاوت از دید یا نمای تجاری آن است، بصورت بهینه ارائه می‌نماید. در مورد الگوی View Model در مباحث بعدی بیشتر صحبت خواهم کرد.

الگوی Facade یک Interface ساده را به منظور کنترل دسترسی به مجموعه ای از Interface‌ها و زیر سیستم‌های پیچیده ارائه می‌کند. در مباحث بعدی در مورد آن بیشتر صحبت خواهم کرد.

کلاسی با نام ProductViewModel را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:

public class ProductViewModel
{
    Public int ProductId {get; set;}
    public string Name { get; set; }
    public string Rrp { get; set; }
    public string SellingPrice { get; set; }
    public string Discount { get; set; }
    public string Savings { get; set; }
}

برای اینکه کلاینت با لایه‌ی سرویس در تعامل باشد باید از الگوی Request/Response Message استفاده کنیم. بخش Request توسط کلاینت تغذیه می‌شود و پارامترهای مورد نیاز را فراهم می‌کند. کلاسی با نام ProductListRequest را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:

using SoCPatterns.Layered.Model;

namespace SoCPatterns.Layered.Service
{
    public class ProductListRequest
    {
        public CustomerType CustomerType { get; set; }
    }
}

در شی Response نیز بررسی می‌کنیم که درخواست به درستی انجام شده باشد، داده‌های مورد نیاز را برای کلاینت فراهم می‌کنیم و همچنین در صورت عدم اجرای صحیح درخواست، پیام مناسب را به کلاینت ارسال می‌نماییم. کلاسی با نام ProductListResponse را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:

public class ProductListResponse
{
    public bool Success { get; set; }
    public string Message { get; set; }
    public IList<ProductViewModel> Products { get; set; }
}

به منظور تبدیل موجودیت Product به ProductViewModel، به دو متد نیاز داریم، یکی برای تبدیل یک Product و دیگری برای تبدیل لیستی از Product. شما می‌توانید این دو متد را به کلاس Product موجود در Domain Model اضافه نمایید، اما این متدها نیاز واقعی منطق تجاری نمی‌باشند. بنابراین بهترین انتخاب، استفاده از Extension Method‌ها می‌باشد که باید برای کلاس Product و در لایه‌ی سرویس ایجاد نمایید. کلاسی با نام ProductMapperExtensionMethods را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:

public static class ProductMapperExtensionMethods
{
    public static ProductViewModel ConvertToProductViewModel(this Model.Product product)
    {
        ProductViewModel productViewModel = new ProductViewModel();
        productViewModel.ProductId = product.Id;
        productViewModel.Name = product.Name;
        productViewModel.RRP = String.Format(“{0:C}”, product.Price.RRP);
        productViewModel.SellingPrice = String.Format(“{0:C}”, product.Price.SellingPrice);
        if (product.Price.Discount > 0)
            productViewModel.Discount = String.Format(“{0:C}”, product.Price.Discount);
        if (product.Price.Savings < 1 && product.Price.Savings > 0)
            productViewModel.Savings = product.Price.Savings.ToString(“#%”);
        return productViewModel;
    }
    public static IList<ProductViewModel> ConvertToProductListViewModel(
        this IList<Model.Product> products)
    {
        IList<ProductViewModel> productViewModels = new List<ProductViewModel>();
        foreach(Model.Product p in products)
        {
            productViewModels.Add(p.ConvertToProductViewModel());
        }
        return productViewModels;
    }
}

حال کلاس ProductService را جهت تعامل با کلاس سرویس موجود در Domain Model و به منظور برگرداندن لیستی از محصولات و تبدیل آن به لیستی از ProductViewModel، ایجاد می‌نماییم. کلاسی با نام ProductService را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:

public class ProductService
{
    private Model.ProductService _productService;
    public ProductService(Model.ProductService ProductService)
    {
        _productService = ProductService;
    }
    public ProductListResponse GetAllProductsFor(
        ProductListRequest productListRequest)
    {
        ProductListResponse productListResponse = new ProductListResponse();
        try
        {
            IList<Model.Product> productEntities =
                _productService.GetAllProductsFor(productListRequest.CustomerType);
            productListResponse.Products = productEntities.ConvertToProductListViewModel();
            productListResponse.Success = true;
        }
        catch (Exception ex)
        {
            // Log the exception…
            productListResponse.Success = false;
            // Return a friendly error message
            productListResponse.Message = ex.Message;
        }
        return productListResponse;
    }
}

کلاس Service تمامی خطاها را دریافت نموده و پس از مدیریت خطا، پیغامی مناسب را به کلاینت ارسال می‌کند. همچنین این لایه محل مناسبی برای Log کردن خطاها می‌باشد. در اینجا کد نویسی لایه سرویس به پایان رسید و در ادامه به کدنویسی Data Layer می‌پردازیم.

Data Layer

برای ذخیره سازی محصولات، یک بانک اطلاعاتی با نام Shop01 ایجاد کنید که شامل جدولی به نام Product با ساختار زیر باشد:

برای اینکه کدهای بانک اطلاعاتی را سریعتر تولید کنیم از روش Linq to SQL در Data Layer استفاده می‌کنم. برای این منظور یک Data Context برای Linq to SQL به این لایه اضافه می‌کنیم. بر روی پروژه SoCPatterns.Layered.Repository کلیک راست نمایید و گزینه Add > New Item را انتخاب کنید. در پنجره ظاهر شده و از سمت چپ گزینه Data و سپس از سمت راست گزینه Linq to SQL Classes را انتخاب نموده و نام آن را Shop.dbml تعیین نمایید.

از طریق پنجره Server Explorer به پایگاه داده مورد نظر متصل شوید و با عمل Drag & Drop جدول Product را به بخش Design کشیده و رها نمایید.

اگر به یاد داشته باشید، در لایه Model برای برقراری ارتباط با پایگاه داده از یک Interface به نام IProductRepository استفاده نمودیم. حال باید این Interface را پیاده سازی نماییم. کلاسی با نام ProductRepository را با کد زیر به پروژه SoCPatterns.Layered.Repository اضافه کنید:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SoCPatterns.Layered.Model;

namespace SoCPatterns.Layered.Repository
{
    public class ProductRepository : IProductRepository
    {
        public IList<Model.Product> FindAll()
        {
            var products = from p in new ShopDataContext().Products
                                select new Model.Product
                                {
                                    Id = p.ProductId,
                                    Name = p.ProductName,
                                    Price = new Model.Price(p.Rrp, p.SellingPrice)
                                };
            return products.ToList();
        }
    }
}

در متد FindAll، با استفاده از دستورات Linq to SQL، لیست تمامی محصولات را برگرداندیم. کدنویسی لایه‌ی Data هم به پایان رسید و در ادامه به کدنویسی لایه‌ی Presentation و UI می‌پردازیم.

Presentation Layer

به منظور جداسازی منطق نمایش (Presentation) از رابط کاربری (User Interface)، از الگوی Model View Presenter یا همان MVP استفاده می‌کنیم که در مباحث بعدی با جزئیات بیشتری در مورد آن صحبت خواهم کرد. یک Interface با نام IProductListView را با کد زیر به پروژه SoCPatterns.Layered.Presentation اضافه کنید:

using SoCPatterns.Layered.Service;

public interface IProductListView
{
    void Display(IList<ProductViewModel> Products);
    Model.CustomerType CustomerType { get; }
    string ErrorMessage { set; }
}

این Interface توسط Web Form‌های ASP.NET و یا Win Form‌ها باید پیاده سازی شوند. کار با Interface‌ها موجب می‌شود تا تست View‌ها به راحتی انجام شوند. کلاسی با نام ProductListPresenter را با کد زیر به پروژه SoCPatterns.Layered.Presentation اضافه کنید:

using SoCPatterns.Layered.Service;

namespace SoCPatterns.Layered.Presentation
{
    public class ProductListPresenter
    {
        private IProductListView _productListView;
        private Service.ProductService _productService;
        public ProductListPresenter(IProductListView ProductListView,
            Service.ProductService ProductService)
        {
            _productService = ProductService;
            _productListView = ProductListView;
        }
        public void Display()
        {
            ProductListRequest productListRequest = new ProductListRequest();
            productListRequest.CustomerType = _productListView.CustomerType;
            ProductListResponse productResponse =
                _productService.GetAllProductsFor(productListRequest);
            if (productResponse.Success)
            {
                _productListView.Display(productResponse.Products);
            }
            else
            {
                _productListView.ErrorMessage = productResponse.Message;
            }
        }
    }
}

کلاس Presenter وظیفه‌ی واکشی داده ها، مدیریت رویدادها و بروزرسانی UI را دارد. در اینجا کدنویسی لایه‌ی Presentation به پایان رسیده است. از مزایای وجود لایه‌ی Presentation این است که تست نویسی مربوط به نمایش داده‌ها و تعامل بین کاربر و سیستم به سهولت انجام می‌شود بدون آنکه نگران دشواری Unit Test نویسی Web Form‌ها باشید. حال می‌توانید کد نویسی مربوط به UI را انجام دهید که در ادامه به کد نویسی در Win Forms و Web Forms خواهیم پرداخت. 

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

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

یک تجربه
سالها پیش یکی از همکاران تعریف می‌کردند که یک شرکت نرم افزاری برای مشاوره معماری نرم افزار از ایشان دعوت به همکاری کرده است. پس از مراجعه به شرکت متوجه شدند که تیم اصلی برنامه نویسان درگیر تولید ORM ای برای پروژه جدید شرکت هستند که برای تولید این ابزار بیش از 4 ماه را وقت صرف کرده‌اند؛ اما در مراحل نهایی کار دچار مشکلات زیادی شده اند. به نحوی که از ایشان برای کمک به رفع مشکل ORM ( به جای تولید نرم افزار مشتری) دعوت کرده‌اند.
 
در آن زمان یادم هست که EF 5 (که تقریبا نسخه سوم  بعد از 3.5 و 4 می‌باشد - جزئیات در اینجا) توسط مایکروسافت ارائه شده بود. همچنین NHibernate هم همزمان با EFها (تاریخچه نسخه‌ها در اینجا) قابل دسترسی بوده‌است. با این حال تیم فنی به این دلیل که کوئری‌های تولیدی توسط EF کند هستند، اقدام به ساخت ORM کرده بودند. جالب اینکه با بررسی بیشتر مشخص شده‌است که حجم داده‌های پروژه در بدترین حالت در یک جدول به 5 هزار رکورد می‌رسد.

4 ماه صرف وقت و هزینه تیم 2 نفره برای طراحی و پیاده سازی و تست ORM ای که در نهایت به دلیل مشکلات Performance کنار گذاشته شد و از EF استفاده کردند. شاید در این 4 ماه می‌توانستند 30 درصد پروژه اصلی را پیاده سازی کنند.

شاید بتوان 3 دلیل عمده «فنی» شکست برخی از پروژه‌های نرم افزاری در ایران را به شرح زیر عنوان کرد:
- عدم استفاده مناسب از ابزارها و راهکار‌های موجود و انجام دوباره کاری
- استفاده غیر ضروری و عجولانه از تکنولوژی‌های جدید (بدون داشتن نیروی کار مسلط)
- پایین بودن سطح فنی و به‌روز نبودن برخی از برنامه نویسان ایرانی


متن باز (Open Source)
با پیشرفت توسعه نرم افزار و تمایل شرکت‌های بزرگ دنیا به تولید کامپوننت‌های متن باز (Open Source) ریسک استفاده از این نوع ابزار‌ها نیز کمتر شده است. بطوریکه درصورت نیاز می‌توان کامپوننت را برای پروژه‌ها سفارش سازی کرد.
شاید کمتر کسی باور می‌کرد که روزی شرکت مایکروسافت محصولات خود را Open Source کند. اما امروز، در سال 2017 میلادی، شرکت مایکروسافت اقدامات مهمی را در این زمینه انجام داده است که می‌توانید جزئیات پروژه‌های متن باز این غول کامپیوتری دنیا را در اینجا و همچنین اینجا ملاحظه کنید.

 
یک سناریو
فرض کنید یک پروژه تحت وب را شروع کرده اید. بدون در نظر گرفتن جزئیات پروژه می‌توان گفت به ابزارهای زیر نیاز خواهید داشت:

ابزار
مثال
  ORM   EF , NHibernate , Dapper , LLBLGEN 
 IOC COntainer   Unity , StructureMap , Autofac , Castle.Windsor, LightInject , Ninject 
 Report Tools   CrsytalReport , Stimusoft , DevExpress Report, Telerik Report Tools, EasyReport 
 UI Component   Telerik , JqueryUI , Bootsrap ,CompnentArt, ComponentOne 
 Error Logger   ELMAH , NLog , log4net 
 Mapper Tools   AutoMapper , ValueInjecter 
همانطور که ملاحظه می‌کنید برای همه‌ی موارد فوق ابزارهای مناسبی وجود دارند که برای پیاده سازی هر کدام، سالها وقت و هزینه صرف شده‌است. همچنین قابلیت اطمینان این ابزار‌ها به مراتب بالاتر از ابزارهای دست ساز خواهد بود. شاید برای ساده‌ترین ابزار فوق 3 ماه زمان لازم باشد تا یک نسخه  باگ دار تهیه شود!


ملاحظات استفاده از ابزارها
توجه به چند نکته در استفاده از ابزارها و کتابخانه‌های آماده ضروری می‌باشد، بدین شرح:
- ابزار مورد نیاز را با R&D (تحقیق و توسعه) انتخاب کنید. ابزارهایی که در پروژه‌های واقعی استفاده شده‌اند، بسیار مناسب می‌باشند.
- توجه داشته باشیدکه استفاده از چندین ابزار باعث ایجاد تداخل در پروژه نشود (این مورد معمولا در کامپوننت‌های UI مانند JqueryUI و Bootsrtap اتفاق می‌افتد)
- مستندات مربوط به ابزار‌ها را حتما مطالعه کنید. لطفا بدون تسلط از ابزاری استفاده نکنید.

گاهی پیش می‌آید که یک برنامه نویس بدون مطالعه مستندات مربوط به یک IOC Container از آن ابزار استفاده میکند و در Register اولیه ویژگی LifeCycle مربوط به Context  را با حالت Singleton مقداردهی میکند. بدین ترتیب پس از نیم ساعت، پروژه به دلیل آنچه که می‌توان "چاقی Context" نامید، DONE یا حداقل کند می‌شود که رفع این مشکل ساعت‌ها زمان می‌برد.

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

فرض کنید در پروژه WPF از کامپوننت‌های زیبای DevExpress استفاده میکنید. به ازای هر کامپوننت یک کلاس به پروژه اضافه کنید که از کلاس اصلی آن کامپوننت Devexspress ارث می‌برد و در لایه UI خود از کلاس جدید خود استفاده کنید. با این کار می‌توانید ویژگی‌های عمومی کامپوننت‌ها را یکبار برای کل پروژه اعمال کنید.


  نتیجه گیری
  اگر بخواهیم چرخ را اختراع نکنیم و از تجربیات موفق موجود استفاده کنیم، می‌توان نتیجه گرفت که استفاده از ابزارهای آماده برای توسعه نرم افزار با رعایت دستورالعمل استفاده امری مفید می‌باشد. اما باید توجه داشته باشیم که استفاده از هر ابزاری به هرقیمتی در هرپروژه‌ای، حرفه ای نیست. همه‌ی راهکارها، ابزراها و تکنولوژی‌های مورد استفاده باید در راستای هدف اصلی «تولید و تحویل به موقع نرم افزار با کیفیت به مشتری» باشد؛ هدفی که در بسیاری از موارد فراموش شده و بیشتر زمان پروژه، صرف کارهای غیر ضروری می‌شود.
اشتراک‌ها
گیت هاب ! ( GitHub ) شبکه اجتماعی برنامه نویسان

برای خیلی ممکن است سوال پیش آمده باشد چطور یک برنامه نویس از پروژه ای که به صورت اوپن سورس منتشر میکند محافظت کرده و از سوء استفاده جلوگیری میکند ؟ بر اساس همین سوال شخص لینوس توروالدز Git را ایجاد کرد برای ذخیره پروژه‌های متن باز و حفظ حقوق برنامه نویس پروژه

سایت گیت هاب (github.com) بر پایه Git تشکیل شده و به همین منظور استفاده میشود. البته برنامه نویس میتواند پروژه را بصورت خصوصی ذخیره کند و از انتشار عمومی پروژه خودداری کند. با استفاده از این سیستم برنامه نویسان پروژه‌های متن باز را با خیال راحت و با حفظ حقوق منتشر کنند و به این ترتیب پروژه به نام آن برنامه نویس ثبت خواهد شد. در این سیکل برنامه نویس یک اکانت در این سایت ایجاد و برای هر پروژه متن باز که منتشر میکند یک صفحه (مخزن) ساخته و پروژه را در آن ذخیره میکند.

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

گیت هاب امکانات بیشتری را در خود پیاده کرده که این سایت را تبدیل به شبکه اجتماعی برای برنامه نویسان کرده است. موارد از قبیل انجمن برای پرسش و مشکلات ، ارسال پیغام خصوصی برای سایر اعضا و ….

گیت هاب ! ( GitHub ) شبکه اجتماعی برنامه نویسان
مطالب
محدود سازی نرخ دسترسی به منابع در برنامه‌های ASP.NET Core - قسمت اول - بررسی مفاهیم
به ASP.NET Core 7، یک میان‌افزار جدید به نام Rate limiter اضافه شده‌است که امکان محدود سازی دسترسی به منابع برنامه‌ی ما را میسر می‌کند. این میان‌افزار، طراحی جامع و مفصلی را دارد. به همین جهت نیاز است در ابتدا با مفاهیم مرتبط با آن آشنا شد و سپس به سراغ پیاده سازی و استفاده‌ی از آن رفت.


چرا باید میزان دسترسی به منابع یک برنامه‌ی وب را محدود کرد؟

فرض کنید در حال ساخت یک web API هستید که کارش ذخیره سازی لیست وظایف اشخاص است و برای مثال از یک GET /api/todos برای دریافت لیست ظایف، یک POST /api/todos برای ثبت و یک PUT /api/todos/{id} برای تغییر موارد ثبت شده، تشکیل می‌شود.
سؤال: چه مشکلی ممکن است به همراه این سه endpoint بروز کند؟
پاسخ: به حداقل چهار مورد زیر می‌توان اشاره کرد:
- یک مهاجم سعی می‌کند با برنامه‌ای که تدارک دیده، هزاران وظیفه‌ی جدید را در چند ثانیه به سمت برنامه ارسال کند تا سبب خاتمه‌ی سرویس آن شود.
- برنامه‌ی ما در حین سرویس دهی، به یک سرویس ثالث نیز وابسته‌است و آن سرویس ثالث، اجازه‌ی استفاده‌ی بیش از اندازه‌ی از منابع خود را نمی‌دهد. با رسیدن تعداد زیادی درخواست به برنامه‌ی ما تنها از طرف یک کاربر، به سقف مجاز استفاده‌ی از آن سرویس ثالث رسیده‌ایم و اکنون برنامه، برای تمام کاربران آن قابل استفاده نیست.
- شخصی در حال دریافت اطلاعات تک تک کاربران است. از شماره یک شروع کرده و به همین نحو جلو می‌رود. برای دریافت اطلاعات کاربران، نیاز است شخص به سیستم وارد شده و اعتبارسنجی شود؛ یعنی به ازای هر درخواست، یک کوئری نیز به سمت بانک اطلاعاتی جهت بررسی وضعیت فعلی و آنی کاربر ارسال می‌شود. به همین جهت عدم کنترل میزان دسترسی به لیست اطلاعات کاربران، بار سنگینی را به بانک اطلاعاتی و CPU سیستم وارد می‌کند.
- هم اکنون چندین موتور جستجو و بات‌هایی نظر آن‌ها در حال پیمایش سایت و برنامه‌ی شما هستند که هر کدام از آن‌ها می‌توانند در حد یک مهاجم رفتار کنند.

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


محدود کردن نرخ دسترسی به برنامه چیست؟

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


محدود کردن نرخ دسترسی را باید به چه منابعی اعمال کرد؟

پاسخ دقیق به این سؤال: «همه چیز» است! بله! همه چیز را کنترل کنید! در اینجا منظور از همه چیز، همان endpointهایی هستند که استفاده‌ی نابجای از آن‌ها می‌توانند سبب کند شدن برنامه یا از دسترس خارج شدن آن شوند. برای مثال هر endpoint‌ای که از CPU، حافظه، دسترسی به دیسک سخت، بانک اطلاعاتی، APIهای ثالث و خارجی و امثال آن استفاده می‌کند، باید کنترل و محدود شود تا استفاده‌ی ناصحیح یک کاربر از آن‌ها، استفاده‌ی از برنامه را برای سایر کاربران غیرممکن نکند. البته باید دقت داشت که هدف از اینکار، عصبی کردن کاربران عادی و معمولی برنامه نیست. هدف اصلی در اینجا، تشویق به استفاده‌ی منصفانه از منابع سیستم است.


الگوریتم‌های محدود کردن نرخ دسترسی

پیاده سازی ابتدایی محدود کردن نرخ دسترسی به منابع یک برنامه کار مشکلی است و در صورت استفاده از الگوریتم‌های متداولی مانند تعریف یک جدول که شامل user-id، action-id و timestamp، به همراه یکبار ثبت اطلاعات به ازای هر درخواست و همچنین خواندن اطلاعات موجود است که جدول آن نیز به سرعت افزایش حجم می‌دهد. به همین جهت تعدادی الگوریتم بهینه برای اینکار طراحی شده‌اند:

الگوریتم‌های بازه‌ی زمانی مشخص

در این روش، یک شمارشگر در یک بازه‌ی زمانی مشخص فعال می‌شود و بر این مبنا است که محدودیت‌ها اعمال خواهند شد. یک مثال آن، مجاز دانستن فقط «100 درخواست در یک دقیقه» است که نام دیگر آن «Quantized buckets / Fixed window limit» نیز هست.
برای مثال «نام هر اکشن + یک بازه‌ی زمانی»، یک کلید دیکشنری نگهدارنده‌ی اطلاعات محدود کردن نرخ دسترسی خواهد بود که به آن کلید، «bucket name» هم می‌گویند؛ مانند مقدار someaction_106062120. سپس به ازای هر درخواست رسیده، شمارشگر مرتبط با این کلید، یک واحد افزایش پیدا می‌کند و محدود کردن دسترسی‌ها بر اساس مقدار این کلید صورت می‌گیرد. در ادامه با شروع هر بازه‌ی زمانی جدید که در اینجا window نام دارد، یک کلید یا همان «bucket name» جدید تولید شده و مقدار متناظر با این کلید، به صفر تنظیم می‌شود.
اگر بجای دیکشنری‌های #C از بانک اطلاعاتی Redis برای نگهداری این key/valueها استفاده شود، می‌توان برای هر کدام از مقادیر آن، طول عمری را نیز مشخص کرد تا خود Redis، کار حذف خودکار اطلاعات غیرضروری را انجام دهد.

یک مشکل الگوریتم‌های بازه‌ی زمانی مشخص، غیر دقیق بودن آن‌ها است. برای مثال فرض کنید که به ازای هر 10 ثانیه می‌خواهید تنها اجازه‌ی پردازش 4 درخواست رسیده را بدهید. مشکل اینجا است که در این حالت یک کاربر می‌تواند 5 درخواست متوالی را بدون مشکل ارسال کند؛ 3 درخواست را در انتهای بازه‌ی اول و دو درخواست را در ابتدای بازه‌ی دوم:


به یک بازه‌ی زمانی مشخص، fixed window و به انتها و ابتدای دو بازه‌ی زمانی مشخص متوالی، sliding window می‌گویند. همانطور که در تصویر فوق هم مشاهده می‌کنید، در این اگوریتم، امکان محدود سازی دقیقی تنها در یک fixed window میسر است و نه در یک sliding window.

سؤال: آیا این مساله عدم دقت الگوریتم‌های بازه‌ی زمانی مشخص مهم است؟
پاسخ: بستگی دارد! اگر هدف شما، جلوگیری از استفاده‌ی سهوی یا عمدی بیش از حد از منابع سیستم است، این مساله مشکل مهمی را ایجاد نمی‌کند. اما اگر دقت بالایی را انتظار دارید، بله، مهم است! در این حالت از الگوریتم‌های «sliding window limit » بیشتر استفاده می‌شود که در پشت صحنه از همان روش استفاده‌ی از چندین fixed window کوچک، کمک می‌گیرند.


الگوریتم‌های سطل توکن‌ها (Token buckets)

در دنیای مخابرات، از الگوریتم‌های token buckets جهت کنترل میزان مصرف پهنای باند، زیاد استفاده می‌شود. از واژه‌ی سطل در اینجا استفاده شده، چون عموما به همراه آب بکارگرفته می‌شود:
فرض کنید سطل آبی را دارید که در کف آن نشتی دارد. اگر نرخ پر کردن این سطل، با آب، از نرخ نشتی کف آن بیشتر باشد، آب از سطل، سرریز خواهد شد. به این معنا که با سرریز توکن‌ها یا آب در این مثال، هیچ درخواست جدید دیگری پردازش نمی‌شود؛ تا زمانیکه مجددا سطل، به اندازه‌ای خالی شود که بتواند توکن یا آب بیشتری را بپذیرد.

یکی از مزیت‌های این روش، نداشتن مشکل عدم دقت به همراه بازه‌های زمانی مشخص است. در اینجا اگر تعداد درخواست زیادی به یکباره به سمت برنامه ارسال شوند، سطل پردازشی آن‌ها سرریز شده و دیگر پردازش نمی‌شوند.
مزیت دیگر آن‌ها، امکان بروز انفجاری یک ترافیک (bursts in traffic) نیز هست. برای مثال اگر قرار است سطلی با 60 توکن در دقیقه پر شود و این سطل نیز هر ثانیه یکبار تخلیه می‌شود، کلاینت‌ها هنوز می‌توانند 60 درخواست را در طی یک ثانیه ارسال کنند (ترافیک انفجاری) و پس از آن نرخ پردازشی، یک درخواست به ازای هر ثانیه خواهد شد.


آیا باید امکان بروز انفجار در ترافیک را داد؟

عموما در اکثر برنامه‌ها وجود یک محدود کننده‌ی نرخ دسترسی کافی است. برای مثال یک محدود کننده‌ی نرخ دسترسی سراسری 600 درخواست در هر دقیقه، برای هر endpoint ای شاید مناسب باشد. اما گاهی از اوقات نیاز است تا امکان بروز انفجار در ترافیک (bursts) را نیز درنظر گرفت. برای مثال زمانیکه یک برنامه‌ی موبایل شروع به کار می‌کند، در ابتدای راه اندازی آن تعداد زیادی درخواست، به سمت سرور ارسال می‌شوند و پس از آن، این سرعت کاهش پیدا می‌کند. در این حالت بهتر است چندین محدودیت را تعریف کرد: برای مثال امکان ارسال 10 درخواست در هر ثانیه و حداکثر 3600 درخواست در هر ساعت.


روش تشخیص کلاینت‌ها چگونه باشد؟

تا اینجا در مورد bucket name یا کلید دیکشنری اطلاعات محدود کردن دسترسی به منابع، از روش «نام هر اکشن + یک بازه‌ی زمانی» استفاده کردیم. به این کار «پارتیشن بندی درخواست‌ها» هم گفته می‌شود. روش‌های دیگری نیز برای انجام اینکار وجود دارند:
پارتیشن بندی به ازای هر
- endpoint
- آدرس IP. البته باید دقت داشت که کاربرانی که در پشت یک پروکسی قرار دارند، از یک IP آدرس اشتراکی استفاده می‌کنند.
- شماره کاربری. البته باید در اینجا بحث کاربران اعتبارسنجی نشده و anonymous را نیز مدنظر قرار داد.
- شمار سشن کاربر. در این حالت باید بحث ایجاد سشن‌های جدید به ازای دستگاه‌های مختلف مورد استفاده‌ی توسط کاربر را هم مدنظر قرار داد.
- نوع مروگر.
- هدر ویژه رسیده مانند X-Api-Token

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


درنظر گرفتن حالت‌های استثنائی

هرچند همانطور که عنوان شد تمام قسمت‌های برنامه باید از لحاظ میزان دسترسی محدود شوند، اما استثناءهای زیر را نیز باید درنظر گرفت:
- عموما تیم مدیریتی یا فروش برنامه، بیش از سایر کاربران، با برنامه کار می‌کنند.
- بیش از اندازه محدود کردن Web crawlers می‌تواند سبب کاهش امتیاز SEO سایت شما شود.
- گروه‌های خاصی از کاربران برنامه نیز می‌توانند دسترسی‌های بیشتری را خریداری کنند.


نحوه‌ی خاتمه‌ی اتصال و درخواست

اگر کاربری به حد نهایی استفاده‌ی از منابع خود رسید، چه باید کرد؟ آیا باید صرفا درخواست او را برگشت زد یا اطلاعات بهتری را به او نمایش داد؟
برای مثال GitHub یک چنین خروجی را به همراه هدرهای ویژه‌ای جهت مشخص سازی وضعیت محدود سازی دسترسی به منابع و علت آن‌، ارائه می‌دهد:
> HTTP/2 403
> Date: Tue, 20 Aug 2013 14:50:41 GMT
> x-ratelimit-limit: 60
> x-ratelimit-remaining: 0
> x-ratelimit-used: 60
> x-ratelimit-reset: 1377013266
> {
> "message": "API rate limit exceeded for xxx.xxx.xxx.xxx. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)",
> "documentation_url": "https://docs.github.com/rest/overview/resources-in-the-rest-api#rate-limiting"
> }
بنابراین بسته به نوع خروجی برنامه که اگر خروجی آن یک API از نوع JSON است و یا یک صفحه‌ی HTML، می‌توان از ترکیبی از هدرها و اطلاعات متنی و HTML استفاده کرد.
حتی یکسری از APIها از status codeهای ویژه‌ای مانند 403 (دسترسی ممنوع)، 503 (سرویس در دسترس نیست) و یا 429 (تعداد درخواست‌های زیاد) برای پاسخ دهی استفاده می‌کنند.



محل ذخیره سازی اطلاعات محدود سازی دسترسی به منابع کجا باشد؟

اگر محدودسازی دسترسی به منابع، جزئی از مدل تجاری برنامه‌ی شما است، نیاز است حتما از یک بانک اطلاعاتی توزیع شده مانند Redis استفاده کرد تا بتواند اطلاعات تمام نمونه‌های در حال اجرای برنامه را پوشش دهد. اما اگر هدف از این محدود سازی تنها میسر ساختن دسترسی منصفانه‌ی به منابع آن است، ذخیره سازی آن‌ها در حافظه‌ی همان نمونه‌ی در حال اجرای برنامه هم کافی است.
مطالب
بررسی بهبودهای ProblemDetails در ASP.NET Core 7x
در زمان ارائه‌ی ASP.NET Core 2.1، ویژگی جدیدی به نام [ApiController] ارائه شد که با استفاده از آن، یکسری اعمال توکار جهت سهولت کار با Web API توسط خود فریم‌ورک انجام می‌شوند؛ برای مثال عدم نیاز به بررسی وضعیت ModelState و بررسی خودکار آن با علامتگذاری یک کنترلر به صورت ApiController. یکی دیگر از این ویژگی‌های توکار، تبدیل خروجی تمام status codeهای بزرگتر و یا مساوی 400 یا همان Bad Request، به شیء جدید و استاندارد ProblemDetails است:
{
    "type": "https://example.com/probs/out-of-credit",
    "title": "You do not have enough credit.",
    "detail": "Your current balance is 30, but that costs 50.",
    "instance": "/account/12345/msgs/abc",
    "status": 403,
}
 بازگشت یک چنین خروجی یک‌دست و استانداردی، استفاده‌ی از آن‌را توسط کلاینت‌ها، ساده و قابل پیش‌بینی می‌کند. البته باید درنظر داشت که اگر در این‌حالت، برنامه یک استثنای معمولی را سبب شود، ProblemDetails ای بازگشت داده نمی‌شود. اگر برنامه در حالت توسعه اجرا شود، با استفاده از میان‌افزار app.UseDeveloperExceptionPage، یک صفحه‌ی نمایش جزئیات خطا ظاهر می‌شود و اگر برنامه در حالت تولید و ارائه‌ی نهایی اجرا شود، یک صفحه‌ی خالی (بدون داشتن response body) با status code مساوی 500 بازگشت داده می‌شود. این کمبود ویژه و امکانات سفارشی سازی بیشتر آن، به صورت توکار به ASP.NET Core 7x اضافه شده‌اند و دیگر نیازی به استفاده از کتابخانه‌های ثالث دیگری برای انجام آن نیست.


ProblemDetails بر اساس RFC7807 طراحی شده‌است

RFC7807، قالب استانداردی را برای ارائه‌ی خطاهای HTTP APIها تعریف می‌کند تا نیازی به وجود تعاریف متعددی در این زمینه نباشد و خروجی آن قابل پیش‌بینی و قابل بررسی توسط تمام کلاینت‌های یک API باشد. کلاس ProblemDetails در ASP.NET Core نیز بر همین اساس طراحی شده‌است.
این RFC دو فرمت خروجی را بر اساس مقدار مشخص شده‌ی در هدر Content-Type بازگشت داده شده، مجاز می‌داند:
  • JSON: “application/problem+json” media type
  • XML: “application/problem+xml” media type

که با توجه به این هدر ارسالی، اگر از یک کلاینت از نوع HttpClient استفاده کنیم، می‌توان بر اساس مقدار ویژه‌ی «application/problem+json» تشخیص داد که خروجی API دریافتی، به همراه خطا است و نحوه‌ی پردازش آن به صورت زیر خواهد بود:
var mediaType = response.Content.Headers.ContentType?.MediaType;
if (mediaType != null && mediaType.Equals("application/problem+json", StringComparison.InvariantCultureIgnoreCase))
{
   var problemDetails = await response.Content.ReadFromJsonAsync<ProblemDetails>(null, ct) ?? new ProblemDetails();
   // ...
}
در اینجا بدنه‌ی اصلی شیء ProblemDetails بازگشت داده شده، می‌تواند به همراه اعضای زیر باشد:
- type: یک رشته‌است که به آدرس مستندات HTML ای مرتبط با خطای بازگشت داده شده، اشاره می‌کند.
- title: رشته‌ای است که خلاصه‌ی خطای رخ‌داده را بیان می‌کند.
- detail: رشته‌ای است که توضیحات بیشتری را در مورد خطای رخ‌داده، بیان می‌کند.
- instance: رشته‌ای است که به آدرس محل بروز خطا اشاره می‌کند.
- status: عددی است که بیانگر HTTP status code بازگشتی از سمت سرور است.


البته اگر ویژگی ApiController بر روی کنترلرهای خود استفاده نمی‌کنید، می‌توانید این خروجی را به صورت زیر هم با استفاده از return Problem، تولید کنید:
[HttpPost("/sales/products/{sku}/availableForSale")]
public async Task<IActionResult> AvailableForSale([FromRoute] string sku)
{
   return Problem(
            "Product is already Available For Sale.",
            "/sales/products/1/availableForSale",
            400,
            "Cannot set product as available.",
            "http://example.com/problems/already-available");
}


امکان افزودن اعضای سفارشی به شیء ProblemDetails

امکان بسط این خروجی، با افزودن اعضای سفارشی نیز پیش‌بینی شده‌است. یک نمونه‌ی متداول و پرکاربرد آن، بازگشت خطاهای مرتبط با اعتبارسنجی اطلاعات رسیده‌است:
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
Content-Language: en
{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "errors": {
        "User": [
            "The user name is not verified."
        ]
    }
}
در اینجا عضو جدید errors را بنابر نیاز این مساله‌ی خاص، مشاهده می‌کنید که در صورت استفاده از ویژگی ApiController بر روی کنترلرهای Web API، به صورت خودکار توسط ASP.NET Core تولید می‌شود و نیازی به تنظیم خاصی و یا کدنویسی اضافه‌تری ندارد. کلاس مخصوص آن نیز ValidationProblemDetails‌ است.


جهت افزودن اعضای سفارشی دیگری به شیء ProblemDetails می‌توان به صورت زیر عمل کرد:
namespace WebApplication.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class DemoController : ControllerBase
    {
        [HttpPost]
        public ActionResult Post()
        {
            var problemDetails = new ProblemDetails
            {
                Detail = "The request parameters failed to validate.",
                Instance = null,
                Status = 400,
                Title = "Validation Error",
                Type = "https://example.net/validation-error",
            };

            problemDetails.Extensions.Add("invalidParams", new List<ValidationProblemDetailsParam>()
            {
                new("name", "Cannot be blank."),
                new("age", "Must be great or equals to 18.")
            });

            return new ObjectResult(problemDetails)
            {
                StatusCode = 400
            };
        }
    }

    public class ValidationProblemDetailsParam
    {
        public ValidationProblemDetailsParam(string name, string reason)
        {
            Name = name;
            Reason = reason;
        }

        public string Name { get; set; }
        public string Reason { get; set; }
    }
}
شیء ProblemDetails، به همراه خاصیت Extensions است که می‌توان به آن یک <Dictionary<string, object را انتساب داد و نمونه‌ای از آن‌را در مثال فوق مشاهده می‌کنید. این مثال سبب می‌شود تا عضو جدیدی با کلید دلخواه invalidParams، به همراه لیستی از name و reasonها به خروجی نهایی اضافه شود. مقدار این کلید، از نوع object است؛ یعنی هر شیء دلخواهی را در اینجا می‌توان تعریف و استفاده کرد.


معرفی سرویس جدید ProblemDetails در دات نت 7

در دات نت 7 می‌توان سرویس‌های جدید ProblemDetails را به نحو زیر به برنامه اضافه کرد:
services.AddProblemDetails();
پس از آن به 3 روش مختلف می‌توان از امکانات این سرویس‌ها استفاده کرد:
الف) با اضافه کردن میان‌افزار مدیریت خطاها
app.UseExceptionHandler();
پس از آن، هر استثنای مدیریت نشده‌ای نیز به صورت یک ProblemDetails ظاهر می‌شود و دیگر همانند قبل، سبب نمایش یک صفحه‌ی خالی نخواهد شد.

ب) با افزودن میان‌افزار StatusCodePages
app.UseStatusCodePages();
در این حالت مواردی که استثناء شمرده نمی‌شوند مانند 404، در صورت بروز رسیدن به یک مسیریابی یافت نشده و یا 405، در صورت درخواست یک HTTP method غیرمعتبر نیز توسط یک ProblemDetails استاندارد مدیریت می‌شوند.

ج) با افزودن میان‌افزار صفحه‌ی استثناءهای توسعه دهنده‌ها
app.UseDeveloperExceptionPage();
به این ترتیب در خروجی ProblemDetails، اطلاعات بیشتری از استثناء رخ‌داده، مانند استک‌تریس آن ظاهر خواهد شد.


امکان بازگشت ساده‌تر یک ProblemDetails سفارشی در دات نت 7

برای سفارشی سازی خروجی ProblemDetails، علاوه بر راه‌حلی که پیشتر در این مطلب مطرح شد، می‌توان در دات نت 7 از روش تکمیلی ذیل نیز استفاده کرد:
builder.Services.AddProblemDetails(options =>
    options.CustomizeProblemDetails = ctx =>
            ctx.ProblemDetails.Extensions.Add("MachineName", Environment.MachineName));
به این ترتیب در صورت لزوم می‌توان یک عضو سفارشی سراسری را به تمام اشیاء ProblemDetails برنامه به صورت خودکار اضافه کرد و یا اگر می‌خواهیم این مورد را کمی اختصاصی‌تر کنیم، می‌توان به صورت زیر عمل کرد:

الف) تعریف یک ErrorFeature سفارشی
public class MyErrorFeature
{
    public ErrorType Error  { get; set; }
}
​
public enum ErrorType
{
    ArgumentException
}
در ASP.NET Core می‌توان به شیء HttpContext.Features قابل تنظیم در هر اکشن متدی، اشیاء دلخواهی را مانند شیء سفارشی فوق، اضافه کرد و سپس در قسمت options.CustomizeProblemDetails تنظیماتی که ذکر شد، به دریافت و تنظیم آن، واکنش نشان داد.

ب) تنظیم مقدار ErrorFeature سفارشی در اکشن متدها
    [HttpGet("{value}")]
    public IActionResult MyErrorTest(int value)
    {
        if (value <= 0)
        {
            var errorType = new MyErrorFeature
            {
                Error = ErrorType.ArgumentException
            };
            HttpContext.Features.Set(errorType);
            return BadRequest();
        }
​
        return Ok(value);
    }
پس از تعریف شیءایی که قرار است به HttpContext.Features اضافه شود، اکنون روش تنظیم و مقدار دهی آن‌را در یک اکشن متد، در مثال فوق مشاهده می‌کنید.

ج) واکنش نشان دادن به دریافت ErrorFeature سفارشی
services.AddProblemDetails(options =>
    options.CustomizeProblemDetails = ctx =>
    {
        var MyErrorFeature = ctx.HttpContext.Features.Get<MyErrorFeature>();
​
        if (MyErrorFeature is not null)
        {
            (string Title, string Detail, string Type) details = MyErrorFeature.Error switch
            {
                ErrorType.ArgumentException =>
                (
                    nameof(ArgumentException),
                    "This is an argument-exception.",
                    "https://www.rfc-editor.org/rfc/rfc7231#section-6.5.1"
                ),
                _ =>
                (
                    nameof(Exception),
                    "default-exception",
                    "https://www.rfc-editor.org/rfc/rfc7231#section-6.6.1"
                )
            };
​
            ctx.ProblemDetails.Title = details.Title;
            ctx.ProblemDetails.Detail = details.Detail;
            ctx.ProblemDetails.Type = details.Type;
        }
    }
);
پس از تنظیم HttpContext.Features در اکشن متدی، می‌توان در options.CustomizeProblemDetails فوق، توسط متد ctx.HttpContext.Features.Get به آن شیء خاص تنظیم شده، در صورت وجود دسترسی یافت و سپس جزئیات بیشتری را از آن استخراج و مقادیر ctx.ProblemDetails جاری را که قرار است به کاربر بازگشت داده شوند، بازنویسی کرد و یا تغییر داد.
 

امکان تبدیل ساده‌تر اطلاعات استثناءهای سفارشی به یک ProblemDetails سفارشی در دات نت 7

بجای استفاده از تنظیمات services.AddProblemDetails جهت بازنویسی مقدار شیء ProblemDetails بازگشتی، می‌توان جزئیات میان‌افزار app.UseExceptionHandler را نیز سفارشی سازی کرد و به بروز استثناءهای خاصی واکنش نشان داد. برای مثال فرض کنید یک استثنای سفارشی را به صورت زیر طراحی کرده‌اید:
public class MyCustomException : Exception
{
    public MyCustomException(
        string message,
        HttpStatusCode statusCode = HttpStatusCode.BadRequest
    ) : base(message)
    {
        StatusCode = statusCode;
    }
​
    public HttpStatusCode StatusCode { get; }
}
و سپس در اکشن متدی، سبب بروز آن شده‌اید:
    [HttpGet("{value}")]
    public IActionResult MyErrorTest(int value)
    {
        if (value <= 0)
        {
            throw new MyCustomException("The value should be positive!");
        }
​
        return Ok(value);
    }
اکنون می‌توان در میان‌افزار مدیریت استثناءهای برنامه، نسبت به مدیریت این استثناء خاص، واکشن نشان داد و ProblemDetails متناظری را تولید و بازگشت داد:
app.UseExceptionHandler(exceptionHandlerApp =>
{
    exceptionHandlerApp.Run(async context =>
    {
        context.Response.ContentType = "application/problem+json";
​
        if (context.RequestServices.GetService<IProblemDetailsService>() is { } problemDetailsService)
        {
            var exceptionHandlerFeature = context.Features.Get<IExceptionHandlerFeature>();
            var exceptionType = exceptionHandlerFeature?.Error;
​
            if (exceptionType is not null)
            {
                (string Title, string Detail, string Type, int StatusCode) details = exceptionType switch
                {
                    MyCustomException MyCustomException =>
                    (
                        exceptionType.GetType().Name,
                        exceptionType.Message,
                        "https://www.rfc-editor.org/rfc/rfc7231#section-6.5.1",
                        context.Response.StatusCode = (int)MyCustomException.StatusCode
                    ),
                    _ =>
                    (
                        exceptionType.GetType().Name,
                        exceptionType.Message,
                        "https://www.rfc-editor.org/rfc/rfc7231#section-6.6.1",
                        context.Response.StatusCode = StatusCodes.Status500InternalServerError
                    )
                };
​
                await problemDetailsService.WriteAsync(new ProblemDetailsContext
                {
                    HttpContext = context,
                    ProblemDetails =
                    {
                        Title = details.Title,
                        Detail = details.Detail,
                        Type = details.Type,
                        Status = details.StatusCode
                    }
                });
            }
        }
    });
});
​
در اینجا نحوه‌ی کار با سرویس توکار IProblemDetailsService و سپس دسترسی به IExceptionHandlerFeature و استثنای صادر شده را مشاهده می‌کنید. پس از آن بر اساس نوع و اطلاعات این استثناء، می‌توان یک ProblemDetails مخصوص را تولید و در خروجی ثبت کرد.
مطالب
بررسی فرمت کوکی‌های 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 فایل آغارین برنامه). کار این تنظیم، بررسی اعتبار کوکی، در بازه‌های زمانی مشخص شده‌است.
مطالب
React 16x - قسمت 27 - احراز هویت و اعتبارسنجی کاربران - بخش 2 - استخراج و نمایش اطلاعات JWT و خروج از سیستم
در قسمت قبل، در هر دو حالت ثبت نام یک کاربر جدید و همچنین ورود به سیستم، یک JSON Web Token را از سرور دریافت کرده و در local storage مرورگر، ذخیره کردیم. اکنون قصد داریم محتوای این توکن را استخراج کرده و از آن جهت نمایش اطلاعات کاربر وارد شده‌ی به سیستم، استفاده کنیم. همچنین کار بهبود کیفیت کدهایی را هم که تاکنون پیاده سازی کردیم، انجام خواهیم داد.


نگاهی به محتوای JSON Web Token تولیدی

اگر مطلب قسمت قبل را پیگیری کرده باشید، پس از لاگین، یک چنین خروجی را در کنسول توسعه دهندگان مرورگر می‌توان مشاهده کرد که همان return Ok(new { access_token = jwt }) دریافتی از سمت سرور است:


اکنون این رشته‌ی طولانی را در حافظه کپی کرده و سپس به سایت https://jwt.io/#debugger-io مراجعه و در قسمت دیباگر آن، این رشته‌ی طولانی را paste می‌کنیم تا آن‌را decode کند:


برای نمونه payload آن حاوی یک چنین اطلاعاتی است:
{
  "jti": "b2921057-32a4-fbb2-0c18-5889c1ab8e70",
  "iss": "https://localhost:5001/",
  "iat": 1576402824,
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "1",
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "Vahid N.",
  "DisplayName": "Vahid N.",
  "http://schemas.microsoft.com/ws/2008/06/identity/claims/userdata": "1",
  "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Admin",
  "nbf": 1576402824,
  "exp": 1576402944,
  "aud": "Any"
}
در اینجا یک‌سری از اطلاعات کاربر، مانند id ، name ، DisplayName و یا حتی role او درج شده‌است؛ به همراه تاریخ صدور (iat) و انقضای (exp) این token که به صورت Unix time format بیان می‌شوند. به هر کدام از این خواصی که در اینجا ذکر شده‌اند، یک user claim گفته می‌شود. به عبارتی، این token ادعا می‌کند (claims) که نقش کاربر وارد شده‌ی به سیستم، Admin است. برای بررسی صحت این ادعا نیز یک امضای دیجیتال (مشخص شده‌ی با رنگ آبی) را به همراه این توکن سه قسمتی (قسمت‌های مختلف آن، با 2 نقطه از هم جدا شده‌اند که در تصویر نیز با سه رنگ متمایز، مشخص است)، ارائه کرده‌است. به این معنا که اگر قسمتی از اطلاعات این توکن، در سمت کاربر دستکاری شود، دیگر در سمت سرور تعیین اعتبار مجدد نخواهد شد؛ چون نیاز به یک امضای دیجیتال جدید را دارد که کلیدهای خصوصی تولید آن، تنها در سمت سرور مهیا هستند و به سمت کلاینت ارسال نمی‌شوند.


استخراج اطلاعات کاربر وارد شده‌ی به سیستم، از JSON Web Token دریافتی

همانطور که در payload توکن دریافتی از سرور نیز مشخص است، اطلاعات ارزشمندی از کاربر، به همراه آن ارائه شده‌اند و مزیت کار با آن، عدم نیاز به کوئری گرفتن مداوم از سرور و بانک اطلاعاتی، جهت دریافت مجدد این اطلاعات است. بنابراین اکنون در برنامه‌ی React خود، قصد داریم مشابه کاری را که سایت jwt.io انجام می‌دهد، پیاده سازی کرده و به این اطلاعات دسترسی پیدا کنیم و برای مثال DisplayName را در Navbar نمایش دهیم. برای این منظور فایل app.js را گشوده و تغییرات زیر را به آن اعمال می‌کنیم:
- می‌خواهیم اطلاعات کاربر جاری را در state کامپوننت مرکزی App قرار دهیم. سپس زمانیکه کار رندر کامپوننت NavBar درج شده‌ی در متد رندر آن فرا می‌رسد، می‌توان این اطلاعات کاربر را به صورت props به آن ارسال کرد؛ و یا به هر کامپوننت دیگری در component tree برنامه.
- بنابراین ابتدا کامپوننت تابعی بدون حالت App را تبدیل به یک کلاس کامپوننت استاندارد مشتق شده‌ی از کلاس پایه‌ی Component می‌کنیم. اکنون می‌توان state را نیز به آن اضافه کرد:
class App extends Component {
  state = {};
- سپس متد componentDidMount را به این کامپوننت اضافه می‌کنیم؛ در آن ابتدا token ذخیره شده‌ی در local storage را دریافت کرده و سپس decode می‌کنیم تا payload اطلاعات کاربر وارد شده‌ی به سیستم را استخراج کنیم. در آخر state را توسط این اطلاعات به روز می‌کنیم.
- برای decode کردن توکن، نیاز به نصب کتابخانه‌ی زیر را داریم:
> npm install --save jwt-decode
- پس از نصب آن، ابتدا امکانات آن‌را import کرده و سپس از آن در متد componentDidMount استفاده می‌کنیم:
import jwtDecode from "jwt-decode";
// ...

class App extends Component {
  state = {};

  componentDidMount() {
    try {
      const jwt = localStorage.getItem("token");
      const currentUser = jwtDecode(jwt);
      console.log("currentUser", currentUser);
      this.setState({ currentUser });
    } catch (ex) {
      console.log(ex);
    }
  }
ابتدا آیتمی با کلید token از localStorage استخراج می‌شود. سپس توسط متد jwtDecode، تبدیل به یک شیء حاوی اطلاعات کاربر جاری وارد شده‌ی به سیستم گشته و در آخر در state درج می‌شود. در اینجا درج try/catch ضروری است؛ از این جهت که متد jwtDecode، در صورت برخورد به توکنی غیرمعتبر، یک استثناء را صادر می‌کند و این استثناء نباید بارگذاری برنامه را با اخلال مواجه کند. از این جهت که اگر توکنی غیرمعتبر است (و یا حتی در localStorage وجود خارجی ندارد؛ برای کاربران لاگین نشده)، کاربر باید مجددا برای دریافت نمونه‌ی معتبر آن، لاگین کند.

- اکنون می‌توان شیء currentUser را به صورت props، به کامپوننت NavBar ارسال کرد:
  render() {
    return (
      <React.Fragment>
        <ToastContainer />
        <NavBar user={this.state.currentUser} />
        <main className="container">


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

پس از ارسال شیء کاربر به صورت props به کامپوننت src\components\navBar.jsx، کدهای این کامپوننت را به صورت زیر جهت نمایش نام کاربر جاری وارد شده‌ی به سیستم تغییر می‌دهیم:
const NavBar = ({ user }) => {
چون این کامپوننت به صورت یک کامپوننت تابعی بدون حالت تعریف شده، برای دریافت props می‌توان یا آن‌را به صورت مستقیم به عنوان پارامتر تعریف کرد و یا خواص مدنظر را با استفاده از Object Destructuring به عنوان پارامتر دریافت نمود.
سپس می‌توان لینک‌های Login و Register را به صورت شرطی رندر کرد و نمایش داد:
{!user && (
  <React.Fragment>
    <NavLink className="nav-item nav-link" to="/login">
      Login
    </NavLink>
    <NavLink className="nav-item nav-link" to="/register">
      Register
    </NavLink>
  </React.Fragment>
)}
در اینجا اگر شیء user تعریف شده باشد (یعنی کاربر، توکن ذخیره شده‌ای در local storage داشته باشد)، دیگر لینک‌های login و register نمایش داده نمی‌شوند. به علاوه برای اعمال && به چند المان React، نیاز است آن‌ها را داخل یک والد، مانند React.Fragment محصور کرد.

شبیه به همین حالت را برای هنگامیکه کاربر، تعریف شده‌است، جهت نمایش نام او و لینک به Logout، نیاز داریم:
{user && (
  <React.Fragment>
    <NavLink className="nav-item nav-link" to="/logout">
      Logout
    </NavLink>
    <NavLink className="nav-item nav-link" to="/profile">
      {user.DisplayName}
    </NavLink>
  </React.Fragment>
)}
user.DisplayName درج شده‌ی در اینجا، اطلاعات خودش را از payload توکن decode شده‌ی دریافتی از سرور، تامین می‌کند؛ با این خروجی:


فعلا تا پیش از پیاده سازی Logout، برای آزمایش آن، به کنسول توسعه دهندگان مرورگر مراجعه کرده و توکن ذخیره شده‌ی در ذیل قسمت application->storage را دستی حذف کنید. سپس صفحه را ریفرش کنید. اینبار لینک‌های به Login و Register نمایان می‌شوند.
یک مشکل! در این حالت (زمانیکه توکن حذف شده‌است)، از طریق قسمت Login به برنامه وارد شوید. هرچند این قسمت‌ها به درستی کار خود را انجام می‌دهند، اما هنوز در منوی بالای سایت، نام کاربری و لینک به Logout ظاهر نشده‌اند. علت اینجا است که در کامپوننت App، کار دریافت توکن در متد componentDidMount انجام می‌شود و این متد نیز تنها یکبار در طول عمر برنامه فراخوانی می‌شود. برای رفع این مشکل به src\components\loginForm.jsx مراجعه کرده و بجای استفاده از history.push برای هدایت کاربر به صفحه‌ی اصلی برنامه، نیاز خواهیم داشت تا کل برنامه را بارگذاری مجدد کنیم. یعنی بجای:
this.props.history.push("/");
باید از سطر زیر استفاده کرد:
window.location = "/";
این سطر سبب full page reload برنامه شده و در نتیجه متد componentDidMount کامپوننت App، یکبار دیگر فراخوانی خواهد شد. شبیه به همین کار را در کامپوننت src\components\registerForm.jsx نیز باید انجام داد.


پیاده سازی Logout کاربر وارد شده‌ی به سیستم

برای logout کاربر تنها کافی است توکن او را از local storage حذف کنیم. به همین جهت مسیریابی جدید logout را که به صورت لینکی به NavBar اضافه کردیم:
<NavLink className="nav-item nav-link" to="/logout">
   Logout
</NavLink>
به فایل src\App.js اضافه می‌کنیم.
import Logout from "./components/logout";
// ...

class App extends Component {
  render() {
    return (
          // ...
          <Switch>
            // ...
            <Route path="/logout" component={Logout} />
البته برای اینکار نیاز است کامپوننت جدید src\components\logout.jsx را با محتوای زیر ایجاد کنیم:
import { Component } from "react";

class Logout extends Component {
  componentDidMount() {
    localStorage.removeItem("token");
    window.location = "/";
  }

  render() {
    return null;
  }
}

export default Logout;
که در متد componentDidMount آن، کار حذف توکن ذخیره شده‌ی در localStorage انجام شده و سپس کاربر را با یک full page reload، به ریشه‌ی سایت هدایت می‌کنیم.


بهبود کیفیت کدهای نوشته شده

اگر به کامپوننت App دقت کنید، کلید token استفاده شده‌ی در آن، در چندین قسمت برنامه مانند login و logout، تکرار و پراکنده شده‌است. بنابراین بهتر است جزئیات پیاده سازی مرتبط با اعتبارسنجی کاربران، به ماژول مختص به آن‌ها (src\services\authService.js) منتقل شود تا سایر قسمت‌های برنامه، به صورت یک‌دستی از آن استفاده کنند و اگر در این بین نیاز به تغییری بود، فقط یک ماژول نیاز به تغییر، داشته باشد.
برای این منظور، ابتدا متد login قبلی را طوری تغییر می‌دهیم که کار ذخیره سازی توکن را نیز در authService.js انجام دهد:
const tokenKey = "token";

export async function login(email, password) {
  const {
    data: { access_token }
  } = await http.post(apiEndpoint + "/login", { email, password });
  console.log("JWT", access_token);
  localStorage.setItem(tokenKey, access_token);
}
سپس متد doSumbit کامپوننت src\components\loginForm.jsx، به صورت زیر ساده می‌شود:
const { data } = this.state;
await auth.login(data.username, data.password);
window.location = "/";

همین‌کار را برای logout نیز در authService انجام داده:
export function logout() {
  localStorage.removeItem(tokenKey);
}
و در ادامه متد componentDidMount کامپوننت Logout را برای استفاده‌ی از آن، اصلاح می‌کنیم:
import * as auth from "../services/authService";

class Logout extends Component {
  componentDidMount() {
    auth.logout();

منطق دریافت اطلاعات کاربر جاری نیز باید به authService منتقل شود؛ چون مسئولیت دریافت توکن و سپس decode آن، نباید به کامپوننت App واگذار شود:
import jwtDecode from "jwt-decode";
//...

export function getCurrentUser() {
  try {
    const jwt = localStorage.getItem(tokenKey);
    const currentUser = jwtDecode(jwt);
    console.log("currentUser", currentUser);
    return currentUser;
  } catch (ex) {
    console.log(ex);
    return null;
  }
}
سپس متد componentDidMount کامپوننت App، به صورت زیر خلاصه خواهد شد:
import * as auth from "./services/authService";

class App extends Component {
  state = {};

  componentDidMount() {
    const currentUser = auth.getCurrentUser();
    this.setState({ currentUser });
  }

جای دیگری که از localStorage استفاده شده، متد doSumbit کامپوننت ثبت نام کاربران است. این قسمت را نیز به صورت زیر به authService اضافه می‌کنیم:
export function loginWithJwt(jwt) {
   localStorage.setItem(tokenKey, jwt);
}
سپس ابتدای متد doSumbit را برای استفاده‌ی از آن به صورت زیر تغییر می‌دهیم:
import * as auth from "../services/authService";
// ...
const response = await userService.register(this.state.data);
auth.loginWithJwt(response.headers["x-auth-token"]);


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-27-backend.zip و sample-27-frontend.zip
نظرات مطالب
مدیریت ساده‌تر امور Async (غیر همزمان) در نسخه‌ی بعدی زبان‌های دات نتی
- اتفاقا اخیرا مایکروسافت تمرکز کمتری روی زبان‌ها داشته و تمرکز بیشتر اون بر روی کتابخانه‌ها و افزونه‌های مرتبط با دات نت فریم ورک بوده مثل افزونه‌های برنامه نویسی موازی و بهبودهای زیادی که در زمینه برنامه نویسی چند ریسمانی به دات نت 4 اضافه شده. این مبحث ساده سازی async هم در جهت تکمیل این بحث است.
- هنوز هم با تمام این اضافات، حجم مستندات زبان سی شارپ در حد یک دانشنامه‌ی قطور مثل مستندات زبان CPP در نیامده است.
- اینکه مثلا زبان جاوا در چند سال اخیر درجا زده و مثلا در مورد LINQ یا قابلیت‌های پویای زبان یا همین مبحث ساده سازی Async کاری نکرده آیا به نظر شما دلیل مناسبی است برای کم کاری دیگران؟