نظرات مطالب
نحوه‌ی محاسبه‌ی هش کلمات عبور کاربران در ASP.NET Identity
روش تغییر «تعداد بار تکرار» الگوریتم هش کردن اطلاعات در ASP.NET Core Identity

کلیات مطلب عنوان شده‌ی در اینجا در ASP.NET Core Identity هم صادق است؛ اما به همراه امکانات تنظیمی بیشتر، مانند امکان تنظیم تعداد بار تکرار الگوریتم هش کردن کلمات عبور:
public void ConfigureServices(IServiceCollection services)
{
    services.Configure<PasswordHasherOptions>(options => options.IterationCount = 100_000);
که البته مقدار پیش‌فرض آن همان 100 هزار بار است که هرچقدر مقدار آن بیشتر باشد، کار brute force آن مشکل‌تر می‌شود.

نکته 1: تغییر این مقدار، مشکلی را برای کاربران فعلی ایجاد نمی‌کند؛ از این جهت که این تعداد بار نیز جزو اطلاعاتی است که به همراه هش نهایی در بانک اطلاعاتی ذخیره می‌شود و اگر مقدار آن‌را تغییر دادید، صرفا به کاربران جدید و یا کاربرانی که کلمه‌ی عبور خود را تغییر می‌دهند، اعمال خواهد شد.
نکته 2: این تغییر، زمانی مؤثر واقع خواهد شد که مقدار تعداد تکرار جدید، بیشتر از مقدار قبلی باشد. یعنی اگر مقدار جدیدی را مساوی 200 هزار اعمال کردید و بر اساس آن هش کلمات عبور تغییر یافت و سپس این مقدار را به 100 هزار تغییر دادید، چون عدد جدید کمتر است از عدد قبلی، ندید گرفته می‌شود.
نکته 3: بهتر است این عدد را بیش از اندازه بزرگ انتخاب نکنید؛ چون کار سرور را بیشتر کرده و همچنین پروسه‌ی لاگین را کند می‌کنید. زمان پیشنهادی برای اینکار، 1 ثانیه به ازای هر کلمه‌ی عبور است. یعنی عددی را انتخاب کنید که پس از انجام تمام تکرارها برای محاسبه‌ی هش، بیش از 1 ثانیه طول نکشد و بر این اساس، عدد 500 هزار تکرار، شاید انتخاب بهتری باشد.
مطالب
Soft Delete در Entity Framework 6
برای حذف نمودن یک رکورد از دیتابیس 2 راه وجود دارد : 1- حذف به صورت فیزیکی 2- حذف به صورت منطقی ( مورد بحث این مطلب )
در حذف رکورد به صورت منطقی، طراحان دیتابیس، فیلدی را با نام‌های متفاوتی همچون Flag , IsDeleted , IsActive , و غیره، در جداول ایجاد می‌نمایند. خوب، این روش مزایا و معایب خاص خودش را دارد. مثلا شما در هر پرس و جویی که ایجاد می‌نمایید، بایستی این مورد را چک نموده و رکوردهایی را فراخوانی نمایید که فیلد IsDeleted آن برابر با false باشد. و همچنین در زمان حذف رکورد، برنامه نویس بایستی از متد Update به جای حذف فیزیکی استفاده نماید که تمام این موارد حاکی از مشکلات خاص این روش است. 
در این مقاله سعی داریم که مشکلات ذکر شده در بالا را با ایجاد SoftDelete در EF 6 برطرف نماییم .*یکی از پیش نیاز‌های این پست مطالعه ( سری آموزشی EF CodeFirst ) در سایت جاری می‌باشد.
برای شروع، ما نیاز به داشتن یک Attribute برای مشخص ساختن موجودیت هایی داریم که بایستی بر روی آنها SoftDelete فعال گردد. پس برای اینکار کلاسی را به شکل زیر طراحی مینماییم:
using System.Data.Entity.Core.Metadata.Edm;
public class SoftDeleteAttribute : Attribute
    {
        public string ColumnName { get; set; }
        public SoftDeleteAttribute(string column)
        {
            ColumnName = column;
        }
        public static string GetSoftDeleteColumnName(EdmType type)
        {
            MetadataProperty column = type.MetadataProperties.Where(x => x.Name.EndsWith("customannotation:SoftDeleteColumnName")).SingleOrDefault();
            return column == null ? null : (string)column.Value;
        }
    }
توضیحات کد بالا: در متد سازنده، نام فیلدی را که قرار است بر روی آن SoftDelete به صورت اتوماتیک ایجاد شود، دریافت می‌نماییم و متد GetSoftDeleteColumnName در واقع با استفاده از متادیتاهایی که بر روی فیلد‌ها وجود دارد، فیلدی که انتهای نام آن متادیتای "customannotation:SoftDeleteColumnName" را دارد، انتخاب نموده و برگشت می‌دهد.
سؤال: متادیتای  "customannotation:SoftDeleteColumnName"  از کجا آمد؟ برای پاسخ به این سوال کافیست ادامه‌ی مطلب را کامل مطالعه نمایید.
حال این Attribute برای استفاده در موجودیت‌های ما آمده است. برای استفاده کافیست به روش زیر عمل نمایید .
    [SoftDelete("IsDeleted")]
    public class TblUser 
    {        
        [Key]
        public int TblUserID { get; set; }

        [MaxLength(30)]
        public string Name { get; set; }

        public bool IsDeleted { get; set; }
    }
برای معرفی این قابلیت جدید به EF 6 کافیست در DbContext برنامه در متد OnModelCreating به نحو زیر عمل نماییم.
 protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            var Conv = new AttributeToTableAnnotationConvention<SoftDeleteAttribute, string>(
                "SoftDeleteColumnName",
                (type, attribute) => attribute.Single().ColumnName);
            modelBuilder.Conventions.Add(Conv);

        }
در واقع ما در اینجا به Ef می‌گوییم که یک Annotation جدید، با نام SoftDeleteColumnName به Entity که توسط این Attribute مزین شده است، اضافه نماید و همچنین مقدار این Annotation را نام فیلدی که در متد سازنده SoftDeleteAttribute معرفی گردیده است قرار دهد.
برای اطمینان حاصل کردن از اینکه آیا Annotation جدید به مدل برنامه اضافه شده است یا نه کافیست بر روی فایل cs کانتکست DbContext، کلیک راست نموده و در منوی نمایش داده شده گزینه‌ی EntityFramework و سپس گزینه View Entity Data Model را انتخاب نمایید . مانند تصویر زیر:

در پنجره باز شده به قسمت سوم یعنی <StorageModels> مراجعه نمایید و بایستی گزینه زیر را مشاهده نمایید .

 <EntityType Name="TblUser" customannotation:SoftDeleteColumnName="IsDeleted">

تا اینجای کار ما توانستیم یک Annotation جدید را به Ef اضافه نماییم .

در مرحله بعد بایستی به Ef دستور دهیم که در تولید Query بر روی این Entity، این مورد را نیز لحاظ کند.

برای این کار کلاسی را ایجاد می‌نماییم که از اینترفیس IDbCommandTreeInterceptor ارث بری می‌نماید. مانند کد زیر :

public class SoftDeleteInterceptor : IDbCommandTreeInterceptor
    {
        public void TreeCreated(DbCommandTreeInterceptionContext interceptionContext)
        {
            if (interceptionContext.OriginalResult.DataSpace == System.Data.Entity.Core.Metadata.Edm.DataSpace.SSpace)
            {
                var QueryCommand = interceptionContext.Result as DbQueryCommandTree;
                if (QueryCommand != null)
                {
                    var newQuery = QueryCommand.Query.Accept(new SoftDeleteQueryVisitor());
                    interceptionContext.Result = new DbQueryCommandTree(QueryCommand.MetadataWorkspace, QueryCommand.DataSpace, newQuery);
                }
            }
       }
}

در ابتدا تشخیص داده می‌شود که نوع خروجی Query آیا از نوع Storage Model است . ( برای توضیحات بیشتر ) سپس پرس و جوی تولید شده را با استفاده از الگوی visitor تغییر داده و Query جدید را تولید نموده و در انتها Query جدیدی را به جای Query قبلی جایگزین می‌نماییم.

در اینجا ما نیاز به داشتن کلاس  SoftDeleteQueryVisitor  برای تغییر دادن Query و اضافه نمودن IsDeleted <>1 به Query می‌باشیم.

یک کلاس دیگری با نام  SoftDeleteQueryVisitor  به شکل زیر  به برنامه اضافه می‌نماییم.

  public class SoftDeleteQueryVisitor : DefaultExpressionVisitor
    {
        public override DbExpression Visit(DbScanExpression expression)
        {
            var column = SoftDeleteAttribute.GetSoftDeleteColumnName(expression.Target.ElementType);
            if (column!=null)
            {
                var Binding = DbExpressionBuilder.Bind(expression);
                return DbExpressionBuilder.Filter(Binding, DbExpressionBuilder.NotEqual(DbExpressionBuilder.Property(DbExpressionBuilder.Variable(Binding.VariableType, Binding.VariableName), column), DbExpression.FromBoolean(true)));
            }
            else
            {
                return base.Visit(expression);
            }
        }
    }
در متد Visit تشخیص داده می‌شود که آیا Query ساخته شده دارای customannotation:SoftDeleteColumnName است؟ چنانچه این Annotation را دارا باشد، نام فیلدی را که بالای Entity ذکر شده است، بازگشت می‌دهد و در خط بعدی، نام این فیلد را با مقدار مخالف True به Query تولید شده اضافه می‌نماید.

در نهایت برای اینکه EF تشخیص دهد که یک‌چنین Interceptor ایی وجود دارد، بایستی در کلاس DbContextConfig، کلاس SoftDeleteInterceptor را اضافه نماییم؛ همانند کد زیر:

 public class DbContextConfig : DbConfiguration
    {
        public DbContextConfig()
        {
             AddInterceptor(new SoftDeleteInterceptor());
        }
    }

تا اینجا در تمام Query‌های تولید شده بر روی Entity که با خاصیت SoftDelete مزین شده است، مقدار IsDeleted <> 1 را به صورت اتوماتیک اعمال می‌نماید. حتی به صورت هوشمند چنانچه این موجودیت در یک Join استفاده شده باشد این شرط را قبل از Join به Query تولید شده اضافه می‌نماید.

در مقاله بعدی در مورد تغییر کد Remove به کد Update توضیح داده خواهد شد.


برای مطالعه بیشتر

Entity Framework: Building Applications with Entity Framework 6

مطالب
پَرباد - آموزش پیاده‌سازی پرداخت آنلاین در دات نت - تنظیمات
در قسمت قبل یاد گرفتیم چگونه عملیات پرداخت را انجام دهیم. در این قسمت قصد داریم با تنظیمات پَرباد آشنا شویم.

این تنظیمات در حالت کلی شامل موارد زیر است:

  • درگاه‌ها (اجباری)
  • HttpContext (اجباری)
  • پایگاه داده (اجباری)
  • پیام‌ها (اختیاری)

روش‌های تنظیم:
  • وارد کردن تنظیمات به صورت ثابت (استاتیک)
  • تنظیم به صورت داینامیک (برای مثال استفاده از یک منبع، مانند پایگاه داده وب سایت شما)
  • تنظیم توسط اینترفیس مایکروسافت IConfiguration

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

برای روش اول، تنظیمات در حالت کلی به صورت زیر است:
(نمونه مثال در یک اپلیکیشن ASP.NET CORE)
using Parbad.Builder;

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddParbad()
         // .configurations
         // .configurations
         // .configurations
}
همانطور که می‌بینید، با استفاده از سرویس موجود در اپلیکیشن، به راحتی می‌توانید تنظیمات مورد نیاز را انجام دهید.

و برای روش دوم، تنظیمات در حالت کلی به صورت زیر است:
(نمونه مثال در یک اپلیکیشن ASP.NET MVC)
using Parbad.Builder;

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        ParbadBuilder.CreateDefaultBuilder()
                  // .configurations
                  // .configurations
                  // .configurations
    }
}


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

تنظیمات درگاه‌ها

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

روش اول: وارد کردن اطلاعات ثابت

نمونه کد‌های تنظیم درگاه بانک ملت:
services.AddParbad()
        .ConfigureGateways(gateways =>
        {
            gateways
                .AddMellat()
                .WithOptions(options =>
                {
                     options.TerminalId = 123;
                     options.UserName = "MyId";
                     options.UserPassword = "MyPassword";
                });
        });
نکته: تنظیم سایر درگاه‌ها نیز کاملا مشابه فرمت کد‌های بالا است. 

روش دوم: تنظیم به صورت داینامیک

برای تنظیم به صورت داینامیک، کلاسی را تعریف کنید که اینترفیس IParbadOptionsProvider را پیاده‌سازی می‌کند. مقدار T در این اینترفیس، معادل کلاس مورد نظر جهت تنظیم است.
مثال: تنظیم درگاه ملت توسط یک منبع:
ما قصد داریم اطلاعات مربوط به درگاه بانک ملت را از پایگاه داده فروشگاه خود دریافت کنیم. بنابراین یک منبع را به صورت زیر تعریف می‌کنیم:
public class MellatOptionsProvider : IParbadOptionsProvider<MellatGatewayOptions>
{
    private readonly IMySettingsService _settingsService;

    public MellatOptionsProvider(IMySettingsService settingsService)
    {
        _settingsService = settingsService;
    }

    public void Provide(MellatGatewayOptions options)
    {
        var settings = _settingsService.GetSettings();

        options.TerminalId = settings.TerminalId;
        options.UserName = settings.UserName;
        options.UserPassword = settings.UserPassword;
    }
}
کد بالا اطلاعات مربوط به درگاه بانک ملت را از پایگاه داده (وب سایت شما) دریافت کرده و سپس در متد Provide، آنها را نسبت می‌دهد.
نکته: همانطور که در مثال بالا می‌بینید، در تعریف یک منبع، شما همچنین قادر به تزریق وابستگی‌ها نیز هستید (در صورت نیاز). بدیهی است که در اینجا، اینترفیس IMySettingsService توسط تزریق وابستگی اپلیکیشن شما باید ثبت شده باشد، در غیر اینصورت پَرباد قادر به ساخت منبع شما نخواهد بود.
در نهایت منبع را به پَرباد معرفی می‌کنیم:
services.AddParbad()
        .ConfigureGateways(gateways =>
        {
            gateways
                .AddMellat()
                .WithOptionsProvider<MellatOptionsProvider>(ServiceLifetime.Transient);
        });
نکته: ServiceLifetime در اینجا تعیین کننده طول عمر منبع ما است.
نمونه مثال کامل را در اینجا می‌توانید پیدا کنید.

روش سوم: تنظیم توسط IConfiguration

اگر با اپلیکیشن‌های ASP.NET CORE آشنایی داشته باشید، پس قطعا IConfiguration را نیز می‌شناسید. این اینترفیس به شما کمک می‌کند تنظیمات مورد نیاز در یک اپلیکیشن را از منابع مختلفی (مانند فایل‌های JSON ) دریافت و استفاده کنید.
کد زیر نمونه تنظیم درگاه بانک ملت، با استفاده از IConfiguration و یک فایل JSON است.
services.AddParbad()
        .ConfigureGateways(gateways =>
        {
            gateways
                .AddMellat()
                .WithConfiguration(IConfiguration.GetSection("Mellat");
        });

و محتوای فایل JSON:
"Mellat": {
    "TerminalId": 123,
    "UserName": "MyUsername",
    "UserPassword": "MyPassword"
}



تنظیمات HttpContext

پَرباد برای تبادل اطلاعات با درگاه‌های بانکی، نیاز به یک HttpContext دارد.

ASP.NET WebForms, ASP.NET MVC
ParbadBuilder.CreateDefaultBuilder()
             .ConfigureHttpContext(builder => builder.UseOwinFromCurrentHttpContext());


در کد بالا، پَرباد HttpContext مورد نیاز خود را توسط Owin تامین می‌کند. متد UseOwin همچنین شامل گزینه‌های دیگری جهت تنظیمات بیشتر نیز می‌باشد.

ASP.NET CORE
services.AddParbad()
        .ConfigureHttpContext(builder => builder.UseDefaultAspNetCore());
در کد بالا، پَرباد از اینترفیس پیش فرض IHttpContextAccessor در اپلیکیشن ASP.NET CORE استفاده می‌کند.

نکته: اگر این اینترفیس قبلا توسط شما و یا اپلیکیشن شما ثبت شده باشد، پَرباد از آن استفاده خواهد کرد؛ در غیر اینصورت، کلاس پیش فرض HttpContextAccessor را به صورت خودکار جهت استفاده ثبت می‌کند.

تنظیمات پایگاه داده

پایگاه داده استفاده شده در پَرباد سیستم مشهور و شناخته شده‌ی EntityFrameworkCore است. این بدان معناست که شما می‌توانید پایگاه داده مورد نیاز پَرباد را توسط منابع بسیار مختلفی از جمله SQL Server, MySql, Oracle, SQLite و غیره تامین کنید.
SQL Server و InMemory به صورت پیش فرض با پکیج پَرباد در اپلیکیشن شما نصب خواهند شد. اما اگر نیاز به پایگاه داده‌ی دیگری دارید، می‌توانید آن را از بین تامین کننده‌های مختلف انتخاب، نصب و استفاده کنید.
نکته: پایگاه داده، برای مصرف و عملکرد داخلی پَرباد است و نه مصرف خارجی در اپلیکیشن شما. در واقع شما نیازی به داشتن اطلاعات درونی پایگاه داده پَرباد ندارید و موارد مهمی مانند کد رهگیری، شماره تراکنش بانکی، مبلغ، نام بانک و غیره را پس از هر عمل پرداخت می‌توانید توسط پَرباد دریافت کنید و در پایگاه داده خود برای فاکتور مورد نظر ذخیره کنید.

نمونه کد‌های تنظیم را در زیر می‌توانید مشاهده کنید:
SQL Server
services.AddParbad()
        .ConfigureStorage(builder => builder.UseParbadSqlServer("ConnectionString"));
نکته: همانطور که می‌دانید، متد اصلی دیگری به نام UseSqlServer وجود دارد. تفاوت آن با متد استفاده شده‌ی در کد بالا این است که UseParbadSqlServer ، به صورت خودکار Migration‌های مرتبط با پروژه پَرباد را نیز اعمال می‌کند. هر چند که این عمل توسط خود شما نیز امکان پذیر است.
In-Memory Database
services.AddParbad()
        .ConfigureStorage(builder => builder.UseInMemoryDatabase("MyMemoryName"));
نکته: اگر به هر دلیلی، سرور و یا وب سایت شما، ری‌استارت شود، اطلاعات موجود در این پایگاه داده ( In-Memory Database ) نیز از بین خواهند رفت. به عبارت دیگر، این پایگاه داده پایدار نیست و صرفا جهت اهداف تست از آن استفاده می‌شود.

تنظیمات پیام‌ها (اختیاری)

منظور از پیام‌ها، پیام‌های متنی‌ای است که پس از انجام عملیات‌های مختلف به شما بازگشت داده می‌شوند؛ برای مثال: پرداخت با موفقیت انجام شد.
شما می‌توانید این پیام‌ها را به شکل زیر تنظیم کنید:
services.AddParbad()
        .ConfigureMessages(options => 
        {
                options.PaymentSucceed = "Payment was successful.";
                options.PaymentFailed = "Payment was not successful.";
                // other messages...
        });

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

نمونه پروژه‌ها:
مقاله‌های مرتبط:
مطالب
ذخیره سازی تنظیمات برنامه‌های ASP.NET Core در بانک اطلاعاتی به کمک Entity Framework Core
در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایل‌های config » با مقدمات کار با فایل‌های تنظیمات برنامه و تامین کننده‌های مختلف آن‌ها آشنا شدیم. در این مطلب قصد داریم یک نمونه‌ی سفارشی تامین کننده‌های تنظیمات برنامه را بر اساس دریافت و ذخیره سازی اطلاعات در بانک اطلاعاتی، تهیه کنیم.


ساختار موجودیت تنظیمات برنامه

تنظیمات برنامه با هر قالبی که تهیه شوند، دست آخر به صورت یک <Dictionary<string,string در برنامه پردازش شده و قابل دسترسی می‌شوند. بنابراین موجودیت معادل این Dictionary را به صورت زیر تعریف می‌کنیم:
namespace DbConfig.Web.DomainClasses
{
    public class ConfigurationValue
    {
        public int Id { get; set; }
        public string Key { get; set; }
        public string Value { get; set; }
    }
}


ساختار Context برنامه و مقدار دهی اولیه‌ی آن

پس از تعریف موجودیت تنظیمات برنامه، آن‌را به صورت زیر به Context برنامه معرفی می‌کنیم:
    public class MyAppContext : DbContext, IUnitOfWork
    {
        public MyAppContext(DbContextOptions options) : base(options)
        { }

        public virtual DbSet<ConfigurationValue> Configurations { set; get; }
همچنین، برای مقدار دهی مقادیر اولیه‌ی تنظیمات برنامه نیز اینبار می‌توان به کمک متد HasData، به صورت زیر عمل کرد:
        protected override void OnModelCreating(ModelBuilder builder)
        {
            // it should be placed here, otherwise it will rewrite the following settings!
            base.OnModelCreating(builder);

            // Custom application mappings
            builder.Entity<ConfigurationValue>(entity =>
            {
                entity.Property(e => e.Key).HasMaxLength(450).IsRequired();
                entity.HasIndex(e => e.Key).IsUnique();
                entity.Property(e => e.Value).IsRequired();
                entity.HasData(new ConfigurationValue
                {
                    Id = 1,
                    Key = "key-1",
                    Value = "value_from_ef_1"
                });
                entity.HasData(new ConfigurationValue
                {
                    Id = 2,
                    Key = "key-2",
                    Value = "value_from_ef_2"
                });
            });
        }

ایجاد یک IConfigurationSource سفارشی مبتنی بر بانک اطلاعاتی

انواع و اقسام تامین کننده‌های تنظیمات برنامه در پروژه‌های ASP.NET Core، در حقیقت یک پیاده سازی سفارشی از اینترفیس IConfigurationSource هستند. به همین جهت در ادامه یک نمونه‌ی مبتنی بر EF Core آن را تهیه می‌کنیم:
    public class EFConfigurationSource : IConfigurationSource
    {
        private readonly IServiceProvider _serviceProvider;

        public EFConfigurationSource(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }

        public IConfigurationProvider Build(IConfigurationBuilder builder)
        {
            return new EFConfigurationProvider(_serviceProvider);
        }
    }
در اینجا چون می‌خواهیم به IUnitOfWork دسترسی پیدا کنیم، IServiceProvider را به سازنده‌ی این تامین کننده تزریق کرده‌ایم. کار اصلی ساخت آن نیز در متد Build، با ارائه‌ی یک IConfigurationProvider سفارشی انجام می‌شود. اینجا است که اطلاعات را از بانک اطلاعاتی خوانده و در اختیار سیستم تنظیمات برنامه قرار می‌دهیم:
    public class EFConfigurationProvider : ConfigurationProvider
    {
        private readonly IServiceProvider _serviceProvider;

        public EFConfigurationProvider(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
            ensureDatabaseIsCreated();
        }

        public override void Load()
        {
            using (var scope = _serviceProvider.CreateScope())
            {
                var uow = scope.ServiceProvider.GetRequiredService<IUnitOfWork>();
                this.Data?.Clear();
                this.Data = uow.Set<ConfigurationValue>()
                               .AsNoTracking()
                               .ToList()
                               .ToDictionary(c => c.Key, c => c.Value);
            }
        }

        private void ensureDatabaseIsCreated()
        {
            using (var scope = _serviceProvider.CreateScope())
            {
                var uow = scope.ServiceProvider.GetRequiredService<IUnitOfWork>();
                uow.Migrate();
            }
        }
    }
در ConfigurationProvider فوق، متد Load، در آغاز برنامه فراخوانی شده و در اینجا فرصت داریم تا خاصیت this.Data آن‌را که از نوع <Dictionary<string,string است، مقدار دهی کنیم. بنابراین از serviceProvider تزریق شده‌ی در سازنده‌ی کلاس استفاده کرده و به وهله‌ای از IUnitOfWork دسترسی پیدا می‌کنیم. سپس بر این اساس تمام رکوردهای جدول متناظر با ConfigurationValue را دریافت و توسط متد ToDictionary، تبدیل به ساختار مدنظر خاصیت this.Data می‌کنیم.
در اینجا فراخوانی متد ensureDatabaseIsCreated را نیز مشاهده می‌کنید. کلاس EFConfigurationProvider در آغاز برنامه و پیش از هر عمل دیگری وهله سازی شده و سپس متد Load آن فراخوانی می‌شود. به همین جهت نیاز است یا پیشتر، بانک اطلاعاتی را توسط دستورات Migration ایجاد کرده باشید و یا متد ensureDatabaseIsCreated، اطلاعات Migration موجود را به بانک اطلاعاتی برنامه اعمال می‌کند.


معرفی EFConfigurationSource به برنامه

جهت معرفی ساده‌تر EFConfigurationSource تهیه شده، ابتدا یک متد الحاقی را بر اساس آن تهیه می‌کنیم:
    public static class EFExtensions
    {
        public static IConfigurationBuilder AddEFConfig(this IConfigurationBuilder builder,
            IServiceProvider serviceProvider)
        {
            return builder.Add(new EFConfigurationSource(serviceProvider));
        }
    }
سپس می‌توان این متد AddEFConfig را به صورت زیر به تنظیمات برنامه در کلاس Startup اضافه و معرفی کرد:
namespace DbConfig.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IUnitOfWork, MyAppContext>();
            services.AddScoped<IConfigurationValuesService, ConfigurationValuesService>();

            var connectionString = Configuration.GetConnectionString("SqlServerConnection")
                     .Replace("|DataDirectory|", Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "app_data"));
            services.AddDbContext<MyAppContext>(options =>
                    {
                        options.UseSqlServer(
                            connectionString,
                            dbOptions =>
                                {
                                    var minutes = (int)TimeSpan.FromMinutes(3).TotalSeconds;
                                    dbOptions.CommandTimeout(minutes);
                                    dbOptions.EnableRetryOnFailure();
                                });
                    });

            var serviceProvider = services.BuildServiceProvider();
            var configuration = new ConfigurationBuilder()
                                       .AddConfiguration(Configuration) // Adds all of the existing configurations
                                       .AddEFConfig(serviceProvider)
                                       .Build();
            services.AddSingleton<IConfigurationRoot>(sp => configuration); // Replace
            services.AddSingleton<IConfiguration>(sp => configuration); // Replace
در اینجا ابتدا نیاز است یک ConfigurationBuilder جدید را ایجاد کنیم تا بتوان AddEFConfig را بر روی آن فراخوانی کرد. در این بین، خود برنامه نیز تعدادی تامین کننده‌ی تنظیمات پیش‌فرض را نیز دارد که قصد نداریم سبب پاک شدن آن‌ها شویم. به همین جهت آن‌ها را توسط متد AddConfiguration، افزوده‌ایم. پس از تعریف این ConfigurationBuilder جدید، نیاز است آن‌را جایگزین IConfiguration و IConfigurationRoot پیش‌فرض برنامه کنیم که روش آن‌را در دو متد services.AddSingleton ملاحظه می‌کنید.
همچنین روش دسترسی به serviceProvider مورد نیاز AddEFConfig، توسط متد services.BuildServiceProvider نیز در کدهای فوق مشخص است. به همین جهت مجبور شدیم این تعریف را در اینجا قرار دهیم و گرنه می‌شد از کلاس Program و یا حتی سازنده‌ی کلاس Startup نیز استفاده کرد. مشکل این دو مکان عدم دسترسی به سرویس IUnitOfWork و سایر تنظیمات برنامه است.


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

اگر به قسمت «ساختار Context برنامه و مقدار دهی اولیه‌ی آن» مطلب جاری دقت کرده باشید، دو کلید پیش‌فرض در اینجا ثبت شده‌اند. به همین جهت در ادامه با تزریق سرویس IConfiguration به سازنده‌ی یک کنترلر، سعی در خواندن مقادیر آن‌ها خواهیم کرد:
namespace DbConfig.Web.Controllers
{
    public class HomeController : Controller
    {
        private readonly IConfiguration _configuration;

        public HomeController(IConfiguration configuration)
        {
            _configuration = configuration;
        }

        public IActionResult Index()
        {
            return Json(
                new
                {
                    key1 = _configuration["key-1"],
                    key2 = _configuration["key-2"]
                });
        }
با این خروجی:



به روز رسانی بانک اطلاعاتی برنامه و بارگذاری مجدد اطلاعات IConfiguration

فرض کنید توسط سرویسی، اطلاعات جدول ConfigurationValue را تغییر داده‌اید. نکته‌ی مهم اینجا است که اینکار سبب فراخوانی مجدد متد Load کلاس EFConfigurationProvider نخواهد شد و عملا این تغییرات در سراسر برنامه توسط تزریق اینترفیس IConfiguration قابل دسترسی نخواهند بود (مگر اینکه برنامه مجددا ری‌استارت شود). نکته‌ی به روز رسانی این اطلاعات به صورت زیر است:
    public class ConfigurationValuesService : IConfigurationValuesService
    {
        private readonly IConfiguration _configuration;

        public ConfigurationValuesService(IConfiguration configuration)
        {
            _configuration = configuration;
        }

        private void reloadEFConfigurationProvider()
        {
            ((IConfigurationRoot)_configuration).Reload();
        }
در جائیکه نیاز است پس از به روز رسانی بانک اطلاعاتی، تنظیمات برنامه را نیز بارگذاری مجدد کنید، ابتدا اینترفیس IConfiguration را به سازنده‌ی آن تزریق کرده و سپس به نحو فوق، متد Reload را فراخوانی کنید. اینکار سبب می‌شود تا یکبار دیگری متد Load کلاس EFConfigurationProvider نیز فراخوانی شود که باعث بارگذاری مجدد تنظیمات برنامه خواهد شد.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: EFCoreDbConfig.zip
مطالب
طراحی افزونه پذیر با ASP.NET MVC 4.x/5.x - قسمت اول
در طی چند قسمت، نحوه‌ی طراحی یک سیستم افزونه پذیر را با ASP.NET MVC بررسی خواهیم کرد. عناوین مواردی که در این سری پیاده سازی خواهند شد به ترتیب ذیل هستند:
1- چگونه Area‌های استاندارد را تبدیل به یک افزونه‌ی مجزا و منتقل شده‌ی به یک اسمبلی دیگر کنیم.
2- چگونه ساختار پایه‌ای را جهت تامین نیازهای هر افزونه جهت تزریق وابستگی‌ها تا ثبت مسیریابی‌ها و امثال آن تدارک ببینیم.
3- چگونه فایل‌های CSS ، JS و همچنین تصاویر ثابت هر افزونه را داخل اسمبلی آن قرار دهیم تا دیگر نیازی به ارائه‌ی مجزای آ‌ن‌ها نباشد.
4- چگونه Entity Framework Code-First را با این طراحی یکپارچه کرده و از آن جهت یافتن خودکار مدل‌ها و موجودیت‌های خاص هر افزونه استفاده کنیم؛ به همراه مباحث Migrations خودکار و همچنین پیاده سازی الگوی واحد کار.


در مطلب جاری، موارد اول و دوم بررسی خواهند شد. پیشنیازهای آن مطالب ذیل هستند:
الف) منظور از یک Area چیست؟
ب) توزیع پروژه‌های ASP.NET MVC بدون ارائه فایل‌های View آن
ج) آشنایی با تزریق وابستگی‌ها در ASP.NET MVC و همچنین اصول طراحی یک سیستم افزونه پذیر به کمک StructureMap
د) آشنایی با رخدادهای Build


تبدیل یک Area به یک افزونه‌ی مستقل

روش‌های زیادی برای خارج کردن Areaهای استاندارد ASP.NET MVC از یک پروژه و قرار دادن آن‌ها در اسمبلی‌های دیگر وجود دارند؛ اما در حال حاضر تنها روشی که نگهداری می‌شود و همچنین اعضای آن همان اعضای تیم نیوگت و ASP.NET MVC هستند، همان روش استفاده از Razor Generator است.
بنابراین ساختار ابتدایی پروژه‌ی افزونه پذیر ما به صورت ذیل خواهد بود:
1) ابتدا افزونه‌ی Razor Generator را نصب کنید.
2) سپس یک پروژه‌ی معمولی ASP.NET MVC را آغاز کنید. در این سری نام MvcPluginMasterApp برای آن در نظر گرفته شده‌است.
3) در ادامه یک پروژه‌ی معمولی دیگر ASP.NET MVC را نیز به پروژه‌ی جاری اضافه کنید. برای مثال نام آن در اینجا MvcPluginMasterApp.Plugin1 تنظیم شده‌است.
4) به پروژه‌ی MvcPluginMasterApp.Plugin1 یک Area جدید و معمولی را به نام NewsArea اضافه کنید.
5) از پروژه‌ی افزونه، تمام پوشه‌های غیر Area را حذف کنید. پوشه‌های Controllers و Models و Views حذف خواهند شد. همچنین فایل global.asax آن‌را نیز حذف کنید. هر افزونه، کنترلرها و Viewهای خود را از طریق Area مرتبط دریافت می‌کند و در این حالت دیگر نیازی به پوشه‌های Controllers و Models و Views واقع شده در ریشه‌ی اصلی پروژه‌ی افزونه نیست.
6) در ادامه کنسول پاور شل نیوگت را باز کرده و دستور ذیل را صادر کنید:
  PM> Install-Package RazorGenerator.Mvc
این دستور را باید یکبار بر روی پروژه‌ی اصلی و یکبار بر روی پروژه‌ی افزونه، اجرا کنید.


همانطور که در تصویر نیز مشخص شده‌است، برای اجرای دستور نصب RazorGenerator.Mvc نیاز است هربار پروژه‌ی پیش فرض را تغییر دهید.
7) اکنون پس از نصب RazorGenerator.Mvc، نوبت به اجرای آن بر روی هر دو پروژه‌ی اصلی و افزونه است:
  PM> Enable-RazorGenerator
بدیهی است این دستور را نیز باید همانند تصویر فوق، یکبار بر روی پروژه‌ی اصلی و یکبار بر روی پروژه‌ی افزونه اجرا کنید.
همچنین هربار که View جدیدی اضافه می‌شود نیز باید این‌کار را تکرار کنید یا اینکه مطابق شکل زیر، به خواص View جدید مراجعه کرده و Custom tool آن‌را به صورت دستی به RazorGenerator تنظیم نمائید. دستور Enable-RazorGenerator این‌کار را به صورت خودکار انجام می‌دهد.


تا اینجا موفق شدیم Viewهای افزونه را داخل فایل dll آن مدفون کنیم. به این ترتیب با کپی کردن افزونه به پوشه‌ی bin پروژه‌ی اصلی، دیگر نیازی به ارائه‌ی فایل‌های View آن نیست و تمام اطلاعات کنترلرها، مدل‌ها و Viewها به صورت یکجا از فایل dll افزونه‌ی ارائه شده خوانده می‌شوند.


کپی کردن خودکار افزونه به پوشه‌ی Bin پروژه‌ی اصلی

پس از اینکه ساختار اصلی کار شکل گرفت، هربار پس از کامپایل افزونه (یا افزونه‌ها)، نیاز است فایل‌های پوشه‌ی bin آن‌را به پوشه‌ی bin پروژه‌ی اصلی کپی کنیم (پروژه‌ی اصلی در این حالت هیچ ارجاع مستقیمی را به افزونه‌ی جدید نخواهد داشت). برای خودکار سازی این کار، به خواص پروژه‌ی افزونه مراجعه کرده و قسمت Build events آن‌را به نحو ذیل تنظیم کنید:


در اینجا دستور ذیل در قسمت Post-build event نوشته شده است:
 Copy "$(ProjectDir)$(OutDir)$(TargetName).*" "$(SolutionDir)MvcPluginMasterApp\bin\"
و سبب خواهد شد تا پس از هر کامپایل موفق، فایل‌های اسمبلی افزونه به پوشه‌ی bin پروژه‌ی MvcPluginMasterApp به صورت خودکار کپی شوند.


تنظیم فضاهای نام کلیه مسیریابی‌های پروژه

در همین حالت اگر پروژه را اجرا کنید، موتور ASP.NET MVC به صورت خودکار اطلاعات افزونه‌ی کپی شده به پوشه‌ی bin را دریافت و به Application domain جاری اعمال می‌کند؛ برای اینکار نیازی به کد نویسی اضافه‌تری نیست و خودکار است. برای آزمایش آن فقط کافی است یک break point را داخل کلاس RazorGeneratorMvcStart افزونه قرار دهید.
اما ... پس از اجرا، بلافاصله پیام تداخل فضاهای نام را دریافت می‌کنید. خطاهای حاصل عنوان می‌کند که در App domain جاری، دو کنترلر Home وجود دارند؛ یکی در پروژه‌ی اصلی و دیگری در پروژه‌ی افزونه و مشخص نیست که مسیریابی‌ها باید به کدامیک ختم شوند.
برای رفع این مشکل، به فایل NewsAreaAreaRegistration.cs پروژه‌ی افزونه مراجعه کرده و مسیریابی آن‌را به نحو ذیل تکمیل کنید تا فضای نام اختصاصی این Area صریحا مشخص گردد.
using System.Web.Mvc;
 
namespace MvcPluginMasterApp.Plugin1.Areas.NewsArea
{
    public class NewsAreaAreaRegistration : AreaRegistration
    {
        public override string AreaName
        {
            get
            {
                return "NewsArea";
            }
        }
 
        public override void RegisterArea(AreaRegistrationContext context)
        {
            context.MapRoute(
                "NewsArea_default",
                "NewsArea/{controller}/{action}/{id}",
                // تکمیل نام کنترلر پیش فرض
                new { controller = "Home", action = "Index", id = UrlParameter.Optional },
                // مشخص کردن فضای نام مرتبط جهت جلوگیری از تداخل با سایر قسمت‌های برنامه
                namespaces: new[] { string.Format("{0}.Controllers", this.GetType().Namespace) }
            );
        }
    }
}
همینکار را باید در پروژه‌ی اصلی و هر پروژه‌ی افزونه‌ی جدیدی نیز تکرار کرد. برای مثال به فایل RouteConfig.cs پروژه‌ی اصلی مراجعه کرده و تنظیم ذیل را اعمال نمائید:
using System.Web.Mvc;
using System.Web.Routing;
 
namespace MvcPluginMasterApp
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
                // مشخص کردن فضای نام مرتبط جهت جلوگیری از تداخل با سایر قسمت‌های برنامه
                namespaces: new[] { string.Format("{0}.Controllers", typeof(RouteConfig).Namespace) }
            );
        }
    }
}
بدون تنظیم فضاهای نام هر مسیریابی، امکان استفاده‌ی بهینه و بدون خطا از Areaها وجود نخواهد داشت.


طراحی قرارداد پایه افزونه‌ها

تا اینجا با نحوه‌ی تشکیل ساختار هر پروژه‌ی افزونه آشنا شدیم. اما هر افزونه در آینده نیاز به مواردی مانند منوی اختصاصی در منوی اصلی سایت، تنظیمات مسیریابی اختصاصی، تنظیمات EF و  امثال آن نیز خواهد داشت. به همین منظور، یک پروژه‌ی class library جدید را به نام MvcPluginMasterApp.PluginsBase آغاز کنید.
سپس قرار داد IPlugin را به نحو ذیل به آن اضافه نمائید:
using System;
using System.Reflection;
using System.Web.Optimization;
using System.Web.Routing;
using StructureMap;
 
namespace MvcPluginMasterApp.PluginsBase
{
    public interface IPlugin
    {
        EfBootstrapper GetEfBootstrapper();
        MenuItem GetMenuItem(RequestContext requestContext);
        void RegisterBundles(BundleCollection bundles);
        void RegisterRoutes(RouteCollection routes);
        void RegisterServices(IContainer container);
    }
 
    public class EfBootstrapper
    {
        /// <summary>
        /// Assemblies containing EntityTypeConfiguration classes.
        /// </summary>
        public Assembly[] ConfigurationsAssemblies { get; set; }
 
        /// <summary>
        /// Domain classes.
        /// </summary>
        public Type[] DomainEntities { get; set; }
 
        /// <summary>
        /// Custom Seed method.
        /// </summary>
        //public Action<IUnitOfWork> DatabaseSeeder { get; set; }
    }
 
    public class MenuItem
    {
        public string Name { set; get; }
        public string Url { set; get; }
    }
}
پروژه‌ی این قرارداد برای کامپایل شدن، نیاز به بسته‌های نیوگت ذیل دارد:
PM> install-package EntityFramework
PM> install-package Microsoft.AspNet.Web.Optimization
PM> install-package structuremap.web
همچنین باید به صورت دستی، در قسمت ارجاعات پروژه، ارجاعی را به اسمبلی استاندارد System.Web نیز به آن اضافه نمائید.


توضیحات قرار داد IPlugin

از این پس هر افزونه باید دارای کلاسی باشد که از اینترفیس IPlugin مشتق می‌شود. برای مثال فعلا کلاس ذیل را به افزونه‌ی پروژه اضافه نمائید:
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using MvcPluginMasterApp.PluginsBase;
using StructureMap;
 
namespace MvcPluginMasterApp.Plugin1
{
    public class Plugin1 : IPlugin
    {
        public EfBootstrapper GetEfBootstrapper()
        {
            return null;
        }
 
        public MenuItem GetMenuItem(RequestContext requestContext)
        {
            return new MenuItem
            {
                Name = "Plugin 1",
                Url = new UrlHelper(requestContext).Action("Index", "Home", new { area = "NewsArea" })
            };
        }
 
        public void RegisterBundles(BundleCollection bundles)
        {
            //todo: ...
        }
 
        public void RegisterRoutes(RouteCollection routes)
        {
            //todo: add custom routes.
        }
 
        public void RegisterServices(IContainer container)
        {
            // todo: add custom services.
 
            container.Configure(cfg =>
            {
                //cfg.For<INewsService>().Use<EfNewsService>();
            });
        }
    }
}
در قسمت جاری فقط از متد GetMenuItem آن استفاده خواهیم کرد. در قسمت‌های بعد، تنظیمات EF، تنظیمات مسیریابی‌ها و Bundling و همچنین ثبت سرویس‌های افزونه را نیز بررسی خواهیم کرد.
برای اینکه هر افزونه در منوی اصلی ظاهر شود، نیاز به یک نام، به همراه آدرسی به صفحه‌ی اصلی آن خواهد داشت. به همین جهت در متد GetMenuItem نحوه‌ی ساخت آدرسی را به اکشن متد Index کنترلر Home واقع در Area‌ایی به نام NewsArea، مشاهده می‌کنید.


بارگذاری و تشخیص خودکار افزونه‌ها

پس از اینکه هر افزونه دارای کلاسی مشتق شده از قرارداد IPlugin شد، نیاز است آن‌ها را به صورت خودکار یافته و سپس پردازش کنیم. این‌کار را به کتابخانه‌ی StructureMap واگذار خواهیم کرد. برای این منظور پروژه‌ی جدیدی را به نام MvcPluginMasterApp.IoCConfig آغاز کرده و سپس تنظیمات آن‌را به نحو ذیل تغییر دهید:
using System;
using System.IO;
using System.Threading;
using System.Web;
using MvcPluginMasterApp.PluginsBase;
using StructureMap;
using StructureMap.Graph;
 
namespace MvcPluginMasterApp.IoCConfig
{
    public static class SmObjectFactory
    {
        private static readonly Lazy<Container> _containerBuilder =
            new Lazy<Container>(defaultContainer, LazyThreadSafetyMode.ExecutionAndPublication);
 
        public static IContainer Container
        {
            get { return _containerBuilder.Value; }
        }
 
        private static Container defaultContainer()
        {
            return new Container(cfg =>
            {
                cfg.Scan(scanner =>
                {
                    scanner.AssembliesFromPath(
                        path: Path.Combine(HttpRuntime.AppDomainAppPath, "bin"),
                            // یک اسمبلی نباید دوبار بارگذاری شود
                        assemblyFilter: assembly =>
                        {
                            return !assembly.FullName.Equals(typeof(IPlugin).Assembly.FullName);
                        });
 
                    scanner.WithDefaultConventions(); //Connects 'IName' interface to 'Name' class automatically.
                    scanner.AddAllTypesOf<IPlugin>().NameBy(item => item.FullName);
                });
            });
        }
    }
}
این پروژه‌ی class library جدید برای کامپایل شدن نیاز به بسته‌های نیوگت ذیل دارد:
PM> install-package EntityFramework
PM> install-package structuremap.web
همچنین باید به صورت دستی، در قسمت ارجاعات پروژه، ارجاعی را به اسمبلی استاندارد System.Web نیز به آن اضافه نمائید.

کاری که در کلاس SmObjectFactory انجام شده، بسیار ساده است. مسیر پوشه‌ی Bin پروژه‌ی اصلی به structuremap معرفی شده‌است. سپس به آن گفته‌ایم که تنها اسمبلی‌هایی را که دارای اینترفیس IPlugin هستند، به صورت خودکار بارگذاری کن. در ادامه تمام نوع‌های IPlugin را نیز به صورت خودکار یافته و در مخزن تنظیمات خود، اضافه کن.


تامین نیازهای مسیریابی و Bundling هر افزونه به صورت خودکار

در ادامه به پروژه‌ی اصلی مراجعه کرده و در پوشه‌ی App_Start آن کلاس ذیل را اضافه کنید:
using System.Linq;
using System.Web.Optimization;
using System.Web.Routing;
using MvcPluginMasterApp;
using MvcPluginMasterApp.IoCConfig;
using MvcPluginMasterApp.PluginsBase;
 
[assembly: WebActivatorEx.PostApplicationStartMethod(typeof(PluginsStart), "Start")]
 
namespace MvcPluginMasterApp
{
    public static class PluginsStart
    {
        public static void Start()
        {
            var plugins = SmObjectFactory.Container.GetAllInstances<IPlugin>().ToList();
            foreach (var plugin in plugins)
            {
                plugin.RegisterServices(SmObjectFactory.Container);
                plugin.RegisterRoutes(RouteTable.Routes);
                plugin.RegisterBundles(BundleTable.Bundles);
            }
        }
    }
}
بدیهی است در این حالت نیاز است ارجاعی را به پروژه‌ی MvcPluginMasterApp.PluginsBase به پروژه‌ی اصلی اضافه کنیم.
دراینجا با استفاده از کتابخانه‌ای به نام WebActivatorEx (که باز هم توسط نویسندگان اصلی Razor Generator تهیه شده‌است)، یک متد PostApplicationStartMethod سفارشی را تعریف کرده‌ایم.
مزیت استفاده از اینکار این است که فایل Global.asax.cs برنامه شلوغ نخواهد شد. در غیر اینصورت باید تمام این کدها را در انتهای متد Application_Start قرار می‌دادیم.
در اینجا با استفاده از structuremap، تمام افزونه‌های موجود به صورت خودکار بررسی شده و سپس پیشنیازهای مسیریابی و Bundling و همچنین تنظیمات IoC Container مورد نیاز آن‌ها به هر افزونه به صورت مستقل، تزریق خواهد شد.


اضافه کردن منو‌های خودکار افزونه‌ها به پروژه‌ی اصلی

پس از اینکه کار پردازش اولیه‌ی IPluginها به پایان رسید، اکنون نوبت به نمایش آدرس اختصاصی هر افزونه در منوی اصلی سایت است. برای این منظور فایل جدیدی را به نام PluginsMenu.cshtml_، در پوشه‌ی shared پروژه‌ی اصلی اضافه کنید؛ با این محتوا:
@using MvcPluginMasterApp.IoCConfig
@using MvcPluginMasterApp.PluginsBase
@{
    var plugins = SmObjectFactory.Container.GetAllInstances<IPlugin>().ToList();
}
 
@foreach (var plugin in plugins)
{
    var menuItem = plugin.GetMenuItem(this.Request.RequestContext);
    <li>
        <a href="@menuItem.Url">@menuItem.Name</a>
    </li>
}
در اینجا تمام افزونه‌ها به کمک structuremap یافت شده و سپس آیتم‌های منوی آن‌ها به صورت خودکار دریافت و اضافه می‌شوند.
سپس به فایل Layout.cshtml_ پروژه‌ی اصلی مراجعه و توسط فراخوانی Html.RenderPartial، آن‌را در بین سایر آیتم‌های منوی اصلی اضافه می‌کنیم:
<div class="navbar navbar-inverse navbar-fixed-top">
    <div class="container">
        <div class="navbar-header">
            <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            @Html.ActionLink("MvcPlugin Master App", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" })
        </div>
        <div class="navbar-collapse collapse">
            <ul class="nav navbar-nav">
                <li>@Html.ActionLink("Master App/Home", "Index", "Home", new {area = ""}, null)</li>
                @{ Html.RenderPartial("_PluginsMenu"); }
            </ul>
        </div>
    </div>
</div>
اکنون اگر پروژه را اجرا کنیم، یک چنین شکلی را خواهد داشت:



بنابراین به صورت خلاصه

1) هر افزونه، یک پروژه‌ی کامل ASP.NET MVC است که پوشه‌های ریشه‌ی اصلی آن حذف شده‌اند و اطلاعات آن توسط یک Area جدید تامین می‌شوند.
2) تنظیم فضای نام مسیریابی‌های تمام پروژه‌ها را فراموش نکنید. در غیر اینصورت شاهد تداخل پردازش کنترلرهای هم نام خواهید بود.
3) جهت سهولت کار، می‌توان فایل‌های bin هر افزونه را توسط رخداد post-build، به پوشه‌ی bin پروژه‌ی اصلی کپی کرد.
4) Viewهای هر افزونه توسط Razor Generator در فایل dll آن مدفون خواهند شد.
5) هر افزونه باید دارای کلاسی باشد که اینترفیس IPlugin را پیاده سازی می‌کند. از این اینترفیس برای ثبت اطلاعات هر افزونه یا دریافت اطلاعات سفارشی از آن کمک می‌گیریم.
6) با استفاده از استراکچرمپ و قرارداد IPlugin، منوهای هر افزونه را به صورت خودکار یافته و سپس به فایل layout اصلی اضافه می‌کنیم.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید:
MvcPluginMasterApp-Part1.zip
مطالب
نمایش تقویم ماهیانه شمسی توسط PdfReport
در نگارش 1.6، قالب سلول جدیدی به نام MonthCalendar اضافه شده است که امکان نمایش تقویم ماهیانه شمسی و میلادی را فراهم می‌کند. در ادامه نحوه استفاده از آن‌را بررسی خواهیم کرد. کدهای کامل این مثال را از اینجا نیز می‌توانید دریافت کنید: (^)
فرض کنید اطلاعات حضور و غیاب کارمندان را به نحو زیر در اختیار دارید:
namespace PdfReportSamples.Models
{
    public class UserWorkedHours
    {
        public int Id { set; get; }
        public string Name { set; get; }
        public int DayNumber { set; get; }
        public int Month { set; get; }
        public int Year { set; get; }
        public string Description { set; get; }
    }
}
و برای نمونه منبع داده فرضی ما نیز به صورت زیر است (تعدادی روز، به همراه ساعات کارکرد):
        private static List<UserWorkedHours> createUsersWorkedHours()
        {
            var usersWorkedHours = new List<UserWorkedHours>();
            for (int i = 1; i < 11; i++)
            {
                for (int j = 1; j < 28; j++)
                {
                    usersWorkedHours.Add(new UserWorkedHours
                    {
                        Id = i,
                        Name = "کارمند " + i,
                        Year = 1391, // سال و ماه بر اساس نوع تقویم انتخابی مشخص می‌شود
                        Month = i,
                        DayNumber = j,
                        Description = i % 2 == 0 ? "05:00" : "08:00"
                    });
                }
            }
            return usersWorkedHours;
        }
در این منبع داده فرضی، متن Description ذیل شماره روز، در تقویم ماهیانه نمایش داده خواهد شد.
سلولی که قرار است قالب MonthCalendar را نمایش دهد نیاز به شیء‌ایی از نوع PdfRpt.Calendar.CalendarData دارد که به نحو زیر تعریف شده است:
using System.Collections.Generic;

namespace PdfRpt.Calendar
{
    public class CalendarData
    {
        public int Month { set; get; }
        public int Year { set; get; }
        public IList<DayInfo> MonthDaysInfo { set; get; }
    }
}

namespace PdfRpt.Calendar
{
    public class DayInfo
    {
        public int DayNumber { set; get; }
        public int Month { set; get; }
        public int Year { set; get; }

        public string Description { set; get; }
        public bool ShowDescriptionInFooter { set; get; }
    }
}
این ساختار بر اساس اطلاعات یک ماه و روزهای آن است. متن Description در صورت false بودن ShowDescriptionInFooter ذیل شماره روز نمایش داده خواهد شد، در غیراینصورت در پایان ماه به شکل یک سطر جدید نمایش داده می‌شود. در اینجا روزهای ماه و سال بر اساس نوع تقویم معنا خواهند شد.

اکنون نیاز است تا اطلاعات منبع داده خود را به CalendarData نگاشت کنیم تا بتوان از آن در قالب سلول جدید MonthCalendar استفاده کرد. انجام اینکار با استفاده از امکانات LINQ به نحو زیر است:
        public static IList<UserMonthCalendar> CreateDataSource()
        {
            var usersWorkedHours = createUsersWorkedHours();
            // Mapping a list of normal Users WorkedHours to a list of Users + CalendarData
            return usersWorkedHours
                        .GroupBy(x => new
                        {
                            Id = x.Id,
                            Name = x.Name
                        })
                        .Select(
                                 x => new UserMonthCalendar
                                 {
                                     Id = x.Key.Id,
                                     Name = x.Key.Name,
                                     // Calendar's cell data type should be PdfRpt.Calendar.CalendarData
                                     MonthCalendarData = new CalendarData
                                     {
                                         Year = x.First().Year,
                                         Month = x.First().Month,
                                         MonthDaysInfo = x.ToList().Select(y => new DayInfo
                                         {
                                             Description = y.Description,
                                             ShowDescriptionInFooter = false,
                                             DayNumber = y.DayNumber
                                         }).ToList()
                                     }
                                 }).ToList();
        }
UserMonthCalendar، شامل ستون‌هایی است که قرار است در گزارش ما ظاهر شوند:
using PdfRpt.Calendar;

namespace PdfReportSamples.Models
{
    public class UserMonthCalendar
    {
        public int Id { set; get; }
        public string Name { set; get; }
        // Calendar's cell data type should be CalendarData
        public CalendarData MonthCalendarData { set; get; }
    }
}
ستون اول، شماره شخص، ستون دوم شامل نام شخص و ستون سوم، شامل اطلاعات یک ماه شخص است.
برای نمایش این اطلاعات توسط PdfReport، دو ستون اول یاد شده نکته خاصی ندارند، اما نحوه تعریف ستون تقویم ماهیانه آن به صورت زیر خواهد بود:
                columns.AddColumn(column =>
                {
                    // Calendar's cell data type should be PdfRpt.Calendar.CalendarData
                    column.PropertyName<UserMonthCalendar>(x => x.MonthCalendarData);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(3);
                    column.Width(3);
                    column.HeaderCell("تقویم ماهیانه");
                    column.ColumnItemsTemplate(itemsTemplate =>
                    {
                        itemsTemplate.MonthCalendar(new CalendarAttributes
                        {
                            CalendarType = CalendarType.PersianCalendar,
                            UseLongDayNamesOfWeek = true,
                            Padding = 3,
                            DescriptionHorizontalAlignment = HorizontalAlignment.Center,
                            SplitRows = true,
                            CellsCustomizer = info =>
                            {
                                if (info.Year == 1391 && info.Month == 1 && info.DayNumber == 1)
                                {
                                    info.NumberCell.BackgroundColor = new BaseColor(System.Drawing.Color.LimeGreen);
                                    var phrase = info.NumberCell.Phrase;
                                    foreach (var chunk in phrase.Chunks)
                                        chunk.Font.Color = new BaseColor(System.Drawing.Color.Yellow);
                                }
                            }
                        });
                    });
                });
توسط CalendarAttributes می‌توان یک سری از خواص تقویم نمایش داده شده را تغییر داد. برای مثال CalendarType مشخص می‌کند که نوع تقویم شمسی است یا میلادی؛ UseLongDayNamesOfWeek برای نمایش نام روزها به صورت کامل «شنبه» یا «ش» (نام کوتاه شده آن) بکار می‌رود. SplitRows مشخص می‌کند که اگر تقویم در یک صفحه جا نشد، به صفحه بعد منتقل شود یا تا جایی که ممکن است در صفحه جاری اطلاعات آن نمایش داده شده و سپس مابقی را در صفحه بعد ترسیم کند (مقدار true آن). به علاوه توسط CellsCustomizer می‌توان فرمت کردن شرطی اطلاعات را انجام داد. برای مثال در اینجا اگر روز مورد نظر، روز اول سال 91 باشد، رنگ زمینه سلول و رنگ متن عدد آن تغییر خواهد کرد.
 

مطالب
آشنایی با JSON؛ ساده - خوانا - کم حجم

(JSON (JavaScript Object Notation یک راه مناسب برای نگهداری اطلاعات است و از لحاظ ساختاری شباهت زیادی به XML، رقیب قدیمی خود دارد.

وب سرویس و آجاکس برای انتقال اطلاعات از این روش استفاده می‌کنند و بعضی از پایگاه‌های داده مانند RavenDB بر مبنای این تکنولوژی پایه گذاری شده اند.

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

اگر یک شئ با ساختار زیر در سی شارپ داشته باشید :

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

ساختار JSON متناظر با آن ( در صورت این که مقدار دهی شده باشد ) به صورت زیر است: 

{
   "Id":1,
   "FirstName":"John",
   "LastName":"Doe"
}

و در یک مثال پیچیده‌تر :

class Customer
{
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public Car Car { get; set; }
        public IEnumerable<Location> Locations { get; set; }
}

class Location
{
        public int Id { get; set; }
        public string Address { get; set; }
        public int Zip { get; set; }
}

class Car
{
        public int Id { get; set; }
        public string Model { get; set; }
}
{
      "Id":1,
      "FirstName":"John",
      "LastName":"Doe",
      "Car": {
                     "Id":1,
                     "Model":"Nissan GT-R"
               },
      "Locations":[
                            {
                                  "Id":1,
                                  "Address":"30 Mortensen Avenue, Salinas",
                                  "Zip":93905
                            },
                            {
                                  "Id":2,
                                  "Address":"65 West Alisal Street, #210, Salinas",
                                  "Zip":95812
                            }
                      ]
}

ساختار JSON را مجموعه ای از ( نام - مقدار ) تشکیل می‌دهد. ساختار مشابه آن در زبان سی شارپ KeyValuePair است.

مشاهده این تصاویر، بهترین درک را از ساختار JSON به شما می‌دهد.

Json.net یکی از بهترین کتابخانه هایی است که برای کار با این تکنولوژی در net. ارائه شده است. بهترین روش اضافه نمودن آن به پروژه NuGet است.برای این کار دستور زیر را در Package Manager Console وارد کنید.

PM> Install-Package Newtonsoft.Json

با استفاده از کد زیر می‌توانید یک Object را به فرمت JSON تبدیل کنید.

 var customer = new Customer
                               {
                                   Id = 1,
                                   FirstName = "John",
                                   LastName = "Doe",
                                   Car = new Car
                                             {
                                                 Id = 1,
                                                 Model = "Nissan GT-R"
                                             },
                                   Locations = new[]
                                                   {
                                                       new Location
                                                           {
                                                               Id = 1,
                                                               Address = "30 Mortensen Avenue, Salinas",
                                                               Zip = 93905
                                                           },
                                                       new Location
                                                           {
                                                               Id = 2,
                                                               Address = "65 West Alisal Street, #210, Salinas",
                                                               Zip = 95812
                                                           },
                                                   }
                               };
 var data = Newtonsoft.Json.JsonConvert.SerializeObject(customer);

خروجی تابع SerializeObject رشته ای است که محتوی آن را در چهارمین بلاک کد که در بالا‌تر آمده است، می‌توانید مشاهده کنید.

برای Deserialize کردن (Cast اطلاعات با فرمت JSON به کلاس موردنظر) از روش زیر بهره می‌گیریم :

var customer = Newtonsoft.Json.JsonConvert.DeserializeObject<Customer>(data);

آشنایی با این تکنولوژی، پیش درآمدی برای چشیدن طعم NoSQL و معرفی کارآمد‌ترین روش‌های آن است که در آینده خواهیم آموخت...
خوشحال می‌شوم اگر نظرات شما را در باره این موضوع بدانم.
نظرات مطالب
مباحث تکمیلی مدل‌های خود ارجاع دهنده در EF Code first
ممکن است در کلاس پایه، تنظیمات پیش فرضی وجود داشته باشند. این فراخوانی، این تنظیمات را حفظ خواهد کرد. برای مثال در ASP.NET Identity، در کلاس پایه Context آن، یک سری تنظیمات پیش فرض نام جداول، ایندکس‌ها و روابط هست. اگر این فراخوانی صورت نگیرد، تمام آن‌ها را از دست خواهید داد.
نظرات مطالب
EF Code First #8
سلام در قسمت Self Referencing Entity اگر بخواهیم کلید خارجی نام دیگری داشته باشد طبق گفته شما که عمل کردم خطای Sequence contains no elements را میدهد.
using System.Collections.Generic;
 
namespace EF_Sample04.Models
{
    public class Employee
    {
        public int Id { set; get; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
 
        public int? ManagerID { get; set; }
        public virtual Employee Manager { get; set; }       
    }
}
using EF_Sample04.Models;
using System.Data.Entity.ModelConfiguration; 
namespace EF_Sample04.Mappings
{
    public class EmployeeConfig : EntityTypeConfiguration<Employee>
    {
        public EmployeeConfig()
        {
            this.HasOptional(x => x.Manager)
                .WithMany()
                .HasForeignKey(x => x.ManagerID)
                .WillCascadeOnDelete(false);
        }
    }
}
مطالب
ارائه‌ی قالبی عمومی برای استفاده از تقویم‌های جاوااسکریپتی در Blazor
در این مطلب قصد داریم کتابخانه‌ای با قابلیت استفاده‌ی مجدد را جهت بکارگیری «PersianDatePicker یک DatePicker شمسی به زبان JavaScript که از تاریخ سرور استفاده می‌کند» ارائه دهیم. نکات ارائه شده‌ی در آن‌را می‌توان جهت تبدیل و استفاده‌ی از تمام DatePickerهای مشابه نیز بکاربرد.



نیازهای یک ورودی تاریخ سازگار با EditForm

- باید قابلیت استفاده‌ی مجدد را داشته باشد. یعنی باید به صورت یک کامپوننت مجزا و یا به صورت یک کتابخانه‌ی مجزا ارائه شود.
- باید با سیستم اعتبارسنجی EditForm یکپارچه باشد.
- باید جنریک باشد. یعنی باید بتوان در صورت نیاز DateTime ، DateTimeOffset و DateOnly و نمونه‌های nullable آن‌هارا توسط این کامپوننت دریافت کرد و ورودی و خروجی آن رشته‌ای نباشد.


نیاز به ارث‌بری از <InputBase<T جهت ارائه‌ی کامپوننت‌هایی سازگار با EditForm

تقریبا تمام کامپوننت‌های استاندارد EditForm ارائه شده‌ی توسط Blazor، از کامپوننت پایه‌ای به نام <InputBase<T مشتق می‌شوند. این کلاس، یک کلاس abstract است که قابلیت‌های بیشتری را نسبت به یک input ساده‌ی HTML ای مانند اعتبارسنجی سازگار با EditForm ارائه می‌دهد. به همین جهت توصیه می‌شود تا اگر خواستید یک کامپوننت ورودی را برای استفاده‌ی در Blazor و EditForm آن طراحی کنید، با ارث‌بری از این کلاس شروع کنید و صرفا کار را با یک input ساده، شروع نکنید.
برای استفاده‌ی از آن، ابتدای کامپوننت Blazor ما به این صورت شروع خواهد شد:
@typeparam T
@inherits InputBase<T>
که دو متد را برای بازنویسی در اختیار ما قرار می‌دهد:
    protected override bool TryParseValueFromString(
        string? value,
        [MaybeNullWhen(false)] out T result,
        [NotNullWhen(false)] out string? validationErrorMessage)
    {
      // ...
    }

    protected override string FormatValueAsString(T? value)
    {
      // ...
    }
علت وجود این دو متد، این است که مرورگرها، رشته‌ها را در اختیار ما قرار می‌دهند و ما باید راهی را برای تبدیل T به یک رشته و عکس آن را ارائه دهیم. با بازنویسی متد TryParseValueFromString، می‌توان رشته‌ی دریافتی از کاربر را تبدیل به T کرد و اگر این تبدیل میسر نبود، با مقدار دهی validationErrorMessage، مشکل را به کاربر، با یک پیام شکست اعتبارسنجی، اعلام کرد. کار متد FormatValueAsString، تبدیل T به یک رشته‌است تا در input واقع در صفحه، نمایش داده شود. در اینجا می‌توان فرمت خاصی را به شیء دریافتی اعمال و نمایش داد.


ایجاد یک کتابخانه‌ی جدید برای محصور سازی DatePicker جاوااسکریپتی

چون قصد استفاده‌ی مجدد از این کامپوننت جدید را در پروژه‌های مختلف داریم، بهتر است آن‌را تبدیل به یک «کتابخانه‌ی Blazor» کنیم. به همین جهت کتابخانه‌ی فرضی BlazorPersianJavaScriptDatePicker.Lib را در اینجا ایجاد کرده‌ایم.
در ابتدا دو فایل PersianDatePicker.js و PersianDatePicker.css موجود و مدنظر را در پوشه‌های js و css پوشه‌ی wwwroot این کتابخانه کپی می‌کنیم. بنابراین استفاده کننده‌ی از آن، مانند پروژه‌ی blazor wasm جدیدی به نام BlazorPersianJavaScriptDatePicker، باید ارجاعاتی را به آن‌ها به صورت زیر اضافه کند:
<link href="_content/BlazorPersianJavaScriptDatePicker.Lib/css/PersianDatePicker.css" rel="stylesheet"/>
<script src="_content/BlazorPersianJavaScriptDatePicker.Lib/js/PersianDatePicker.js?v=1"></script>
همچنین در فایل Imports.razor_ آن نیز بهتر است فضای نام این کتابخانه، ذکر شود تا به سادگی بتوان از کامپوننت PersianDatePicker در آن استفاده کرد:
@using BlazorPersianJavaScriptDatePicker.Lib


شروع به پیاده سازی کامپوننت PersianDatePicker

در ادامه کامپوننت جدید PersianDatePicker.razor را به پروژه‌ی کتابخانه اضافه می‌کنیم. قسمت razor آن به صورت زیر است:
@typeparam T
@inherits InputBase<T>

<div>
    <span style="cursor:pointer"
          onclick="PersianDatePicker.Show(document.getElementById('@ElementId'), '@Today')">
        📅
    </span>
    <input
        @attributes="@AdditionalAttributes"
        type="text" dir="ltr"
        @ref="ElementReference"
        name="@ElementId" id="@ElementId"
        autocapitalize="off" autocorrect="off" autocomplete="off"
       
        value="@EnteredValue"
        @oninput="OnInput"/>

    @if (ValueExpression is not null)
    {
        <ValidationMessage For="@ValueExpression"/>
    }
</div>
همانطور که مشاهده می‌کنید، کار با جنریک تعریف کردن و ارث‌بری از InputBase شروع می‌شود.
در اینجا با کلیک بر روی دکمه‌ی 📅، کار فراخوانی متد PersianDatePicker.Show مربوط به datePicker جاوا اسکریپتی صورت می‌گیرد. همچنین هر طراحی را که در اینجا ارائه دهیم، قالب UI پیش‌فرض InputBase را بازنویسی می‌کند.


نیاز به دریافت تاریخ تنظیم شده‌ی توسط کدهای جاوااسکریپتی در کامپوننت Blazor

کتابخانه‌های جاوااسکریپتی با مقداردهی مستقیم textbox.value سبب تغییر مقدار آن می‌شوند. نکته‌ی مهم اینجا است که نه فقط Blazor این تغییرات را ردیابی نمی‌کند، بلکه اگر با استفاده از متد استاندارد جاوااسکریپتی addEventListener به تغییرات این input گوش فرا دهیم، هیچ رخدادی را مشاهده نخواهیم کرد. به همین جهت نیاز است اندکی کدهای PersianDatePicker.js را تغییر دهیم (و این مورد جهت تمام کتابخانه‌های مشابه یکسان است):
    function setValue(date) {
        _textBox.value = date;

        // NOTE: To notify the addEventListener('change', fn)
        _textBox.dispatchEvent(new Event('change'));

        _textBox.focus();
        hide();
        try {
            _textBox.onchange();
        }catch(ex) {}
    }
در اینجا پس از تغییر خاصیت value، باید به صورت دستی سبب بروز رخداد change شد تا addEventListenerها بتوانند این رخداد را ردیابی کنند. به همین جهت فایل مجزایی را به نام wwwroot\js\activateDatePicker.js به کتابخانه‌ی blazor اضافه می‌کنیم:
window.activateDatePicker = {
  enableDatePicker: function (element, objectReference) {
       element.addEventListener('change', function (evt) {    
            objectReference.invokeMethodAsync("OnInputFieldChanged", this.value);
       });
  }
};
هدف از این کدها این است که جهت element یا همان datePicker جاری، بتوان رخ‌داد change را ثبت کرد و به تغییرات آن گوش فرا داد تا هر زمانیکه کدهای جاوا اسکریپتی datePicker سبب تغییری در خاصیت value شدند، بتوان آن‌را به کامپوننت Blazor ارسال کرد. وهله‌ای از این کامپوننت توسط objectReference در اینجا دریافت شده و سپس متد OnInputFieldChanged کامپوننت را با مقدار جدید وارد شده، فراخوانی می‌کند.
بنابراین این فایل جدید نیز باید به index.html مصرف کننده اضافه شود:
<script src="_content/BlazorPersianJavaScriptDatePicker.Lib/js/activateDatePicker.js?v=1"></script>


فعالسازی DatePicker در اولین بار نمایش کامپوننت Blazor

تا اینجا زیرساخت دریافت مقدار تنظیمی توسط کاربر را در کامپوننت Blazor فراهم کردیم. اکنون نوبت به استفاده‌ی از آن است:
public partial class PersianDatePicker<T> : IDisposable
{
    private bool _isDisposed;
    private DotNetObjectReference<PersianDatePicker<T>>? _objectReference;
    private string ElementId { get; } = Guid.NewGuid().ToString("N");
    private ElementReference? ElementReference { set; get; }
    private string Today { get; } = DateTime.Now.ToShortPersianDateString();

    [Inject] private IJSRuntime JsRuntime { set; get; } = default!;

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            _objectReference = DotNetObjectReference.Create(this);
            await JsRuntime.InvokeVoidAsync("activateDatePicker.enableDatePicker", ElementReference, _objectReference);
            EnteredValue = CurrentValueAsString;
            StateHasChanged();
        }
    }

    protected override void Dispose(bool disposing)
    {
        base.Dispose(disposing);
        if (!_isDisposed)
        {
            try
            {
                _objectReference?.Dispose();
            }
            finally
            {
                _isDisposed = true;
            }
        }
    }
}
- اگر دقت کرده باشید در تعاریف razor کامپوننت، "ref="ElementReference@ وجود دارد که یک ElementReference است و توسط آن می‌توان در متد OnAfterRenderAsync، ارجاعی را به المان جاری، به کدهای جاوااسکریپتی متد enableDatePicker ارسال کرد.
- همچنین چون نمی‌خواهیم متد OnInputFieldChanged را به صورت static تعریف کنیم، نیاز است تا یک DotNetObjectReference را ایجاد و به متد enableDatePicker ارسال کرد تا توسط آن بتوان به یک instance method کلاس جاری دسترسی یافت و به سادگی مقادیر کامپوننت را تغییر داد:
[JSInvokable]
public void OnInputFieldChanged(string? value)
- در پایان کار کامپوننت، باید این DotNetObjectReference را Dispose کرد.


نیاز به تبدیل T به تاریخ رشته‌ای و برعکس

زیر ساخت تبدیلات جنریک تاریخ میلادی به شمسی در کتابخانه‌ی « DNTPersianUtils.Core » پیش‌بینی شده‌است و فقط کافی است از آن استفاده کنیم. با وجود این زیرساخت، تهیه‌ی کامپوننت‌های جنریک تاریخ شمسی بسیار ساده می‌شود:
public partial class PersianDatePicker<T> : IDisposable
{
    private string? _enteredValue;

    private string? EnteredValue
    {
        set => _enteredValue = value;
        get => UsePersianNumbers ? _enteredValue.ToPersianNumbers() : _enteredValue;
    }

    [Parameter] public bool UsePersianNumbers { set; get; }

    [Parameter] public string ParsingErrorMessage { get; set; } = "لطفا در ورودی {0} تاریخ شمسی معتبری را وارد نمائید.";

    [Parameter] public int BeginningOfCentury { set; get; } = 1400;

    private void OnInput(ChangeEventArgs e)
    {
        SetCurrentValue(e.Value as string);
    }

    private void SetCurrentValue(string? value)
    {
        EnteredValue = value;
        CurrentValueAsString = value;
    }

    [JSInvokable]
    public void OnInputFieldChanged(string? value)
    {
        SetCurrentValue(value);
    }

    protected override void OnInitialized()
    {
        base.OnInitialized();
        SanityCheck();
    }

    protected override bool TryParseValueFromString(
        string? value,
        [MaybeNullWhen(false)] out T result,
        [NotNullWhen(false)] out string? validationErrorMessage)
    {
        validationErrorMessage = string.Format(CultureInfo.InvariantCulture, ParsingErrorMessage, DisplayName);
        if (!value.TryParsePersianDateToDateTimeOrDateTimeOffset(out result, BeginningOfCentury))
        {
            return false;
        }

        if (result is null)
        {
            throw new InvalidOperationException(validationErrorMessage);
        }

        validationErrorMessage = null;
        return true;
    }

    protected override string FormatValueAsString(T? value)
    {
        return !string.IsNullOrWhiteSpace(EnteredValue) ? EnteredValue : value.FormatDateToShortPersianDate();
    }

    private void SanityCheck()
    {
        if (!Value.IsDateTimeOrDateTimeOffsetType())
        {
            throw new InvalidOperationException(
                "The `Value` type is not a supported `date` type. DateTime, DateTime?, DateTimeOffset and DateTimeOffset? are supported.");
        }
    }

    // ...
}
در اینجا قسمت نهایی و تکمیلی کامپوننت محصور کننده‌ی DatePicker را مشاهده می‌کنید که بسیار ساده‌است:
- InputBase به همراه یک خاصیت عمومی دوطرفه‌ی Value است که امکان تعریفی مانند bind-Value@ را میسر می‌کند.
- این Value به همراه یک خاصیت متناظر رشته‌ای به نام CurrentValueAsString نیز هست که در اینجا از آن استفاده می‌کنیم و کار با آن، بایندینگ دوطرفه و همچنین اعتبارسنجی خودکار و فعالسازی متدهای بازنویسی شده‌ی InputBase را میسر می‌کند.
- پیاده سازی متدهای بازنویسی شده‌ی جنریک TryParseValueFromString و FormatValueAsString، با استفاده از دو متد TryParsePersianDateToDateTimeOrDateTimeOffset و FormatDateToShortPersianDate کتابخانه‌ی « DNTPersianUtils.Core » انجام شده‌اند و اصل کار تهیه‌ی یک کامپوننت جنریک تاریخ شمسی را انجام می‌دهند.


استفاده‌ی از کامپوننت Blazor تهیه شده‌

یک کامپوننت تاریخ شمسی باید بتواند تمام حالات و نوع‌های زیر را پوشش دهد که به لطف جنریک بودن کامپوننت تهیه شده، این امر میسر است:
using System.ComponentModel.DataAnnotations;

namespace BlazorPersianJavaScriptDatePicker.ViewModels;

public class InputPersianDateViewModel
{
    [Required] public string Name { set; get; } = default!;

    [Required] public DateTime BirthDayGregorian { set; get; } = DateTime.Now.AddYears(-40);

    public DateTime? LoginAt { set; get; } = DateTime.Now.AddMinutes(-2);

    [Required] public DateTimeOffset LogoutAt { set; get; }

    public DateTimeOffset? RegisterAt { set; get; } = DateTimeOffset.Now.AddMinutes(-10);
}
سپس از این کامپوننت، در صفحه‌ی Index مثال پیوست به صورت زیر استفاده شده‌است:
<EditForm Model="Model" OnValidSubmit="DoSave">
    <DataAnnotationsValidator/>

    <div>
        <label>تاریخ تولد</label>
        <div>
            <PersianDatePicker
                @bind-Value="Model.BirthDayGregorian"
                UsePersianNumbers="false"
               />
        </div>
    </div>

    <button type="submit">ارسال</button>
</EditForm>


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