مطالب
مروری کوتاه بر کارکرد Ocelot

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

مشکلی که در این حالت میتواند رخ دهد، زیاد شدن مسیرهای متفاوت برای اتصال به هر یک از سرویس‌ها و سخت‌تر شدن به روزرسانی این مسیرها می‌باشد. به همین دلیل در این بخش، نیاز به ابزاری میباشد تا بتوان از طریق آن، مسیردهی ساده‌ای را ایجاد کرد و در پشت صحنه  مسیردهی‌های متفاوتی را کنترل نمود. با ایجاد چنین ابزاری در واقع شما   API Gateway ایجاد نموده‌اید. یکی از معروفترین کتابخانه‌های این حوزه، Ocelot میباشد. کار با این ابزار بسیار ساده بوده و امکانات بسیار زیاد و قدرتمندی را فراهم مینماید.

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

پروژه اول نوع Api : با دریافت Id در اکشن‌متد مورد نظر، شیء user بازگردانده میشود:

public class User
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string UserName { get; set; }


    public static List<User> GetUsers()
    {
        return new List<User>()
        {
            new()
            {
                Id = 1,
                FirstName = "علی",
                LastName = "یگانه مقدم",
                UserName = "yeganehaym"
            },
            new ()
            {
                Id = 2,
                FirstName = "وحید",
                LastName = "نصیری",
                UserName = "VahidN"
            },
        };
    }
}
[ApiController]
[Route("/api/[controller]/{id?}")]
public class UserController : ControllerBase
{

    [HttpGet]
    public User GetUser(int id)
    {
        var users = Users.User.GetUsers();
        var user = users.FirstOrDefault(x => x.Id == id);
        return user;
    }
}

 

پروژه دوم نوع Api : دریافت لیستی از محصولات:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Price { get; set; }
    public  int Quantity { get; set; }


    public static List<Product> GetProducts()
    {
        return new List<Product>()
        {
            new()
            {
                Id = 1,
                Name = "LCD",
                Price = 20000,
                Quantity = 10
            },
            new()
            {
                Id = 1,
                Name = "Mouse",
                Price = 320000,
                Quantity = 15
            },
            new()
            {
                Id = 1,
                Name = "Keyboard",
                Price = 50000,
                Quantity = 25
            },
        };
    }
}
[ApiController]
[Route("api/[controller]")]
public class ProductController : ControllerBase
{

    [HttpGet]
    public List<Product> GetProducts()
    {
        return Product.GetProducts();
    }

}


پروژه سوم همان ApiGateway هست و همین‌که یک پروژه‌ی وب خالی باشد، کفایت میکند. در این پروژه   Ocelot  را نصب نموده  و سپس فایلی با نام  ocelot.json را با محتوای زیر به ریشه‌ی پروژه همانند فایل‌های appsettings.json اضافه میکنیم:

{
    "Routes":[
        
        {
        "DownstreamPathTemplate":"/api/User/{id}",
        "DownstreamScheme":"https",
        "DownstreamHostAndPorts":[
            {
                "Host":"localhost",
                "Port":"7279"
            }
        ],
        "UpstreamPathTemplate":"/GetUser/{id}",
        "UpstreamHttpMethod":[
            "GET"
        ]},
        {
        "DownstreamPathTemplate":"/api/Product",
        "DownstreamScheme":"https",
        "DownstreamHostAndPorts":[
            {
                "Host":"localhost",
                "Port":"7261"
            }
        ],
     
        "UpstreamPathTemplate":"/Products",
        "UpstreamHttpMethod":[
            "GET"
        ]
        }
    ]
    
   
}

این فایل‌ها شامل دو قسمتUpStream و DownStream میشوند. آپ‌استریم‌ها در واقع آدرسی است که شما قصد اتصال به آن‌را دارید و قسمت داون‌استریم، سرویس مقصدی است که ocelot باید درخواست شما را به سمت آن ارسال نماید. به‌عنوان مثل شما با ارسال درخواستی به آدرس Products ، در پشت صحنه به آدرس localhost:7261/api/product ارسال میگردد. بدین صورت سیستم نهایی تنها به یک دامنه و آدرس منسجم ارسال شده، ولی در پشت صحنه این آدرس‌ها ممکن است به تعداد زیادی سرویس در آدرس‌های متفاوتی ارسال گردند.

جهت راه اندازی نهایی، کد زیر را به فایل Program.cs اضافه میکنیم:

builder.Services.AddOcelot();
app.UseOcelot();


پس از اضافه کردن پیکربندی و middleware آن، کد زیر را نیز جهت شناسایی فایل ocelot به فایل Program.cs نیز اضافه مینماییم:

builder.Configuration.SetBasePath(builder.Environment.ContentRootPath)    
    .AddJsonFile("ocelot.json", optional: false, reloadOnChange: true);

همچنین در صورت تمایل میتوانید کد را به شکل زیر هم نوشته تا بتوانید تنظیمات متفاوتی را برای محیط اجرایی متفاوتی ایجاد نمایید:

builder.Configuration.SetBasePath(builder.Environment.ContentRootPath)    
    .AddJsonFile("ocelot.json", optional: false, reloadOnChange: true)
    .AddJsonFile($"ocelot.{builder.Environment.EnvironmentName}.json", optional: false, reloadOnChange: true);

هر سه برنامه را با هم اجرا نمایید و با استفاده از برنامه‌ی PostMan درخواستی را برای هر یک از موارد مورد نظر /Products و /GetUser/{1,2} به سمت پروژه ApiGateway ارسال نمایید.

Ocelot موارد دیگری از قبیل تنظیم Load Balancer بین سرویس ها، اتصال به سرویس‌های Service Discoveryچون Consul   یا  یوریکا  و کش کردن و ... را نیز فراهم می‌نماید.


عملیات کشینگ

جهت بحث کشینگ، ابتدا بسته زیر را اضافه نمایید:

Install-Package Ocelot.Cache.CacheManager

سپس پیکربندی ابتدایی را به شکل زیر تغییر دهید:

builder.Services.AddOcelot()
    .AddCacheManager(x => x.WithDictionaryHandle());

در ادامه در فایل Ocelot جیسون، برای هر بخشی که مدنظر شماست تا کشی را انجام دهد، کد زیر اضافه نمایید:

"FileCacheOptions":{
      "TtlSeconds":30,
       "Region":"custom"
}

TtlSeconds : مدت زمان کش به ثانیه

Region : یک عبارت رشته‌ای همانند یک عنوان یا نام که بعدا میتوانید از طریق api ‌ها به آن متصل شوید و عملیاتی چون خالی کردن کش را صادر نمایید.

حال برای بخش محصولات این تنظیمات ذکر میگردد:

{
    "Routes":[
        
        {
        "DownstreamPathTemplate":"/api/User/{id}",
        "DownstreamScheme":"https",
        "DownstreamHostAndPorts":[
            {
                "Host":"localhost",
                "Port":"7279"
            }
        ],
        "UpstreamPathTemplate":"/GetUser/{id}",
        "UpstreamHttpMethod":[
            "GET"
        ]
        },
        {
        "DownstreamPathTemplate":"/api/Product",
        "DownstreamScheme":"https",
        "DownstreamHostAndPorts":[
            {
                "Host":"localhost",
                "Port":"7261"
            }
        ],
     
        "UpstreamPathTemplate":"/Products",
        "UpstreamHttpMethod":[
            "GET"
        ],
            "FileCacheOptions":{
                "TtlSeconds":30,
                "Region":"custom"
            }
        }
    ]
    
   
}

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

این مورد را برای بخش کاربران هم انجام دهید و می‌بینید که برای هر userId و هر شکل  Url، یک پاسخ منحصر به فرد، دریافت و کش خواهد شد.


جلوگیری از درخواست‌های بیش از حد

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

       "RateLimitOptions":{
                "ClientWhitelist":[
                ],
                "EnableRateLimiting":true,
                "Period":"5s",
                "PeriodTimespan":1,
                "Limit":1,
                "HttpStatusCode":429
            }


WhiteClients : برای مشخص کردن کلاینت‌هایی که نباید اعمال محدودیت روی آن‌ها صورت بگیرد.

EnableRateLimiting   : این مورد باعث فعالسازی آن میگردد.

Period: مدت زمانیکه حداکثر تعداد درخواست باید در آن بازه صورت بگیرد. به ترتیب برای ثانیه، دقیقه، ساعت و روز حروف s - m - h و d استفاده میگردد.

PeriodTimespan: بعد از محدود شدن، بعد از چه مدتی دوباره بتواند درخواستی را ارسال نماید. در اینجا بعد از محدودیت ارسال درخواست، بعد از یک ثانیه مجدد اجازه ارسال درخواست باز میگردد.

Limit: در بازه زمانی مشخص شده چند درخواست مورد قبول واقع میشود و بعد از آن دیگر اجازه ارسال درخواست را نخواهد داشت.

HttpStatusCode: در صورت فیلتر شدن درخواست‌های رسیده، چه کد وضعیتی باید برگردانده شود که عدد 429 به معنای Too Many Request میباشد.

با تنظیمات بالا هر کلاینت میتواند در 5 ثانیه، نهایتا یک درخواست را ارسال نماید و با ارسال بقیه درخواست‌ها، Ocelot بجای هدایت درخواست به سرویس مربوطه، کد وضعیت 429 را باز میگرداند و یک ثانیه بعد از گذشت 5 ثانیه میتواند مجددا درخواست خود را ارسال نماید.

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

{
    "Routes":[
        
        {
        "DownstreamPathTemplate":"/api/User/{id}",
        "DownstreamScheme":"https",
        "DownstreamHostAndPorts":[
            {
                "Host":"localhost",
                "Port":"7279"
            }
        ],
        "UpstreamPathTemplate":"/GetUser/{id}",
        "UpstreamHttpMethod":[
            "GET"
        ],
        "FileCacheOptions":{
            "TtlSeconds":30,
            "Region":"custom"
        }
        },
        {
        "DownstreamPathTemplate":"/api/Product",
        "DownstreamScheme":"https",
        "DownstreamHostAndPorts":[
            {
                "Host":"localhost",
                "Port":"7261"
            }
        ],
     
        "UpstreamPathTemplate":"/Products",
        "UpstreamHttpMethod":[
            "GET"
        ],
            "RateLimitOptions":{
                "ClientWhitelist":[
                ],
                "EnableRateLimiting":true,
                "Period":"5s",
                "PeriodTimespan":1,
                "Limit":1,
                "HttpStatusCode":429
            }
        }
    ],
    "DangerousAcceptAnyServerCertificateValidator": true
    
   
}

برای تست آن با استفاد از PostMan مرتبا به آدرس Products/ درخواست ارسال نمایید. 

فایل پروژه : Ocelot.zip

مطالب
استفاده از Fluent Validation در برنامه‌های ASP.NET Core - قسمت دوم - اجرای قواعد اعتبارسنجی تعریف شده
در قسمت قبل، روش تعریف قواعد اعتبارسنجی را با استفاده از کتابخانه‌ی Fluent Validation بررسی کردیم. در این قسمت می‌خواهیم این قواعد را به صورت خودکار به یک برنامه‌ی ASP.NET Core معرفی کرده و سپس از آن‌ها استفاده کنیم.


روش اول: استفاده‌ی دستی از اعتبارسنج کتابخانه‌ی Fluent Validation

روش‌های زیادی برای استفاده‌ی از قواعد تعریف شده‌ی توسط کتابخانه‌ی Fluent Validation وجود دارند. اولین روش، فراخوانی دستی اعتبارسنج، در مکان‌های مورد نیاز است. برای اینکار در ابتدا نیاز است با اجرای دستور «dotnet add package FluentValidation.AspNetCore»، این کتابخانه را در پروژه‌ی وب خود نیز نصب کنیم تا بتوانیم از کلاس‌ها و متدهای آن استفاده نمائیم. پس از آن، روش دستی کار با کلاس RegisterModelValidator که در قسمت قبل آن‌را تعریف کردیم، به صورت زیر است:
using FluentValidationSample.Models;
using Microsoft.AspNetCore.Mvc;

namespace FluentValidationSample.Web.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }

        [HttpPost]
        public IActionResult RegisterValidateManually(RegisterModel model)
        {
            var validator = new RegisterModelValidator();
            var validationResult = validator.Validate(model);
            if (!validationResult.IsValid)
            {
                return BadRequest(validationResult.Errors[0].ErrorMessage);
            }

            // TODO: Save the model

            return Ok();
        }
    }
}
ساده‌ترین روش کار با RegisterModelValidator تعریف شده، ایجاد یک وهله‌ی جدید از آن و سپس فراخوانی متد Validate آن شیء است. در این حالت می‌توان کنترل کاملی را بر روی قالب پیام نهایی بازگشت داده شده داشت. برای مثال در اینجا اولین خطای بازگشت داده شده، به اطلاع کاربر رسیده‌است. حتی می‌توان کل شیء Errors را نیز بازگشت داد.

یک نکته: متد الحاقی AddToModelState که در فضای نام FluentValidation.AspNetCore قرار دارد، امکان تبدیل نتیجه‌ی اعتبارسنجی حاصل را به ModelState استاندارد ASP.NET Core نیز میسر می‌کند:
public IActionResult RegisterValidateManually(RegisterModel model)
{
    var validator = new RegisterModelValidator();
    var validationResult = validator.Validate(model);
    if (!validationResult.IsValid)
    {
       validationResult.AddToModelState(ModelState, null);
       return BadRequest(ModelState);
    }

    // TODO: Save the model

    return Ok();
}


روش دوم: تزریق اعتبارسنج تعریف شده در سازنده‌ی کنترلر

بجای وهله سازی دستی RegisterModelValidator و ایجاد وابستگی مستقیمی به آن، می‌توان از روش تزریق وابستگی‌های آن نیز استفاده کرد. در این حالت اعتبارسنج RegisterModelValidator با طول عمر Transient به سیستم تزریق وابستگی‌ها معرفی شده:
namespace FluentValidationSample.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddTransient<IValidator<RegisterModel>, RegisterModelValidator>();
            services.AddControllersWithViews();
        }
 و پس از آن با تزریق <IValidator<RegisterModel به سازنده‌ی کنترلر مدنظر، می‌توان به امکانات آن همانند روش اول، دسترسی یافت:
namespace FluentValidationSample.Web.Controllers
{
    public class HomeController : Controller
    {
        private readonly IValidator<RegisterModel> _registerModelValidator;

        public HomeController(IValidator<RegisterModel> registerModelValidator)
        {
            _registerModelValidator = registerModelValidator;
        }

        [HttpPost]
        public IActionResult RegisterValidatorInjection(RegisterModel model)
        {
            var validationResult = _registerModelValidator.Validate(model);
            if (!validationResult.IsValid)
            {
                return BadRequest(validationResult.Errors[0].ErrorMessage);
            }

            // TODO: Save the model

            return Ok();
        }
    }
}
به این ترتیب new RegisterModelValidator را با وهله‌ای از <IValidator<RegisterModel، تعویض کردیم. کار با این روش بسیار انعطاف پذیر بوده و همچنین قابلیت آزمون پذیری بالایی را نیز دارد.


روش سوم: خودکار سازی اجرای یک تک اعتبارسنج تعریف شده

اگر متد الحاقی AddFluentValidation را به صورت زیر به سیستم معرفی کنیم:
namespace FluentValidationSample.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddTransient<IValidator<RegisterModel>, RegisterModelValidator>();
            services.AddControllersWithViews().AddFluentValidation();
        }
سبب اجرای خودکار تمام IValidatorهای اضافه شده‌ی به سیستم، پیش از اجرای اکشن متد مرتبط با آن‌ها می‌شود. برای مثال اگر اکشن متدی دارای پارامتری از نوع RegisterModel بود، چون IValidator مخصوص به آن به سیستم تزریق وابستگی‌ها معرفی شده‌است، متد الحاقی AddFluentValidation، کار وهله سازی خودکار این IValidator و سپس فراخوانی متد Validate آن‌را به صورت خودکار انجام می‌دهد. به این ترتیب، قطعه کدهایی را که تاکنون نوشتیم، به صورت زیر خلاصه خواهند شد که در آن‌ها اثری از بکارگیری کتابخانه‌ی FluentValidation مشاهده نمی‌شود:
namespace FluentValidationSample.Web.Controllers
{
    public class HomeController : Controller
    {
        [HttpPost]
        public IActionResult RegisterValidatorAutomatically(RegisterModel model)
        {
            if (!ModelState.IsValid)
            {
                // re-render the view when validation failed.
                return View(model);
            }

            // TODO: Save the model

            return Ok();
        }
    }
}
زمانیکه model به سمت اکشن متد فوق ارسال می‌شود، زیرساخت model-binding موجود در ASP.NET Core، اینبار کار اعتبارسنجی آن‌را توسط RegisterModelValidator به صورت خودکار انجام داده و نتیجه‌ی آن‌را به ModelState اضافه می‌کند که برای مثال در اینجا سبب رندر مجدد فرم شده که تمام مباحث tag-helper‌های استانداردی مانند asp-validation-summary و asp-validation-for پس از آن به صورت متداولی و همانند قبل، قابل استفاده خواهند بود.

نکته 1: تنظیمات فوق برایASP.NET Web Pages و PageModels نیز یکی است. فقط با این تفاوت که اعتبارسنج‌ها را فقط می‌توان به مدل‌هایی که به صورت خواص یک page model تعریف شده‌اند، اعمال کرد و نه به کل page model.

نکته 2: اگر کنترلر شما به ویژگی [ApiController] مزین شده باشد:
namespace FluentValidationSample.Web.Controllers
{
    [Route("[controller]")]
    [ApiController]
    public class HomeController : Controller
    {
        [HttpPost]
        public IActionResult RegisterValidatorAutomatically(RegisterModel model)
        {
            // TODO: Save the model

            return Ok();
        }
    }
}
در این حالت دیگر نیازی به ذکر if (!ModelState.IsValid) نیست و خطای حاصل از شکست اعتبارسنجی، به صورت خودکار توسط FluentValidation تشکیل شده و بازگشت داده می‌شود (پیش از رسیدن به بدنه‌ی اکشن متد فوق) و برای نمونه یک چنین شکل و خروجی خودکاری را پیدا می‌کند:
{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "traceId": "|84df05e2-41e0d4841bb61293.",
    "errors": {
        "FirstName": [
            "'First Name' must not be empty."
        ]
    }
}
اگر علاقمند به سفارشی سازی این خروجی خودکار هستید، باید به این صورت با تنظیم ApiBehaviorOptions و مقدار دهی نحوه‌ی تشکیل ModelState نهایی، عمل کرد:
        public void ConfigureServices(IServiceCollection services)
        {
            // ...

            // override modelstate
            services.Configure<ApiBehaviorOptions>(options =>
            {
                options.InvalidModelStateResponseFactory = context =>
                {
                    var errors = context.ModelState.Values.SelectMany(x => x.Errors.Select(p => p.ErrorMessage)).ToList();
                    return new BadRequestObjectResult(new
                    {
                        Code = "00009",
                        Message = "Validation errors",
                        Errors = errors
                    });
                };
            });
        }


روش چهارم: خودکار سازی ثبت و اجرای تمام اعتبارسنج‌های تعریف شده

و در آخر بجای معرفی دستی تک تک اعتبارسنج‌های تعریف شده به سیستم تزریق وابستگی‌ها، می‌توان تمام آن‌ها را با فراخوانی متد RegisterValidatorsFromAssemblyContaining، به صورت خودکار از یک اسمبلی خاص استخراج نمود و با طول عمر Transient، به سیستم معرفی کرد. در این حالت متد ConfigureServices به صورت زیر خلاصه می‌شود:
namespace FluentValidationSample.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews().AddFluentValidation(
                fv => fv.RegisterValidatorsFromAssemblyContaining<RegisterModelValidator>()
            );
        }
در اینجا امکان استفاده‌ی از متد fv.RegisterValidatorsFromAssembly نیز برای معرفی اسمبلی خاصی مانند ()Assembly.GetExecutingAssembly نیز وجود دارد.


سازگاری اجرای خودکار FluentValidation با اعتبارسنج‌های استاندارد ASP.NET Core

به صورت پیش‌فرض، زمانیکه FluentValidation اجرا می‌شود، اگر اعتبارسنج دیگری نیز در سیستم تعریف شده باشد، اجرا خواهد شد. به این معنا که برای مثال می‌توان FluentValidation و DataAnnotations attributes و IValidatableObject‌ها را با هم ترکیب کرد.
اگر می‌خواهید این قابلیت را غیرفعال کنید و فقط سبب اجرای خودکار FluentValidationها شوید، نیاز است تنظیم زیر را انجام دهید:
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews().AddFluentValidation(
       fv =>
       {
           fv.RegisterValidatorsFromAssemblyContaining<RegisterModelValidator>();
           fv.RunDefaultMvcValidationAfterFluentValidationExecutes = false;
       }
    );
}
مطالب
C# 6 - The nameof Operator
یکی دیگر از قابلیت‌های جذاب نسخه‌ی جدید سی‌شارپ، عملگر nameof است. هدف اصلی آن ارائه کدهایی با قابلیت Refactoring بهتر است؛ زیرا به جای نوشتن نام فیلدها و یا متدها در صورت نیاز به صورت hard-coded، می‌توانیم از این عملگر استفاده کنیم. به عنوان مثال در زمان صدور استثناءیی از نوع ArgumentNullException باید نام آرگومان را به سازنده‌ی این کلاس پاس دهیم. متاسفانه یکی از مشکلاتی که با رشته‌ها در حالت کلی وجود دارد این است که امکان دیباگ در زمان کامپایل را از دست خواهیم داد و با تغییر هر المنت، تغییرات به صورت خودکار به رشته پاس داده شده، به سازنده‌ی کلاس ArgumentNullException اعمال نخواهد شد:
public static void DoWork(string name)
{
       if (name == null)
       {
           throw new ArgumentNullException("name");
       }
}
اما با استفاده از عملگر nameof، کد امن‌تری را خواهیم داشت؛ زیرا همیشه نام واقعی آرگومان به سازنده‌ی کلاس ArgumentNullException پاس داده می‌شود:
public static void DoWork(string name)
{
       if (name == null)
       {
           throw new ArgumentNullException(nameof(name));
       }
}
اگر ReSharper را نصب کرده باشید، به شما پیشنهاد می‌دهد که از nameof به جای یک رشته‌ی جادویی (magic string) استفاده نمائید:


یک مثال دیگر می‌تواند در زمان فراخوانی رخدادهای مربوط به OnPropertyChanged باشد. در اینجا باید نام خصوصیتی را که تغییر یافته است، به آن پاس دهیم: 
        public string Name
        {
            get { return _name; }
            set
            {
                _name = value;
                OnPropertyChanged("Name");
            }
        }
اما با کمک عملگر nameof می‌توانیم قسمت فراخوانی متد OnPropertyChanged را به اینصورت نیز بازنویسی کنیم:
OnPropertyChanged(nameof(Name));
ممکن است عنوان کنید قبلاً در سی‌شارپ 5 هم می‌توانستیم از ویژگی [CallerMemberName] استفاده کنیم، پس دیگر نیازی به استفاده از عملگر nameof نخواهد بود. اما تفاوت کلیدی این است که CallerMemberName در زمان اجرا نام فیلد فراخوان را دریافت میکند (run time)، در حالیکه با استفاده از عملگر nameof می‌توانید در زمان کامپایل به نام فیلد دسترسی داشته باشید (compile time).

محدودیت‌های عملگر nameof
این عملگر حالت‌هایی را که مشاهده می‌کنید، فعلاً پشتیبانی نخواهد کرد:
nameof(f()); // where f is a method - you could use nameof(f) instead
nameof(c._Age); // where c is a different class and _Age is private. Nameof can't break accessor rules.
nameof(List<>); // List<> isn't valid C# anyway, so this won't work
nameof(default(List<int>)); // default returns an instance, not a member
nameof(int); // int is a keyword, not a member- you could do nameof(Int32)
nameof(x[2]); // returns an instance using an indexer, so not a member
nameof("hello"); // a string isn't a member
nameof(1 + 2); // an int isn't a member
برای آزمایش عملگر nameof می‌توانیم یک تست را در حالت‌های زیر بنویسیم:


همانطور که مشاهده می‌کنید، همه‌ی حالت‌های فوق با موفقیت پاس شده‌اند.
مطالب
کار با Kendo UI DataSource
Kendo UI DataSource جهت تامین داده‌های سمت کلاینت ویجت‌های مختلف KendoUI طراحی شده‌است و به عنوان یک اینترفیس استاندارد قابل استفاده توسط تمام کنترل‌های داده‌ای Kendo UI کاربرد دارد. Kendo UI DataSource امکان کار با منابع داده محلی، مانند اشیاء و آرایه‌های جاوا اسکریپتی و همچنین منابع تامین شده از راه دور، مانند JSON، JSONP و XML را دارد. به علاوه توسط آن می‌توان اعمال ثبت، ویرایش و حذف اطلاعات، به همراه صفحه بندی، گروه بندی و مرتب سازی داده‌ها را کنترل کرد.


استفاده از منابع داده محلی
در ادامه مثالی را از نحوه‌ی استفاده از یک منبع داده محلی جاوا اسکریپتی، مشاهده می‌کنید:
    <script type="text/javascript">
        $(function () {
            var cars = [
                { "Year": 2000, "Make": "Hyundai", "Model": "Elantra" },
                { "Year": 2001, "Make": "Hyundai", "Model": "Sonata" },
                { "Year": 2002, "Make": "Toyota", "Model": "Corolla" },
                { "Year": 2003, "Make": "Toyota", "Model": "Yaris" },
                { "Year": 2004, "Make": "Honda", "Model": "CRV" },
                { "Year": 2005, "Make": "Honda", "Model": "Accord" },
                { "Year": 2000, "Make": "Honda", "Model": "Accord" },
                { "Year": 2002, "Make": "Kia", "Model": "Sedona" },
                { "Year": 2004, "Make": "Fiat", "Model": "One" },
                { "Year": 2005, "Make": "BMW", "Model": "M3" },
                { "Year": 2008, "Make": "BMW", "Model": "X5" }
            ];

            var carsDataSource = new kendo.data.DataSource({
                data: cars
            });

            carsDataSource.read();
            alert(carsDataSource.total());
        });
    </script>
در اینجا cars آرایه‌ای از اشیاء جاوا اسکریپتی بیانگر ساختار یک خودرو است. سپس برای معرفی آن به Kendo UI، کار با مقدار دهی خاصیت data مربوط به new kendo.data.DataSource شروع می‌شود.
ذکر new kendo.data.DataSource به تنهایی به معنای مقدار دهی اولیه است و در این حالت منبع داده مورد نظر، استفاده نخواهد شد. برای مثال اگر متد total آن‌را جهت یافتن تعداد عناصر موجود در آن فراخوانی کنید، صفر را بازگشت می‌دهد. برای شروع به کار با آن، نیاز است ابتدا متد read را بر روی این منبع داده مقدار دهی شده، فراخوانی کرد.


استفاده از منابع داده راه دور

در برنامه‌های کاربردی، عموما نیاز است تا منبع داده را از یک وب سرور تامین کرد. در اینجا نحوه‌ی خواندن اطلاعات JSON بازگشت داده شده از جستجوی توئیتر را مشاهده می‌کنید:
    <script type="text/javascript">
        $(function () {
            var twitterDataSource = new kendo.data.DataSource({
                transport: {
                    read: {
                        url: "http://search.twitter.com/search.json",
                        dataType: "jsonp",
                        contentType: 'application/json; charset=utf-8',
                        type: 'GET',
                        data: { q: "#kendoui" }
                    },
                    schema: { data: "results" }
                },
                error: function (e) {
                    alert(e.errorThrown.stack);
                }
            });
        });
    </script>
در قسمت transport، جزئیات تبادل اطلاعات با سرور راه دور مشخص می‌شود؛ برای مثال url ارائه دهنده‌ی سرویس، dataType بیانگر نوع داده مورد انتظار و data کار مقدار دهی پارامتر مورد انتظار توسط سرویس توئیتر را انجام می‌دهد. در اینجا چون صرفا عملیات خواندن اطلاعات صورت می‌گیرد، خاصیت read مقدار دهی شده‌است.
در قسمت schema مشخص می‌کنیم که اطلاعات JSON بازگشت داده شده توسط توئیتر، در فیلد results آن قرار دارد.


کار با منابع داده OData

علاوه بر فرمت‌های یاد شده، Kendo UI DataSource امکان کار با اطلاعاتی از نوع OData را نیز دارا است که تنظیمات ابتدایی آن به صورت ذیل است:
    <script type="text/javascript">
            var moviesDataSource = new kendo.data.DataSource({
                type: "odata",
                transport: {
                    read: "http://demos.kendoui.com/service/Northwind.svc/Orders"
                },
                error: function (e) {
                    alert(e.errorThrown.stack);
                }
            });
        });
    </script>
همانطور که ملاحظه می‌کنید، تنظیمات ابتدایی آن اندکی با حالت remote data پیشین متفاوت است. در اینجا ابتدا نوع داده‌ی بازگشتی مشخص می‌شود و در قسمت transport، خاصیت read آن، آدرس سرویس را دریافت می‌کند.


یک مثال: دریافت اطلاعات از ASP.NET Web API

یک پروژه‌ی جدید ASP.NET را آغاز کنید. تفاوتی نمی‌کند که Web forms باشد یا MVC؛ از این جهت که مباحث Web API در هر دو یکسان است.
سپس یک کنترلر جدید Web API را به نام ProductsController با محتوای زیر ایجاد کنید:
using System.Collections.Generic;
using System.Web.Http;

namespace KendoUI02
{
    public class Product
    {
        public int Id { set; get; }
        public string Name { set; get; }
    }

    public class ProductsController : ApiController
    {
        public IEnumerable<Product> Get()
        {
            var products = new List<Product>();
            for (var i = 1; i <= 100; i++)
            {
                products.Add(new Product { Id = i, Name = "Product " + i });
            }
            return products;
        }
    }
}
در این مثال، هدف صرفا ارائه یک خروجی ساده JSON از طرف سرور است.
در ادامه نیاز است تعریف مسیریابی ذیل نیز به فایل Global.asax.cs برنامه اضافه شود تا بتوان به آدرس api/products در سایت، دسترسی یافت:
using System;
using System.Web.Http;
using System.Web.Routing;

namespace KendoUI02
{
    public class Global : System.Web.HttpApplication
    {
        protected void Application_Start(object sender, EventArgs e)
        {
            RouteTable.Routes.MapHttpRoute(
               name: "DefaultApi",
               routeTemplate: "api/{controller}/{id}",
               defaults: new { id = RouteParameter.Optional }
               );
        }
    }
}

در ادامه فایلی را به نام Index.html (یا در یک View و یا یک فایل aspx دلخواه)، محتوای ذیل را اضافه کنید:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title>Kendo UI: Implemeting the Grid</title>

    <link href="styles/kendo.common.min.css" rel="stylesheet" type="text/css" />
    <link href="styles/kendo.default.min.css" rel="stylesheet" type="text/css" />
    <script src="js/jquery.min.js" type="text/javascript"></script>
    <script src="js/kendo.all.min.js" type="text/javascript"></script>
</head>
<body>

    <div id="report-grid"></div>
    <script type="text/javascript">
        $(function () {
            var productsDataSource = new kendo.data.DataSource({
                transport: {
                    read: {
                        url: "api/products",
                        dataType: "json",
                        contentType: 'application/json; charset=utf-8',
                        type: 'GET'
                    }
                },
                error: function (e) {
                    alert(e.errorThrown.stack);
                },
                pageSize: 5,
                sort: { field: "Id", dir: "desc" }
            });

            $("#report-grid").kendoGrid({
                dataSource: productsDataSource,
                autoBind: true,
                scrollable: false,
                pageable: true,
                sortable: true,
                columns: [
                    { field: "Id", title: "#" },
                    { field: "Name", title: "Product" }
                ]
            });
        });
    </script>
</body>
</html>
- ابتدا فایل‌های اسکریپت و CSS مورد نیاز Kendo UI اضافه شده‌اند.
- گرید صفحه، در محل div ایی با id مساوی report-grid تشکیل خواهد شد.
- سپس DataSource ایی که به آدرس api/products اشاره می‌کند، تعریف شده و در آخر productsDataSource را توسط یک kendoGrid نمایش داده‌ایم.
- نحوه‌ی تعریف productsDataSource، در قسمت استفاده از منابع داده راه دور ابتدای بحث توضیح داده شد. در اینجا فقط دو خاصیت pageSize و sort نیز به آن اضافه شده‌اند. این دو خاصیت بر روی نحوه‌ی نمایش گرید نهایی تاثیر گذار هستند. pageSize تعداد رکورد هر صفحه را مشخص می‌کند و sort نحوه‌ی مرتب سازی را بر اساس فیلد Id و در حالت نزولی قرار می‌دهد.
- در ادامه، ابتدایی‌ترین حالت کار با kendoGrid را ملاحظه می‌کنید.
- تنظیم dataSource و autoBind: true (حالت پیش فرض)، سبب خواهند شد تا به صورت خودکار، اطلاعات JSON از مسیر api/products خوانده شوند.
- سه خاصیت بعدی صفحه بندی و مرتب سازی خودکار ستون‌ها را فعال می‌کنند.
- در آخر هم دو ستون گرید، بر اساس نام‌های خواص کلاس Product تعریف شده‌اند.


سورس کامل این قسمت را از اینجا می‌توانید دریافت کنید:
KendoUI02.zip
 
نظرات مطالب
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 3#
برای برطرف کردن این مسئله هم می‌توانیم همانطور که در ورودی foreColor را دریافت کردیم brush را نیز دریافت کنیم.

public static void DrawPreview(Graphics g, PointF startPoint, PointF endPoint, Color foreColor, byte thickness, bool isFill, Color backgroundColor, ShapeType shapeType)
//--------------------------------[Change to]-------=>
public static void DrawPreview(Graphics g, PointF startPoint, PointF endPoint, Color foreColor, byte thickness, bool isFill, Brush backgroundBrush , ShapeType shapeType)


//---------------------------------------------------
    case ShapeType.Ellipse:
                    if (isFill)
                        g.FillEllipse(new SolidBrush(backgroundColor), x, y, width, height);
                    //else
                    g.DrawEllipse(new Pen(foreColor, thickness), x, y, width, height);
                    break;
 
//--------------------------------[Change to]-------=>
 
               case ShapeType.Ellipse:
                    if (isFill)
                        g.FillEllipse(backgroundBrush, x, y, width, height);
                    //else
                    g.DrawEllipse(new Pen(foreColor, thickness), x, y, width, height);
                    break;

مطالب
نحوه ارتقاء برنامه‌های موجود MVC3 به MVC4
در ادامه، مراحل ارتقاء پروژه‌های قدیمی MVC3 را به ساختار جدید پروژه‌های MVC4 مرور خواهیم کرد.

1) نصب پیشنیاز
الف) نصب VS 2012
و یا
ب) نصب بسته MVC4 مخصوص VS 2010 (این مورد جهت سرورهای وب نیز توصیه می‌شود)

پس از نصب باید به این نکته دقت داشت که پوشه‌های زیر حاوی اسمبلی‌های جدید MVC4 هستند و نیازی نیست الزاما این موارد را از NuGet دریافت و نصب کرد:
C:\Program Files\Microsoft ASP.NET\ASP.NET Web Pages\v2.0\Assemblies
C:\Program Files\Microsoft ASP.NET\ASP.NET MVC 4\Assemblies
پس از نصب پیشنیازها
2) نیاز است نوع پروژه ارتقاء یابد
به پوشه پروژه MVC3 خود مراجعه کرده و تمام فایل‌های csproj و web.config موجود را با یک ادیتور متنی باز کنید (از خود ویژوال استودیو استفاده نکنید، زیرا نیاز است محتوای فایل‌های پروژه نیز دستی ویرایش شوند).
در فایل‌های csproj (یا همان فایل پروژه؛ که vbproj هم می‌تواند باشد) عبارت
{E53F8FEA-EAE0-44A6-8774-FFD645390401}
را جستجو کرده و با
{E3E379DF-F4C6-4180-9B81-6769533ABE47}
جایگزین کنید. به این ترتیب نوع پروژه به MVC4 تبدیل می‌شود.

3) به روز رسانی شماره نگارش‌های قدیمی
سپس تعاریف اسمبلی‌های قدیمی نگارش سه MVC و نگارش یک Razor را یافته (در تمام فایل‌ها، چه فایل‌های پروژه و چه تنظیمات):
System.Web.Mvc, Version=3.0.0.0
System.Web.WebPages, Version=1.0.0.0
System.Web.Helpers, Version=1.0.0.0
System.Web.WebPages.Razor, Version=1.0.0.0
و این‌ها را با نگارش چهار MVC و نگارش دو Razor جایگزین کنید:
System.Web.Mvc, Version=4.0.0.0
System.Web.WebPages, Version=2.0.0.0
System.Web.Helpers, Version=2.0.0.0
System.Web.WebPages.Razor, Version=2.0.0.0
این کارها را با replace in all open documents توسط notepad plus-plus به سادگی می‌توان انجام داد.

4) به روز رسانی مسیرهای قدیمی
به علاوه اگر در پروژه‌های خود از اسمبلی‌های قدیمی به صورت مستقیم استفاده شده:
C:\Program Files\Microsoft ASP.NET\ASP.NET Web Pages\v1.0\Assemblies
C:\Program Files\Microsoft ASP.NET\ASP.NET MVC 3\Assemblies
این‌ها را یافته و به نگارش MVC4 و Razor2 تغییر دهید:
C:\Program Files\Microsoft ASP.NET\ASP.NET Web Pages\v2.0\Assemblies
C:\Program Files\Microsoft ASP.NET\ASP.NET MVC 4\Assemblies

5) به روز رسانی قسمت appSettings فایل‌های کانفیگ
در کلیه فایل‌های web.config برنامه، webpages:Version را یافته و شماره نگارش آن‌را از یک به دو تغییر دهید:
<appSettings>
  <add key="webpages:Version" value="2.0.0.0" />
  <add key="PreserveLoginUrl" value="true" />
</appSettings>
همچنین یک سطر جدید PreserveLoginUrl را نیز مطابق تنظیم فوق اضافه نمائید.

6) رسیدگی به وضعیت اسمبلی‌های شرکت‌های ثالث
ممکن است در این زمان از تعدادی کامپوننت و اسمبلی MVC3 تهیه شده توسط شرکت‌های ثالث نیز استفاده نمائید. برای اینکه این اسمبلی‌ها را وادار نمائید تا از نگارش‌های MVC4 و Razor2 استفاده کنند، نیاز است bindingRedirect‌های زیر را به فایل‌های web.config برنامه اضافه کنید (در فایل کانفیگ ریشه پروژه):
<configuration>
  <!--... elements deleted for clarity ...-->
 
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="System.Web.Helpers" 
             publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="1.0.0.0" newVersion="2.0.0.0"/>
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="System.Web.Mvc" 
             publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="1.0.0.0-3.0.0.0" newVersion="4.0.0.0"/>
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="System.Web.WebPages" 
             publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="1.0.0.0" newVersion="2.0.0.0"/>
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
</configuration>
اکنون فایل solution را در VS.NET گشوده و یکبار گزینه rebuild را انتخاب کنید تا پروژه مجددا بر اساس اسمبلی‌های جدید معرفی شده ساخته شود.

7) استفاده از NuGet برای به روز رسانی بسته‌های نصب شده
یک سری از بسته‌های تشکیل دهنده MVC3 مانند موارد ذیل نیز به روز شده‌اند که لازم است از طریق NuGet دریافت و جایگزین شوند:
Unobtrusive.Ajax.2
Unobtrusive.Validation.2
Web.Optimization.1.0.0
و ....

برای اینکار در solution explorer روی references کلیک راست کرده و گزینه Manage NuGet Packages را انتخاب کنید. در صفحه باز شده گزینه updates/all را انتخاب کرده و مواردی را که لیست می‌کند به روز نمائید (شامل جی کوئری، EF، structureMap و غیره خواهد بود).


8) اضافه کردن یک فضای نام جدید
بسته Web Optimization را از طریق NuGet دریافت کنید (برای یافتن آن bundling را جستجو کنید؛ نام کامل آن Microsoft ASP.NET Web Optimization Framework 1.0.0 است). این مورد به همراه پوشه MVC4 نیست و باید از طریق NuGet دریافت و نصب شود. (البته پروژه‌های جدید MVC4 شامل این مورد هستند)
در فایل وب کانفیگ، فضای نام System.Web.Optimization را نیز اضافه نمائید:
    <pages>
      <namespaces>
        <add namespace="System.Web.Optimization" />
      </namespaces>
    </pages>

پس از ارتقاء
اولین مشکلی که مشاهده شد:
بعد از rebuild به مقدار پارامتر salt که به نحو زیر در MVC3 تعریف شده بود، ایراد خواهد گرفت:
[ValidateAntiForgeryToken(Salt = "data123")]
Salt را در MVC4 منسوخ شده معرفی کرده‌اند: (^)
علت هم این است که salt را اینبار به نحو صحیحی خودشان در پشت صحنه تولید و اعمال می‌کنند. بنابراین این یک مورد را کلا از کدهای خود حذف کنید که نیازی نیست.


مشکل بعدی:
در EF 5 جای یک سری از کلاس‌ها تغییر کرده. مثلا ویژگی‌های ForeignKey، ComplexType و ... به فضای نام System.ComponentModel.DataAnnotations.Schema منتقل شده‌اند. در همین حد تغییر جهت کامپایل مجدد کدها کفایت می‌کند.
همچنین فایل‌های پروژه موجود را باز کرده و EntityFramework, Version=4.1.0.0 را جستجو کنید. نگارش جدید 4.4.0.0 است که باید اصلاح شود (این موارد را بهتر است توسط یک ادیتور معمولی خارج از VS.NET ویرایش کنید).
در زمان نگارش این مطلب EF Mini Profiler با EF 5 سازگار نیست. بنابراین اگر از آن استفاده می‌کنید نیاز است غیرفعالش کنید.


اولین استفاده از امکانات جدید MVC4:
استفاده از امکانات System.Web.Optimization که ذکر گردید، می‌تواند اولین تغییر مفید محسوب شود.
برای اینکه با نحوه کار آن بهتر آشنا شوید، یک پروژه جدید MVC4 را در VS.NET (از نوع basic) آغاز کنید. به صورت خودکار یک پوشه جدید را به نام App_Start به ریشه پروژه اضافه می‌کند. داخل آن فایل مثال BundleConfig قرار دارد. این کلاس در فایل global.asax برنامه نیز ثبت شده‌است. باید دقت داشت در حالت دیباگ (compilation debug=true در وب کانفیگ) تغییر خاصی را ملاحظه نخواهید کرد.
تمام این‌ها خوب؛ اما من به نحو زیر از این امکان جدید استفاده می‌کنم:
using System.Collections.Generic;
using System.IO;
using System.Web;
using System.Web.Optimization;

namespace Common.WebToolkit
{
    /// <summary>
    /// A custom bundle orderer (IBundleOrderer) that will ensure bundles are 
    /// included in the order you register them.
    /// </summary>
    public class AsIsBundleOrderer : IBundleOrderer
    {
        public IEnumerable<FileInfo> OrderFiles(BundleContext context, IEnumerable<FileInfo> files)
        {
            return files;
        }
    }

    public static class BundleConfig
    {
        private static void addBundle(string virtualPath, bool isCss, params string[] files)
        {
            BundleTable.EnableOptimizations = true;

            var existing = BundleTable.Bundles.GetBundleFor(virtualPath);
            if (existing != null)
                return;

            var newBundle = isCss ? new Bundle(virtualPath, new CssMinify()) : new Bundle(virtualPath, new JsMinify());
            newBundle.Orderer = new AsIsBundleOrderer();

            foreach (var file in files)
                newBundle.Include(file);

            BundleTable.Bundles.Add(newBundle);
        }

        public static IHtmlString AddScripts(string virtualPath, params string[] files)
        {
            addBundle(virtualPath, false, files);
            return Scripts.Render(virtualPath);
        }

        public static IHtmlString AddStyles(string virtualPath, params string[] files)
        {
            addBundle(virtualPath, true, files);
            return Styles.Render(virtualPath);
        }

        public static IHtmlString AddScriptUrl(string virtualPath, params string[] files)
        {
            addBundle(virtualPath, false, files);
            return Scripts.Url(virtualPath);
        }

        public static IHtmlString AddStyleUrl(string virtualPath, params string[] files)
        {
            addBundle(virtualPath, true, files);
            return Styles.Url(virtualPath);
        }
    }
}
کلاس BundleConfig فوق را به مجموعه کلاس‌های کمکی خود اضافه کنید.
چند نکته مهم در این کلاس وجود دارد:
الف) توسط AsIsBundleOrderer فایل‌ها به همان ترتیبی که به سیستم اضافه می‌شوند، در حاصل نهایی ظاهر خواهند شد. حالت پیش فرض مرتب سازی، بر اساس حروف الفباء است و ... خصوصا برای اسکریپت‌هایی که ترتیب معرفی آن‌ها مهم است، مساله ساز خواهد بود.
ب)BundleTable.EnableOptimizations سبب می‌شود تا حتی در حالت debug نیز فشرده سازی را مشاهده کنید.
ج) متدهای کمکی تعریف شده این امکان را می‌دهند تا بدون نیاز به کامپایل مجدد پروژه، به سادگی در کدهای Razor بتوانید اسکریپت‌ها را اضافه کنید.

 سپس نحوه جایگزینی تعاریف قبلی موجود در فایل‌های Razor با سیستم جدید، به نحو زیر است:
@using Common.WebToolkit

<link href="@BundleConfig.AddStyleUrl("~/Content/blueprint/print", "~/Content/blueprint/print.css")" rel="stylesheet" type="text/css" media="print"/>

@BundleConfig.AddScripts("~/Scripts/js",
                            "~/Scripts/jquery-1.8.0.min.js",
                            "~/Scripts/jquery.unobtrusive-ajax.min.js",
                            "~/Scripts/jquery.validate.min.js")

@BundleConfig.AddStyles("~/Content/css",
                            "~/Content/Site.css",
                            "~/Content/buttons.css")
پارامتر اول این متدها، سبب تعریف خودکار routing می‌شود. برای مثال اولین تعریف، آدرس خودکار زیر را تولید می‌کند:
http://site/Content/blueprint/print?v=hash
بنابراین تعریف دقیق آن مهم است. خصوصا اگر فایل‌های شما در پوشه‌ها و زیرپوشه‌های متعددی قرار گرفته نمی‌توان تمام آن‌ها را در طی یک مرحله معرفی نمود. هر سطح را باید از طریق یک بار معرفی به سیستم اضافه کرد. مثلا اگر یک زیر پوشه به نام noty دارید (Content/noty)، چون در یک سطح و زیرپوشه مجزا قرار دارد، باید نحوه تعریف آن به صورت زیر باشد:
@BundleConfig.AddStyles("~/Content/noty/css",
                                "~/Content/noty/jquery.noty.css",
                                "~/Content/noty/noty_theme_default.css")
این مورد خصوصا در مسیریابی تصاویر مرتبط با اسکریپت‌ها و شیوه نامه‌ها مؤثر است؛ وگرنه این تصاویر تعریف شده در فایل‌های CSS یافت نخواهند شد (تمام مثال‌های موجود در وب با این مساله مشکل دارند و فرض آن‌ها بر این است که کلیه فایل‌های خود را در یک پوشه، بدون هیچگونه زیرپوشه‌ای تعریف کرده‌اید).
پارامترهای بعدی، محل قرارگیری اسکریپت‌ها و CSSهای برنامه هستند و همانطور که عنوان شد اینبار با خیال راحت می‌توانید ترتیب معرفی خاصی را مدنظر داشته باشید؛ زیرا توسط AsIsBundleOrderer به صورت پیش فرض لحاظ خواهد شد.

 
نظرات مطالب
AngularJS #1
These files are parsed by our docs parser (nodejs script), source can be found here:
https://github.com/angular/angular.js/tree/master/docs/src
It is combined together with docs parsed from the source and result is html, served at docs.angularjs.org
ماخذ
نظرات مطالب
افزودن و اعتبارسنجی خودکار Anti-Forgery Tokens در برنامه‌های Angular مبتنی بر ASP.NET Core
با پیاده سازی same-site cookies و همچنین content security policies ، نیازی به نکات بحث جاری ندارید.
به صورت ساده و خلاصه
- بحث anti-forgery token مربوط به برنامه‌هایی هست که از کوکی استفاده می‌کنند و نه از JWT. تا زمانیکه JWT را به صورت دستی به هدر درخواست اضافه می‌کنید (مانند مثال این سری) و به صورت خودکار از طریق کوکی‌ها ارسال نمی‌شود، اساسا CSRF attacks در مورد آن‌ها بی‌معنا است.
مطالب
بومی سازی منابع در پروژه‌های ASP.NET Core Web API
اگر پروژه‌ی ما فقط از یک Web API تشکیل شده و نیاز است در قسمت‌های مختلف آن، مانند کنترلرها، سرویس‌ها، اعتبارسنج‌ها و غیره از منابع بومی شده استفاده شود، می‌توان از یک راه حل ساده‌ی «SharedResource» استفاده کرد؛ با این مزایا و شرایط:
 - تمام تعاریف بومی سازی مورد نیاز برنامه در یک تک فایل SharedResource.fa.resx قرار می‌گیرند. این فایل نیز در یک اسمبلی مستقل از برنامه‌ی اصلی اضافه می‌شود.
 - با استفاده از تزریق سرویس IStringLocalizer می‌توان به کلیدهای فایل SharedResource.fa.resx در هر قسمتی از برنامه‌ی Web API دسترسی یافت.
 - در این بین اگر کلیدی یافت نشد، خطایی با ذکر دقیق جزئیات منبع جستجو شده، لاگ می‌شود.
 - کلیدهای بومی سازی data annotations نیز قابل دریافت از فایل SharedResource.fa.resx می‌باشند.
 
در ادامه روش پیاده سازی یک چنین امکاناتی را بررسی می‌کنیم.
 
 
قرار دادن فایل منبع اشتراکی در اسمبلی ExternalResources

پس از ایجاد پروژه‌ی ابتدایی Web API به نام Core3xSharedResource.WebApi، یک اسمبلی جدید را برای مثال به نام Core3xSharedResource.ExternalResources تعریف کرده و در داخل آن پوشه‌ی جدید Resources را تعریف می‌کنیم. به این پوشه، فایل منبع جدیدی را به نام SharedResource.fa.resx اضافه می‌کنیم. در کنار آن باید یک کلاس خالی به نام SharedResource.cs نیز وجود داشته باشد.

کار با ین فایل (و یا فایل‌های دیگری مانند SharedResource.en.resx) همانند تمام فایل‌های منبع استاندارد است و نکته‌ی خاصی را به همراه ندارد.


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

پس از ایجاد و تکمیل فایل منبع اشتراکی، برای معرفی آن به برنامه، ابتدا کلاس جدید LocalizationConfig را تعریف کرده و در آن متد جدید AddCustomLocalization را به صورت زیر معرفی می‌کنیم:
    public static class LocalizationConfig
    {
        public static IMvcBuilder AddCustomLocalization(this IMvcBuilder mvcBuilder, IServiceCollection services)
        {
            mvcBuilder.AddDataAnnotationsLocalization(options =>
                    {
                        const string resourcesPath = "Resources";
                        string baseName = $"{resourcesPath}.{nameof(SharedResource)}";
                        var location = new AssemblyName(typeof(SharedResource).GetTypeInfo().Assembly.FullName).Name;

                        options.DataAnnotationLocalizerProvider = (type, factory) =>
                        {
                            // to use `SharedResource.fa.resx` file
                            return factory.Create(baseName, location);
                        };
                    });

            services.AddLocalization();
            services.AddScoped<IStringLocalizer>(provider =>
                            provider.GetRequiredService<IStringLocalizer<SharedResource>>());

            services.AddScoped<ISharedResourceService, SharedResourceService>();
            return mvcBuilder;
        }
    }
- در اینجا در ابتدا توسط متد AddDataAnnotationsLocalization، کار معرفی اسمبلی ثالثی که باید تعاریف بومی سازی را از آن دریافت کرد، صورت گرفته‌است.
- سپس با استفاده از متد AddLocalization، سرویس‌های پایه‌ی بومی سازی ASP.NET Core به برنامه اضافه می‌شوند. برای مثال پس از این تعریف اگر در هر جائی از برنامه سرویس <IStringLocalizer<SharedResource را تزریق کنید، می‌توان به مداخل فایل منبع اشتراکی، دسترسی یافت.
- در ادامه امکان تزریق سرویس غیرجنریک IStringLocalizer را نیز میسر کرده‌ایم که تعاریف خودش را از همان سرویس توکار <IStringLocalizer<SharedResource دریافت می‌کند. مزیت اینکار، فراهم شدن امکانات بومی سازی، برای مثال در کتابخانه‌هایی مانند Fluent Validation است که دقیقا از سرویس غیرجنریک IStringLocalizer برای دریافت منابع استفاده می‌کنند.
- در آخر تعریف یک سرویس سفارشی را نیز مشاهده می‌کنید که در ادامه‌ی بحث تکمیل خواهد شد.

هدف از متد AddCustomLocalization فوق، خلوت کردن فایل startup برنامه است. این متد به صورت زیر مورد استفاده قرار می‌گیرد:
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddHttpContextAccessor();
            services.AddControllers().AddCustomLocalization(services);
        }

پس از آن نیاز است میان‌افزار بومی سازی را نیز فعال کرد. متد UseCustomRequestLocalization زیر، اینکار را انجام می‌دهد:
    public static class LocalizationConfig
    {
        public static IApplicationBuilder UseCustomRequestLocalization(this IApplicationBuilder app)
        {
            var requestLocalizationOptions = new RequestLocalizationOptions
            {
                DefaultRequestCulture = new RequestCulture(new CultureInfo("fa-IR")),
                SupportedCultures = new[]
                {
                    new CultureInfo("en-US"),
                    new CultureInfo("fa-IR")
                },
                SupportedUICultures = new[]
                {
                    new CultureInfo("en-US"),
                    new CultureInfo("fa-IR")
                }
            };
            app.UseRequestLocalization(requestLocalizationOptions);
            return app;
        }
    }
محل قرارگیری متد UseCustomRequestLocalization فوق در فایل آغازین برنامه، باید به صورت زیر باید باشد:
    public class Startup
    {
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseHttpsRedirection();

            app.UseCustomRequestLocalization();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }


تعریف مدل برنامه به همراه ویژگی‌های بومی سازی شده

در اینجا تعریف RegisterModel را مشاهده می‌کنید که ErrorMessage‌های آن هرچند به ظاهر یک رشته‌ی معمولی هستند، اما در عمل از فایل منبع اشتراکی خوانده می‌شوند:
using System.ComponentModel.DataAnnotations;

namespace Core3xSharedResource.Models.Account
{
    public class RegisterModel
    {
        [Required(ErrorMessage = "Please enter an email address")] // -->> from the shared resources
        [EmailAddress(ErrorMessage = "Please enter a valid email address")] // -->> from the shared resources
        public string Email { get; set; }
    }
}

فایل resx ما دارای یک چنین کلیدهایی است:
<?xml version="1.0" encoding="utf-8"?>
<root>
  <data name="&lt;b&gt;Hello&lt;/b&gt;&lt;i&gt; {0}&lt;/i&gt;" xml:space="preserve">
    <value>&lt;b&gt;سلام&lt;/b&gt;&lt;i&gt; {0}&lt;/i&gt;</value>
  </data>
  <data name="About Title" xml:space="preserve">
    <value>درباره</value>
  </data>
  <data name="DNT" xml:space="preserve">
    <value>.NET Tips</value>
  </data>
  <data name="SiteName" xml:space="preserve">
    <value>DNT</value>
  </data>
  <data name="Please enter an email address" xml:space="preserve">
    <value>لطفا ایمیلی را وارد کنید</value>
  </data>
  <data name="Please enter a valid email address" xml:space="preserve">
    <value>لطفا ایمیل معتبری را وارد کنید</value>
  </data>
</root>
یک نکته: در اینجا بهتر است کلیدها را به صورت جملات کامل انگلیسی وارد کرد، تا اگر منبع فارسی معادل آن‌ها یافت نشدند، دقیقا از همان کلید، به عنوان مقدار خروجی سیستم بومی سازی استفاده کند.


آزمایش برنامه

اکنون برنامه‌ی Web API، ‌برای آزمایش آماده‌است. برای مثال در کنترلر زیر، سرویس عمومی IStringLocalizer به سازنده‌ی کلاس تزریق شده‌است و سپس قصد بازگشت مقدار کلید «About Title» را دارد. همچنین خطاهای بومی شده‌ی مدل برنامه را نیز بررسی می‌کنیم:
using Core3xSharedResource.Models.Account;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;

namespace Core3xSharedResource.WebApi.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class NormalIStringLocalizerController : ControllerBase
    {
        private readonly IStringLocalizer _localizer;

        public NormalIStringLocalizerController(IStringLocalizer localizer)
        {
            _localizer = localizer;
        }

        [HttpGet]
        public ActionResult<string> Get()
        {
            var localizedString = _localizer["About Title"];
            if (localizedString.ResourceNotFound)
            {
                return NotFound($"The localization resource with ID:`{localizedString.Name}` not found. SearchedLocation: `{localizedString.SearchedLocation}`.");
            }
            return localizedString.Value;
        }

        [HttpPost]
        public ActionResult<RegisterModel> Post(RegisterModel model)
        {
            return model;
        }
    }
}


حالت get را در تصویر فوق مشاهده می‌کنید. در Web API برای تنظیم زبان مورد استفاده می‌توان از هدری به نام Accept-Language استفاده کرد که برای مثال در اینجا به fa تنظیم شده‌است و نتیجه‌ی آن مراجعه به فایل SharedResource.fa.resx خواهد بود. اگر en-us وارد شود، نیاز خواهد بود تا فایل منبع اشتراکی دیگری را تعریف کنید. البته اگر این هدر تنظیم نشود، با توجه به تنظیمات متد UseCustomRequestLocalization، مقدار پیش‌فرض آن همان fa-IR خواهد بود.

حالت post را نیز در تصویر زیر می‌توان مشاهده کرد:


در اینجا چون ایمیل وارد نشده، هر دو خطای تنظیم شده‌ی در مدل برنامه را دریافت کرده‌ایم و این خطاها نیز فارسی هستند. به این معنا که بومی سازی data annotations نیز به درستی کار می‌کند.


تعریف یک سرویس عمومی برای محصور سازی قابلیت‌های بومی سازی، در برنامه‌های Web API

در ادامه تعریف سرویس SharedResourceService را مشاهده می‌کنید که ثبت آن‌را پیشتر انجام دادیم:
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Http;

namespace Core3xSharedResource.Services
{
    public interface ISharedResourceService
    {
        string this[string index] { get; }

        IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures);
        string GetString(string name, params object[] arguments);
        string GetString(string name);
    }

    public class SharedResourceService : ISharedResourceService
    {
        private readonly IStringLocalizer _sharedLocalizer;
        private readonly ILogger<SharedResourceService> _logger;
        private readonly IHttpContextAccessor _httpContextAccessor;

        public SharedResourceService(
            IStringLocalizer sharedHtmlLocalizer,
            IHttpContextAccessor httpContextAccessor,
            ILogger<SharedResourceService> logger
            )
        {
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
            _sharedLocalizer = sharedHtmlLocalizer ?? throw new ArgumentNullException(nameof(sharedHtmlLocalizer));
            _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
        }

        public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
        {
            return _sharedLocalizer.GetAllStrings(includeParentCultures);
        }

        public string this[string index] => GetString(index);

        public string GetString(string name, params object[] arguments)
        {
            var result = _sharedLocalizer.GetString(name, arguments);
            logError(name, result);
            return result;
        }

        private void logError(string name, LocalizedString result)
        {
            if (result.ResourceNotFound)
            {
                var acceptLanguage = _httpContextAccessor?.HttpContext?.Request?.Headers["Accept-Language"];
                _logger.LogError($"The localization resource with Accept-Language:`{acceptLanguage}` & ID:`{name}` not found. SearchedLocation: `{result.SearchedLocation}`.");
            }
        }

        public string GetString(string name)
        {
            var result = _sharedLocalizer.GetString(name);
            logError(name, result);
            return result;
        }
    }
}
این سرویس نه فقط دسترسی به IStringLocalizer را محصور می‌کند، بلکه در متد logError آن اینبار خطای بسیار مفیدی جهت دیباگ کردن سیستم بومی سازی لاگ خواهد شد. اگر کلیدی یافت نشود، فایلی یافت نشود و یا زبان ارسالی تنظیمی یافت نشود، خطای آن‌را در لاگ‌های برنامه می‌توانید مشاهده کنید که در حالت عادی کار با IStringLocalizer، لاگ نمی‌شوند و همچنین هیچ خطا و یا استثنائی را نیز سبب نمی‌شوند. به همین جهت دیباگ کردن سیستم بومی سازی بدون این لاگ‌ها، تقریبا غیرممکن است. برای مثال مقدار baseNameهایی را که در کدهای این مطلب مشاهده می‌کنید، بر اساس همین لاگ‌ها تشخیص داده شدند و بدون آن‌ها تشکیل این مقادیر غیرممکن بودند.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Core3xSharedResource.zip
مطالب
از سرگیری مجدد، لغو درخواست و سعی مجدد دریافت فایل‌های حجیم توسط HttpClient
پس از آشنایی با «نکات دریافت فایل‌های حجیم توسط HttpClient»، در ادامه می‌توان سه قابلیت مهم از سرگیری مجدد، لغو درخواست و سعی مجدد دریافت فایل‌های حجیم را با HttpClient، همانند برنامه‌های download manager نیز پیاده سازی کرد.


از سرگیری مجدد درخواست ارسالی توسط HttpClient

یک نمونه از سرگیری مجدد درخواست را در مطلب «اضافه کردن قابلیت از سرگیری مجدد (resume) به HttpWebRequest» پیشتر در این سایت مطالعه کرده‌اید. اصول کلی آن نیز در اینجا صادق است. HTTP 1.1 از مفهوم range headers‌، برای دریافت پاسخ‌های جزئی پشتیبانی می‌کند. به این ترتیب در صورت پیاده سازی چنین قابلیتی در برنامه‌ی سمت سرور، می‌توان دریافت بازه‌ای از بایت‌ها را بجای دریافت فایل از ابتدا، از سرور درخواست کرد. به یک چنین قابلیتی Resume و یا از سرگیری مجدد گرفته می‌شود و درحین دریافت فایل‌های حجیم بسیار حائز اهمیت است.
var fileInfo = new FileInfo(outputFilePath);
long resumeOffset = 0;
if (fileInfo.Exists)
{
    resumeOffset = fileInfo.Length;
}
if (resumeOffset > 0)
{
    _client.DefaultRequestHeaders.Range = new RangeHeaderValue(resumeOffset, null);
}
در اینجا نحوه‌ی تنظیم یک RangeHeader را مشاهده می‌کنید. ابتدا نیاز است بررسی کنیم آیا فایل دریافتی از پیش موجود است؟ آیا قسمتی از این درخواست پیشتر دریافت شده و محتوای آن هم اکنون به صورت ذخیره شده وجود دارد؟ اگر بله، درخواست دریافت این فایل را بر اساس اندازه‌ی دریافتی فعلی آن، به سرور ارائه می‌کنیم.

یک نکته: تمام وب سرورها و یا برنامه‌های وب از یک چنین قابلیتی پشتیبانی نمی‌کنند.
روش تشخیص آن نیز به صورت زیر است:
var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
if (response.Headers.AcceptRanges == null && resumeOffset > 0)
{
    // resume not supported, starting over
}
پس از خواندن هدر درخواست، اگر خاصیت AcceptRanges آن نال بود، یعنی قابلیت از سرگیری مجدد را ندارد. در این حالت باید فایل موجود فعلی را حذف و یا از نو (FileMode.CreateNew) بازنویسی کرد (بجای حالت FileMode.Append).


لغو درخواست ارسالی توسط HttpClient

پس از شروع غیرهمزمان client.GetAsync می‌توان متد CancelPendingRequests آن‌را فراخوانی کرد تا کلیه درخواست‌های مرتبط با این client لغو شوند. اما این متد صرفا برای حالت پیش‌فرض client.GetAsync که دریافت هدر + محتوا است کار می‌کند (یعنی حالت HttpCompletionOption.ResponseContentRead). اگر همانند نکات بررسی شده‌ی در مطلب «دریافت فایل‌های حجیم توسط HttpClient» صرفا درخواست خواندن هدر را بدهیم (HttpCompletionOption.ResponseHeadersRead)، چون کنترل ادامه‌ی بحث را خودمان بر عهده گرفته‌ایم، لغو آن نیز به عهده‌ی خودمان است و متد CancelPendingRequests بر روی آن تاثیر نخواهد داشت.
این نکته در مورد تنظیم خاصیت TimeOut نیز صادق است. این خاصیت فقط زمانیکه دریافت کل هدر + محتوا توسط متد GetAsync مدیریت شوند، تاثیر گذار است.
بنابراین درحالتیکه نیاز به کنترل بیشتر است، هرچند فراخوانی متد CancelPendingRequests ضرری ندارد، اما الزاما سبب قطع کل درخواست نمی‌شود و باید این لغو را به صورت ذیل پیاده سازی کرد:
ابتدا یک منبع توکن لغو عملیات را به صورت ذیل ایجاد می‌کنیم:
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
سپس، متد لغو برنامه، تنها کافی است متد Cancel این cts را فراخوانی کند؛ تا عملیات دریافت فایل خاتمه یابد.
پس از این فراخوانی (()cts.Cancel)، نحوه‌ی واکنش به آن به صورت ذیل خواهد بود:
var result = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, _cts.Token);
using(var stream = await result.Content.ReadAsStreamAsync())
{
   byte[] buffer = new byte[80000];
   int bytesRead;
   while((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0 &&
         !_cts.IsCancellationRequested)
   {
      outputStream.Write(buffer, 0, bytesRead);
   }
}
در اینجا از cts.Token به عنوان پارامتر سوم متد GetAsync استفاده شده‌است. همچنین قسمت ثبت اطلاعات دریافتی، در استریم خروجی نیز به صورت یک حلقه درآمده‌است تا بتوان خاصیت IsCancellationRequested این توکن لغو را بررسی کرد و نسبت به آن واکنش نشان داد.


سعی مجدد درخواست ارسالی توسط HttpClient

یک روش پیاده سازی سعی مجدد درخواست شکست خورده، توسط کتابخانه‌ی Polly است. روش دیگر آن نیز به صورت ذیل است:
public async Task DownloadFileAsync(string url, string outputFilePath, int maxRequestAutoRetries)
{
            var exceptions = new List<Exception>();

            do
            {
                --maxRequestAutoRetries;
                try
                {
                    await doDownloadFileAsync(url, outputFilePath);
                }
                catch (TaskCanceledException ex)
                {
                    exceptions.Add(ex);
                }
                catch (HttpRequestException ex)
                {
                    exceptions.Add(ex);
                }
                catch (Exception ex) when (isNetworkError(ex))
                {
                    exceptions.Add(ex);
                }

                // Wait a bit and try again later
               if (exceptions.Any())  await Task.Delay(2000, _cts.Token);
            } while (maxRequestAutoRetries > 0 &&
                     !_cts.IsCancellationRequested);

            var uniqueExceptions = exceptions.Distinct().ToList();
            if (uniqueExceptions.Any())
            {
                if (uniqueExceptions.Count() == 1)
                    throw uniqueExceptions.First();
                throw new AggregateException("Could not process the request.", uniqueExceptions);
            }
}

private static bool isNetworkError(Exception ex)
{
    if (ex is SocketException || ex is WebException)
        return true;
    if (ex.InnerException != null)
        return isNetworkError(ex.InnerException);
    return false;
}
در اینجا متد doDownloadFileAsync، پیاده سازی همان متدی است که در قسمت «لغو درخواست ارسالی توسط HttpClient» در مورد آن بحث شد. این قسمت دریافت فایل را در یک حلقه که حداقل یکبار اجرا می‌شود، قرار می‌دهیم. متد GetAsync استثناءهایی مانند TaskCanceledException (در حین TimeOut و یا فراخوانی متد CancelPendingRequests که البته همانطور که توضیح داده شد، بر روی روش کنترل Response تاثیری ندارند)، HttpRequestException پس از فراخوانی متد response.EnsureSuccessStatusCode (جهت اطمینان حاصل کردن از دریافت پاسخی بدون مشکل از طرف سرور) و یا SocketException و WebException را درصورت بروز مشکلی در شبکه، صادر می‌کند. نیازی به بررسی سایر استثناءها در اینجا نیست.
اگر یکی از این استثناءهای یاد شده رخ‌دادند، اندکی صبر کرده و مجددا درخواست را از ابتدا صادر می‌کنیم.
در پایان این سعی‌های مجدد، اگر استثنایی ثبت شده بود و همچنین عملیات نیز با موفقیت به پایان نرسیده بود، آن‌را به فراخوان صادر می‌کنیم.