مطالب
استفاده از SQLDom برای آنالیز عبارات T-SQL، قسمت دوم
مدتی قبل مطلبی را در مورد کتابخانه‌ی ویژه SQL Server که یک T-SQL Parser تمام عیار است، در این سایت مطالعه کردید. در این قسمت، همان مطلب را به نحو بهتر و ساده‌تری بازنویسی خواهیم کرد.
مشکلی که در دراز مدت با SQLDom وجود خواهد داشت، مواردی مانند SelectStarExpression و CreateProcedureStatement و امثال آن هستند. این‌ها را از کجا باید تشخیص داد؟ همچنین مراحل بررسی این اجزاء، نسبتا طولانی هستند و نیاز به یک راه حل عمومی‌تر در این زمینه وجود دارد.

راه حلی برای این مشکل در مطلب «XML ‘Visualizer’ for the TransactSql.ScriptDom parse tree» ارائه شده‌است. در اینجا تمام اجزای TSqlFragment توسط Reflection مورد بررسی و استخراج قرار گرفته و نهایتا یک فایل XML از آن حاصل می‌شود.
اگر نکات ذکر شده در این مقاله را تبدیل به یک برنامه با استفاده مجدد کنیم، به چنین شکلی خواهیم رسید:


این برنامه را از اینجا می‌توانید دریافت کنید:
DomToXml.zip

همانطور که در تصویر مشاهده می‌کنید، اینبار به سادگی، SelectStarExpression قابل تشخیص است و تنها کافی است در T-SQL پردازش شده، به دنبال SelectStarExpression‌ها بود. برای اینکار جهت ساده شدن آنالیز می‌توان با ارث بری از کلاس پایه TSqlFragmentVisitor شروع کرد:
using System;
using System.Linq;
using Microsoft.SqlServer.TransactSql.ScriptDom;

namespace DbCop
{
    public class SelectStarExpressionVisitor : TSqlFragmentVisitor
    {
        public override void ExplicitVisit(SelectStarExpression node)
        {
            Console.WriteLine(
                  "`Select *` detected @StartOffset:{0}, Line:{1}, T-SQL: {2}",
                  node.StartOffset,
                  node.StartLine,
                  string.Join(string.Empty, node.ScriptTokenStream.Select(x => x.Text)).Trim());

            base.ExplicitVisit(node);
        }
    }
}
در کلاس پایه TSqlFragmentVisitor به ازای تمام اشیاء شناخته شده‌ی ScriptDom، یک متد ExplicitVisit قابل بازنویسی درنظر گرفته شده‌است. در اینجا برای مثال نمونه‌ی SelectStarExpression آن را بازنویسی کرده‌ایم.
مرحله‌ی بعد، اجرای این کلاس Visitor است:
    public static class GenericVisitor
    {
        public static void Start(string tSql, TSqlFragmentVisitor visitor)
        {
            IList<ParseError> errors;
            TSqlScript sqlFragment;
            using (var reader = new StringReader(tSql))
            {
                var parser = new TSql120Parser(initialQuotedIdentifiers: true);
                sqlFragment = (TSqlScript)parser.Parse(reader, out errors);
            }

            if (errors != null && errors.Any())
            {
                var sb = new StringBuilder();
                foreach (var error in errors)
                    sb.AppendLine(error.Message);

                throw new InvalidOperationException(sb.ToString());
            }
            sqlFragment.Accept(visitor);
        }
    }
در اینجا متد Accept کلاس TSql120Parser، امکان پذیرش یک Visitor را دارد. به این معنا که Parser در حال کار، هر زمانیکه در حال آنالیز قسمتی از T-SQL دریافتی بود، نتیجه را به اطلاع یکی از متدهای کلاس پایه TSqlFragmentVisitor نیز خواهد رساند. بنابراین دیگر نیازی به نوشتن حلقه و بررسی تک تک اجزای خروجی TSql120Parser نیست. اگر نیاز به بررسی SelectStarExpression داریم، فقط کافی است Visitor آن‌را طراحی کنیم.

مثالی از نحوه‌ی استفاده از کلاس GenericVisitor فوق را در اینجا ملاحظه می‌کنید:
 var tsql = @"WITH ctex AS (
SELECT * FROM sys.objects
)
SELECT * FROM ctex";
GenericVisitor.Start(tsql, new SelectStarExpressionVisitor());
مطالب دوره‌ها
معرفی پروژه NotifyPropertyWeaver
پس از معرفی مباحث IL Code Weaving و همچنین ارائه راه حلی در مورد «استفاده از AOP Interceptors برای حذف کدهای تکراری INotifyPropertyChanged در WPF» راه حل مشابهی به نام NotifyPropertyWeaver ارائه شده است که همان کار AOP Interceptors را انجام می‌دهد؛ اما بدون نیاز به تشکیل پروکسی و سربار اضافی. کار نهایی را توسط ویرایش اسمبلی و افزودن کدهای IL لازم انجام می‌دهد؛ البته بدون استفاده از PostSharp. این پروژه از کتابخانه سورس باز پایه‌ای به نام Fody استفاده می‌کند که جهت IL Code weaving طراحی شده است.
اگر به Wiki آن مراجعه نمائید، لیست افزونه‌های قابل توجهی را در مورد آن خواهید یافت که PropertyChanged تنها یکی از آن‌ها است.


پیشنیازها
الف) صفحه پروژه در GitHub
ب) دریافت از طریق نوگت


روش استفاده

پس از نصب بسته نوگت پروژه PropertyChanged.Fody
 PM> Install-Package PropertyChanged.Fody
کلاسی را که باید پس از کامپایل، پیاده سازی‌های خودکار OnPropertyChanged را شامل شود، با ویژگی ImplementPropertyChanged مزین کنید.
using PropertyChanged;

namespace AOP02
{
    [ImplementPropertyChanged]
    public class Person
    {
        public string Id { set; get; }
        public string Name { set; get; }
    }
}
و سپس پروژه را کامپایل نمائید. خروجی کنسول Build در VS.NET :
------ Build started: Project: AOP02, Configuration: Debug x86 ------
  Fody (version 1.13.6.1) Executing
  Finished Fody 287ms.
  AOP02 -> D:\Prog\AOP02\bin\Debug\AOP02.exe
========== Build: 1 succeeded or up-to-date, 0 failed, 0 skipped ==========
اکنون اگر فایل اسمبلی نهایی پروژه را در برنامه ILSpy باز کنیم، چنین پیاده سازی را می‌توان شاهد بود:
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace AOP02
{
    public class Person : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        public string Id
        {
            [System.Runtime.CompilerServices.CompilerGenerated]
            get
            {
                return this.<Id>k__BackingField;
            }
            [System.Runtime.CompilerServices.CompilerGenerated]
            set
            {
              if (string.Equals(this.<Id>k__BackingField, value, System.StringComparison.Ordinal))
              {
                  return;
              }
              this.<Id>k__BackingField = value;
              this.OnPropertyChanged("Id");
            }
        }
        public string Name
        {
            [System.Runtime.CompilerServices.CompilerGenerated]
            get
            {
               return this.<Name>k__BackingField;
            }
            [System.Runtime.CompilerServices.CompilerGenerated]
            set
           {
             if (string.Equals(this.<Name>k__BackingField, value, System.StringComparison.Ordinal))
             {
                return;
             }
             this.<Name>k__BackingField = value;
             this.OnPropertyChanged("Name");
            }
        }
        public virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler propertyChanged = this.PropertyChanged;
            if (propertyChanged != null)
            {
                propertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}
مطالب
ساخت منوهای چند سطحی در ASP.NET MVC
پیش نیاز مطلب جاری مطالب زیر می‌باشند:
1- EF Code First #8
2- مباحث تکمیلی مدل‌های خود ارجاع دهنده در EF Code First
3- نگاهی به اجزای تعاملی Twitter Bootstrap 

هدف از مطلب جاری نحوه نمایش منوی‌های چند سطحی می‌باشد، ابتدا مثال کامل زیر را در نظر بگیرید :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Menu.Models.Entities
{
    public class Category
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int? ParentId { get; set; }
        public virtual Category Parent { get; set; }
        public virtual ICollection<Category> Children { get; set; }
    }
}

public class MyContext : DbContext
{
        public DbSet<Category> Category { get; set; }
 
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            // Self Referencing Entity
            modelBuilder.Entity<Category>()
                        .HasOptional(x => x.Parent)
                        .WithMany(x => x.Children)
                        .HasForeignKey(x => x.ParentId)
                        .WillCascadeOnDelete(false);
 
            base.OnModelCreating(modelBuilder);
        }
}

همانطور که ملاحظه می‌کنید، مدل ما شامل مشخصات گروه محصولات می‌باشد که به صورت خود ارجاع دهنده (خاصیت Parent به همین کلاس اشاره میکند) تعریف شده است. در مورد خواص مدل‌های خود ارجاع دهنده، مطالبی را در سایت مطالعه کردید (خواص مربوط در مطالب گفته شده دقیقاً به همان صورت می‌باشد و نیازی به توضیح اضافه‌تری نیست).
هدف از این بحث، نحوه نمایش گروه محصولات داخل منو به صورت چند سطحی می‌باشد، جهت نمایش می‌بایست از تکنیک recursive function استفاده کنید، ابتدا در نظر داشته باشید که ساختار منوی تشکیل شده می‌بایست بدین صورت باشد :
 

این حالت می‌تواند تا n سطح پیش برود، حال نحوه نمایش در View مربوطه باید به صورت زیر باشد :

@using Menu.Helper
@model IEnumerable<.Models.Entities.Category>
@ShowTree(Model)
 
@helper ShowTree(IEnumerable<Menu.Models.Entities.Category> categories)
{
    foreach (var item in categories)
    {
    <li class="@(item.Children.Any() ? "dropdown-submenu" : "")">
 
        @Html.ActionLink(item.Name, actionName: "Category", controllerName: "Product", routeValues: new { Id = item.Id, productName = item.Name.ToSeoUrl() }, htmlAttributes: null)
        @if (item.Children.Any())
        {
            <ul>
                @ShowTree(item.Children)
            </ul>
                }
    </li>
 
        }
}
توجه داشته باشید که رندر نهایی توسط Bootstrap انجام شده است. ساختار منو همانطور که ملاحظه می‌کنید با استفاده از کلاس‌های drop-down که از کلاس‌های پیش فرض بوت استرپ می‌باشد تشکیل شده است همچنین کلاس dropdown-submenu که از نسخه 2 به بعد بوت استرپ موجود می‌باشد، استفاده شده است.

یک نکته :
در خط 9 این مورد را که آیا آیتم جاری فرزندی دارد چک کرده ایم اگر داشته باشد کلاس dropdown-submenu  را به li جاری اضافه میکند.
مطالب
ذخیره تنظیمات متغیر مربوط به یک وب اپلیکیشن ASP.NET MVC با استفاده از EF
طی این  مقاله، نحوه‌ی ذخیره سازی تنظیمات متغیر و پویای یک برنامه را به صورت Strongly Typed ارائه خواهم داد. برای این منظور، یک API را که از Lazy Loading ، Cache ، Reflection و Entity Framework بهره میگیرد، خواهیم ساخت.
برنامه‌ی هدف ما که از این API استفاده می‌کند، یک اپلیکیشن Asp.net MVC است. قبل از شروع به ساخت API مورد نظر، یک دید کلی در مورد آنچه که قرار است در نهایت توسعه یابد، در زیر مشاهده میکنید:
public SettingsController(ISettings settings)
{
  // example of saving 
  _settings.General.SiteName = "دات نت تیپس";
  _settings.Seo.HomeMetaTitle = ".Net Tips";
  _settings.Seo.HomeMetaKeywords = "َAsp.net MVC,Entity Framework,Reflection";
  _settings.Seo.HomeMetaDescription = "ذخیره تنظیمات برنامه";
  _settings.Save();
}

همانطور که در کدهای بالا مشاهده میکنید، شی setting_ ما دارای دو پراپرتی فقط خواندنی بنام‌های General و Seo است که شامل  تنظیمات مورد نظر ما هستند و این دو کلاس از کلاس پایه‌ی SettingBase ارث بری کرده‌اند. دو دلیل برای انجام این کار وجود دارد:
  1. تنظیمات به صورت گروه بندی شده در کنار  هم قرار گرفته‌اند و یافتن تنظیمات برای زمانی که نیاز به دسترسی  به آنها داریم، راحت‌تر و ساده‌تر خواهد بود. 
  2. به این شکل تنظیمات قابل دسترس در یک گروه، از دیتابیس بازیابی خواهند شد.

اصلا چرا باید این تنظیمات را در دیتابیس ذخیره کنیم؟ 

شاید فکر کنید چرا باید تنظیمات را در دیتابیس ذخیره کنیم در حالی که فایل web.config در درسترس است و می‌توان توسط کلاس ConfigurationManager به اطلاعات آن دسترسی داشت.
جواب: دلیل این است که با تغییر فایل web.config، برنامه‌ی وب شما ری استارت خواهد شد (چه زمان‌هایی یک برنامه Asp.net ری استارت میشود).
برای جلوگیری از این مساله، راه حل مناسب برای ذخیره سازی اطلاعاتی که نیاز به تغییر در زمان اجرا دارند، استفاده از از دیتابیس می‌باشد. در این مقاله از Entity Framework و پایگاه داده Sql Sever استفاده می‌کنم.

مراحل ساخت Setting API مورد نظر به شرح زیر است:
  1. ساخت یک Asp.net Web Application 
  2. ساخت مدل Setting و افزودن آن به کانتکست Entity Framework 
  3. ساخت کلاس SettingBase برای بازیابی و ذخیره سازی تنظیمات با رفلکشن
  4. ساخت کلاس GenralSettins و SeoSettings که از کلاس SettingBase ارث بری کرده‌اند.
  5. ساخت کلاس Settings به منظور مدیریت تمام انواع تنظیمات 

یک برنامه‌ی Asp.Net Web Application را از نوع MVC ایجاد کنید. تا اینجا مرحله‌ی اول ما به پایان رسید؛ چرا که ویژوال استودیو کار‌های مورد نیاز ما را انجام خواهد داد.
 لازم است مدل خود را به ApplicationDbContext موجود در فایل IdentityModels.cs معرفی کنیم. به شکل زیر:
namespace DynamicSettingAPI.Models
{
    public interface IUnitOfWork
    {
        DbSet<Setting> Settings { get; set; }
        int SaveChanges();
    }
} 

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>,IUnitOfWork
    {
        public DbSet<Setting> Settings { get; set; }
        public ApplicationDbContext()
            : base("DefaultConnection", throwIfV1Schema: false)
        {
        }

        public static ApplicationDbContext Create()
        {
            return new ApplicationDbContext();
        }
    }


namespace DynamicSettingAPI.Models
{
    public class Setting
    {
        public string Name { get; set; }
        public string Type { get; set; }
        public string Value { get; set; }
    }
}
مدل تنظیمات ما خیلی ساده است و دارای سه پراپرتی به نام‌های Name ، Type ، Value هست که به ترتیب برای دریافت مقدار تنظیمات، نام کلاسی که از کلاس SettingBase ارث برده و نام تنظیمی که لازم داریم ذخیره کنیم، در نظر گرفته شده‌اند. 
لازم است تا متد OnModelCreating مربوط به ApplicationDbContext را نیز تحریف کنیم تا کانفیگ مربوط به مدل خود را نیز اعمال نمائیم.
 protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Setting>()
                    .HasKey(x => new { x.Name, x.Type });

            modelBuilder.Entity<Setting>()
                        .Property(x => x.Value)
                        .IsOptional();

            base.OnModelCreating(modelBuilder);
        }
ساختاری به شکل زیر مد نظر ماست:

  کلاس SettingBase ما همچین ساختاری را خواهد داشت:
namespace DynamicSettingAPI.Service
{
    public abstract class SettingsBase
    {
        //1
        private readonly string _name;
        private readonly PropertyInfo[] _properties;

        protected SettingsBase()
        {
            //2
            var type = GetType();
            _name = type.Name;
            _properties = type.GetProperties();
        }

        public virtual void Load(IUnitOfWork unitOfWork)
        {
            //3 get setting for this type name
            var settings = unitOfWork.Settings.Where(w => w.Type == _name).ToList();

            foreach (var propertyInfo in _properties)
            {
                //get the setting from setting list
                var setting = settings.SingleOrDefault(s => s.Name == propertyInfo.Name);
                if (setting != null)
                {
                    //4 set 
                    propertyInfo.SetValue(this, Convert.ChangeType(setting.Value, propertyInfo.PropertyType));
                }
            }
        }
        public virtual void Save(IUnitOfWork unitOfWork)
        {
            //5 get all setting for this type name
            var settings = unitOfWork.Settings.Where(w => w.Type == _name).ToList();

            foreach (var propertyInfo in _properties)
            {
                var propertyValue = propertyInfo.GetValue(this, null);
                var value = (propertyValue == null) ? null : propertyValue.ToString();

                var setting = settings.SingleOrDefault(s => s.Name == propertyInfo.Name);
                if (setting != null)
                {
                    // 6 update existing value
                    setting.Value = value;
                }
                else
                {
                    // 7 create new setting
                    var newSetting = new Setting()
                    {
                        Name = propertyInfo.Name,
                        Type = _name,
                        Value = value,
                    };
                    unitOfWork.Settings.Add(newSetting);
                }
            }
        }
    }
}
این کلاس قرار است توسط کلاس‌های تنظیمات ما به ارث برده شود و در واقع کارهای مربوط به رفلکشن را در این کلاس کپسوله کرده‌ایم. همانطور که مشخص است ما دو فیلد را به نام‌های name_ و properties_ به صورت فقط خواندنی در نظر گرفته ایم که نام کلاس مورد نظر ما که از این کلاس به ارث خواهد برد، به همراه پراپرتی‌های آن، در این ظرف‌ها قرار خواهند گرفت.
متد Load وظیفه‌ی واکشی تمام تنظیمات مربوط به Type و ست کردن مقادیر به دست آمده را به خصوصیات کلاس ما، برعهده دارد. کد زیر مقدار دریافتی از دیتابیس را به نوع داده پراپرتی مورد نظر تبدیل کرده و نتیجه را به عنوان Value پراپرتی ست میکند. 
propertyInfo.SetValue(this, Convert.ChangeType(setting.Value, propertyInfo.PropertyType));
متد Save نیز وظیفه‌ی ذخیره سازی مقادیر موجود در خصوصیات کلاس تنظیماتی را که از کلاس SettingBase ما به ارث برده است، به عهده دارد. 
این متد دیتا‌های موجود دردیتابیس را که متعلق به کلاس ارث برده مورد نظر ما هستند، واکشی میکند و در یک حلقه، اگر خصوصیتی در دیتابیس موجود بود، آن را ویرایش کرده وگرنه یک رکورد جدید را ثبت میکند.

  کلاس‌های تنظیمات شخصی سازی شده خود را به شکل زیر تعریف میکنیم :
  public class GeneralSettings : SettingsBase
    {
        public string SiteName { get; set; }
        public string AdminEmail { get; set; }
        public bool RegisterUsersEnabled { get; set; }
    }

 public class GeneralSettings : SettingsBase
    {
        public string SiteName { get; set; }
        public string AdminEmail { get; set; }
    }
نیازی به توضیح ندارد.
برای اینکه تنظیمات را به صورت یکجا داشته باشیم و Abstraction ای را برای استفاده از این API ارائه دهیم، یک اینترفیس و یک کلاس که اینترفیس مذکور را پیاده کرده است در نظر میگیریم: 
public interface ISettings
{
    GeneralSettings General { get; }
    SeoSettings Seo { get; }
    void Save();
}

public class Settings : ISettings
{
    // 1
    private readonly Lazy<GeneralSettings> _generalSettings;
    // 2
    public GeneralSettings General { get { return _generalSettings.Value; } }

    private readonly Lazy<SeoSettings> _seoSettings;
    public SeoSettings Seo { get { return _seoSettings.Value; } }

    private readonly IUnitOfWork _unitOfWork;
    public Settings(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
        // 3
        _generalSettings = new Lazy<GeneralSettings>(CreateSettings<GeneralSettings>);
        _seoSettings = new Lazy<SeoSettings>(CreateSettings<SeoSettings>);
    }

    public void Save()
    {
        // only save changes to settings that have been loaded
        if (_generalSettings.IsValueCreated)
            _generalSettings.Value.Save(_unitOfWork);

        if (_seoSettings.IsValueCreated)
            _seoSettings.Value.Save(_unitOfWork);

        _unitOfWork.SaveChanges();
    }
    // 4
    private T CreateSettings<T>() where T : SettingsBase, new()
    {
        var settings = new T();
        settings.Load(_unitOfWork);
        return settings;
    }
}
این اینترفیس مشخص می‌کند که ما به چه نوع تنظیماتی، دسترسی داریم و متد Save آن برای آپدیت کردن تنظیمات، در نظر گرفته شده است. هر کلاسی که از کلاس SettingBase ارث بری کرده را به صورت فیلد فقط خواندنی و با استفاده از کلاس Lazy درون آن ذکر میکنیم و به این صورت کلاس تنظیمات ما زمانی ساخته خواهد شد که برای اولین بار به آن دسترسی داشته باشیم.
متد CreateSetting وظیفه‌ی لود دیتا را از دیتابیس، بر عهده دارد که برای این منظور، متد لود Type مورد نظر را فراخوانی میکند. این متد وقتی به کلاس تنظیمات مورد نظر برای اولین بار دسترسی پیدا کنیم، فراخوانی خواهد شد.

 حتما امکان این وجود دارد که شما از امکان Caching هم بهره ببرید برای مثال همچین متد و سازنده‌ای را در کلاس Settings در نظر بگیرید:
private readonly ICache _cache;
public Settings(IUnitOfWork unitOfWork, ICache cache)
{
    // ARGUMENT CHECKING SKIPPED FOR BREVITY
    _unitOfWork = unitOfWork;
    _cache = cache;
    _generalSettings = new Lazy<GeneralSettings>(CreateSettingsWithCache<GeneralSettings>);
    _seoSettings = new Lazy<SeoSettings>(CreateSettingsWithCache<SeoSettings>);
}

private T CreateSettingsWithCache<T>() where T : SettingsBase, new()
{
    // this is where you would implement loading from ICache
    throw new NotImplementedException();
}
در آخر هم به شکل زیر میتوان (به عنوان دمو فقط ) از این API استفاده کرد.
   public ActionResult Index()
        {
            using (var uow = new ApplicationDbContext())
            {
                var _settings = new Settings(uow);
                _settings.General.SiteName = "دات نت تیپس";
                _settings.General.AdminEmail = "admin@gmail.com";
                _settings.General.RegisterUsersEnabled = true;
                _settings.Seo.HomeMetaTitle = ".Net Tips";
                _settings.Seo.MetaKeywords = "Asp.net MVC,Entity Framework,Reflection";
                _settings.Seo.HomeMetaDescription = "ذخیره تنظیمات برنامه";

                var settings2 = new Settings(uow);
                var output = string.Format("SiteName: {0} HomeMetaDescription: {1}  MetaKeywords:  {2}  MetaTitle:  {3}  RegisterEnable:  {4}",
                    settings2.General.SiteName,
                    settings2.Seo.HomeMetaDescription,
                    settings2.Seo.MetaKeywords,
                    settings2.Seo.HomeMetaTitle,
                    settings2.General.RegisterUsersEnabled.ToString()
                    );
                return Content(output);
            }

        }

خروجی :

نکته: در پروژه ای که جدیدا در سایت ارائه داده‌ام و در حال تکمیل آن هستم، از بهبود یافته‌ی این مقاله استفاده می‌شود. حتی برای اسلاید شو‌های سایت هم میشود از این روش استفاده کرد و از فرمت json بهره برد برای این منظور. حتما در پروژه‌ی مذکور همچین امکانی را هم در نظر خواهم گرفتم.
پیشنها میکنم سورس SmartStore را بررسی کنید. آن هم به شکل مشابهی ولی پیشرفته‌تر از این مقاله، همچین امکانی را دارد.
نظرات مطالب
EF Code First #9
من یک سری مدل به شکل زیر دارم:
    public class Context : DbContext
    {
        public Context():base("Server=.;Database=EfTest;Trusted_Connection=True;Integrated Security=true;")
        {
            
        }

        public DbSet<Student> Students { get; set; }
        public DbSet<Teacher> Teachers { get; set; }

    }


    public class Person
    {
        public int Id { get; set; }
        public DateTime InsertTime { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }

    }


    public class Student:Person
    {
        public string Student1 { get; set; }
    }

    public class Teacher:Person
    {
        public string Teacher1 { get; set; }

    }
بدون هیچ تنظیماتی، یعنی طبق تعاریف بالا الان باید حالت به شکل TPH باشد ولی در ساخت نهایی دو جدول دانش آموز و معلم تشکیل میشه. من در این مدتی که ef کار کردم روی این حالت بودم و راضی هستم ولی برای یکی از دوستان EF سعی داره جدولی به اسم People رو از روی Person تشکیل بده.
مطالب
مستند سازی ASP.NET Core 2x API توسط OpenAPI Swagger - قسمت سوم - تکمیل مستندات یک API با کامنت‌ها
در قسمت قبل موفق شدیم بر اساس OpenAPI specification endpoint تنظیم شده، رابط کاربری خودکاری را توسط ابزار Swagger-UI تولید کنیم. در ادامه می‌خواهیم این مستندات تولید شده را غنی‌تر کرده و کیفیت آن‌را بهبود دهیم.


استفاده از XML Comments برای بهبود کیفیت مستندات API

نوشتن توضیحات XML ای برای متدها و پارامترها در پروژه‌های دات‌نتی، روشی استاندارد و شناخته شده‌است. برای نمونه در AuthorsController، می‌خواهیم توضیحاتی را به اکشن متد GetAuthor آن اضافه کنیم:
/// <summary>
/// Get an author by his/her id
/// </summary>
/// <param name="authorId">The id of the author you want to get</param>
/// <returns>An ActionResult of type Author</returns>
[HttpGet("{authorId}")]
public async Task<ActionResult<Author>> GetAuthor(Guid authorId)
در این حالت اگر برنامه را اجرا کنیم، این توضیحات XMLای هیچ تاثیری را بر روی OpenAPI specification تولیدی و در نهایت Swagger-UI تولید شده‌ی بر اساس آن، نخواهد داشت. برای رفع این مشکل، باید به فایل OpenAPISwaggerDoc.Web.csproj مراجعه نمود و تولید فایل XML متناظر با این توضیحات را فعال کرد:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>netcoreapp2.2</TargetFramework>
    <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
  </PropertyGroup>
پس از تنظیم خاصیت GenerateDocumentationFile به true، با هر بار Build برنامه، فایل xml ای مطابق نام اسمبلی برنامه، در پوشه‌ی bin آن تشکیل خواهد شد؛ مانند فایل bin\Debug\netcoreapp2.2\OpenAPISwaggerDoc.Web.xml در این مثال.
اکنون نیاز است وجود این فایل را به تنظیمات SwaggerDoc در کلاس Startup برنامه، اعلام کنیم:
namespace OpenAPISwaggerDoc.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSwaggerGen(setupAction =>
            {
                setupAction.SwaggerDoc(
                    // ... 
                   );

                var xmlCommentsFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
                var xmlCommentsFullPath = Path.Combine(AppContext.BaseDirectory, xmlCommentsFile);
                setupAction.IncludeXmlComments(xmlCommentsFullPath);
            });
        }
در متد IncludeXmlComments، بجای ذکر صریح نام و مسیر فایل OpenAPISwaggerDoc.Web.xml، بر اساس نام اسمبلی جاری، نام فایل XML مستندات تعیین و مقدار دهی شده‌است.
پس از این تنظیمات اگر برنامه را اجرا کنیم، در Swagger-UI حاصل، این تغییرات قابل مشاهده هستند:




افزودن توضیحات به Response

تا اینجا توضیحات پارامترها و متدها را افزودیم؛ اما response از نوع 200 آن هنوز فاقد توضیحات است:


علت را نیز در تصویر فوق مشاهده می‌کنید. قسمت responses در OpenAPI specification، اطلاعات خودش را از اسکیمای مدل‌های مرتبط دریافت می‌کند. بنابراین نیاز است کلاس DTO متناظر با Author را به نحو ذیل تکمیل کنیم:
using System;

namespace OpenAPISwaggerDoc.Models
{
    /// <summary>
    /// An author with Id, FirstName and LastName fields
    /// </summary>
    public class Author
    {
        /// <summary>
        /// The id of the author
        /// </summary>
        public Guid Id { get; set; }

        /// <summary>
        /// The first name of the author
        /// </summary>
        public string FirstName { get; set; }

        /// <summary>
        /// The last name of the author
        /// </summary>
        public string LastName { get; set; }
    }
}
مشکل! در این حالت اگر برنامه را اجرا کنیم، خروجی این توضیحات را در قسمت schemas مشاهده نخواهیم کرد. علت اینجا است که چون اسمبلی OpenAPISwaggerDoc.Models با اسمبلی OpenAPISwaggerDoc.Web یکی نیست و آن‌را از پروژه‌ی اصلی خارج کرده‌ایم، به همین جهت نیاز است ابتدا به فایل OpenAPISwaggerDoc.Models.csproj مراجعه و GenerateDocumentationFile آن‌را فعال کرد:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
  </PropertyGroup>
</Project>
سپس باید فایل xml مستندات آن‌را به صورت مجزایی به تنظیمات ابتدایی برنامه معرفی نمود:
namespace OpenAPISwaggerDoc.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSwaggerGen(setupAction =>
            {
                setupAction.SwaggerDoc(
  // ...
                   );
                var xmlFiles = Directory.GetFiles(AppContext.BaseDirectory, "*.xml", SearchOption.TopDirectoryOnly).ToList();
                xmlFiles.ForEach(xmlFile => setupAction.IncludeXmlComments(xmlFile));
            });
        }
با توجه به اینکه تمام فایل‌های xml تولید شده در آخر به پوشه‌ی bin\Debug\netcoreapp2.2 کپی می‌شوند، فقط کافی است حلقه‌ای را تشکیل داده و تمام آن‌ها را یکی یکی توسط متد IncludeXmlComments به تنظیمات AddSwaggerGen اضافه کرد.

در این حالت اگر مجددا برنامه را اجرا کنیم، خروجی ذیل را در قسمت schemas مشاهده خواهیم کرد:



بهبود مستندات به کمک Data Annotations

اگر به اکشن متد UpdateAuthor در کنترلر نویسندگان دقت کنیم، چنین امضایی را دارد:
[HttpPut("{authorId}")]
public async Task<ActionResult<Author>> UpdateAuthor(Guid authorId, AuthorForUpdate authorForUpdate)
جائیکه موجودیت Author را در پروژه‌ی OpenAPISwaggerDoc.Entities تعریف کرده‌ایم، نام و نام خانوادگی اجباری بوده و دارای حداکثر طول 150 حرف، هستند. قصد داریم همین ویژگی‌ها را به DTO دریافتی این متد، یعنی AuthorForUpdate نیز اعمال کنیم:
using System.ComponentModel.DataAnnotations;

namespace OpenAPISwaggerDoc.Models
{
    /// <summary>
    /// An author for update with FirstName and LastName fields
    /// </summary>
    public class AuthorForUpdate
    {
        /// <summary>
        /// The first name of the author
        /// </summary>
        [Required]
        [MaxLength(150)]
        public string FirstName { get; set; }

        /// <summary>
        /// The last name of the author
        /// </summary>
        [Required]
        [MaxLength(150)]
        public string LastName { get; set; }
    }
}
پس از افزودن ویژگی‌های Required و MaxLength به این DTO، خروجی Sawgger-UI به صورت زیر بهبود پیدا می‌کند:



بهبود مستندات متد HttpPatch با ارائه‌ی یک مثال

دو نگارش از اکشن متد UpdateAuthor در این مثال موجود هستند:
یکی HttpPut است
[HttpPut("{authorId}")]
public async Task<ActionResult<Author>> UpdateAuthor(Guid authorId, AuthorForUpdate authorForUpdate)
و دیگری HttpPatch:
[HttpPatch("{authorId}")]
public async Task<ActionResult<Author>> UpdateAuthor(
            Guid authorId,
            JsonPatchDocument<AuthorForUpdate> patchDocument)
این مورد آرام آرام در حال تبدیل شدن به یک استاندارد است؛ چون امکان Partial updates را فراهم می‌کند. به همین جهت نسبت به HttpPut، کارآیی بهتری را ارائه می‌دهد. اما چون پارامتر دریافتی آن از نوع ویژه‌ی JsonPatchDocument است و مثال پیش‌فرض مستندات آن، آنچنان مفهوم نیست:


بهتر است در این حالت مثالی را به استفاده کنندگان از آن ارائه دهیم تا در حین کار با آن، به مشکل برنخورند:
/// <summary>
/// Partially update an author
/// </summary>
/// <param name="authorId">The id of the author you want to get</param>
/// <param name="patchDocument">The set of operations to apply to the author</param>
/// <returns>An ActionResult of type Author</returns>
/// <remarks>
/// Sample request (this request updates the author's first name) \
/// PATCH /authors/id \
/// [ \
///     { \
///       "op": "replace", \
///       "path": "/firstname", \
///       "value": "new first name" \
///       } \
/// ] \
/// </remarks>
[HttpPatch("{authorId}")]
public async Task<ActionResult<Author>> UpdateAuthor(
    Guid authorId,
    JsonPatchDocument<AuthorForUpdate> patchDocument)
در اینجا در حین کامنت نویسی، می‌توان از المان remarks، برای نوشتن توضیحات اضافی مانند ارائه‌ی یک مثال، استفاده کرد که در آن op و path معادل‌های بهتری را نسبت به مستندات پیش‌فرض آن پیدا کرده‌‌اند. در اینجا برای ذکر خطوط جدید باید از \ استفاده کرد؛ وگرنه خروجی نهایی، در یک سطر نمایش داده می‌شود:



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

با فعالسازی GenerateDocumentationFile در فایل csproj برنامه، کامپایلر، بلافاصله برای تمام متدها و خواص عمومی که دارای کامنت نیستند، یک warning را صادر می‌کند. یک روش برطرف کردن این مشکل، افزودن کامنت به تمام قسمت‌های برنامه است. روش دیگر آن، تکمیل خواص کامپایلر، جهت مواجه شدن با عدم وجود کامنت‌ها در فایل csproj برنامه است:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>netcoreapp2.2</TargetFramework>
    <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>

    <GenerateDocumentationFile>true</GenerateDocumentationFile>
    <TreatWarningsAsErrors>false</TreatWarningsAsErrors>
    <WarningsAsErrors>NU1605;</WarningsAsErrors>
    <NoWarn>1701;1702;1591</NoWarn>
  </PropertyGroup>
توضیحات:
- اگر می‌خواهید خودتان را مجبور به کامنت نویسی کنید، می‌توانید نبود کامنت‌ها را تبدیل به error کنید. برای این منظور خاصیت TreatWarningsAsErrors را به true تنظیم کنید. در این حالت هر کامنت نوشته نشده، به صورت یک error توسط کامپایلر گوشزد شده و برنامه کامپایل نخواهد شد.
- اگر TreatWarningsAsErrors را خاموش کردید، هنوز هم می‌توانید یکسری از warningهای انتخابی را تبدیل به error کنید. برای مثال NU1605 ذکر شده‌ی در خاصیت WarningsAsErrors، مربوط به package downgrade detection warning است.
- اگر به warning نبود کامنت‌ها دقت کنیم به صورت عبارات warning CS1591: Missing XML comment for publicly visible type or member شروع می‌شود. یعنی  CS1591 مربوط به کامنت‌های نوشته نشده‌است. می‌توان برای صرفنظر کردن از آن، شماره‌ی این خطا را بدون CS، توسط خاصیت NoWarn ذکر کرد.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: OpenAPISwaggerDoc-03.zip

در قسمت بعد، مشکل خروجی تولید response از نوع 200 را که در قسمت دوم به آن اشاره کردیم، بررسی خواهیم کرد.
مطالب
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 میباشد و از مطرح کردن آن در مقاله خودداری میکنیم.
نتیجه به شکل زیر خواهد بود:


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


مطالب
SQL Server CE و ثبت متون طولانی در EF Code first
زمانیکه در EF Code first تعریف خاصیتی به نحو زیر باشد
public string Content { get; set; }
در حین کار با SQL Server به صورت خودکار به nvarchar max نگاشت می‌شود. اما همین تعریف در SQL Server CE به nvarchar 4000 نگاشت خواهد شد؛ چون این بانک اطلاعاتی نوع‌های max دار را پشتیبانی نمی‌کند.
بنابراین اگر هدف، ثبت اطلاعات در فیلدی از نوع ntext در این بانک اطلاعاتی باشد باید به یکی از دو روش زیر عمل کرد:
[MaxLength]
public string Content { get; set; }
بله. فقط کافی است یک MaxLength را بالای خاصیت قرار داد (بدون تعیین طول آن) تا به صورت خودکار در SQL Server CE به ntext نگاشت شود و یا می‌توان نوع ستون را صریحا تعیین کرد:
[Column(TypeName = "ntext")]
public string Text { get; set; }
روش اول بهتر است از این جهت که با بانک‌های اطلاعاتی مختلف سازگاری بهتری دارد. برای مثال نوع ntext در SQL Server کامل، منسوخ شده درنظر گرفته می‌شود اما اگر از ویژگی MaxLength در اینجا استفاده گردد به صورت خودکار به nvarchar max نگاشت خواهد شد و در SQL Server CE به ntext .
بنابراین قید MaxLength بر روی خواصی که قرار است حاوی متونی طولانی باشند، می‌تواند به عنوان یک کار مفید جهت سازگاری با بانک‌های مختلف، به شمار آید.
 
نظرات مطالب
ASP.NET MVC #11
در حالتی که چنین رابطه ای وجود داره :
public class Project
{
   [ForeignKey("FK_ProjectType_Id")]
   public ProjectType ProjectType{get;set;}
   public int  FK_ProjectType_Id{get;set;}
}

public class ProjectType
{
   public int id{get;set;}
   public string TypeName{get;set;}
}
آیا ViewModel متناظر با کلاس مدل Project باید بصورت زیر باشه :
public class ProjectViewModel
{
   public ProjectType ProjectType {get;set;}
}
نظرات مطالب
C# 8.0 - Nullable Reference Types
بهبودهای نوع‌های ارجاعی نال‌نپذیر در NET Core 3.0 Preview 7.

پس از نصب SDK جدید، نحوه‌ی فعالسازی این قابلیت در فایل csproj، به صورت زیر درآمده‌است:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <LangVersion>8.0</LangVersion>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>

این موارد در Preview 7 جدید هستند:
1) امکان اضافه کردن قید جدید notnull در حین تعریف نوع‌های جنریک
    interface IDoStuff<TIn, TOut> where TIn : notnull where TOut : notnull
    {
        TOut DoStuff(TIn input);
    }

2) اضافه شدن یک سری ویژگی توکار جدید برای «پیش» بررسی کار با نوع‌های ارجاعی نال نپذیر

ویژگی AllowNull به فراخوان‌ها امکان ارسال نال را حتی اگر مجاز نباشد، می‌دهد.  این ویژگی جدید در فضای نام System.Diagnostics.CodeAnalysis تعریف شده‌است. برعکس آن ویژگی DisallowNull، سبب خواهد شد تا فراخوان‌ها حتی در صورت مجاز بودن نیز نتوانند نال ارسال کنند.
using System;
using System.Diagnostics.CodeAnalysis;

namespace ConsoleApp
{
    public class MyClass
    {
        private string _innerValue = string.Empty;

        [AllowNull]
        public string MyValue
        {
            get
            {
                return _innerValue;
            }
            set
            {
                _innerValue = value ?? string.Empty;
            }
        }
    }
در مثال فوق ویژگی AllowNull سبب می‌شود تا در قسمت setter امکان دریافت نال نیز میسر شود؛ برای مثال جهت سازگاری با نگارش‌های قبلی برنامه.
دو ویژگی یاد شده را می‌توان بر روی پارامترهای متدها، پارامترهایی از نوع in و ref، فیلدها، خواص و ایندکسرها اعمال کرد.

3) اضافه شدن یک سری ویژگی توکار جدید برای «پس» بررسی کار با نوع‌های ارجاعی نال نپذیر

دو ویژگی جدید MaybeNull و NotNull کار پس بررسی نال پذیری را انجام می‌دهند:
    public class MyArray
    {
        // Result is the default of T if no match is found
        [return: MaybeNull]
        public static T Find<T>(T[] array, Func<T, bool> match)
        {
            //...
        }

        // Never gives back a null when called
        public static void Resize<T>([NotNull] ref T[]? array, int newSize)
        {
            //...
        }
    }
در این مثال، متد Find با تعریف ویژگی return: MaybeNull، ممکن است نال برگرداند. برای مثال اگر چیزی یافت نشد، default را بر گرداند.
در متد Resize، پارامتر array، می‌تواند نال دریافت کند، چون نال‌پذیر تعریف شده‌است؛ اما چون به ویژگی NotNull مزین است، حاصل تغییرات بر روی آن (خروجی از متد، از طریق پارامتری از نوع ref) نمی‌تواند نال باشد.

دو ویژگی یاد شده را می‌توان بر روی خروجی متدها، پارامترهایی از نوع out و ref، فیلدها، خواص و ایندکسرها اعمال کرد.

4) اضافه شدن یک سری ویژگی توکار جدید برای «پس» بررسی «شرطی» کار با نوع‌های ارجاعی نال نپذیر

در مثال‌های زیر کاربردهای دو ویژگی شرطی جدید NotNullWhen و MaybeNullWhen را مشاهده می‌کنید:
    public class MyString
    {
        // True when 'value' is null
        public static bool IsNullOrEmpty([NotNullWhen(false)] string? value)
        {
            //...
        }
    }
در اینجا با بکارگیری ویژگی [NotNullWhen(false)] به فراخوان اعلام می‌کنیم که اگر IsNullOrEmpty مقدار false را بر‌گرداند، مقدار value ارسال شده‌ی به آن، نال نیست.

    public class MyVersion
    {
        // If it parses successfully, the Version will not be null.
        public static bool TryParse(string? input, [NotNullWhen(true)] out Version? version)
        {
            //...
        }
    }
در اینجا با بکارگیری ویژگی [NotNullWhen(true)] به فراخوان اعلام می‌کنیم که اگر TryParse مقدار true را بر‌گرداند، مقدار version خروجی آن، نال نیست.

    public class MyQueue<T>
    {
        // 'result' could be null if we couldn't Dequeue it.
        public bool TryDequeue([MaybeNullWhen(false)] out T result)
        {
            //...
        }
    }
در اینجا با بکارگیری ویژگی [MaybeNullWhen(false)] به فراخوان اعلام می‌کنیم که اگر TryDequeue مقدار false را برگرداند، مقدار result خروجی آن، می‌تواند نال هم باشد.

5) اضافه شدن یک سری ویژگی توکار جدید برای شرط گذاشتن بین ورودی و خروجی، در حین کار با نوع‌های ارجاعی نال نپذیر

در متد زیر، هم خروجی و هم ورودی آن می‌توانند نال باشند. اما می‌خواهیم اگر path نال نباشد، اطمینان حاصل کنیم که استفاده کننده می‌داند، خروجی این متد، حتما نال نخواهد بود:
    class MyPath
    {
        [return: NotNullIfNotNull("path")]
        public static string? GetFileName(string? path)
        {
            //...
        }
    }
برای انجام یک چنین اطلاع رسانی‌هایی می‌توان از ویژگی جدید NotNullIfNotNull استفاده کرد. از آن می‌توان برای مزین سازی خروجی متدها و یا پارامترهایی از نوع ref استفاده کرد.

6) اضافه شدن یک سری ویژگی توکار جدید برای بررسی سیلان برنامه، در حین کار با نوع‌های ارجاعی نال نپذیر

در اینجا نحوه‌ی استفاده از دو ویژگی جدید DoesNotReturn و DoesNotReturnIf را مشاهده می‌کنید:
    internal static class ThrowHelper
    {
        [DoesNotReturn]
        public static void ThrowArgumentNullException(ExceptionArgument arg)
        {
            //...
        }
    }
اگر متد ThrowArgumentNullException در جائی فراخوانی شود، سبب بروز یک استثناء می‌شود. استفاده از DoesNotReturn سبب می‌شود تا به کامپایلر اعلام کند، پس از این نقطه، دیگر نیازی به بررسی نال بودن اشیاء نیست؛ چون آن قطعه از کد، غیرقابل اجرا و رسیدن می‌شود. این ویژگی را تنها بر روی متدها می‌توان قرار داد.

    public static class MyAssertionLibrary
    {
        public static void MyAssert([DoesNotReturnIf(false)] bool condition)
        {
            //...
        }
    }
اگر متد MyAssert فراخوانی شود و ورودی آن false باشد، یک استثناء را صادر می‌کند. با بکارگیری ویژگی [DoesNotReturnIf(false)] این موضوع را به کامپایلر اعلام کرده و از آن درخواست می‌کنیم تا کار بررسی نال بودن اشیاء را از آن سطر به بعد، انجام ندهد. این ویژگی را تنها بر روی پارامترها می‌توان اعمال کرد.