مطالب
وی‍‍ژگی های پیشرفته ی AutoMapper - قسمت دوم
در ادامه قسمت قبلی به برسی ویژگی‌های پیشرفته‌ی AutoMapper می‌پردازیم...


Custom type converters
همانطور که از اسمش مشخصه، زمانی کاربرد داره که نوع عضو یا اعضای یک شی در مبداء، با معادلشون در مقصد یکی نیستند. مثلا فرض کنید نوع Bool در مبداء رو می‌خواهیم به نوع String در مقصد نگاشت کنیم؛ همون Yes و No  معروف بجای True یا False .
کلاس‌های زیر رو در نظر بگیرید:
public class Source
{
    public string Value1 { get; set; }
    public string Value2 { get; set; }
    public string Value3 { get; set; }
}

public class Destination
{
    public int Value1 { get; set; }
    public DateTime Value2 { get; set; }
    public Type Value3 { get; set; }
}
طبق مستندات AutoMapper  اگه بخواهیم این دو رو نگاشت کنیم Exception  میده چون AutoMapper  نمیدونه چطوری باید مثلا Int  رو به String تبدیل کنه؛ برای همین ما باید به AutoMapper  بگیم چطور این تبدیل نوع رو انجام بده.

نکته: در تستی که من انجام دادم، AutoMapper  تبدیل نوع‌های ابتدایی رو خودش انجام میده؛ مثلا همین تبدیل Int به String  رو!

یکی از روش‌های مهیا کردن تبدیل کننده‌ی نوع، پیاده سازی اینترفیس ITypeConverter<TSource, TDestination> هست. تقریبا مثل کاری که در WPF  و SL با پیاده سازی اینترفیس IValueConverter انجام می‌دادیم.   
من برای تست از همون  تبدیل نوع Bool  به String استفاده میکنم و البته بخاطر ساده بودن دیگه  Model ‌ها رو نمی‌نویسم.
ابتدا تعریف کلاس تبدیل کننده‌ی نوع:
public class BooltoStringTypeConvertor : ITypeConverter<bool, string>
    {
        public string Convert(ResolutionContext context)
        {
            return (bool)context.SourceValue ? "Yes" : "No";
        }
    }
و نحوه استفاده:
Mapper.CreateMap<bool,string>().ConvertUsing<BooltoStringTypeConvertor>();
            Mapper.CreateMap<Product, ProductDto>();
            Mapper.AssertConfigurationIsValid();

var product = new Product { Id = 1,Name ="PC" ,InStock = true };
var productDto = Mapper.Map<Product, ProductDto>(product);
خروجی به شکل زیر میشه.

نکته: TypeConvertor‌ها میدان دیدشون سراسریه و نیازی نیست به ازای هر نگاشتی اونو به AutoMapper  معرفی کنیم Global Scope.

Custom value resolvers

کلاس‌های زیر رو در نظر بگیرید

public class Person
{
    public int Id { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }
}

public class PersonDTO
{
    public int Id { get; set; }

    public string RawData { get; set; }
}
فرض کنید داخل RawData  تمامی اعضای شی مبداء رو به صورت Comma Delimited ذخیره کنیم. برای این کار می‌تونیم از Value Resolver استفاده کنیم.
یک روش برای این کار ارث بری از کلاس Abstract  ی بنام ValueResolver<TSource, TDestination> هست.
public class CommaDelimetedResolver:ValueResolver<Person,string>
    {
        protected override string ResolveCore(Person source)
        {
            return string.Join(",", source.Id, source.FirstName, source.LastName);
        }
    }
و نحوه استفاده
Mapper.CreateMap<Person, PersonDTO>().ForMember(
                des => des.RawData, op => op.ResolveUsing<CommaDelimetedResolver>());


var person = new Person
{
Id = 1,
FirstName = "Mohammad",
LastName = "Saheb",
};

var personDTO = Mapper.Map<Person, PersonDTO>(person);
و خروجی به شکل زیر میشه

نکته: توجه کنید این فقط یک مثال بود و این کار رو با روش‌های دیگه هم میشه انجام داد مثلا MapFrom  و...
نکته: میدان دید Value Resolver‌ها سراسری نیست و باید به ازای هر نگاشتی اونو معرفی کنیم.

Custom Value Formatters
فرض کنید تاریخ رو در بانک، به صورت میلادی ذخیره کرده‌اید و می‌خواهید سمت View به صورت شمسی نمایش بدید. بنابراین در مبدا ویژگی بنام MiladiDate از نوع DateTime دارید و در مقصد ویژگی بنام ShamsiDate از نوع String. هنگام نگاشت، AutoMapper  به صورت پیش فرض ToString رو فراخونی میکنه که بدرد ما نمیخوره و...
برای این کار میشه  از Value Formatter استفاده کرد با پیاده سازی اینترفیس IValueFormatter.
public class ShamsiFormatter:IValueFormatter
    {
        public string FormatValue(ResolutionContext context)
        {
            return ToShamsi(context.SourceValue.ToString());
        }
    }
نحوه استفاده
Mapper.CreateMap<Person, PersonDTO>().ForMember(
            des => des.ShamsiDate, op => op.AddFormatter<ShamsiFormatter>());
مطالب
امکان داشتن خروجی‌های Covariant در C# 9.0
در زبان #C، زمانیکه از کلاسی ارث‌بری می‌شود، امکان بازنویسی متدهای کلاس پایه، در صورت معرفی آن‌ها به صورت virtual و یا abstract، وجود دارد؛ اما در این بازنویسی‌ها، تغییر نوع خروجی این متدها مجاز نیست. این محدودیت در C# 9.0 با معرفی Covariant returns برداشته شده‌است؛ با یک شرط: نوع جدید این خروجی، باید covariant نوع اصلی متدی باشد که از کلاس پایه‌ی آن ارث‌بری شده‌است.


وضعیت خروجی متدهای بازنویسی شده تا پیش از C# 9.0

برای توضیح بهتر Covariant returns، نیاز است مثال زیر را بررسی کنیم:
public abstract class Product
{
  public string Name { get; set; }
  public abstract ProductOrder Order(int quantity);
}

public class Book : Product
{
  public string ISBN { get; set; }
  public override ProductOrder Order(int quantity) =>
new BookOrder { Quantity = quantity, Product = this };
}

public class ProductOrder
{
  public int Quantity { get; set; }
}

public class BookOrder : ProductOrder
{
  public Book Product { get; set; }
}
در اینجا یک کلاس abstract و پایه‌ی Product وجود دارد که می‌توان متد abstract سفارش دهی آن‌را در کلاس‌های مشتق شده‌ی از آن، مانند Book، بازنویسی کرد.
همانطور که مشاهده می‌کنید، در کلاس Book، تنها خروجی که برای متد Order بازنویسی شده می‌توان درنظر گرفت، همانی است که در کلاس پایه‌ی Product تعریف شده‌است و قابل تغییر نیست؛ یعنی همان ProductOrder.
همچنین در حین استفاده‌ی از این کلاس‌ها، تبدیل خروجی متد Order، به BookOrder ضروری است:
var book = new Book
{
  Name = "My book",
  ISBN = "11-1-12-22-0"
};
BookOrder orderBook = (BookOrder)book.Order(1);


امکان تغییر خروجی متدهای بازنویسی شده در C# 9.0

در C# 9.0 با مجاز اعلام شدن خروجی‌های covariant، می‌توان تغییرات زیر را به کدهای فوق اعمال کرد:
public class Book : Product
{
  public string ISBN { get; set; }
  public override BookOrder Order(int quantity) =>
      new BookOrder { Quantity = quantity, Product = this };
}
چون کلاس BookOrder از ProductOrder تعریف شده‌ی در کلاس پایه مشتق شده‌است (مفهوم covariant بودن خروجی متد)، می‌توان در C# 9.0 آن‌را به عنوان خروجی متد Order بازنویسی شده‌ی در کلاس Book، تنظیم کرد.
مزایای این ویژگی:
- داشتن یک خروجی مختص و متناسب با کلاس کتاب، مانند BookOrder؛ بجای ارائه‌ی یک خروجی بسیار عمومی ProductOrder.
- نیاز به کار با Generics را برای اینگونه اختصاصی سازی‌ها منتفی می‌کند.
- با این تغییر، دیگر نیازی به تبدیل نوع خروجی متد Order یک کتاب نیست و سطر سفارش دهی را می‌توان به صورت زیر خلاصه کرد:
BookOrder orderBook = book.Order(1);
مطالب
بومی سازی منابع در پروژه‌های ASP.NET Core Web API
اگر پروژه‌ی ما فقط از یک Web API تشکیل شده و نیاز است در قسمت‌های مختلف آن، مانند کنترلرها، سرویس‌ها، اعتبارسنج‌ها و غیره از منابع بومی شده استفاده شود، می‌توان از یک راه حل ساده‌ی «SharedResource» استفاده کرد؛ با این مزایا و شرایط:
 - تمام تعاریف بومی سازی مورد نیاز برنامه در یک تک فایل SharedResource.fa.resx قرار می‌گیرند. این فایل نیز در یک اسمبلی مستقل از برنامه‌ی اصلی اضافه می‌شود.
 - با استفاده از تزریق سرویس IStringLocalizer می‌توان به کلیدهای فایل SharedResource.fa.resx در هر قسمتی از برنامه‌ی Web API دسترسی یافت.
 - در این بین اگر کلیدی یافت نشد، خطایی با ذکر دقیق جزئیات منبع جستجو شده، لاگ می‌شود.
 - کلیدهای بومی سازی data annotations نیز قابل دریافت از فایل SharedResource.fa.resx می‌باشند.
 
در ادامه روش پیاده سازی یک چنین امکاناتی را بررسی می‌کنیم.
 
 
قرار دادن فایل منبع اشتراکی در اسمبلی ExternalResources

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

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


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

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

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

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

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

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

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

            app.UseHttpsRedirection();

            app.UseCustomRequestLocalization();

            app.UseRouting();

            app.UseAuthorization();

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


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

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

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

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


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

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

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

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

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

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


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

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


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


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

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

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

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

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

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

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

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

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

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

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


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Core3xSharedResource.zip
نظرات مطالب
اضافه کردن سعی مجدد به اجرای عملیات Migration در EF Core
یک نکته‌ی تکمیلی: مشکل تراکنش‌های صریح با EnableRetryOnFailure

زمانیکه از EnableRetryOnFailure استفاده می‌شود، هر کوئری و هر SaveChanges، یک «عملیات قابل‌تکرار» در صورت شکست خواهد شد (که تعداد بار تکرار آن بستگی به تنظیمات آغازین این متد دارد)؛ البته این در مورد تراکنش‌های ضمنی است. اگر در برنامه، یک تراکنش صریح را با فراخوانی BeginTransaction آغاز کنیم، یعنی قصد داریم یک واحد عملیاتی جدید را خودمان به نحو صریحی مشخص کنیم. در این حالت اگر خطایی رخ دهد، نیاز است کل عملیات تراکنش یکبار دیگر تکرار شود و نه اینکه هر کوئری آن یکبار دیگر؛ که در این حالت به استثنای زیر هم خواهیم رسید:
System.InvalidOperationException: The configured execution strategy ‘SqlServerRetryingExecutionStrategy’ 
does not support user initiated transactions. Use the execution strategy returned by 
‘DbContext.Database.CreateExecutionStrategy()’ to execute all the operations in the transaction as a retriable unit.
یا می‌توان این تنظیمات تکرار عملیات شکست خورده را حذف کرد و یا می‌توان همانطور که در متن خطا هم عنوان کرده، یک CreateExecutionStrategy جدید را سبب شد:
public async Task<IActionResult> UpdateProduct([FromBody]CatalogItem productToUpdate)
{
   var strategy =  _catalogContext.Database.CreateExecutionStrategy();
   await strategy.ExecuteAsync(async () =>
   {
     using (var transaction = _catalogContext.Database.BeginTransaction())
     {
        _catalogContext.CatalogItems.Update(productToUpdate);
        await _catalogContext.SaveChangesAsync();

        transaction.Commit();
     }
   });
}
در این حالت اگر مشکلی روی دهد، «عملیات قابل‌تکرار» همان کدهای async delegate تعریف شده هستند و اینبار حد و مرز آن مشخص شده‌است. اگر هم خواستید این راه حل را عمومی کنید، می‌توان از قطعه کد زیر جهت فراخوانی کدهای سفارشی داخل async delegate استفاده کرد:
public class ResilientTransaction
{
    private readonly DbContext _context;
    private ResilientTransaction(DbContext context) =>
        _context = context ?? throw new ArgumentNullException(nameof(context));

    public static ResilientTransaction New(DbContext context) => new(context);

    public async Task ExecuteAsync(Func<Task> action)
    {
        var strategy = _context.Database.CreateExecutionStrategy();
        await strategy.ExecuteAsync(async () =>
        {
            await using var transaction = await _context.Database.BeginTransactionAsync();
            await action();
            await transaction.CommitAsync();
        });
    }
}

// How to use it

await ResilientTransaction.New(_dbContext).ExecuteAsync(async () =>
{
      // ...
      await _dbContext.SaveChangesAsync();
});
مطالب
سری بررسی SQL Smell در EF Core - ایجاد روابط Polymorphic - بخش دوم
در مطلب قبل نحوه‌ی ایجاد روابط Polymorphic را بررسی کردیم و همچنین چندین راه‌حل جایگزین را نیز ارائه دادیم. همانطور که عنوان شد این نوع روابط اساساً از لحاظ طراحی دیتابیس اصولی نیستند و تا حد امکان نباید استفاده شوند. این نوع روابط بیشتر ORM friendly هستند و اکثر فریم‌ورک‌های غیردات‌نتی به عنوان یک گزینه‌ی توکار، امکان ایجاد این روابط را فراهم میکنند. به عنوان مثال درLaravel Eloquent ORM به صورت توکار از این قابلیت پشتیبانی می‌شود: 
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    /**
     * Get the owning commentable model.
     */
    public function commentable()
    {
        return $this->morphTo();
    }
}

class Post extends Model
{
    /**
     * Get all of the post's comments.
     */
    public function comments()
    {
        return $this->morphMany('App\Comment', 'commentable');
    }
}

class Video extends Model
{
    /**
     * Get all of the video's comments.
     */
    public function comments()
    {
        return $this->morphMany('App\Comment', 'commentable');
    }
}

اما در Entity Framework Core هنوز این قابلیت پیاده‌سازی نشده است. در اینجا می‌توانید وضعیت پیاده‌سازی Polymorphic Association را پیگیری کنید. پیاده‌سازی این قابلیت از این جهت مهم است که امکان کوئری نویسی را برای این نوع روابط ساده‌تر خواهد کرد به عنوان مثال در کدهای PHP فوق جهت واکشی کامنت‌های یک مطلب می‌توانیم اینگونه عمل کنیم:
$post = App\Post::find(1);

foreach ($post->comments as $comment) {
    //
}

همچنین امکان واکشی owner این رابطه را نیز حین کار با کامنت‌ها را خواهیم داشت:
$comment = App\Comment::find(1);

$commentable = $comment->commentable;

در ادامه می‌خواهیم معادل LINQ آن را پیاده‌سازی کنیم. در مطلب قبل مدل Comment این چنین ساختاری داشت:
public enum CommentType
{
    Article,
    Video,
    Event
}

public class Comment
{
    public int Id { get; set; }
    public string CommentText { get; set; }
    public string User { get; set; }
    public int? TypeId { get; set; }
    public CommentType CommentType { get; set; }
}

در اینجا همانطور که مشاهده میکنید هیچگونه ارتباط معناداری بین Comment و همچنین owner رابطه (که ممکن است هر کدام از مقادیر Enum فوق باشد) وجود ندارد. اگر این مدل به تنها یک مدل مثلاً Article اشاره داشته باشد، نیاز به تعیین Navigation Property در دو طرف رابطه خواهد بود:
public class Comment
{
    public int Id { get; set; }
    public string CommentText { get; set; }
    public string User { get; set; }

    public virtual Article Article { get; set; }
    public int ArticleId { get; set; }
}

public class Article
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Slug { get; set; }
    public string Description { get; set; }
    
    public virtual ICollection<Article> Articles { get; set; }
}

اما از آنجائیکه رابطه یک حالت پویا دارد، نمی‌توانیم به صورت صریح نوع ارجاعات را در دو طرف رابطه تعیین کنیم. برای داشتن همچین قابلیتی می‌توانیم Navigation Property را به صورت [NotMapped] تعیین کنیم که EF Core آنها را در نظر نگیرید. بنابراین به صورت دستی عملکرد آنها را پیاده‌سازی خواهیم کرد. برای اینکار می‌توانیم یک اینترفیس با عنوان ICommentable را تعریف کنیم و برای هر مدلی که نیاز به قابلیت کامنت دارد، این اینترفیس را پیاده‌سازی کنیم. همچنین یک ارجاع به لیستی از کامنت‌ها را به صورت Navigation Property به هر کدام از مدلها نیز اضافه خواهیم کرد:  
interface ICommentable
{
    int Id { get; set; }
}

public class Article : ICommentable
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Slug { get; set; }
    public string Description { get; set; }
    
    [NotMapped]
    public ICollection<Comment> Comments { get; set; }
}

public class Video : ICommentable
{
    public int Id { get; set; }
    public string Url { get; set; }
    public string Description { get; set; }
    
    [NotMapped]
    public ICollection<Comment> Comments { get; set; }
}

public class Event : ICommentable
{
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTimeOffset? Start { get; set; }
    public DateTimeOffset? End { get; set; }
    
    [NotMapped]
    public ICollection<Comment> Comments { get; set; }
}

سپس درون مدل Comment ارجاع به Polymorphic relation را نیز به صورت [NotMapped] پیاده‌سازی خواهیم کرد:
public class Comment
{
    public int Id { get; set; }
    public string CommentText { get; set; }
    public string User { get; set; }
    public int? TypeId { get; set; }
    public CommentType CommentType { get; set; }

    ICommentable _parent;

    [NotMapped]
    public ICommentable Parent
    {
        get => _parent;
        set
        {
            _parent = value;
            TypeId = value.Id;
            CommentType = (CommentType) Enum.Parse(typeof(CommentType), value.GetType().Name);
        }
    }
}

کاری که در بالا انجام شده، تنظیم تایپ مدلی است که می‌خواهیم واکشی کنیم. یعنی به محض مقداردهی، پراپرتی Comments مدل مورد نظر به همراه Id و در نهایت نوع آن را تنظیم کرده‌ایم. اکنون برای واکشی کامنت‌های یک مطلب خواهیم داشت:
var article = dbContext.Articles.Find(1);
            
article.Comments = dbContext.Comments
    .Where(c => c.TypeId == article.Id
                && c.CommentType == CommentType.Article)
    .ToList();

foreach (var comment in article.Comments)
    comment.Parent = article;

foreach (var comment in article.Comments)
{
    Console.WriteLine($"{comment.User} - ${comment.CommentText} - {((Article) comment.Parent).Title}");
}

همانطور که مشاهده می‌کنید اکنون می‌توانیم از هر دو طرف رابطه به اطلاعات موردنیازمان دسترسی داشته باشیم.
مطالب
مستند سازی ASP.NET Core 2x API توسط OpenAPI Swagger - قسمت دوم - شروع به مستند سازی یک API
پس از معرفی اجمالی OpenAPI و Swagger در قسمت قبل و همچنین ارائه‌ی یک برنامه‌ی نمونه که آن‌را به مرور تکمیل خواهیم کرد، در ادامه کتابخانه‌ی Swashbuckle را نصب کرده و شروع به مستند سازی API ارائه شده خواهیم کرد.


نصب Swashbuckle (سوواَش باکِل)

اگر عبارت Swashbuckle.AspNetCore را در سایت NuGet جستجو کنیم، چندین بسته‌ی مختلف مرتبط با آن‌را خواهیم یافت. ما در این بین، بیشتر به این بسته‌ها علاقمندیم:
- Swashbuckle.AspNetCore.Swagger: کار آن ارائه‌ی خروجی OpenAPI تولیدی بر اساس ASP.NET Core API برنامه‌ی ما، به صورت یک JSON Endpoint است.
- Swashbuckle.AspNetCore.SwaggerGen: کار آن ساخت Swagger document objects است؛ یا همان OpenAPI Specification.
عموما این دو بسته را با هم جهت ارائه‌ی OpenAPI Specification استفاده می‌کنند.
- Swashbuckle.AspNetCore.SwaggerUI: این بسته، نگارش جایگذاری شده‌ی (embedded) ابزار swagger-UI را به همراه دارد. کار آن، ارائه‌ی یک UI خودکار، بر اساس OpenAPI Specification است که از آن برای آزمایش API نیز می‌توان استفاده کرد.

یک نکته: اگر صرفا بسته‌ی Swashbuckle.AspNetCore را نصب کنیم، هر سه بسته‌ی فوق را با هم دریافت خواهیم کرد و اگر از Visual Studio برای نصب آن‌ها استفاده می‌کنید، انتخاب گزینه‌ی Include prerelease را فراموش نکنید؛ از این جهت که قصد داریم از نگارش 5 آن‌ها استفاده کنیم. چون این نگارش است که از OpenAPI 3x، پشتیبانی می‌کند. خلاصه‌ی این موارد، افزودن PackageReference زیر به فایل پروژه‌ی OpenAPISwaggerDoc.Web.csproj است و سپس اجرای دستور dotnet restore:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <ItemGroup>
    <PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc2" />
  </ItemGroup>
</Project>


تنظیم میان‌افزار Swashbuckle

پس از افزودن ارجاعی به Swashbuckle.AspNetCore، اکنون نوبت انجام تنظیمات میان‌افزارهای آن است. برای این منظور ابتدا به کلاس Startup و متد ConfigureServices آن مراجعه می‌کنیم:
namespace OpenAPISwaggerDoc.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
    // ...
            services.AddSwaggerGen(setupAction =>
            {
                setupAction.SwaggerDoc(
                   name: "LibraryOpenAPISpecification",
                   info: new Microsoft.OpenApi.Models.OpenApiInfo()
                   {
                       Title = "Library API",
                       Version = "1",
                       Description = "Through this API you can access authors and their books.",
                       Contact = new Microsoft.OpenApi.Models.OpenApiContact()
                       {
                           Email = "name@site.com",
                           Name = "DNT",
                           Url = new Uri("https://www.dntips.ir")
                       },
                       License = new Microsoft.OpenApi.Models.OpenApiLicense()
                       {
                           Name = "MIT License",
                           Url = new Uri("https://opensource.org/licenses/MIT")
                       }
                   });
            });
        }
در اینجا نحوه‌ی تنظیمات ابتدایی سرویس‌های مرتبط با SwaggerGen را ملاحظه می‌کنید. ابتدا نیاز است یک SwaggerDoc به آن اضافه شود که یک name و info را دریافت می‌کند. این name، جزئی از آدرسی است که در نهایت، OpenAPI Specification تولیدی را می‌توان در آنجا یافت. پارامتر Info آن نیز به همراه یک سری مشخصات عمومی درج شده‌ی در مستندات OpenAPI است.

اکنون در متد Configure، میان‌افزار آن‌را خواهیم افزود:
namespace OpenAPISwaggerDoc.Web
{
    public class Startup
    {
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
    // ...
            app.UseHttpsRedirection();

            app.UseSwagger();
    // ...
        }
بهتر است UseSwagger را پس از UseHttpsRedirection درج کرد تا هر نوع درخواست HTTP به آن، به صورت خودکار به HTTPS تبدیل و هدایت شود.

تا اینجا اگر برنامه را اجرا کنید، می‌توان OpenAPI Specification تولیدی را در آدرس زیر یافت:
 https://localhost:5001/swagger/LibraryOpenAPISpecification/swagger.json


در این آدرس، LibraryOpenAPISpecification، همان نامی است که در قسمت setupAction.SwaggerDoc تنظیم کردیم.


نگاهی به OpenAPI Specification تولیدی

در ابتدای swagger.json تولیدی، همانطور که در تصویر فوق نیز مشخص است، همان مشخصات ذکر شده‌ی در قسمت info متد setupAction.SwaggerDoc، قابل مشاهده‌است. سپس لیست مسیرهای این API مشخص شده‌اند:


این‌ها مسیرهایی هستند که توسط دو کنترلر کتاب‌ها و نویسندگان برنامه‌ی Web API ما عمومی شده‌اند. در اینجا مقابل هر مسیر، تعداد آیتم‌های متناظری نیز ذکر شده‌اند. این موارد مرتبط هستند با HTTP methods پشتیبانی شده‌:


که هر کدام به همراه نام متدها و پارامترهای متناظر با آن‌ها نیز می‌شوند. به علاوه نوع responseهای پشتیبانی شده‌ی توسط این متدها نیز ذکر شده‌اند. هر کدام از خروجی‌ها نیز نوع مشخصی دارند که توسط قسمت components -> schemas تصاویر فوق، جزئیات دقیق آن‌ها بر اساس نوع مدل‌های متناظر، استخراج و ارائه شده‌اند.


مشکل: نوع Response تولیدی در OpenAPI Specification صحیح نیست


اگر به جزئیات مسیر /api/authors/{authorId} دقت کنیم، نوع response آن‌را صرفا 200 یا Ok ذکر کرده‌است؛ در حالیکه GetAuthor تعریف شده، حالت NotFound را نیز دارد:
[HttpGet("{authorId}")]
public async Task<ActionResult<Author>> GetAuthor(Guid authorId)
{
    var authorFromRepo = await _authorsService.GetAuthorAsync(authorId);
    if (authorFromRepo == null)
    {
        return NotFound();
    }
    return Ok(_mapper.Map<Author>(authorFromRepo));
}
نمونه‌ی دیگر آن اکشن متد public async Task<ActionResult<Book>> CreateBook است که می‌تواند NotFound یا 404 و یا CreatedAtRoute را که معادل 201 است، بازگشت دهد و در اینجا فقط 200 را ذکر کرده‌است که اشتباه است. بنابراین برای نزدیک کردن این خروجی به اطلاعات واقعی اکشن متدها، نیاز است کار بیشتری انجام شود.


افزودن و راه اندازی Swagger UI

در ادامه می‌خواهیم یک رابط کاربری خودکار را بر اساس OpenAPI Specification تولیدی، ایجاد کنیم:
namespace OpenAPISwaggerDoc.Web
{
    public class Startup
    {
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
    // ...

            app.UseHttpsRedirection();

            app.UseSwagger();
            app.UseSwaggerUI(setupAction =>
            {
                setupAction.SwaggerEndpoint(
                    "/swagger/LibraryOpenAPISpecification/swagger.json",
                    "Library API");
            });

    // ...
        }
برای این منظور میان‌افزار SwaggerUI را پس از UseSwagger، در متد Configure کلاس Startup، تعریف می‌کنیم. در اینجا باید مشخص کنیم که OpenAPI Specification تولید شده، دقیقا در چه آدرسی قرار دارد که روش انجام آن‌را در متد setupAction.SwaggerEndpoint ملاحظه می‌کنید. پارامتر دوم آن یک نام اختیاری است.
پس از این تنظیم اگر آدرس https://localhost:5001/swagger/index.html را در مرورگر باز کنیم، چنین خروجی قابل مشاهده خواهد بود:


و اگر بر روی هر کدام کلیک کنیم، ریز جزئیات آن‌ها بر اساس OpenAPI Specification ای که بررسی کردیم، تولید شده‌است (از پارامترها تا نوع خروجی):


اکنون اگر بر روی دکمه‌ی try it out آن نیز کلیک کنید، در همینجا می‌توان این API را آزمایش کرد. برای مثال Controls Accept header را بر روی application/json قرار داده و سپس بر روی دکمه‌ی execute که پس از کلیک بر روی دکمه‌ی try it out ظاهر شده‌است، کلیک کنید تا بتوان خروجی Web API را مشاهده کرد.

در انتهای این صفحه، در قسمت schemas آن، مشخصات مدل‌های بازگشت داده شده‌ی توسط Web API نیز ذکر شده‌اند:



یک نکته: تغییر آدرس  https://localhost:5001/swagger/index.html به ریشه‌ی سایت

اگر علاقمند باشید تا زمانیکه برای اولین بار آدرس ریشه‌ی سایت را در مسیر https://localhost:5001 باز می‌کنید، Swagger UI نمایان شود، می‌توانید تنظیم RoutePrefix زیر را اضافه کنید:
app.UseSwaggerUI(setupAction =>
            {
                setupAction.SwaggerEndpoint(
                    "/swagger/LibraryOpenAPISpecification/swagger.json",
                    "Library API");
                setupAction.RoutePrefix = "";
            });


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

در قسمت بعد، به بهبود و غنی سازی جزئیات OpenAPI Specification تولیدی خواهیم پرداخت.
مطالب
آزمون واحد در MVVM به کمک تزریق وابستگی
یکی از خوبی‌های استفاده از Presentation Pattern‌ها بالا بردن تست پذیری برنامه و در نتیجه نگهداری کد می‌باشد.
MVVM الگوی محبوب برنامه نویسان WPF و Silverlight می‌باشد.  به صرف استفاده از الگوی MVVM نمی‌توان اطمینان داشت که ViewModel کاملا تست پذیری داریم. به عنوان مثلا اگر در ViewModel خود مستقیما DialogBox کنیم یا ارجاعی از View دیگری داشته باشیم نوشتن آزمون‌های واحد تقریبا غیر ممکن می‌شود. قبلا درباره‌ی این مشکلات و راه حل آن مطلب در سایت منتشر شده است : 
در این مطلب قصد داریم سناریویی را بررسی کنیم که ViewModel از Background Worker جهت انجام عملیات مانند دریافت داده‌ها استفاده می‌کند.
Background Worker کمک می‌کند تا اعمال طولانی در یک Thread دیگر اجرا شود در نتیجه رابط کاربری Freeze نمی‌شود.
به این مثال ساده توجه کنید : 
    public class BackgroundWorkerViewModel : BaseViewModel
    {
        private List<string> _myData;

        public BackgroundWorkerViewModel()
        {
            LoadDataCommand = new RelayCommand(OnLoadData);
        }

        public RelayCommand LoadDataCommand { get; set; }

        public List<string> MyData
        {
            get { return _myData; }
            set
            {
                _myData = value;
                RaisePropertyChanged(() => MyData);
            }
        }

        public bool IsBusy { get; set; }

        private void OnLoadData()
        {
            var backgroundWorker = new BackgroundWorker();
            backgroundWorker.DoWork += (sender, e) =>
                             {
                                 MyData = new List<string> {"Test"};
                                 Thread.Sleep(1000);
                             };
            backgroundWorker.RunWorkerCompleted += (sender, e) => { IsBusy = false; };
            backgroundWorker.RunWorkerAsync();
        }
    }

در این ViewModel با اجرای دستور LoadDataCommand داده‌ها از یک منبع داده دریافت می‌شود. این عمل می‌تواند چند ثانیه طول بکشد ، در نتیجه برای قفل نشدن رابط کاربر این عمل را به کمک Background Worker به صورت Async در پشت صحنه انجام شده است.
آزمون واحد این ViewModel اینگونه خواهد بود : 
    [TestFixture]
    public class BackgroundWorkerViewModelTest
    {
        #region Setup/Teardown

        [SetUp]
        public void SetUp()
        {
            _backgroundWorkerViewModel = new BackgroundWorkerViewModel();
        }

        #endregion

        private BackgroundWorkerViewModel _backgroundWorkerViewModel;

        [Test]
        public void TestGetData()
        {
              
            _backgroundWorkerViewModel.LoadDataCommand.Execute(_backgroundWorkerViewModel);

            Assert.NotNull(_backgroundWorkerViewModel.MyData);
            Assert.IsNotEmpty(_backgroundWorkerViewModel.MyData);
        }
    }

با اجرای این آزمون واحد نتیجه با آن چیزی که در زمان اجرا رخ می‌دهد متفاوت است و با وجود صحیح بودن کدها آزمون واحد شکست می‌خورد.
چون Unit Test به صورت همزمان اجرا می‌شود و برای عملیات‌های پشت صحنه صبر نمی‌کند در نتیحه این آزمون واحد شکست می‌خورد.

آزمون واحد شکست خورده

یک راه حل تزریق BackgroundWorker به صورت وابستگی به ViewModel می‌باشد. همانطور که قبلا اشاره شده یکی از مزایای استفاده از تکنیک‌های تزریق وابستگی  سهولت Unit testing می‌باشد.
در نتیجه یک Interface عمومی و 2  پیاده سازی همزمان و غیر همزمان جهت استفاده در برنامه‌ی واقعی و آزمون واحد تهیه می‌کنیم : 
   public interface IWorker
    {
        void Run(DoWorkEventHandler doWork);
        void Run(DoWorkEventHandler doWork, RunWorkerCompletedEventHandler onComplete);
    }
جهت استفاده در برنامه‌ی واقعی : 
    public class AsyncWorker : IWorker
    {
        public void Run(DoWorkEventHandler doWork)
        {
            Run(doWork, null);
        }

        public void Run(DoWorkEventHandler doWork, RunWorkerCompletedEventHandler onComplete)
        {
            var backgroundWorker = new BackgroundWorker();
            backgroundWorker.DoWork += doWork;
            if (onComplete != null)
                backgroundWorker.RunWorkerCompleted += onComplete;
            backgroundWorker.RunWorkerAsync();
            

        }
    }
جهت اجرا در آزمون واحد : 
    public class SyncWorker : IWorker
    {
        #region IWorker Members

        public void Run(DoWorkEventHandler doWork)
        {
            Run(doWork, null);
        }

        public void Run(DoWorkEventHandler doWork, RunWorkerCompletedEventHandler onComplete)
        {
            Exception error = null;
            var doWorkEventArgs = new DoWorkEventArgs(null);
            try
            {
                doWork(this, doWorkEventArgs);
            }
            catch (Exception ex)
            {
                error = ex;
                throw;
            }
            finally
            {
                onComplete(this, new RunWorkerCompletedEventArgs(doWorkEventArgs.Result, error, doWorkEventArgs.Cancel));
            }
        }

        #endregion
    }
در نتیجه ViewModel اینگونه تغییر خواهد کرد :
    public class BackgroundWorkerViewModel : BaseViewModel
    {
        private readonly IWorker _worker;
        private List<string> _myData;

        public BackgroundWorkerViewModel(IWorker worker)
        {
            _worker = worker;
            LoadDataCommand = new RelayCommand(OnLoadData);
        }

        public RelayCommand LoadDataCommand { get; set; }

        public List<string> MyData
        {
            get { return _myData; }
            set
            {
                _myData = value;
                RaisePropertyChanged(() => MyData);
            }
        }

        public bool IsBusy { get; set; }

        private void OnLoadData()
        {
            IsBusy = true; // view is bound to IsBusy to show 'loading' message.

            _worker.Run(
                (sender, e) =>
                    {
                        MyData = new List<string> {"Test"};
                        Thread.Sleep(1000);
                    },
                (sender, e) => { IsBusy = false; });
        }
    }

کلاس مربوطه به آزمون واحد را مطابق با تغییرات ViewModel :
    [TestFixture]
    public class BackgroundWorkerViewModelTest
    {
        #region Setup/Teardown

        [SetUp]
        public void SetUp()
        {
            _backgroundWorkerViewModel = new BackgroundWorkerViewModel(new SyncWorker());
        }

        #endregion

        private BackgroundWorkerViewModel _backgroundWorkerViewModel;

        [Test]
        public void TestGetData()
        {
              
            _backgroundWorkerViewModel.LoadDataCommand.Execute(_backgroundWorkerViewModel);

            Assert.NotNull(_backgroundWorkerViewModel.MyData);
            Assert.IsNotEmpty(_backgroundWorkerViewModel.MyData);
        }
    }

اکنون اگر Unit Test را اجرا کنیم نتیجه اینگونه خواهد بود :




 
نظرات مطالب
آشنایی با Gridify
فرض کنید مدل زیر رو داریم
public class Order : AggregateRoot<int>
{
    private DateTime _orderDate;
    public Address Address { get; private set; }
    public int? GetBuyerId => _buyerId;
    public int? _buyerId;
    public OrderStatus OrderStatus { get; private set; }
    public  int _orderStatusId;
    private string _description;
    private bool _isDraft;


    private readonly List<OrderItem> _orderItems;
    public IReadOnlyCollection<OrderItem> OrderItems => _orderItems;

    protected Order()
    {
        _orderItems = new List<OrderItem>();
        _isDraft = false;
    }
    public Order(string userId, string userName, Address address,
        int? buyerId = null) : this()
    {
        _buyerId = buyerId;
        _orderStatusId = OrderStatus.Submitted.Id;
        _orderDate = DateTime.UtcNow;
        Address = address;
        //AddOrderStartedDomainEvent(userId, userName);
    }
}

public class OrderStatus : Enumeration
{
    public static OrderStatus Submitted = new OrderStatus(1, nameof(Submitted).ToLowerInvariant());
    public static OrderStatus AwaitingValidation = new OrderStatus(2, nameof(AwaitingValidation).ToLowerInvariant());
    public static OrderStatus StockConfirmed = new OrderStatus(3, nameof(StockConfirmed).ToLowerInvariant());
    public static OrderStatus Paid = new OrderStatus(4, nameof(Paid).ToLowerInvariant());
    public static OrderStatus Shipped = new OrderStatus(5, nameof(Shipped).ToLowerInvariant());
    public static OrderStatus Cancelled = new OrderStatus(6, nameof(Cancelled).ToLowerInvariant());
    public OrderStatus(int id, string name)
        : base(id, name)
    {
    }
}
حال چگونه میتوانیم از Order بر اساس OrderStatus.Id فیلترینگ انجام بدیم ؟
var query = _orderQueryRepository.GetAll(x => x._buyerId == 52).AsNoTracking();
var s = await query.GridifyAsync(request.queryFilter);
return s.Adapt<Paging<OrderQuery>>();
{
  "buyerid": 0,
  "queryFilter": {
    "page": 1,
    "pageSize": 5,
    "orderBy": "id",
    "filter": "Order_OrderStatus_Id==1"
  }
}
خروجی
 "message": "Property 'Order_OrderStatus_Id' not found.",

مطالب
توسعه سیستم مدیریت محتوای DNTCms - قسمت اول
قصد داریم طی یک سری مقالات به توسعه یک سیستم مدیریت محتوا بپردازیم. مسلما فاصله‌ی زمانی بین انتشار مقالات این سری، کمی زیاد خواهد بود. ولی سعی خواهیم کرد تا قدم به قدم و با تحلیل و توضیح کافی هر بخش به این هدف برسیم.
همکاران این قسمت:
پیشنیاز‌ها:
در زیر بخش هایی که برای این سیستم تا به امروز در نظر گرفتیم (مسلما با ارائه ایده‌ها و بازخورد‌های دوستان این امکانات دستخوش تغییر خواهند شد) ، به شرح زیر میباشد:
  • انجمن
  • ارتباط دوستی
  • سیستم ترفیع رتبه
  • Themeable
  • سیستم Following
  • صفحات داینامیک
  • سیستم پیام رسانی
  • امکان ساخت گروه‌های شخصی برای انتشار مطالب خود (توسط کاربران) با اعمال دسترسی مختلف
  • پیغام خصوصی
  • وبلاگ
  • نظرسنجی ها
  • مدیریت کاربران با دسترسی‌ها داینامیک
  • اخبار
  • آگهی ها
در این قسمت مدل‌های مربوط به بخش وبلاگ را بررسی میکنیم. ابتدا قصد داشتیم تا امکان نگارش یک پست توسط چندین کاربر را به طور همزمان، در سیستم قرار دهیم و ایده کار هم که توسط یکی دوستان (محمد شریفی) پیشنهاد شد به صورت زیر بود:
ابتدا کاربری به عنوان ایجاد کننده اصلی پست، بخش‌ها مختلفی را برای یک پست در نظر بگیرد و هر قسمت را به یک همکار نسبت دهد و با اتمام تمام بخش‌ها و اعلام آن توسط همکاران، کاربر اصلی ایجاد کننده پست بتواند بخش‌ها را باهم ادغام کند و برای آماده انتشار نهایی باشد.
ولی خب به نظر بنده الان سیستم‌های ارتباطی قدرتمندتری هم هست که همکاران در مورد نگارش یک پست به صورت مشارکتی بپردازند و صرفا در مرحله‌ی انتشار، این کاربران به عنوان کاربران همکار به پست منتشر شده نسبت داده شوند تا در زیر پست مشخصات کامل آنها قابل مشاهد باشد (راه حلی که پیاده سازی خواهیم کرد).
مدل‌های در نظر گرفته شده برای سیستم وبلاگ به صورت زیر می‌باشد:

مخزن برچسب ها
/// <summary>
    /// Represents the lable 
    /// </summary>
    public class Tag
    {
        #region Ctor
        /// <summary>
        /// Create one instance of <see cref="Tag"/>
        /// </summary>
        public Tag()
        {
            Id = SequentialGuidGenerator.NewSequentialGuid();
        }
        #endregion

        #region Properties
        /// <summary>
        /// sets or gets Tag's identifier
        /// </summary>
        public virtual Guid  Id { get; set; }
        /// <summary>
        /// sets or gets Tag's name
        /// </summary>
        public virtual string Name { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// sets or gets Tag's posts
        /// </summary>
        public virtual ICollection<BlogPost> BlogPosts { get; set; }
        #endregion
    }
کلاس بالا مشخص کننده‌ی مخزن برچسب‌های سیستم ما خواهد بود. جدول حاصل از این مدل با جداول سایر بخش‌ها که نیاز به داشتن برچسب خواهند داشت، ارتباط چند به چند خواهد داشت. برای این منظور همانطور که مشخص است در قسمت NavigationProperties یک لیست از BlogPost معرفی شده است و در ادامه خواهیم دید که لیستی از کلاس Tag هم در کلاس BlogPost معرفی خواهد شد.

مدل پیش نویس ها
  /// <summary>
    /// Represents the Post's Draft
    /// </summary>
    public  class BlogDraft
    {
        #region Ctor
        /// <summary>
        /// create one instance of <see cref="BlogDraft"/>
        /// </summary>
        public  BlogDraft()
        {
            Id = SequentialGuidGenerator.NewSequentialGuid();
        }
        #endregion

        #region Properties
        /// <summary>
        /// gets or sets Id of post's draft
        /// </summary>
        public virtual  Guid Id { get; set; }
        /// <summary>
        /// gets or sets body of post's draft
        /// </summary>
        public virtual  string Body { get; set; }
        /// <summary>
        /// gets or set title of post's draft
        /// </summary>
        public virtual  string Title { get; set; }
        /// <summary>
        /// gets or sets tags of post's draft that seperated using ','
        /// </summary>
        public virtual  string TagNames { get; set; }
        /// <summary>
        /// gets or sets value indicating whether this draft is ready to publish
        /// </summary>
        public virtual  bool IsReadyForPublish { get; set; }
        /// <summary>
        /// ges ro sets DateTime that this draft added
        /// </summary>
        public virtual  DateTime CreatedOn { get; set; }
        /// <summary>
        /// gets or sets information of User-Agent
        /// </summary>
        public virtual string Agent { get; set; }
        /// <summary>
        /// gets or sets date that this draft publish as ready
        /// </summary>
        public virtual DateTime? ReadyForPublishOn { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// gets or sets Id of user that he is owner of this draft
        /// </summary>
        public virtual  long OwnerId { get; set; }
        /// <summary>
        /// gets or sets user that he is owner of this draft
        /// </summary>
        public virtual  User Owner { get; set; }
        #endregion
    }
کلاس فوق برای ذخیره سازی پست‌های پیشنویس کاربران در نظر گرفته شده است. شاید بهتر بود این پیش نویس‌ها هم در همان مدل پستی که در ادامه مشاهده می‌کنید ادغام شوند. ولی این یک تصمیم شخصی برای این کار بوده و برای سیستی بزرگی که امکان دارد حذف و درج پیشنویس‌ها زیاد باشد و فرض بر اینکه long هم برای آی دی جواب گو نخواهد بود و نیاز است از Guid ای که به صورت متوالی افزایش میابد به منظور جلوگیری از Fragmentation، استفاده کرد. بقیه فیلد‌ها هم مشخص هستند و نیازی به توضیح اضافی نخواهد بود. مسلما هر کاربر می‌تواند چندین پیشنویس داشته باشد. لذا ارتباط یک به چند مابین کاربر و پیشنویس پست، خواهیم داشت.
مدل امتیاز دهی کاربران
/// <summary>
    /// Section of Rating
    /// </summary>
    public enum RatingSection
    {
        News,
        Announcement,
        ForumTopic,
        BlogComment,
        NewsComment,
        PollComment,
        AnnouncementComment,
        ForumPost,
        ...
    }


    /// <summary>
    /// Represents Rating Record regard by section type for Rating System
    /// </summary>
    public class UserRating
    {
        #region Ctor
        /// <summary>
        /// Create one instance of <see cref="UserRating"/>
        /// </summary>
        public UserRating()
        {
            Id = SequentialGuidGenerator.NewSequentialGuid();
        }
        #endregion

        #region Properties
        /// <summary>
        /// gets or sets Id of Rating Record
        /// </summary>
        public virtual Guid Id { get; set; }
        /// <summary>
        /// gets or sets value of rate
        /// </summary>
        public virtual double RatingValue { get; set; }
        /// <summary>
        /// gets or sets Section's Id 
        /// </summary>
        public virtual long SectionId { get; set; }
        /// <summary>
        /// gets or sets Section 
        /// </summary>
        public virtual RatingSection Section { get; set; }
        #endregion

        #region Navigation Properties
        /// <summary>
        /// gets or sets user that rate one section
        /// </summary>
        public virtual User Rater { get; set; }
        /// <summary>
        /// gets or sets Rater Id that rate one section
        /// </summary>
        public virtual long RaterId { get; set; }

        #endregion
    }
نوع داده‌ی شمارشی RatingSection مشخص کننده‌ی بخش‌های مختلف سیستم می‌باشد که نیاز به امتیاز دهی دارند. چون سیستم ما یک سیستم بسته می‌باشد و برای امتیاز دهی نیاز به احراز هویت خواهد بود، لذا باید از امتیاز دادن بیش از یک بار برای هر بخش جلوگیری کنیم. برای این کار می‌توان بین تمام بخش‌های موجود ارتباط‌های چند به چند در نظر گرفت. برای مثال یک ارتباط چند به چند بین کاربر و نظرات که مشخص کننده‌ی این است که یک کاربر می‌تواند به چند نظر امتیاز دهد و هر نظر هم میتواند چندین امتیاز دهنده داشته باشد ولی تعداد جداول بالا خواهد رفت و کار آن هم زیاد خواهد شد. برای این منظور میتوان یک جدول به مانند UserRating در نظر گرفت که صرفا با جدول کاربر ما یک ارتباط یک به چند دارد و با استفاده از خصوصیت RatingSection موجود در این کلاس وخصوصیت SectionId و همچنین در نظر گرفتن یک کلید منحصر به فرد بر روی خصوصیت‌های RatingSection ، RaterId و SectionId، از امتیاز دادن چند باره‌ی کاربر به یک بخش جلوگیری کرد.
/// <summary>
    /// Represent the rating as ComplexType
    /// </summary>
    [ComplexType]
    public class Rating
    {
        /// <summary>
        /// sets or gets total of rating
        /// </summary>
        public virtual double? TotalRating { get; set; }
        /// <summary>
        /// sets or gets rater's count
        /// </summary>
        public virtual long? RatersCount { get; set; }
        /// <summary>
        /// sets or gets average of rating
        /// </summary>
        public virtual double? AverageRating { get; set; }
    }
کلاس بالا یک ComplexType می‌باشد که در نهایت به هیچ جدولی مپ نخواهد شد و صرفا به منظور کپسوله کردن یکسری فیلد مورد استفاده قرار گرفته است و از این کلاس می‌توان در هر بخش که لازم است امتیاز دهی داشته باشیم، به عنوان یک خصوصیت، استفاده کرد.

کلاس پایه‌ی محتوا
/// <summary>
    /// Represents a base class for every content in system
    /// </summary>
    public abstract class BaseContent
    {
        #region Properties

        /// <summary>
        /// get or set identifier of record
        /// </summary>
        public virtual long Id { get; set; }
        /// <summary>
        /// gets or sets date of publishing content
        /// </summary>
        public virtual DateTime PublishedOn { get; set; }
        /// <summary>
        /// gets or sets Last Update's Date
        /// </summary>
        public virtual DateTime ModifiedOn { get; set; }
        /// <summary>
        /// gets or sets the blog pot body
        /// </summary>
        public virtual string Body { get; set; }
        /// <summary>
        /// gets or sets the content title
        /// </summary>
        public virtual string Title { get; set; }
        /// <summary>
        /// gets or sets value  indicating Custom Slug
        /// </summary>
        public virtual string SlugUrl { get; set; }
        /// <summary>
        /// gets or sets meta title for seo
        /// </summary>
        public virtual string MetaTitle { get; set; }
        /// <summary>
        /// gets or sets meta keywords for seo
        /// </summary>
        public virtual string MetaKeywords { get; set; }
        /// <summary>
        /// gets or sets meta description of the content
        /// </summary>
        public virtual string MetaDescription { get; set; }
        /// <summary>
        /// gets or sets 
        /// </summary>
        public virtual string FocusKeyword { get; set; }
        /// <summary>
        /// gets or sets value indicating whether the content use CanonicalUrl
        /// </summary>
        public virtual bool UseCanonicalUrl { get; set; }
        /// <summary>
        /// gets or sets CanonicalUrl That the Post Point to it
        /// </summary>
        public virtual string CanonicalUrl { get; set; }
        /// <summary>
        /// gets or sets value indicating whether the content user no Follow for Seo
        /// </summary>
        public virtual bool UseNoFollow { get; set; }
        /// <summary>
        /// gets or sets value indicating whether the content user no Index for Seo
        /// </summary>
        public virtual bool UseNoIndex { get; set; }
        /// <summary>
        /// gets or sets value indicating whether the content in sitemap
        /// </summary>
        public virtual bool IsInSitemap { get; set; }
        /// <summary>
        /// gets or sets a value indicating whether the content comments are allowed 
        /// </summary>
        public virtual bool AllowComments { get; set; }
        /// <summary>
        /// gets or sets a value indicating whether the content comments are allowed for anonymouses 
        /// </summary>
        public virtual bool AllowCommentForAnonymous { get; set; }
        /// <summary>
        /// gets or sets  viewed count by rss
        /// </summary>
        public virtual long ViewCountByRss { get; set; }
        /// <summary>
        /// gets or sets viewed count 
        /// </summary>
        public virtual long ViewCount { get; set; }
        /// <summary>
        /// Gets or sets the total number of  comments
        /// <remarks>The same as if we run Item.Comments.where(a=>a.Status==Status.Approved).Count()
        /// We use this property for performance optimization (no SQL command executed)
        /// </remarks>
        /// </summary>
        public virtual int ApprovedCommentsCount { get; set; }
        /// <summary>
        /// Gets or sets the total number of  comments
        /// <remarks>The same as if we run Item.Comments.where(a=>a.Status==Status.UnApproved).Count()
        /// We use this property for performance optimization (no SQL command executed)</remarks></summary>
        public virtual int UnApprovedCommentsCount { get; set; }
        /// <summary>
        /// gets or sets value  indicating whether the content is logical deleted or hidden
        /// </summary>
        public virtual bool IsDeleted { get; set; }
        /// <summary>
        /// gets or sets rating complex instance
        /// </summary>
        public virtual Rating Rating { get; set; }
        /// <summary>
        /// gets or sets value indicating whether the content show with rssFeed
        /// </summary>
        public virtual bool ShowWithRss { get; set; }
        /// <summary>
        /// gets or sets value indicating maximum days count that users can send comment
        /// </summary>
        public virtual int DaysCountForSupportComment { get; set; }
        /// <summary>
        /// gets or sets information of User-Agent
        /// </summary>
        public virtual string Agent { get; set; }
        /// <summary>
        /// gets or sets icon name with size 200*200 px for snippet 
        /// </summary>
        public virtual string SocialSnippetIconName { get; set; }
        /// <summary>
        /// gets or sets title for snippet
        /// </summary>
        public virtual string SocialSnippetTitle { get; set; }
        /// <summary>
        /// gets or sets description for snippet
        /// </summary>
        public virtual string SocialSnippetDescription { get; set; }
        /// <summary>
        /// gets or sets body of content's comment
        /// </summary>
        public virtual byte[] RowVersion { get; set; }
        /// <summary>
        /// gets or sets name of tags seperated by comma that assosiated with this content fo increase performance
        /// </summary>
        public virtual string TagNames { get; set; }
        /// <summary>
        /// gets or sets counter for Content's report
        /// </summary>
        public virtual int ReportsCount { get; set; }
        #endregion

        #region NavigationProperties

        /// <summary>
        /// get or set user that create this record
        /// </summary>
        public virtual User Author { get; set; }

        /// <summary>
        /// gets or sets Id of user that create this record
        /// </summary>
        public virtual long AuthorId { get; set; }
        /// <summary>
        /// get or set  the tags integrated with content
        /// </summary>
        public virtual ICollection<Tag> Tags { get; set; }
        #endregion

    }

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

نکته‌ای که وجود دارد فیلد‌های ApprovedCommentsCount  UnApprovedCommentsCount و TagNames می‌باشند که هنگام درج نظر جدید باید تعداد نظرات ذخیره شده را  ویرایش کنیم و هنگام ویرایش خود پست یا خبر با ... و یا حتی ویرایش خود تگ یا حذف آن تگ باید TagNames که لیست برچسب‌های محتوا را به صورت جدا شده با (,) از هم دیگر می‌باشد، ویرایش کنیم (جای بحث دارد).

مشخص است که هر یک از مطالب منتشر شده در بخش‌های وبلاگ، اخبار، نظرسنجی و آگهی‌ها، یک کابر ایجاد کننده (Author نامیده‌ایم) خواهد داشت و هر کاربر هم می‌تواند چندین مطلب را ایجاد کند. لذا رابطه‌ی یک به چند بین تمام این بخش‌ها مذکور و کاربر ایجاد خواهد شد.

مدل  LinkBack

 /// <summary>
    /// Represents link for implemention linkback
    /// </summary>
    public class LinkBack
    {

        #region Ctor
        /// <summary>
        /// create one instance of <see cref="LinkBack"/>
        /// </summary>
        public LinkBack()
        {
            CreatedOn = DateTime.Now;
            Id = SequentialGuidGenerator.NewSequentialGuid();
        }
        #endregion

        #region Properties
        /// <summary>
        /// gets or sets link's Id
        /// </summary>
        public virtual Guid Id { get; set; }
        /// <summary>
        /// gets or sets text for show Link
        /// </summary>
        public virtual string Title { get; set; }
        /// <summary>
        /// gets or sets link's address 
        /// </summary>
        public virtual string Url { get; set; }
        /// <summary>
        /// gets or set value indicating whether this link is internal o external
        /// </summary>
        public virtual LinkBackType Type { get; set; }
        /// <summary>
        /// gets or sets date that this record is added
        /// </summary>
        public virtual DateTime CreatedOn { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// gets or sets Post that associated
        /// </summary>
        public virtual BlogPost Post { get; set; }
        /// <summary>
        /// gets or sets id of Post that associated
        /// </summary>
        public virtual long PostId { get; set; }
        #endregion
    }


 /// <summary>
    /// represents Type of ReferrerLinks
    /// </summary>
    public enum LinkBackType
    {
        /// <summary>
        /// Internal link
        /// </summary>
        Internal,
        /// <summary>
        /// External Link
        /// </summary>
        External
    }

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

مدل پست ها

  /// <summary> 
    /// Represents a blog post
    /// </summary>
    public  class BlogPost : BaseContent
    {
        #region Ctor
        /// <summary>
        /// Create one Instance of <see cref="BlogPost"/>
        /// </summary>
        public BlogPost()
        {
            Rating = new Rating();
            PublishedOn = DateTime.Now;
        }
        #endregion

       #region Properties
        /// <summary>
        /// gets or sets Status of LinkBack Notifications
        /// </summary>
        public virtual LinkBackStatus LinkBackStatus { get; set; }
        #endregion

        #region NavigationProperties

        /// <summary>
        /// get or set  blog post's Reviews
        /// </summary>
        public virtual ICollection<BlogComment> Comments { get; set; }
        /// <summary>
        /// get or set collection of links that reference to this blog post
        /// </summary>
        public virtual ICollection<LinkBack> LinkBacks { get; set; }
        /// <summary>
        /// get or set Collection of Users that Contribute on this post
        /// </summary>
        public virtual ICollection<User> Contributors  { get; set; }
        
        #endregion
    }


  /// <summary>
    /// Represents Status for ReferrerLinks
    /// </summary>
    public enum  LinkBackStatus
    {
        [Display(Name ="غیرفعال")]
        Disable,
        [Display(Name = "فعال")]
        Enable,
        [Display(Name = "لینک‌ها داخلی")]
        JustInternal,
        [Display(Name = "لینک‌ها خارجی")]
        JustExternal
    }
 کلاس بالا نشان دهنده‌ی مدل پست‌های ما در سیستم می‌باشد که بیشتر خصوصیات خود را از کلاس پایه‌ی BaseContent به ارث برده و لازم به تکرار آنها نیست. به علاوه یکسری خصوصیات دیگر، از جلمه‌ی آنها یک لیست از کلاس LinkBack، لیستی از کاربران مشارکت کننده به منظور نمایش مشخصات آنها در زیر پست و لیستی از کلاس BlogComment که نشان دهنده‌ی نظرات پست می‌باشد، هم خواهد داشت. خصوصیتی از نوع داده‌ی LinkBackStatus هم صرفا به منظور تنظیمات مدیریتی در نظر گرفته شده است. 

کلاس پایه نظرات
  /// <summary>
    /// Represents a base class for every comment in system 
    /// </summary>

    public abstract class BaseComment
    {
        #region Properties
        /// <summary>
        /// get or set identifier of record
        /// </summary>
        public virtual long Id { get; set; }
        /// <summary>
        /// gets or sets date of creation 
        /// </summary>
        public virtual DateTime CreatedOn { get; set; }
        /// <summary>
        /// gets or sets displayName of this comment's Creator if  he/she is Anonymous
        /// </summary>
        public virtual string CreatorDisplayName { get; set; }
        /// <summary>
        /// gets or sets body of blog post's comment
        /// </summary>
        public virtual string Body { get; set; }
        /// <summary>
        /// gets or sets body of blog post's comment
        /// </summary>
        public virtual Rating Rating { get; set; }
        /// <summary>
        /// gets or sets informations of agent
        /// </summary>
        public virtual string UserAgent { get; set; }
        /// <summary>
        /// gets or sets siteUrl of Creator if he/she is Anonymous
        /// </summary>
        public virtual string SiteUrl { get; set; }
        /// <summary>
        /// gets or sets Email of Creator if he/she is anonymous
        /// </summary>
        public virtual string Email { get; set; }
        /// <summary>
        /// gets or sets status of comment
        /// </summary>
        public virtual CommentStatus Status { get; set; }
        /// <summary>
        /// gets or sets Ip Address of Creator
        /// </summary>
        public virtual string CreatorIp { get; set; }
        /// <summary>
        /// gets or sets datetime that is modified
        /// </summary>
        public virtual DateTime? ModifiedOn { get; set; }
        /// <summary>
        /// gets or sets counter for report this comment
        /// </summary>
        public virtual int ReportsCount { get; set; }
        #endregion

        #region NavigationProperties

        /// <summary>
        /// get or set user that create this record
        /// </summary>
        public virtual User Creator { get; set; }

        /// <summary>
        /// get or set Id of user that create this record
        /// </summary>
        public virtual long? CreatorId { get; set; }
        #endregion
    }

public enum CommentStatus
    {
        /* 0 - approved, 1 - pending, 2 - spam, -1 - trash */
        [Display(Name = "تأیید شده")]
        Approved = 0,
        [Display(Name = "در انتظار بررسی")]
        Pending = 1,
        [Display(Name = "جفنگ")]
        Spam = 2,
        [Display(Name = "زباله دان")]
        Trash = -1
    }
به مانند BaseContent که برای کپسوله کردن خصوصیات تکراری و نه برای اعمال ارث بری TPH یا TPT، کلاس BaseComment را در نظر گرفته‌ایم. نکته‌ی مهم این است که هر نظری یک درج کننده دارد. ولی اگر فیلد AllowCommentForAnonymous مربوط به مطلب True باشد، این امکان وجود خواهد داشت تا کاربران احراز هویت نشده نیز نظر ارسال کنند. لذا CreatorId در کلاس BaseComment به صورت Nullable در نظر گرفته شده است و یک سری خصوصیت‌ها از قبیل SiteUrl ، CreatorDisplayName ، Email برای کاربران احراز هویت نشده در نظر گرفته شده است. نوع شمارشی CommentStatus نیز برای اعمال مدیریتی در نظر گرفته شده است.
مدل نظرات پست ها
/// <summary>
    /// Represents a blog post's comment
    /// </summary>
    public class BlogComment : BaseComment
    {
        #region Ctor
        /// <summary>
        /// Create One Instance for <see cref="BlogComment"/>
        /// </summary>
        public BlogComment()
        {
            Rating = new Rating();
            CreatedOn = DateTime.Now;
        }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// gets or sets BlogComment's identifier for Replying and impelemention self referencing
        /// </summary>
        public virtual long? ReplyId { get; set; }
        /// <summary>
        /// gets or sets blog's comment for Replying and impelemention self referencing
        /// </summary>
        public virtual BlogComment Reply { get; set; }
        /// <summary>
        /// get or set collection of blog's comment for Replying and impelemention self referencing
        /// </summary>
        public virtual ICollection<BlogComment> Children { get; set; }
        /// <summary>
        /// gets or sets post that this comment sent to it
        /// </summary>
        public virtual BlogPost Post { get; set; }
        /// <summary>
        /// gets or sets post'Id that this comment sent to it
        /// </summary>
        public virtual long PostId { get; set; }
        #endregion
    }
کلاس بالا مشخص کننده‌ی نظرات پست‌های وبلاگ می‌باشند که بیشتر خصوصیت‌های خود را از کلاس پایه‌ی BaseComment به ارث برده است و همچنین ساختار سلسله مراتبی آن نیز  قابل مشاهده است. 
برای سیستم Report یا همان گزارش خطا هم سیستمی شبیه به امتیاز دهی ستاره‌ای در نظر گرفته ایم. برای اینکه مقاله خیلی طولانی شد، بهتر است در مقاله‌ای جدا کار را ادامه داد.
نتیجه این قسمت

نظرات مطالب
آشنایی با مفهوم Indexer در C#.NET
ممنون. Indexer مطلب جالبی است؛ ولی یک مورد کلا از زبان سی شارپ از قلم افتاده به نام indexed property
در حالت عادی می‌شود بر روی یک وهله از کلاس، Index اعمال کرد. اگر نخواهیم روی کل وهله اعمال شود چطور؟ برای مثال فقط روی یک خاصیت خاص.
پیاده سازی آن یک نکته کوچک دارد که به شرح زیر است:

using System;

namespace IndexedProperties
{
    public class Data
    {
        private int[] _localArray;
        private ArrayIndexer _arrayIndexer;

        public Data()
        {
            _localArray = new int[10];
            for (int i = 0; i < 10; i++)
                _localArray[i] = i + 1;
            _arrayIndexer = new ArrayIndexer(this);
        }
        
        public ArrayIndexer Number
        {
            get { return _arrayIndexer; }
        }       

        public class ArrayIndexer
        {
            private Data _arrayOwner;

            public ArrayIndexer(Data arrayOwner)
            {
                _arrayOwner = arrayOwner;
            }

            public int this[int index]
            {
                get { return _arrayOwner._localArray[index]; }
            }

            public int Length
            {
                get { return _arrayOwner._localArray.Length; }
            }
        }
    }


    class Program
    {
        static void Main(string[] args)
        {
            var data = new Data();
            for (int i = 0; i < 10; i++)
                Console.WriteLine(data.Number[i]);
        }
    }
}
در اینجا از کلاس‌های تودرتو برای پیاده سازی Indexed property استفاده شده است.
به این ترتیب تعاریف آرایه مورد استفاده نیازی نیست مستقیما در معرض دید قرار گیرد و همچنین read-only هم تعریف شده است. به علاوه اینکه indexerها محدود به int نیستند و برای مثال می‌توان از string و غیره نیز استفاده کرد.