مطالب دوره‌ها
تغییر ترتیب آیتم‌های یک لیست به کمک افزونه jquery.sortable در ASP.NET MVC
در این مطلب قصد داریم ترتیب عناصر نمایش داده شده توسط یک لیست را به کمک افزونه بسیار سبک وزن jquery.sortable تغییر داده و نتایج را در سمت سرور مدیریت کنیم. این افزونه بر اساس امکانات کشیدن و رها ساختن HTML5 کار می‌کند و با مرورگرهای IE8 به بعد سازگار است.

مدل‌های برنامه

using System.Collections.Generic;

namespace jQueryMvcSample05.Models
{
    public class Survey
    {
        public int Id { set; get; }
        public string Title { set; get; }

        public virtual ICollection<SurveyItem> SurveyItems { set; get; }
    }
}

namespace jQueryMvcSample05.Models
{
    public class SurveyItem
    {
        public int Id { set; get; }
        public string Title { set; get; }
        public int Order { set; get; }

        //[ForeignKey("SurveyId")]
        public virtual Survey Survey { set; get; }
        public int SurveyId { set; get; }
    }
}
به کمک این ساختار قصد داریم اطلاعات یک سیستم نظر سنجی را نمایش دهیم.
تعدادی نظر سنجی به همراه گزینه‌های آن‌ها تعریف خواهند شد (یک رابطه one-to-many است). سپس توسط افزونه sortable می‌خواهیم ترتیب قرارگیری گزینه‌های آن‌را مشخص کنیم یا تغییر دهیم.


منبع داده فرضی برنامه


using System.Collections.Generic;
using jQueryMvcSample05.Models;

namespace jQueryMvcSample05.DataSource
{
    /// <summary>
    /// یک منبع داده فرضی جهت دموی ساده‌تر برنامه
    /// </summary>
    public static class SurveysDataSource
    {
        private static IList<Survey> _surveysCache;
        static SurveysDataSource()
        {
            _surveysCache = createSurveys();
        }

        public static IList<Survey> SystemSurveys
        {
            get { return _surveysCache; }
        }

        private static IList<Survey> createSurveys()
        {
            var results = new List<Survey>();
            for (int i = 1; i < 6; i++)
            {
                results.Add(new Survey
                {
                    Id = i,
                    Title = "نظر سنجی " + i,
                    SurveyItems = new List<SurveyItem>
                    {
                       new SurveyItem{ Id = 1, SurveyId = i, Title = "گزینه 1", Order = 1 },
                       new SurveyItem{ Id = 2, SurveyId = i, Title = "گزینه 2", Order = 2 },
                       new SurveyItem{ Id = 3, SurveyId = i, Title = "گزینه 3", Order = 3 },
                       new SurveyItem{ Id = 4, SurveyId = i, Title = "گزینه 4", Order = 4 }
                    }
                });
            }
            return results;
        }
    }
}
در اینجا نیز از یک منبع داده فرضی تشکیل شده در حافظه جهت سهولت دموی برنامه استفاده خواهد شد.


کدهای کنترلر برنامه

using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using System.Web.UI;
using jQueryMvcSample05.DataSource;
using jQueryMvcSample05.Security;

namespace jQueryMvcSample05.Controllers
{
    public class HomeController : Controller
    {
        [HttpGet]
        public ActionResult Index()
        {
            var surveysList = SurveysDataSource.SystemSurveys;
            return View(surveysList);
        }

        [HttpPost]
        [AjaxOnly]
        [OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
        public ActionResult SortItems(int? surveyId, string[] items)
        {
            if (items == null || items.Length == 0 || surveyId == null)
                return Content("nok");

            updateSurvey(surveyId, items);

            return Content("ok");
        }

        /// <summary>
        /// این متد جهت آشنایی با پروسه به روز رسانی ترتیب گزینه‌ها در اینجا قرار گرفته است
        /// بدیهی است محل قرارگیری آن باید در لایه سرویس برنامه اصلی باشد
        /// </summary>
        private static void updateSurvey(int? surveyId, string[] items)
        {
            var itemIds = new List<int>();
            foreach (var item in items)
            {
                itemIds.Add(int.Parse(item.Replace("item-row-", string.Empty)));
            }

            var survey = SurveysDataSource.SystemSurveys.FirstOrDefault(x => x.Id == surveyId.Value);
            if (survey == null)
                return;

            int order = 0;
            foreach (var itemId in itemIds)
            {
                order++;
                var surveyItem = survey.SurveyItems.FirstOrDefault(x => x.Id == itemId);
                if (surveyItem == null) continue;
                surveyItem.Order = order;
            }

            //todo: call save changes ....
        }
    }
}

و کدهای View متناظر

@model IList<jQueryMvcSample05.Models.Survey>
@{
    ViewBag.Title = "Index";
    var sortUrl = Url.Action(actionName: "SortItems", controllerName: "Home");
}
<h2>
    نظر سنجی‌ها</h2>
@foreach (var survey in Model)
{
    <fieldset>
        <legend>@survey.Title</legend>
        <div id="sortable-@survey.Id">
            @foreach (var surveyItem in survey.SurveyItems.OrderBy(x => x.Order))
            {            
                <div id="item-row-@surveyItem.Id">
                    <span class="handles">::</span>
                    @surveyItem.Title
                </div>
            }
        </div>
    </fieldset>
}
<div>
    لطفا برای تغییر ترتیب آیتم‌های تعریف شده، از امکان کشیدن و رها کردن تعریف شده بر
    روی آیکون‌های :: در کنار هر آیتم استفاده نمائید.
</div>
@section JavaScript
{
    <script type="text/javascript">
        $(document).ready(function () {
            $('div[id^="sortable"]').sortable({ handle: 'span' }).bind('sortupdate', function (e, ui) {
                var sortableItemId = $(ui.item).parent().attr('id');
                var surveyId = sortableItemId.replace('sortable-', '');
                var items = [];
                $('#' + sortableItemId + ' div').each(function () {
                    items.push($(this).attr('id'));
                });
                //alert(items.join('&'));                
                $.ajax({
                    type: "POST",
                    url: "@sortUrl",
                    data: JSON.stringify({ items: items, surveyId: surveyId }),
                    contentType: "application/json; charset=utf-8",
                    dataType: "json",
                    complete: function (xhr, status) {
                        var data = xhr.responseText;
                        if (xhr.status == 403) {
                            window.location = "/login";
                        } else if (status === 'error' || !data || data == "nok") {
                            alert('خطایی رخ داده است');
                        }
                        else {
                            alert('انجام شد');
                        }
                    }
                });
            });
        });
    </script>
}
توضیحات

در اینجا نیاز بود تا ابتدا کدهای کنترلر و View ارائه شوند، تا بتوان در مورد ارتباطات بین آن‌ها بهتر بحث کرد.
در ابتدای نمایش صفحه Home، رکوردهای نظرسنجی‌ها از منبع داده دریافت شده و به View ارسال می‌شوند. در View برنامه یک حلقه تشکیل گردیده و این موارد رندر خواهند شد.
هر نظر سنجی با یک div بیرونی که با id مساوی sortable شروع می‌شود، آغاز گردیده و گزینه‌های آن نظر سنجی نیز توسط divهایی با id مساوی item-row شروع خواهند گردید. هر کدام از این idها حاوی id رکوردهای متناظر هستند. از این id‌ها در کدهای برنامه جهت یافتن یک نظر سنجی یا یک ردیف مشخص برای به روز رسانی ترتیب آن‌ها استفاده خواهیم کرد.
ادامه کار، به تنظیمات و اعمال افزونه sortable مرتبط می‌شود. توسط تنظیم ذیل به jQuery اعلام خواهیم کرد، هرجایی یک div با id شروع شده با sortable یافتی، افزونه sortable را به آن متصل کن:
 $('div[id^="sortable"]').sortable
در ادامه در ناحیه و  div ایی که عمل کشیدن و رها شدن رخ داده، id این div را بدست آورده و سپس کلیه row-itemهای آن را در آرایه‌ای به نام items قرار می‌دهیم:
 var sortableItemId = $(ui.item).parent().attr('id');
var surveyId = sortableItemId.replace('sortable-', '');
var items = [];
$('#' + sortableItemId + ' div').each(function () {
  items.push($(this).attr('id'));
});
اکنون که به id یک نظر سنجی و همچنین idهای ردیف‌های مرتب شده حاصل دسترسی داریم، آن‌ها را توسط jQuery Ajax به کنترلر برنامه ارسال می‌کنیم:
 data: JSON.stringify({ items: items, surveyId: surveyId })
امضای اکشن متد SortItems نیز دقیقا بر همین مبنا تنظیم شده است:
 public ActionResult SortItems(int? surveyId, string[] items)

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


دریافت کدها و پروژه کامل این قسمت
jQueryMvcSample05.zip
 
نظرات مطالب
بالا بردن سرعت DbContext هنگام ثبت داده های زیاد
ممنون.
- نکته دیگری که به شدت روی سرعت bulk insert حین کار با ORMها (فرقی نمی‌کنه؛ تمامشون) تاثیر داره، استراتژی انتخاب نوع primary key است. زمانیکه کلید اصلی از نوع auto generated توسط بانک اطلاعاتی باشه، ORM بعد از insert سعی می‌کنه این Id رو به مصرف کننده برگردونه (چون برنامه نقشی در تعیین اون نداره). یعنی عملا با یک insert و بلافاصله با یک select مواجه خواهیم بود. البته این مورد هم باید اضافه بشه که ORMها برای فرآیندها و تراکنش‌هایی کوتاه طراحی شدن و این مساله در حالت متداول کار با ORMها اهمیتی نداره و اصلا به چشم نمیاد.
همین مساله سرعت insert رو «فقط» در حالت فراخوانی با تعداد بالا در یک حلقه برای مثال به شدت پایین میاره و اگر مثلا کلید اصلی توسط خود برنامه مدیریت بشه (مثلا از نوع Guid تولید شده در برنامه باشه)، سرعت bulk insert به شدت بالا میره چون به select بعد از insert نیازی نخواهد بود.
- در حالت bulk insert اگر شخص مطمئن هست که اطلاعات ارسالی توسط او اعتبارسنجی شدن، بهتره تنظیم context.Configuration.ValidateOnSaveEnabled = false رو هم انجام بده. این مورد اعتبارسنجی در حین ذخیره سازی رو غیرفعال می‌کنه.
- همچنین شخصی در اینجا در مورد تعداد بار فراخوانی متد SaveChanges در یک حلقه، تحقیقی رو انجام داده که جالب است.
نظرات مطالب
شروع به کار با EF Core 1.0 - قسمت 6 - تعیین نوع‌های داده و ویژگی‌های آن‌ها
یک نکته‌ی تکمیلی: چگونه دقت خواصی از نوع decimal را به صورت سراسری تنظیم کنیم؟

فرض کنید قصد دارید دقت خواصی از نوع decimal را تنظیم کنید تا با اخطار زیر مواجه نشوید:
No type was specified for the decimal column 'Price' on entity type 'Movie'. 
This will cause values to be silently truncated if they do not fit in the default precision and scale.
یک روش رفع آن، قرار دادن ویژگی Column بر روی تک تک خواص از نوع decimal، در تمام موجودیت‌های تعریف شده و تنظیم دقت آن‌ها است:
public class Movie
{
    public int ID { get; set; }

    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }
}
روش دیگر، انجام این تنظیم به صورت سراسری است:
protected override void OnModelCreating(ModelBuilder builder)
{
    base.OnModelCreating(builder);

    foreach (var property in builder.Model.GetEntityTypes()
                                    .SelectMany(t => t.GetProperties())
                                    .Where(p => p.ClrType == typeof(decimal)
                                                || p.ClrType == typeof(decimal?)))
    {
       property.SetColumnType("decimal(18, 6)");
    }
}
که هر دو نوع خاصیت decimal و ?decimal را پوشش می‌دهد.
اشتراک‌ها
آیا برای کلید اصلی یک جدول بانک اطلاعاتی GUID بهتر است یا INT؟

Criteria

GUIDs

Seq. GUIDs

BIGINT

INT

Storage

16 bytes

16 bytes

8 bytes

4 bytes

Insert/Update performance

Slowest

Comparable but the index keys are larger in size. For wider tables, this would be slower than Integer values.

Faster than sequential GUIDs

Fastest

Hotspot contention

Very rare

Yes

Yes

Highest, due to smaller size of RIDs

Fragmentation/Page Splits

High

Minimal

Minimal

Minimal

JOIN Performance/SORT operations

Least performance

(Rank 4 = Least performance)

Better than random GUIDs due lesser fragmentation (Rank: 3)

High performance (Rank: 2)

High

Performance

(Rank: 1)

Logical reads

Rank 4=Highest

Rank 3

Rank 2

Rank 1=Least

Merging data across servers

Beneficial

Beneficial

Difficult

Difficult

Uniqueness

Rare chance of duplicates

Globally unique. Virtually no chance of collisions

Limited by range of BIGINT

Limited by range of INT

آیا برای کلید اصلی یک جدول بانک اطلاعاتی GUID بهتر است یا INT؟
نظرات مطالب
Repository ها روی UnitOfWork ایده خوبی نیستند
این generic repository الان از امکانات async در EF 6 داره استفاده می‌کنه. برای مثال NH چنین توانمندی async ایی رو در حال حاضر نداره. آیا در این حالت Persistence Ignorance تامین شده؟ یعنی راحت میشه زیر ساخت این مخزن رو عوض کرد و سوئیچ کرد به یک ORM دیگه؟ و اگر نخواهیم از async استفاده کنیم، خوب یک ORM داریم که توانمندی‌های جدیدش رو باید ازش صرفنظر کرد. خروجی IQueryable آن که جای خودش. ORMهای مختلف متدهای الحاقی خاص خودشون رو دارند و پیاده سازی یکسانی از LINQ رو ندارند. یعنی اگر با EF کار کردید و متد Include آن توسط این generic repository بخاطر خروجی IQueryable در دسترس بود، معادلی در سایر ORMها نداره (متدهای الحاقی اون‌ها فرق می‌کنه). یا مثلا NH سطح دوم کش رو با متد الحاقی Cacheable پیاده سازی کرده. فرض کنید این رو در generic repository قرار دادیم (یک روکش روی این متد تا به ظاهر مستقیما در دسترس نباشه). خوب، الان فلان ORM دیگه که متد Cacheable رو نداره چکار باید باهاش کرد؟ این برنامه و سیستم به این سادگی‌ها قابل تبدیل به یک ORM دیگه نیست. رسیدن به Persistence Ignorance در دنیای واقعی کار ساده‌ای نیست مگر اینکه از توانمندی‌های خوب ORM انتخاب شده صرفنظر کنیم و به قولی دست و پاشو ببریم تا قد بقیه بشه.
گذشته از این‌ها بحث مدل سازی هم هست. نگاشت‌های کلاس‌ها و خواص اون‌ها به جداول بانک اطلاعاتی در ORMهای مختلف 100 درصد با هم متفاوت هست. حداقل EF و NH روش‌های خاص خودشون رو دارند که انطباقی با هم ندارن. یعنی این Persistence Ignorance محدود نیست به روکش کشیدن روی insert/update/delete. اینجا صحبت از یک سیستم هست که اجزای هماهنگ زیادی داره که باید درنظر گرفته بشه؛ از نگاشت‌ها تا اعتبارسنجی‌های خاص تا قابلیت‌های ویژه و صددرصد اختصاصی. به این می‌گن تا خرخره فرو رفتن!
نظرات مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 18 - کار با ASP.NET Web API
ارتقاء به ASP.NET Core 2.1: بهبود اعتبارسنجی پارامترها

تا پیش از نگارش 2.1، برای اعمال اعتبارسنجی به اطلاعات دریافتی از کاربر باید به صورت زیر عمل کرد:
public class UserModel   
{
   [Required, EmailAddress]
   public string Email { get; set; }
 
   [Required, StringLength(1000)]
   public string Name { get; set; }
}
اطلاعات مدنظر به صورت یک کلاس مدل تعریف شده و سپس ویژگی‌های اعتبارسنجی به خواص این کلاس اضافه می‌شوند.
در این حالت در اکشن متد تعریفی با بررسی ModelState.IsValid می‌توان وضعیت اعتبارسنجی اطلاعات دریافتی از سمت کاربر را مشاهده کرد:
public IActionResult SaveUser(UserModel model)
{
     if(!ModelState.IsValid)
     {

 در نگارش 2.1 الزامی به تعریف این کلاس مدل نیست و ویژگی‌های اعتبارسنجی را به پارامترهای تعریف اکشن متد هم می‌توان اعمال کرد:
public IActionResult SaveUser(
     [Required, EmailAddress] string Email  
     [Required, StringLength(1000)] string Name)
{
    if(!ModelState.IsValid)

یک نکته‌ی تکمیلی: اعمال ویژگی Required به non-nullable value types تاثیری ندارد. به همین جهت ویژگی دیگری به نام BindRequired نیز در اینجا اضافه شده‌است تا برای نمونه در مثال زیر اطمینان حاصل شود که testId از مقادیر route و qty از مقادیر کوئری استرینگ مقدار دهی شده‌اند:
public IActionResult Get([BindRequired, FromRoute] Guid testId, [BindRequired, FromQuery] int qty)   
{
   if(!ModelState.IsValid)

- به این ترتیب می‌توان تعداد ViewModelهای مورد نیاز یک برنامه را به شدت کاهش داد. البته نکته‌ی «بررسی Bad code smell ها: تعداد زیاد پارامترهای ورودی» و «آشنایی با Refactoring - قسمت 7» را هم مدنظر داشته باشید و زیاده‌روی نکنید!
- همچنین اگر ویژگی [ApiController] را نیز به کنترلر جاری اعمال کنید، بررسی ModelState.IsValid نیز به صورت خودکار انجام خواهد شد و نیازی به کدنویسی اضافه‌تری نخواهد داشت.
مطالب
غیرمعتبر کردن توکن و یا کوکی سرقت شده در برنامه‌های مبتنی بر ASP.NET Core
چند روز قبل، یکی از کانال‌های فنی معروف یوتیوب با بیش از 15 میلیون مشترک، هک و پاک شد! که داستان آن‌را در اینجا می‌توانید پیگیری کنید. در این هک، مهاجم در سعی اول، پیشنهاد پشتیبانی مالی از شبکه را داده و در ایمیل دوم، پس از جلب اعتماد اولیه، یک فایل به ظاهر PDF مفاد قرارداد را ارسال کرده که با کلیک بر روی آن، تمام کوکی‌های یوتیوب مالک کانال، سرقت و مورد سوء استفاده قرار گرفته! در یک چنین حالتی، مهم نیست که شما اعتبارسنجی دو مرحله‌ای را فعال کرده‌اید و یا از بهترین روش‌های رمزنگاری برای امن کردن اطلاعات کوکی و یا توکن خود استفاده کرده‌اید، همینقدر که اصل محتوای کوکی و یا توکن شما در اختیار شخص دیگری قرار گیرد، می‌تواند بدون نیاز به لاگین و دانستن کلمه‌ی عبور شما، بجای شما وارد سیستم شده و تغییرات دلخواهی را اعمال کند!
بنابراین سؤال اینجاست که ما (توسعه دهندگان) چگونه می‌توانیم یک چنین حملاتی را مشکل‌تر کنیم؟ در این مطلب روشی را در جهت سعی در غیرمعتبر کردن توکن‌ها و یا کوکی‌های سرقت شده، در برنامه‌های مبتنی بر ASP.NET Core بررسی خواهیم کرد.


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

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

namespace ASPNETCore2JwtAuthentication.Services;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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


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

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

در کل تمام تغییرات مورد نیاز مرتبط را جهت یک برنامه‌ی تولید کننده‌ی JWT در اینجا و برای یک برنامه‌ی مبتنی بر کوکی‌ها در اینجا می‌توانید مشاهده کنید.
مطالب
پیاده سازی حذف منطقی در Entity framework
یکی از روش‌هایی که در اکثر پروژه‌های بزرگ استفاده می‌شود، بحث استفاده از حذف منطقی (soft delete) بجای حذف فیزیکی رکورد می‌باشد (اکثرا در برنامه‌هایی که با بخش مالی (پول) در ارتباط هستند) و از آنجاییکه هیچ برنامه‌ای بدون باگ نمی‌باشد، حذف منطقی بجای حذف فیزیکی پیشنهاد می‌شود. در واقع داشتن و حفظ دیتا، یک امتیاز مثبت می‌باشد؛ به علاوه استرس از دست دادن داده به صورت اتفاقی (سهل انگاری کاربر) را هم نخواهیم داشت. لازم به ذکر است کاربران نهایی استفاده کننده از نرم افزار، خبری از نوع حذف منطقی ندارند.

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

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

برای اینکه بخواهیم حذف منطقی را پیاده سازی نماییم، نیاز داریم به هر رکورد، فیلدی اضافه شود تا از طریق آن مشخص نماییم که آیا رکورد حذف شده است یا خیر. برای این منظور باید فیلدی از نوع boolean به تمام کلاس‌ها (جداول) اضافه شود. می‌توانیم این فیلد را به صورت زیر تعریف کنیم:
  public bool IsDeleted { get; set; }
برای جلوگیری از تکرار کد فوق پیشنهاد میکنم اقدام به ایجاد یک اینترفیس به صورت زیر نمایید:
public interface ISoftDelete
{
    bool IsDeleted { get; set; }
}
و در کلاسی که قصد حذف منطقی را در آن دارید، فقط کافیست از اینترفیس فوق ارث بری نماید:
public class Post :ISoftDelete
{
}


بعد از افزودن فیلد فوق، نیاز داریم تا در تمام کوئری‌ها شرطی را اضافه نماییم تا فقط رکوردهایی را از دیتابیس واکشی کند که حذف نشده‌اند. یعنی فیلد فوق برابر False باشد. در ادامه روش‌هایی برای این هدف بیان خواهند شد.

  روش هایی برای فیلتر رکورد‌های حذف شده
1- افزودن فیلتر زیر در تمامی کوئری‌ها:
where (IsDeleted=false && ...)
در روش فوق نیاز است در تمامی کوئری هایمان شرط فوق را اضافه کنیم. همانطور که حدس زده‌‌اید، در این روش احتمال فراموش شدن شرط فوق وجود دارد و از طرفی یک کد را در همه کوئری‌ها تکرار کرده‌ایم.

2- نوشتن یک متد الحاقی‌:
برای جلوگیری از تکرار شرط فوق می‌توان یک متد الحاقی را به صورت زیر پیاده سازی نمود و در تمامی شرط‌ها، آن را فراخوانی کرد:
public static class EntityFrameworkExtentions
{
    public static ObservableCollection<TEntity> Alive<TEntity>(this DbSet<TEntity> set)
        where TEntity : class, ISoftDelete
    {
        var data = set.Where(e => e.IsDeleted == false);
        return new ObservableCollection<TEntity>(data);
    }
}

3-استفاده از کتابخانه  EntityFramework.DynamicFilte
ابتدا اقدام به نصب بسته آن نمایید:
Install-Package EntityFramework.DynamicFilters
و در کلاس Context خود فیلتر  زیر را قرار دهید :
modelBuilder.Filter("IsDeleted", (ISoftDelete d) => d.IsDeleted, false);


مشکلاتی پیرامون حذف منطقی

- کلاس User و Post را در نظر بگیرد که یک User چندین Post دارد. حال اگر حذف، فیزیکی باشد و کاربر اقدام به حذف User مورد نظر کند، با خطای زیر مواجه می‌شود:
The DELETE statement conflicted with the REFERENCE constraint ....
  اما در حذف منطقی چطور؟ در حذف منطقی چون رکوردی حذف نمی‌شود و فقط فیلد IsDeleted به روز رسانی می‌شود، خطای فوق رخ نخواهد داد و همین مورد باعث بروز مشکل می‌شود؛ چون رکوردی که دارای کلید اصلی بوده حذف شده، ولی رکورد‌های وابسته به آن هنوز داخل سیستم موجود می‌باشند. پس برای این مورد نیاز هست ابتدا تمامی Navigation property‌های رکورد مورد نظر یافت شوند و در صورتیکه مقداری وجود نداشت، رکورد مورد نظر حذف فیزیکی شود. برای این منظور دو راه حل پیشنهاد می‌شود:

1- در سرویس‌های مربوط به کلاس‌هایی که از ISoftDelet ارث بری کرده‌اند، متدی تحت عنوان CanDelete، به صورت زیر تعریف شود:
public bool CanDelete(user model)
{
return !model.posts.Any() &&  ! model.news.Any();
}
در متد Candelete، خصوصیات مورد نیاز کلاس را بررسی کرده و در صورتیکه هیچ رکوردی وجود نداشت، کاربر می‌تواند رکورد مورد نظر را حذف نماید.

2- برای جلوگیری از تکرار قطعه کد فوق، میتوان از روش زیر استفاده کرد:
- یک Attribute سفارشی را به صورت زیر تعریف نمایید:
[AttributeUsage(AttributeTargets.Property)]
public class MustBeEmptyToDeleteAttribute : Attribute { }
- به تمامی خصوصیات مورد نیاز که قصد بررسی آنها را داریم، آن‌را به صورت زیر اضافه میکنیم:
public class User
{
    public int Id { get; set; }
    public bool IsDeleted { get; set; }

    [MustBeEmptyToDelete] public virtual ICollection<Post> Posts { get; set; }
    [MustBeEmptyToDelete] public virtual ICollection<File> Files { get; set; }
    // etc...
}
و در پایان متد الحاقی زیر، برای بررسی رکورد‌های وابسته می‌باشد:
public static class EntityExtensions
{
    public static bool CanDelete(this object entity)
    {
        return entity.GetType().GetProperties()
            .Where(x => x.IsDefined(typeof(MustBeEmptyToDeleteAttribute)))
            .Select(x => x.GetValue(entity))
            .OfType<IEnumerable<object>>()
            .All(x => !x.Any());
    }

همانطور که بیان شد، در حذف منطقی فقط رکورد مورد نظر به روز رسانی می‌شود. برای این منظور می‌توان دو متد را همانند زیر در نظر گرفت و هر کدام که مورد نیاز بود، فراخوانی شود:
 public void MarkAsSoftDeleted<TEntity>(TEntity entity) where TEntity : ISoftDelete
        {
            Entry(entity).State = EntityState.Modified;
           // set IsDelete=true 
          // here you can set other logs like who deleted ,when ,...
      }

 public void MarkAsDeleted<TEntity>(TEntity entity) where TEntity : class
    {
            Entry(entity).State = EntityState.Deleted;
    }

پیشنهادها
- اگر از حذف منطقی استفاده میکنید، امکانی را در سیستم قرار دهید تا در صورت تمایل رکورد‌های حذف منطقی را بتوان حذف کرد (تهیه backup و حذف)، حذف منطقی در دراز مدت حجم دیتابیس را بالا می‌برد.
- تا حد امکان به کاربران استفاده کننده، وجود امکان حذف منطقی را اطلاع ندهید. اطلاع از این امر شاید باعث عدم دقت افراد استفاده کننده شود.
مطالب
API Versioning
فرض کنید امروز یک API را برای استفاده عموم ارائه میدهید. آیا با یک breaking change در منابع شما که باعث تغییر در داده‌های ورودی یا خروجی API شود، باید استفاده کنندگان این API در سیستمی که از آن استفاده کرده‌اند، تغییراتی را اعمال کنند یا خیر؟ جواب خیر می‌باشد؛ اصلی‌ترین استفاده از API Versioning دقیقا برای این منظور است که بدون نگرانی از توسعه‌های بعدی، از ورژن‌های قدیمی API بتوانیم استفاده کنیم. 
در این مقاله با روش‌های مختلف ورژن بندی API آشنا خواهید شد.
سه روش اصلی زیر را میتوان برای این منظور در نظر گرفت:
  1.  URI-based versioning 
  2.  Header-based versioning 
  3.  Media type-based versioning 

پیاده سازی URI-based versioning
حداقل به 3 طریق میتوان این مکانیسم را پیاده کرد:
راه حل اول: اگر از Attribute Routing استفاده نمی‌کنید، با تغییر جزئی در تعاریف مسیریابی خود میتوانید به نتیجه مورد نظر برسید. برای ادامه کار دو ویوومدل و دو کنترلر را که هر کدام مربوط به ورژن خاصی از API ما هستند، پیاده سازی میکنیم:
public class ItemViewModel
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Country { get; set; }
}
public class ItemV2ViewModel : ItemViewModel
{
    public double Price { get; set; }
}
ItemViewModel مربوط به ورژن 1 میباشد.
 public class ItemsController : ApiController   
 {
        [ResponseType(typeof(ItemViewModel))]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country = "Japan" };
            return Ok(viewModel);
        }
    }
 public class ItemsV2Controller : ApiController
    {
        [ResponseType(typeof(ItemV2ViewModel))]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemV2ViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 };
            return Ok(viewModel);
        }
    }
ItemsController مربوط به ورژن 1 میباشد.
اگر قرار باشد از مسیرهای متمرکز استفاده کنیم، کافی است که تغییرات زیر را اعمال کنیم:
config.Routes.MapHttpRoute("ItemsV2", "api/v2/items/{id}", new
{
    controller = "ItemsV2",
    id = RouteParameter.Optional
});
config.Routes.MapHttpRoute("Items", "api/items/{id}", new
{
    controller = "Items",
    id = RouteParameter.Optional
});
config.Routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional }
);
در کد بالا به صراحت برای تک تک کنترلرها مسیریابی متناسب با ورژنی که آن را پشتیبانی میکند، معرفی کرده‌ایم.
نکته: این تنظیمات خاص باید قبل از تنظیمات پیش فرض قرار گیرند.

روش بالا به خوبی کار خواهد کرد ولی آنچنان مطلوب نمیباشد. به همین دلیل یک مسیریابی عمومی و کلی را در نظر میگیریم که شامل ورژن مورد نظر، در قالب یک پارامتر میباشد. در روش جایگزین باید به شکل زیر عمل کنید:
config.Routes.MapHttpRoute(
"defaultVersioned",
"api/v{version}/{controller}/{id}",
new { id = RouteParameter.Optional }, new { version = @"\d+" });

config.Routes.MapHttpRoute(
"DefaultApi",
"api/{controller}/{id}",
new { id = RouteParameter.Optional }
);

با این تنظیمات فعلا به مسیریابی ورژن بندی شده‌ای دست نیافته‌ایم. زیرا فعلا به هیچ طریق به Web API اشاره نکرده‌ایم که به چه صورت از این پارامتر version برای پیدا کردن کنترلر ورژن بندی شده استفاده کند و به همین دلیل این دو مسیریابی نوشته شده در عمل نتیجه یکسانی را خواهند داشت. برای رفع مشکل مطرح شده باید فرآیند پیش فرض انتخاب کنترلر را کمی شخصی سازی کنیم.

IHttpControllerSelector مسئول پیدا کردن کنترلر مربوطه با توجه به درخواست رسیده می‌باشد. شکل زیر مربوط است به مراحل ساخت کنترلر بر اساس درخواست رسیده:

 به جای پیاده سازی مستقیم این اینترفیس، از پیاده سازی کننده پیش فرض موجود (DefaultHttpControllerSelector) اسفتاده کرده و HttpControllerSelector جدید ما از آن ارث بری خواهد کرد.

public class VersionFinder
    {
        private static bool NeedsUriVersioning(HttpRequestMessage request, out string version)
        {
            var routeData = request.GetRouteData();
            if (routeData != null)
            {
                object versionFromRoute;
                if (routeData.Values.TryGetValue(nameof(version), out versionFromRoute))
                {
                    version = versionFromRoute as string;
                    if (!string.IsNullOrWhiteSpace(version))
                    {
                        return true;
                    }
                }
            }
            version = null;
            return false;
        }
        private static int VersionToInt(string versionString)
        {
            int version;
            if (string.IsNullOrEmpty(versionString) || !int.TryParse(versionString, out version))
                return 0;
            return version;
        }
        public static int GetVersionFromRequest(HttpRequestMessage request)
        {
            string version;

            return NeedsUriVersioning(request, out version) ? VersionToInt(version) : 0;
        }
    }

کلاس VersionFinder برای یافتن ورژن رسیده در درخواست جاری  مورد استفاده قرار خواهد گرفت. با استفاده از متد NeedsUriVersioning بررسی صورت می‌گیرد که آیا در این درخواست پارامتری به نام version وجود دارد یا خیر که درصورت موجود بودن، مقدار آن واکشی شده و درون پارامتر out قرار می‌گیرد. در متد GetVersionFromRequest بررسی میشود که اگر خروجی متد NeedsUriVersioning برابر با true باشد، با استفاده از متد VersionToInt مقدار به دست آمده را به int تبدیل کند.

 public class VersionAwareControllerSelector : DefaultHttpControllerSelector
    {
        public VersionAwareControllerSelector(HttpConfiguration configuration) : base(configuration) { }
        public override string GetControllerName(HttpRequestMessage request)
        {
            var controllerName = base.GetControllerName(request);
            var version = VersionFinder.GetVersionFromRequest(request);
                
        return version > 0 ? GetVersionedControllerName(request, controllerName, version) : controllerName;
        }
        private string GetVersionedControllerName(HttpRequestMessage request, string baseControllerName,
        int version)
        {
            var versionControllerName = $"{baseControllerName}v{version}";
            HttpControllerDescriptor descriptor;
            if (GetControllerMapping().TryGetValue(versionControllerName, out descriptor))
            {
                return versionControllerName;
            }

            throw new HttpResponseException(request.CreateErrorResponse(
            HttpStatusCode.NotFound,
                $"No HTTP resource was found that matches the URI {request.RequestUri} and version number {version}"));
        }
    }

متد  GetControllerName وظیفه بازگشت دادن نام کنترلر را برعهده دارد. ما نیز با لغو رفتار پیش فرض این متد و تهیه نام ورژن بندی شده کنترلر و معرفی این پیاده سازی از  IHttpControllerSelector به شکل زیر میتوانیم به Web API بگوییم که به چه صورت از پارامتر version موجود در درخواست استفاده کند. 

config.Services.Replace(typeof(IHttpControllerSelector), new VersionAwareControllerSelector(config));

حال با اجرای برنامه به نتیجه زیر خواهیم رسید: 

راه حل دوم: برای زمانیکه Attribute Routing مورد استفاده شما است میتوان به راحتی با تعریف قالب‌های مناسب مسیریابی، API ورژن بندی شده را ارائه دهید.

[RoutePrefix("api/v1/Items")]
    public class ItemsController : ApiController
    {
        [ResponseType(typeof(ItemViewModel))]
        [Route("{id:int}")]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country =        "Japan" };
            return Ok(viewModel);
        }
    }


 [RoutePrefix("api/V2/Items")]
    public class ItemsV2Controller : ApiController
    {
        [ResponseType(typeof(ItemV2ViewModel))]
        [Route("{id:int}")]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemV2ViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 };
            return Ok(viewModel);
        }
    }

اگر توجه کرده باشید در مثال ما، نام‌های کنترلر‌ها متفاوت از هم میباشند. اگر بجای در نظر گرفتن نام‌های مختلف برای یک کنترلر در ورژن‌های مختلف، آن را با یک نام یکسان درون namespace‌های مختلف احاطه کنیم یا حتی آنها را درون Class Library‌های جدا نگهداری کنیم، به مشکل "یافت شدن چندین کنترلر که با درخواست جاری مطابقت دارند" برخواهیم خورد. برای حل این موضوع به راه حل سوم توجه کنید. 

راه حل سوم: استفاده از یک NamespaceControllerSelector که پیاده سازی دیگری از اینترفیس IHttpControllerSelector میباشد. فرض بر این است که قالب پروژه به شکل زیر می‌باشد:

کار با پیاده سازی اینترفیس IHttpRouteConstraint آغاز میشود:

public class VersionConstraint : IHttpRouteConstraint
{
    public VersionConstraint(string allowedVersion)
    {
        AllowedVersion = allowedVersion.ToLowerInvariant();
    }
    public string AllowedVersion { get; private set; }

    public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName,
    IDictionary<string, object> values, HttpRouteDirection routeDirection)
    {
        object value;
        if (values.TryGetValue(parameterName, out value) && value != null)
        {
            return AllowedVersion.Equals(value.ToString().ToLowerInvariant());
        }
        return false;
    }
}

کلاس بالا در واقع برای اعمال محدودیت خاصی که در ادامه توضیح داده میشود، پیاده سازی شده است.

متد Match آن وظیفه چک کردن برابری مقدار کلید parameterName موجود در درخواست را با مقدار allowedVersion ای که API از آن پشتیبانی میکند، برعهده دارد. با استفاده از این Constraint مشخص کرده‌ایم که دقیقا چه زمانی باید Route نوشته شده انتخاب شود.

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

namespace UriBasedVersioning.Namespace.Controllers.V1
{
    using Models.V1;

RoutePrefix("api/{version:VersionConstraint(v1)}/items")]
public class ItemsController : ApiController
    {
        [ResponseType(typeof(ItemViewModel))]
        [Route("{id}")]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country = "Japan" };
            return Ok(viewModel);
        }
    }
}
namespace UriBasedVersioning.Namespace.Controllers.V2
{
    using Models.V2;


RoutePrefix("api/{version:VersionConstraint(v2)}/items")]
public class ItemsController : ApiController
    {
        [ResponseType(typeof(ItemViewModel))]
        [Route("{id}")]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 };
            return Ok(viewModel);
        }
    }
}

با توجه به کد بالا می‌توان دلیل استفاده از VersionConstraint را هم درک کرد؛ از آنجایی که ما دو Route شبیه به هم داریم، لذا باید مشخص کنیم که در چه شرایطی و کدام یک از این Route‌ها انتخاب شود. خوب، اگر برنامه را اجرا کرده و یکی از API‌های بالا را تست کنید، با خطا مواجه خواهید شد؛ زیرا فعلا این Constraint به سیستم Web API معرفی نشده است. تنظیمات زیر را انجام دهید:

var constraintsResolver = new DefaultInlineConstraintResolver();
            constraintsResolver.ConstraintMap.Add(nameof(VersionConstraint), typeof
            (VersionConstraint));
config.MapHttpAttributeRoutes(constraintsResolver);

مرحله بعدی کار، پیاده سازی IHttpControllerSelector می‌باشد:

  public class NamespaceControllerSelector : IHttpControllerSelector
    {
        private readonly HttpConfiguration _configuration;
        private readonly Lazy<Dictionary<string, HttpControllerDescriptor>> _controllers;
        public NamespaceControllerSelector(HttpConfiguration config)
        {
            _configuration = config;
            _controllers = new Lazy<Dictionary<string,
                HttpControllerDescriptor>>(InitializeControllerDictionary);
        }
        public HttpControllerDescriptor SelectController(HttpRequestMessage request)
        {
            var routeData = request.GetRouteData();
            if (routeData == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }

            var controllerName = GetControllerName(routeData);
            if (controllerName == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
            var version = GetVersion(routeData);
            if (version == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
            var controllerKey = string.Format(CultureInfo.InvariantCulture, "{0}.{1}",
                version, controllerName);
            HttpControllerDescriptor controllerDescriptor;
            if (_controllers.Value.TryGetValue(controllerKey, out controllerDescriptor))
            {
                return controllerDescriptor;
            }
            throw new HttpResponseException(HttpStatusCode.NotFound);
        }
        public IDictionary<string, HttpControllerDescriptor> GetControllerMapping()
        {
            return _controllers.Value;
        }
        private Dictionary<string, HttpControllerDescriptor> InitializeControllerDictionary()
        {
            var dictionary = new Dictionary<string,
                HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase);
            var assembliesResolver = _configuration.Services.GetAssembliesResolver();
            var controllersResolver = _configuration.Services.GetHttpControllerTypeResolver();
            var controllerTypes = controllersResolver.GetControllerTypes(assembliesResolver);
            foreach (var controllerType in controllerTypes)
            {
                var segments = controllerType.Namespace.Split(Type.Delimiter);
                var controllerName =
                    controllerType.Name.Remove(controllerType.Name.Length -
                                               DefaultHttpControllerSelector.ControllerSuffix.Length);
                var controllerKey = string.Format(CultureInfo.InvariantCulture, "{0}.{1}",
                    segments[segments.Length - 1], controllerName);

                if (!dictionary.Keys.Contains(controllerKey))
                {
                    dictionary[controllerKey] = new HttpControllerDescriptor(_configuration,
                        controllerType.Name,
                        controllerType);
                }
            }
            return dictionary;
        }
        private static T GetRouteVariable<T>(IHttpRouteData routeData, string name)
        {
            object result;
            if (routeData.Values.TryGetValue(name, out result))
            {
                return (T)result;
            }
            return default(T);
        }
        private static string GetControllerName(IHttpRouteData routeData)
        {
            var subroute = routeData.GetSubRoutes().FirstOrDefault();
            var dataTokenValue = subroute?.Route.DataTokens.First().Value;
            var controllerName =
                ((HttpActionDescriptor[])dataTokenValue)?.First().ControllerDescriptor.ControllerName.Replace("Controller", string.Empty);
            return controllerName;
        }
        private static string GetVersion(IHttpRouteData routeData)
        {
            var subRouteData = routeData.GetSubRoutes().FirstOrDefault();
            return subRouteData == null ? null : GetRouteVariable<string>(subRouteData, "version");
        }
    }

سورس اصلی کلاس بالا از این آدرس قابل دریافت است. در تکه کد بالا بخشی که مربوط به چک کردن تکراری بودن آدرس میباشد، برای ساده سازی کار حذف شده است. ولی نکته‌ی مربوط به SubRoutes که برای واکشی مقادیر پارامترهای مرتبط با Attribute Routing می‌باشد، اضافه شده است. روال کار به این صورت است که ابتدا RouteData موجود در درخواست را واکشی کرده و با استفاده از متدهای GetControllerName و GetVersion، پارامتر‌های controller و version را جستجو میکنیم. بعد با استفاده از مقادیر به دست آمده، controllerKey را تشکیل داده و درون کنترلر‌های موجود در برنامه به دنبال کنترلر مورد نظر خواهیم گشت.

کارکرد متد InitializeControllerDictionary :

همانطور که میدانید به صورت پیش‌فرض Web API توجهی به فضای نام مرتبط با کنترلر‌ها ندارد. از طرفی برای پیاده سازی اینترفیس IHttpControllerSelector نیاز است متدی با امضای زیر را داشته باشیم:

public IDictionary<string, HttpControllerDescriptor> GetControllerMapping()

لذا در کلاس پیاده سازی شده، خصوصیتی به نام ‎‎_controllers را که از به صورت Lazy و از نوع بازگشتی متد بالا می‌باشد، تعریف کرده‌ایم. متد InitializeControllerDictionary کار آماده سازی داده‌های مورد نیاز خصوصیت ‎‎_controllers می‌باشد. به این صورت که تمام کنترلر‌های موجود در برنامه را واکشی کرده و این بار کلید‌های مربوط به دیکشنری را با استفاده از نام کنترلر و آخرین سگمنت فضای نام آن، تولید و درون دیکشنری مورد نظر ذخیره کرده‌ایم.

حال تنظیمات زیر را اعمال کنید:

 public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            var constraintsResolver = new DefaultInlineConstraintResolver();
            constraintsResolver.ConstraintMap.Add(nameof(VersionConstraint), typeof
            (VersionConstraint));

            config.MapHttpAttributeRoutes(constraintsResolver);

            config.Services.Replace(typeof(IHttpControllerSelector), new NamespaceControllerSelector(config));

        }
    }

این بار برنامه را اجرا کرده و API‌های مورد نظر را تست کنید؛ بله بدون مشکل کار خواهد کرد.

نکته تکمیلی: سورس مذکور در سایت کدپلکس برای حالتی که از Centralized Routes استفاده میکنید آماده شده است. روش مذکور در این مقاله هم  فقط قسمت Duplicate Routes آن را کم دارد که میتوانید اضافه کنید. پیاده سازی دیگری را از این راه حل هم میتوانید داشته باشید.

پیاده سازی Header-based versioning  

در این روش کافی است هدری را برای دریافت ورژن API درخواستی مشخص کرده باشید. برای مثال هدری به نام api-version، که استفاده کننده از این طریق، ورژن درخواستی خود را اعلام میکند. همانطور که قبلا اشاره کردیم، با استفاده از Constraint‌ها دست شما خیلی باز خواهد بود. برای مثال این بار به جای واکشی پارامتر version از RouteData، کد همان قسمت را برای واکشی آن از هدر درخواستی تغییر خواهیم داد:
public class HeaderVersionConstraint : IHttpRouteConstraint
    {
        private const string VersionHeaderName = "api-version";

        public HeaderVersionConstraint(int allowedVersion)
        {
            AllowedVersion = allowedVersion;
        }

        public int AllowedVersion { get; }

        public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName,
            IDictionary<string, object> values,
            HttpRouteDirection routeDirection)
        {
            if (!request.Headers.Contains(VersionHeaderName)) return false;

            var version = request.Headers.GetValues(VersionHeaderName).FirstOrDefault();

            return VersionToInt(version) == AllowedVersion;
        }
        private static int VersionToInt(string versionString)
        {
            int version;
            if (string.IsNullOrEmpty(versionString) || !int.TryParse(versionString, out version))
                return 0;
            return version;
        }
    }
این بار در متد Match، به دنبال هدر خاصی به نام api-version می‌گردیم و مقدار آن را با AllowdVersion مقایسه میکنیم تا مشخص کنیم که آیا این Route نوشته شده برای ادامه کار واکشی کنترلر مورد نظر استفاده شود یا خیر. در ادامه یک RouteFactoryAttribute شخصی سازی شده را به منظور معرفی این محدودیت تعریف شده، در نظر میگیریم:
public sealed class HeaderVersionedRouteAttribute : RouteFactoryAttribute
    {
        public HeaderVersionedRouteAttribute(string template) : base(template)
        {
            Order = -1;
        }

        public int Version { get; set; }

        public override IDictionary<string, object> Constraints => new HttpRouteValueDictionary
        {
            {"", new HeaderVersionConstraint(Version)}
        };
    }

با استفاده از خصوصیت Constraints، توانستیم محدودیت پیاده سازی شده خود را به سیستم مسیریابی معرفی کنیم. توجه داشته باشید که این بار هیچ پارامتری در Uri تحت عنوان version را نداریم و از این جهت به صراحت کلیدی برای آن مشخص نکرده ایم (‎‎‎‎‏‏‎‎{"", new HeaderVersionConstraint(Version)} ‎‎ ).
کنترلر‌های ما به شکل زیر خواهند بود:
    [RoutePrefix("api/items")]
    public class ItemsController : ApiController
    {
        [ResponseType(typeof(ItemViewModel))]
        [HeaderVersionedRoute("{id}", Version = 1)]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country = "Japan" };
            return Ok(viewModel);
        }
    }
  [RoutePrefix("api/items")]
    public class ItemsV2Controller : ApiController
    {
        [ResponseType(typeof(ItemV2ViewModel))]
        [HeaderVersionedRoute("{id}", Version = 2)]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemV2ViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 };
            return Ok(viewModel);
        }
    }

این بار از VersionedRoute برای اعمال مسیر یابی Attribute-based استفاده کرده ایم و به صراحت ورژن هر کدام را با استفاده از خصوصیت Version آن مشخص کرده‌ایم. نتیجه:


پیاده سازی Media type-based versioning
قرار است ورژن API مورد نظر را که استفاده کننده درخواست میدهد، از دل درخواست‌هایی به فرم زیر واکشی کنیم:
GET /api/Items HTTP 1.1
Accept: application/vnd.mediatype.versioning-v1+json
GET /api/Items HTTP 1.1
Accept: application/vnd.mediatype.versioning-v2+json
در این روش دست ما کمی بازتر است چرا که میتوانیم بر اساس فرمت درخواستی نیز تصمیماتی را برای ارائه ورژن خاصی از API  داشته باشیم. روال کار شبیه به روش قبلی با پیاده سازی یک Constraint و یک RouteFactoryAttribute انجام خواهد گرفت.
 public class MediaTypeVersionConstraint : IHttpRouteConstraint
    {
        private const string VersionMediaType = "vnd.mediatype.versioning";

        public MediaTypeVersionConstraint(int allowedVersion)
        {
            AllowedVersion = allowedVersion;
        }

        public int AllowedVersion { get; }

        public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values,
            HttpRouteDirection routeDirection)
        {
            if (!request.Headers.Accept.Any()) return false;

            var acceptHeaderVersion =
                request.Headers.Accept.FirstOrDefault(x => x.MediaType.Contains(VersionMediaType));

            //Accept: application/vnd.mediatype.versioning-v1+json
            if (acceptHeaderVersion == null || !acceptHeaderVersion.MediaType.Contains("-v") ||
                !acceptHeaderVersion.MediaType.Contains("+"))
                return false;

            var version = acceptHeaderVersion.MediaType.Between("-v", "+");
            return VersionToInt(version)==AllowedVersion;
        }
}
در متد Match کلاس بالا به دنبال مدیا تایپ مشخصی هستیم که با هدر Accept برای ما ارسال میشود. برای آشنایی با فرمت ارسال Media Type‌ها میتوانید به اینجا مراجعه کنید. کاری که در اینجا انجام شده‌است، یافتن ورژن ارسالی توسط استفاده کننده می‌باشد که آن را با فرمت خاصی که نشان داده شد و مابین (v-) و (+) قرار داده، ارسال میشود. ادامه کار به مانند روش Header-based میباشد و از مطرح کردن آن در مقاله خودداری میکنیم.
نتیجه به شکل زیر خواهد بود:


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.


مطالب
سازگارسازی کلاس‌های اعتبارسنجی Twitter Bootstrap 3 با فرم‌های ASP.NET MVC
چندی پیش در همین وب‌سایت مطلبی تحت عنوان «اعمال کلاس‌های ویژه اعتبارسنجی Twitter bootstrap به فرم‌های ASP.NET MVC» منتشر شد. این مقاله مرتبط با نسخه دوم فریم‌ورک محبوب Bootstrap بود. قصد داریم به بازنویسی کدهای مرتبط بپردازیم و کلاس‌های مرتبط با نسخه سوم این فریم‌ورک را هم با فرم‌های خودمان سازگار کنیم. مثل مقاله‌ی ذکر شده توضیحات را با یک مثال همراه می‌کنم.

مدل برنامه
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace FormValidationWithBootstrap.Models
{
    [Table("Product")]
    public class ProductModel
    {
        [Key]
        public int Id { get; set; }
        [Required(ErrorMessage = "{0} یک فیلد اجباری است و باید آن را وارد کنید.")]
        [StringLength(50, ErrorMessage = "طول {0} باید کمتر از {1} کاراکتر باشد.")]
        [Display(Name = "نام کالا")]
        public string Name { get; set; }
        [Required(ErrorMessage = "{0} یک فیلد اجباری است و باید آن را وارد کنید.")]
        [Display(Name = "قیمت")]
        [DataType(DataType.Currency)]
        public double Price { get; set; }
        [Required(ErrorMessage = "{0} یک فیلد اجباری است و باید آن را وارد کنید.")]
        [Display(Name = "موجودی")]
        public int Qty { get; set; }
    }
}
قرار هست که جدولی داشته باشیم با نام Product برای ثبت محصولات. مدل برنامه شامل خاصیت‌های مرتبط و همچنین اعتبارسنجی‌های مد نظر ما هست.

کنترلر برنامه
using System.Web.Mvc;
using FormValidationWithBootstrap.Models;

namespace FormValidationWithBootstrap.Controllers
{
    public class ProductController : Controller
    {
        // GET: Product
        public ActionResult Index()
        {
            return View();
        }

        public ActionResult New()
        {
            return View();
        }

        [HttpPost]
        public ActionResult New(ProductModel product)
        {
            if (!ModelState.IsValid)
                return View(product);

            if (product.Name != "پفک")
            {
                ModelState.AddModelError("", "لطفا مشکلات را برطرف کنید!");
                ModelState.AddModelError("Name", "فقط محصولی با نام پفک قابل ثبت است :)");
                return View(product);
            }
            // todo:save...
            return RedirectToAction("Index");
        }
    }
}
در قسمت کنترلر نیز اتفاق خاصی نیفتاده و کارهای پایه فقط انجام شده؛ ضمن اینکه آمدیم برای داشتن خطاهای سفارشی نام محصول را چک کردیم و گفتیم اگر نام محصول چیزی غیر از «پفک» بود، از سمت سرور خطایی را صادر کند و بگوید که فقط پفک قابل ثبت هست.

View برنامه
@model FormValidationWithBootstrap.Models.ProductModel
@{
    ViewBag.Title = "New";
}
<h2>کالای جدید</h2>
@using (Html.BeginForm()) 
{
    @Html.AntiForgeryToken()
    <div>
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "alert alert-danger" })
        <div>
            @Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })
            <div>
                @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })
            </div>
        </div>
        <div>
            @Html.LabelFor(model => model.Price, htmlAttributes: new { @class = "control-label col-md-2" })
            <div>
                @Html.EditorFor(model => model.Price, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Price, "", new { @class = "text-danger" })
            </div>
        </div>
        <div>
            @Html.LabelFor(model => model.Qty, htmlAttributes: new { @class = "control-label col-md-2" })
            <div>
                @Html.EditorFor(model => model.Qty, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Qty, "", new { @class = "text-danger" })
            </div>
        </div>
        <div>
            <div>
                <input type="submit" value="ثبت" />
                <input type="reset" value="ریست" />
                @Html.ActionLink("بازگشت به لیست", "Index", "Product", null, new {@class="btn btn-default"})
            </div>
        </div>
    </div>
}
فایل View برنامه با Scafflod Templateها ساخته شده و چون از Visual Studio 2013 استفاده شده، به‌صورت پیش‌فرض با بوت‌استرپ سازگار هست. تغییری که ایجاد شده تعویض کلاس مربوط به ValidationSummary هست که به alert alert-danger تغییر پیدا کرده و همچین دو دکمه «ریست» و «بازگشت به لیست» هم به کنار دکمه «ثبت» اضافه شده.

در فرم بالا شاهد هستیم که با کلیک بر روی دکمه ثبت تنها خطاهای مرتبط با هر ردیف ظاهر شده‌اند و هیچ تغییر رنگی که حاصل از کلاس‌های مرتبط با Bootstrap باشند حاصل نشده. برای رفع این مشکل کافی‌‌است اسکریپت زیر، به انتهای فایل View برنامه اضافه شود تا پیش‌فرض‌های jQuery Validator را تغییر دهیم و آن‌ها را با بوت‌استرپ سازگار کنیم. همچنین در حالت ارسال فرم به سرور و Postback و نمایش خطاهای سفارشی، قسمت بررسی field-validation-error صورت می‌گیرد و در صورتیکه موردی را پیدا کند، به سطر مرتبط با آن کلاس has-error اضافه خواهد شد. 
@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
    <script>
        // override jquery validate plugin defaults
        $.validator.setDefaults({
            highlight: function (element) {
                $(element).closest('.form-group').addClass('has-error');
            },
            unhighlight: function (element) {
                $(element).closest('.form-group').removeClass('has-error').addClass('has-success');
            },
            errorElement: 'span',
            errorClass: 'help-block',
            errorPlacement: function (error, element) {
                if (element.parent('.input-group').length) {
                    error.insertAfter(element.parent());
                } else {
                    error.insertAfter(element);
                }
            }
        });
        $(function () {
            $('form').each(function () {
                $(this).find('div.form-group').each(function () {
                    if ($(this).find('span.field-validation-error').length > 0) {
                        $(this).addClass('has-error');
                    }
                });
            });
        });
    </script>
}

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

همچنین هنگامیکه کاربر فیلد را به درستی وارد کرد، رنگ فیلد و همچین آن ردیف به سبز تغییر خواهد کرد.

و همچنین در حالت رخ‌داد یک خطای سفارشی پس از postback از سمت سرور به حالت زیر خواهیم رسیذ.

کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید 

FormValidationWithBootstrap.rar