نظرات مطالب
کار با کلیدهای اصلی و خارجی در EF Code first
سلام ... 
این مدلو ببینید : 
    public class FileUpload
{
    public int FileUploadId { get; set; }
    public string FileName { get; set; }

}

public class Company
{
    public int CompanyId { get; set; }
    public string Name { get; set; }

    public FileUpload Logo { get; set; }
    public int? LogoId { get; set; }

    public FileUpload Catalog { get; set; }
    public int? CatalogId { get; set; }
}

public class Ads
{
    public int AdsId { get; set; }
    public string Name { get; set; }

    public FileUpload Picture { get; set; }
    public int? PictureId { get; set; }
}

public class TestContext : DbContext
{
    public DbSet<Company> Companies { get; set; }
    public DbSet<Ads> Adses { get; set; }
    public DbSet<FileUpload> FileUploads { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
        modelBuilder.Entity<Ads>()
            .HasOptional(a => a.Picture)
            .WithMany()
            .HasForeignKey(a => a.PictureId)
            .WillCascadeOnDelete();

        modelBuilder.Entity<Company>()
           .HasOptional(a => a.Logo)
           .WithMany()
           .HasForeignKey(a => a.LogoId)
           .WillCascadeOnDelete();

        modelBuilder.Entity<Company>()
           .HasOptional(a => a.Catalog)
           .WithMany()
           .HasForeignKey(a => a.CatalogId)
           .WillCascadeOnDelete();
    }
}
اینو اگه ازش migration بگیرین متوجه اروراش میشین 
من میخوام هر شرکت بتونه به صورت optional لوگو یا کاتالوگ داشته باشه و همین طور قسمت تبلیغات هم همین طور!
همین طور موقعی هم که مثلا یه شرکت پاک میشه لوگو و کاتالوگشم تو جدول FileUpload پاک شه (cascade delete) 
همین طور هر رکورد FileUpload رو بتونم به صورت مستقیم حذف کنم
میشه این کارا که گفتمو کرد؟! چون واقعا یه همچین چیزی نیازه !
مطالب
آشنایی با قابلیت جدید ASP.NET Web Forms Scaffolding
مایکروسافت با افزایش سرعت به روز رسانی توسعه پروژه‌های سورس باز خود جهت پاسخ دادن به نیاز توسعه دهندگان و توسعه ویژوال استادیو مطابق با آخرین تکنولوژی‌های تولید وب سایت، می‌کوشد تعداد بیشتری از توسعه دهندگان را به سمت استفاده از تکنولوژی‌های خود سوق دهد. 

سالها است که برنامه نویسان خبره با توجه به روش کاری خود از امکانات Code Generatorها برای تولید کدهای لایه‌های Data Access ، Logic و یا حتی User Interface استفاده می‌نمایند. پس از عرضه Entity Framework و تولید خودکار کدهای لایه های Data Access و Logic، این بار این امکان علاوه بر ASP.NET MVC در ASP.NET Web Forms نیز فراهم گردیده‌است تا بدون کد نویسی خسته کننده و تکراری، کدهای لایه رابط کاربر (Create-Read-Update-Delete (CRUD را نیز تولید نماییم. 

شروع کار با ASP.NET Scaffolding
پیش نیاز این کار استفاده از Visual Studio 2012 به همراه Web Tools 2012.2 می‌باشد.
  1. اول، ابزار Microsoft ASP.NET Scaffolding را از منوی Tools گزینه Extensions and Updates دریافت و نصب نمایید.
  2. دوم پروژه جدیدی از نوع Visual C# ASP.NET Web Forms Application با فریم ورک 4.5 ایجاد نمایید.
  3. از پنجره NuGet Package manager با دستور install کتابخانه ASP.NET Web Forms Scaffold Generator را دریافت نمایید
    install-package Microsoft.AspNet.Scaffolding.WebForms -pre
  4. کلاس Person را مانند زیر در فولدر Models ایحاد نمایید
     public class Person
        {
            [ScaffoldColumn(false)]
            public int ID { get; set; }
            public string FirstName { get; set; }
            public string LastName { get; set; }
        }
    ویژگی ScaffoldColumn را برای ID، برابر false قرار دهید تا از ایجاد این ستون جلوگیری نمائید.
  5. پروژه را Build نمایید.
  6. بر روی پروژه راست کلیک و از گزینه Add، گزینه ...Scaffold را انتخاب نمایید.

  7. از پنجره Add Scaffold باز شده بر روی گزینه Add، کلیک کنید.

  8. پنجره  Add Web Forms Pages مانند زیر باز می‌شود که امکان انتخاب کلاس،Data Context و MasterPage فراهم می‌باشد.

  9. از گزینه Data Context class گزینه New Data Context را انتخاب نمایید. صفحات مورد نیاز را در فولدر Views/Person ایجاد می‌نمایید.
  10. کد‌های تولید شده را می‌توانید بازبینی نمایید پروژه را اجرا تا خروجی کار را مشاهده نمایید.

مطالب
معرفی واژه‌ی کلیدی جدید required در C# 11
واژه‌ی کلیدی جدید required در C# 11.0، همانند خواص init-only که پیشتر معرفی شدند، با هدف آغاز و نمونه سازی دقیق‌تر و ساده‌تر اشیایی است که برای اینکار، به تعاریف ویژه‌ی سازنده‌ی کلاس‌ها وابسته نیستند.


امکان نمونه سازی بدون قید و شرط کلاس‌ها

تعریف کلاس Article1 را به صورت زیر درنظر بگیرید:
public class Article1
{
    public string Title { get; set; }
    public string? Subtitle { get; set; }
    public string Author { get; set; }
    public DateTime Published { get; set; }
}
ساختار پروژه‌های دات نت 7 نیز به صورت پیش‌فرض به صورت زیر است:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>
یعنی nullable reference types در آن‌ها فعال است. با این فعال بودن، به اخطارهای زیر می‌رسیم:
Non-nullable property 'Title' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [CS11Tests]csharp(CS8618)
Non-nullable property 'Author' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [CS11Tests]csharp(CS8618)
عنوان می‌کند که خاصیت‌های Title و Author، به صورت غیرنال‌پذیر تعریف شده‌اند (و همانند Subtitle نال‌پذیر نیستند)؛ اما تعریف این کلاس به نحوی است که این مساله را الزامی نمی‌کند. یعنی می‌توان نمونه‌ای از Article1 را ایجاد کرد که در آن، هر دوی این خواص نال باشند؛ هرچند در این حالت مشکلی از لحاظ کامپایل وجود نخواهد داشت، اما ممکن است به علت اشتباه استفاده‌ی از آن‌ها، به null reference exceptions برسیم. چون یکی از مهم‌ترین اهداف استفاده از یک چنین تعاریفی و فعال سازی nullable reference type در یک پروژه، ارائه‌ی متادیتای بهتری جهت خواص و پارامترها و خروجی‌های متدهاست تا استفاده کننده دقیقا بداند که آیا این خواص می‌توانند نال باشد یا خیر. اگر  public string ای تعریف شده، یعنی این خاصیت قطعا نال نخواهد بود و می‌توان بدون مشکل و بدون بررسی مقدار آن، از آن استفاده کرد و اگر ?public string ای تعریف شده، یعنی ممکن است مقدار آن نال نیز باشد و بهتر است پیش از استفاده‌ی از آن، حتما مقدار آن بررسی شود. اکنون مشکل اینجا است که هیچگونه قیدی، جهت اجبار به مقدار دهی خواص غیرنال پذیر در اینجا وجود ندارند و می‌توان نمونه‌ای از شیء Article1 را ایجاد کرد که در آن متادیتای خواص غیرنال پذیر تعریفی در آن، نقض شوند.


مدیریت کردن نحوه‌ی نمونه سازی کلاس‌ها، با وابستگی به سازنده‌های آن

یکی از روش‌های مدیریت مشکلی که تا اینجا بررسی شد، تعریف سازنده‌های متعددی برای یک کلاس است؛ تا توسط آن بتوان مقدار دهی یک سری از خواص را اجباری کرد:
public class Article2
{
    public Article2(string title, string subtitle, string author, DateTime published)
    {
        Title = title;
        Subtitle = subtitle;
        Author = author;
        Published = published;
    }

    public Article2(string title, string author, DateTime published)
    {
        Title = title;
        Author = author;
        Published = published;
    }

    public string Title { get; set; }
    public string? Subtitle { get; set; }
    public string Author { get; set; }
    public DateTime Published { get; set; }
}
در این کلاس، نمونه‌ی بهبود یافته‌ی Article1 را مشاهده می‌کنید که استفاده کننده را وادار به مقدار دهی title و author می‌کند. در این حالت اخطارهای کامپایلری را که مشاهده کردید، رفع می‌شوند؛ اما به همراه این مسایل است:
- تعداد سطرهای تعریف این کلاس، به شدت افزایش یافته‌است.
- با اضافه شدن خواص بیشتری به کلاس، به تعاریف بیشتری نیاز خواهد بود.
- سازنده‌ها کار خاصی را بجز نگاشت مقادیر ارائه شده، به خواص کلاس، انجام نمی‌دهند.
- نمونه سازی این کلاس‌ها، شکل طولانی و غیرواضح زیر را پیدا می‌کند و زیبایی inline object initializers را ندارند:
 Article2 article = new("C# 11 Required Keyword", "A new language feature", "Name",  new DateTime(2022, 11, 12));

البته روش دیگر مدیریت یک چنین اخطارهایی، استفاده از مقدار ویژه‌ی !default است که سبب محو اخطارهای یاد شده می‌شود؛ اما باز هم مقدار دهی آن‌را الزامی نمی‌کند. فقط به این معنا است که قول می‌دهیم این خاصیت را در جای دیگری مقدار دهی کنیم و هیچگاه نال نباشد!
 public string Title { get; set; } = default!;


مدیریت کردن نحوه‌ی نمونه سازی کلاس‌ها، بدون وابستگی به سازنده‌های آن در C# 11.0

C# 11 به همراه واژه‌ی کلیدی جدیدی به نام required است تا دیگر نیازی نباشد همانند راه حل فوق، سازنده‌های متعددی را جهت اجبار به مقدار دهی خواص یک شیء، تعریف کنیم. در این حالت تعریف کلاس Article به صورت زیر خلاصه می‌شود و دیگر به همراه اخطارهای کامپایلر نمایش داده شده نیز نیست:
public class Article3
{
    public required string Title { get; set; }
    public string? Subtitle { get; set; }
    public required string Author { get; set; }
    public DateTime Published { get; set; }
}
به این ترتیب هنوز می‌توان از زیبایی و خوانایی به همراه نمونه سازی توسط روش inline object initializers بهره‌مند شد و همچنین مطمئن بود که اگر استفاده کننده خاصیت غیرنال‌پذیر Title را مقدار دهی نکند، اینبار با یک خطای کامپایلر متوقف خواهد شد:



معرفی ویژگی جدید SetsRequiredMembers

کلاس Book زیر را که به همراه یک خاصیت required و دو سازنده‌است، درنظر بگیرید:
public class Book
{
    public Book() => Name = string.Empty;

    public Book(string name) => Name = name;

    public required string Name { get; set; }
}
اکنون فرض کنید که بر این اساس، شیء‌ای را به صورت زیر نمونه سازی کرده‌ایم:
Book book = new("Book's Name");
این قطعه کد با خطای زیر کامپایل نمی‌شود:
Required member 'Book.Name' must be set in the object initializer or attribute constructor. [CS11Tests]csharp(CS9035)
عنوان می‌کند که باید خاصیت Name را حتما مقدار دهی کرد؛ چون از نوع required است. هرچند سازنده‌‌ای که از آن استفاده شده، این مقدار دهی را انجام داده‌است و مشکلی از لحاظ عدم مقدار دهی خاصیت Name در اینجا وجود ندارد. برای رفع این مشکل، باید تغییر زیر را اعمال کرد:
public class Book
{
    [SetsRequiredMembers]
    public Book() => Name = string.Empty;

    [SetsRequiredMembers]
    public Book(string name) => Name = name;

    public required string Name { get; set; }
}
با استفاده از ویژگی جدید SetsRequiredMembers عنوان می‌کنیم که این سازنده‌ی خاص، حتما خواص از نوع required را نیز مقدار دهی می‌کند و نیازی به صدور خطای یاد شده نیست. در این حالت بررسی خواص required توسط کامپایلر غیرفعال می‌شود.


محدودیت‌های کار با خواص required

- واژه‌ی کلیدی required را می‌توان تنها به خواص و فیلدهای نوع‌های class, record, record struct اعمال کرد. امکان اعمال این واژه‌ی کلیدی به اجزای یک اینترفیس وجود ندارد.
- میدان دید اعضای required باید حداقل در حد نوع‌های دربرگیرنده‌ی آن‌ها باشند. برای مثال اگر کلاسی public است، نمی‌توان در آن یک فیلد required با میدان دید protected را تعریف کرد.
- نوع‌های مشتق شده‌ی از یک نوع پایه، نمی‌توانند اعضای required آن‌را مخفی کنند و اگر قصد بازنویسی آن‌را دارند، باید حتما واژه‌ی کلیدی required را لحاظ کنند.
- اگر سازنده‌ای به سازنده‌ی دیگری از طریق ذکر ()base و یا ()this زنجیر شده باشد نیز باید ویژگی SetsRequiredMembers مرتبط را تکرار کند.
مطالب
بومی سازی منابع در پروژه‌های 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
مطالب
یافتن Contextهای Dispose نشده در Entity framework
این دو متد را در نظر بگیرید:
        private static void disposedContext()
        {
            using (var context = new MyContext())
            {
                Debug.WriteLine("Posts count: " + context.BlogPosts.Count());
            }
        }

        private static void nonDisposedContext()
        {
            var context = new MyContext();
            Debug.WriteLine("Posts count: " + context.BlogPosts.Count());
        }
در اولی با استفاده از using، شیء context به صورت خودکار dispose خواهد شد؛ اما در دومی از using استفاده نشده‌است.

سؤال: در یک برنامه‌ی بزرگ چطور می‌توان لیست Contextهای Dispose نشده را یافت؟

در EF 6 با تعریف یک IDbConnectionInterceptor سفارشی می‌توان به متدهای باز، بسته و dispose شدن یک Connection دسترسی یافت. اگر Context ایی dispose نشده باشد، اتصال آن نیز dispose نخواهد شد.
using System.Data;
using System.Data.Common;
using System.Data.Entity.Infrastructure.Interception;

namespace EFNonDisposedContext.Core
{
    public class DatabaseInterceptor : IDbConnectionInterceptor
    {
        public void Closed(DbConnection connection, DbConnectionInterceptionContext interceptionContext)
        {
            Connections.AddOrUpdate(connection, ConnectionStatus.Closed);
        }

        public void Disposed(DbConnection connection, DbConnectionInterceptionContext interceptionContext)
        {
            Connections.AddOrUpdate(connection, ConnectionStatus.Disposed);
        }

        public void Opened(DbConnection connection, DbConnectionInterceptionContext interceptionContext)
        {
            Connections.AddOrUpdate(connection, ConnectionStatus.Opened);
        }
      
        // the rest of the IDbConnectionInterceptor methods ...

    }
}
همانطور که ملاحظه می‌کنید، با پیاده سازی IDbConnectionInterceptor، به سه متد Closed، Opened و Disposed یک DbConnection می‌توان دسترسی یافت.

مشکل مهم! در زمان فراخوانی متد Disposed، دقیقا کدام DbConnection باز شده، رها شده‌است؟
پاسخ به این سؤال را در مطلب «ایجاد خواص الحاقی» می‌توانید مطالعه کنید. با استفاده از یک ConditionalWeakTable به هر کدام از اشیاء DbConnection یک Id را انتساب خواهیم داد و پس از آن به سادگی می‌توان وضعیت این Id را ردگیری کرد.
برای این منظور، لیستی از ConnectionInfo را تشکیل خواهیم داد:
    public enum ConnectionStatus
    {
        None,
        Opened,
        Closed,
        Disposed
    }

    public class ConnectionInfo
    {
        public string ConnectionId { set; get; }
        public string StackTrace { set; get; }
        public ConnectionStatus Status { set; get; }

        public override string ToString()
        {
            return string.Format("{0}:{1} [{2}]",ConnectionId, Status, StackTrace);
        }
    }
در اینجا ConnectionId را به کمک ConditionalWeakTable محاسبه می‌کنیم.
StackTrace توسط نکته‌ی مطلب «کدام سلسله متدها، متد جاری را فراخوانی کرده‌اند؟ » تهیه می‌شود.
Status نیز وضعیت جاری اتصال است که بر اساس متدهای فراخوانی شده در پیاده سازی IDbConnectionInterceptor مشخص می‌گردد.

در پایان کار برنامه فقط باید یک گزارش تهیه کنیم از لیست ConnectionInfoهایی که Status آن‌ها مساوی Disposed نیست. این موارد با توجه به مشخص بودن Stack trace هر کدام، دقیقا محل متدی را که در آن context مورد استفاده dispose نشده‌است، مشخص می‌کنند.


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید
EFNonDisposedContext.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
مطالب
آشنایی با NHibernate - قسمت هفتم

مدیریت بهینه‌ی سشن فکتوری

ساخت یک شیء SessionFactory بسیار پر هزینه و زمانبر است. به همین جهت لازم است که این شیء یکبار حین آغاز برنامه ایجاد شده و سپس در پایان کار برنامه تخریب شود. انجام اینکار در برنامه‌های معمولی ویندوزی (WinForms ،WPF و ...)، ساده است اما در محیط Stateless وب و برنامه‌های ASP.Net ، نیاز به راه حلی ویژه وجود خواهد داشت و تمرکز اصلی این مقاله حول مدیریت صحیح سشن فکتوری در برنامه‌های ASP.Net است.

برای پیاده سازی شیء سشن فکتوری به صورتی که یکبار در طول برنامه ایجاد شود و بارها مورد استفاده قرار گیرد باید از یکی از الگوهای معروف طراحی برنامه نویسی شیء گرا به نام Singleton Pattern استفاده کرد. پیاده سازی نمونه‌ی thread safe آن که در برنامه‌های ذاتا چند ریسمانی وب و همچنین برنامه‌های معمولی ویندوزی می‌تواند مورد استفاده قرار گیرد، در آدرس ذیل قابل مشاهده است:



از پنجمین روش ذکر شده در این مقاله جهت ایجاد یک lazy, lock-free, thread-safe singleton استفاده خواهیم کرد.

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

در این مدل ساده ما یک یا چند پارکینگ داریم که در هر پارکینگ یک یا چند خودرو می‌توانند پارک شوند.


یک برنامه ASP.Net را آغاز کرده و ارجاعاتی را به اسمبلی‌های زیر به آن اضافه نمائید:
FluentNHibernate.dll
NHibernate.dll
NHibernate.ByteCode.Castle.dll
NHibernate.Linq.dll
و همچنین ارجاعی به اسمبلی استاندارد System.Data.Services.dll دات نت فریم ورک سه و نیم

تصویر نهایی پروژه ما به شکل زیر خواهد بود:



پروژه ما دارای یک پوشه domain ، تعریف کننده موجودیت‌های برنامه جهت تهیه نگاشت‌های لازم از روی ‌آن‌ها است. سپس یک پوشه جدید را به نام NHSessionManager به آن جهت ایجاد یک Http module مدیریت کننده سشن‌های NHibernate در برنامه اضافه خواهیم کرد.

ساختار دومین برنامه (مطابق کلاس دیاگرام فوق):

namespace NHSample3.Domain
{
public class Car
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual string Color { get; set; }
}
}

using System.Collections.Generic;

namespace NHSample3.Domain
{
public class Parking
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual string Location { get; set; }
public virtual IList<Car> Cars { get; set; }

public Parking()
{
Cars = new List<Car>();
}
}
}
مدیریت سشن فکتوری در برنامه‌های وب

در این قسمت قصد داریم Http Module ایی را جهت مدیریت سشن‌های NHibernate ایجاد نمائیم.

در ابتدا کلاس Config را در پوشه مدیریت سشن NHibernate با محتویات زیر ایجاد کنید:

using FluentNHibernate.Automapping;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHibernate.Tool.hbm2ddl;

namespace NHSessionManager
{
public class Config
{
public static FluentConfiguration GetConfig()
{
return
Fluently.Configure()
.Database(
MsSqlConfiguration
.MsSql2008
.ConnectionString(x => x.FromConnectionStringWithKey("DbConnectionString"))
)
.ExposeConfiguration(
x => x.SetProperty("current_session_context_class", "managed_web")
)
.Mappings(
m => m.AutoMappings.Add(
new AutoPersistenceModel()
.Where(x => x.Namespace.EndsWith("Domain"))
.AddEntityAssembly(typeof(NHSample3.Domain.Car).Assembly))
);
}

public static void CreateDb()
{
bool script = false;//آیا خروجی در کنسول هم نمایش داده شود
bool export = true;//آیا بر روی دیتابیس هم اجرا شود
bool dropTables = false;//آیا جداول موجود دراپ شوند
new SchemaExport(GetConfig().BuildConfiguration()).Execute(script, export, dropTables);
}
}
}
با این کلاس در قسمت‌های قبل آشنا شده‌اید. در این کلاس با کمک امکانات Auto mapping موجود در Fluent Nhibernate (مطلب قسمت قبلی این سری آموزشی) اقدام به تهیه نگاشت‌های خودکار از کلاس‌های قرار گرفته در پوشه دومین خود خواهیم کرد (فضای نام این پوشه به دومین ختم می‌شود که در متد GetConfig مشخص است).
دو نکته جدید در متد GetConfig وجود دارد:
الف) استفاده از متد FromConnectionStringWithKey ، بجای تعریف مستقیم کانکشن استرینگ در متد مذکور که روشی است توصیه شده. به این صورت فایل وب کانفیگ ما باید دارای تعریف کلید مشخص شده در متد GetConfig به نام DbConnectionString باشد:

<connectionStrings>
<!--NHSessionManager-->
<add name="DbConnectionString"
connectionString="Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true" />
</connectionStrings>
ب) قسمت ExposeConfiguration آن نیز جدید است.
در اینجا به AutoMapper خواهیم گفت که قصد داریم از امکانات مدیریت سشن مخصوص وب فریم ورک NHibernate استفاده کنیم. فریم ورک NHibernate دارای کلاسی است به نام NHibernate.Context.ManagedWebSessionContext که جهت مدیریت سشن‌های خود در پروژه‌های وب ASP.Net پیش بینی کرده است و از این متد در Http module ایی که ایجاد خواهیم کرد جهت ردگیری سشن جاری آن کمک خواهیم گرفت.

اگر متد CreateDb را فراخوانی کنیم، جداول نگاشت شده به کلاس‌های پوشه دومین برنامه، به صورت خودکار ایجاد خواهند شد که دیتابیس دیاگرام آن به صورت زیر می‌باشد:



سپس کلاس SingletonCore را جهت تهیه تنها و تنها یک وهله از شیء سشن فکتوری در کل برنامه ایجاد خواهیم کرد (همانطور که عنوان شده، ایده پیاده سازی این کلاس thread safe ، از مقاله معرفی شده در ابتدای بحث گرفته شده است):

using NHibernate;

namespace NHSessionManager
{
/// <summary>
/// lazy, lock-free, thread-safe singleton
/// </summary>
public class SingletonCore
{
private readonly ISessionFactory _sessionFactory;

SingletonCore()
{
_sessionFactory = Config.GetConfig().BuildSessionFactory();
}

public static SingletonCore Instance
{
get
{
return Nested.instance;
}
}

public static ISession GetCurrentSession()
{
return Instance._sessionFactory.GetCurrentSession();
}

public static ISessionFactory SessionFactory
{
get { return Instance._sessionFactory; }
}

class Nested
{
// Explicit static constructor to tell C# compiler
// not to mark type as beforefieldinit
static Nested()
{
}

internal static readonly SingletonCore instance = new SingletonCore();
}
}
}
اکنون می‌توان از این Singleton object جهت تهیه یک Http Module کمک گرفت. برای این منظور کلاس SessionModule را به برنامه اضافه کنید:

using System;
using System.Web;
using NHibernate;
using NHibernate.Context;

namespace NHSessionManager
{
public class SessionModule : IHttpModule
{
public void Dispose()
{ }

public void Init(HttpApplication context)
{
if (context == null)
throw new ArgumentNullException("context");

context.BeginRequest += Application_BeginRequest;
context.EndRequest += Application_EndRequest;
}

private void Application_BeginRequest(object sender, EventArgs e)
{
ISession session = SingletonCore.SessionFactory.OpenSession();
ManagedWebSessionContext.Bind(HttpContext.Current, session);
session.BeginTransaction();
}

private void Application_EndRequest(object sender, EventArgs e)
{
ISession session = ManagedWebSessionContext.Unbind(
HttpContext.Current, SingletonCore.SessionFactory);
if (session == null) return;

try
{
if (session.Transaction != null &&
!session.Transaction.WasCommitted &&
!session.Transaction.WasRolledBack)
{
session.Transaction.Commit();
}
else
{
session.Flush();
}
}
catch (Exception)
{
session.Transaction.Rollback();
}
finally
{
if (session != null && session.IsOpen)
{
session.Close();
session.Dispose();
}
}
}
}
}
کلاس فوق کار پیاده سازی اینترفیس IHttpModule را جهت دخالت صریح در request handling pipeline برنامه ASP.Net جاری انجام می‌دهد. در این کلاس مدیریت متدهای استاندارد Application_BeginRequest و Application_EndRequest به صورت خودکار صورت می‌گیرد.
در متد Application_BeginRequest ، در ابتدای هر درخواست یک سشن جدید ایجاد و به مدیریت سشن وب NHibernate بایند می‌شود، همچنین یک تراکنش نیز آغاز می‌گردد. سپس در پایان درخواست، این انقیاد فسخ شده و تراکنش کامل می‌شود، همچنین کار پاکسازی اشیاء نیز صورت خواهد گرفت.

با توجه به این موارد، دیگر نیازی به ذکر using جهت dispose کردن سشن جاری در کدهای ما نخواهد بود، زیرا در پایان هر درخواست اینکار به صورت خودکار صورت می‌گیرد. همچنین نیازی به ذکر تراکنش نیز نمی‌باشد، چون مدیریت آن‌را خودکار کرده‌ایم.

جهت استفاده از این Http module تهیه شده باید چند سطر زیر را به وب کانفیگ برنامه اضافه کرد:

<httpModules>
<!--NHSessionManager-->
<add name="SessionModule" type="NHSessionManager.SessionModule"/>
</httpModules>
بدیهی است اگر نخواهید از Http module استفاده کنید باید این کدها را در فایل Global.asax برنامه قرار دهید.

اکنون مثالی از نحوه‌ی استفاده از امکانات فراهم شده فوق به صورت زیر می‌تواند باشد:
ابتدا کلاس ParkingContext را جهت مدیریت مطلوب‌تر LINQ to NHibernate تشکیل می‌دهیم.

using System.Linq;
using NHibernate;
using NHibernate.Linq;
using NHSample3.Domain;

namespace NHSample3
{
public class ParkingContext : NHibernateContext
{
public ParkingContext(ISession session)
: base(session)
{ }

public IOrderedQueryable<Car> Cars
{
get { return Session.Linq<Car>(); }
}

public IOrderedQueryable<Parking> Parkings
{
get { return Session.Linq<Parking>(); }
}
}
}
سپس در فایل Default.aspx.cs برنامه ، برای نمونه تعدادی رکورد را افزوده و نتیجه را در یک گرید ویوو نمایش خواهیم داد:

using System;
using System.Collections.Generic;
using System.Linq;
using NHibernate;
using NHSample3.Domain;
using NHSessionManager;

namespace NHSample3
{
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
//ایجاد دیتابیس در صورت نیاز
//Config.CreateDb();

//ثبت یک سری رکورد در دیتابیس
ISession session = SingletonCore.GetCurrentSession();

Car car1 = new Car() { Name = "رنو", Color = "مشکلی" };
session.Save(car1);
Car car2 = new Car() { Name = "پژو", Color = "سفید" };
session.Save(car2);

Parking parking1 = new Parking()
{
Location = "آدرس پارکینگ مورد نظر",
Name = "پارکینگ یک",
Cars = new List<Car> { car1, car2 }
};

session.Save(parking1);

//نمایش حاصل در یک گرید ویوو
ParkingContext db = new ParkingContext(session);
var query = from x in db.Cars select new { CarName = x.Name, CarColor = x.Color };
GridView1.DataSource = query.ToList();
GridView1.DataBind();
}
}
}
مدیریت سشن فکتوری در برنامه‌های غیر وب

در برنامه‌های ویندوزی مانند WinForms ، WPF و غیره، تا زمانیکه یک فرم باز باشد، کل فرم و اشیاء مرتبط با آن به یکباره تخریب نخواهند شد، اما در یک برنامه ASP.Net جهت حفظ منابع سرور در یک محیط چند کاربره، پس از پایان نمایش یک صفحه وب، اثری از آثار اشیاء تعریف شده در کدهای آن صفحه در سرور وجود نداشته و همگی بلافاصله تخریب می‌شوند. به همین جهت بحث‌های ویژه state management در ASP.Net در اینباره مطرح است و مدیریت ویژه‌ای باید روی آن صورت گیرد که در قسمت قبل مطرح شد.
از بحث فوق، تنها استفاده از کلاس‌های Config و SingletonCore ، جهت استفاده و مدیریت بهینه‌ی سشن فکتوری در برنامه‌های ویندوزی کفایت می‌کنند.

دریافت سورس برنامه قسمت هفتم

ادامه دارد ....

مطالب
آموزش Cache در ASP.NET Core - (قسمت اول : مفاهیم اولیه)
امروزه در وب‌سایت‌های شخصی و تجاری، یکی از مهم‌ترین پارامتر‌ها، سرعت پاسخگویی درخواست‌ها به وب‌سایت است. طبق آمار، کاربران آنلاین کنونی که ما با آن‌ها طرفیم، سطح تحملشان به سه ثانیه در یک صفحه میرسد؛ پس ما باید بتوانیم سرعت وب‌سایت‌های خودمان را تا حد ممکن بهبود بخشیم. از طرفی پارامتر سرعت، روی سئو گوگل هم تاثیر بسزایی دارد و Ranking وب‌سایت شمارا تا حد زیادی افزایش می‌دهد. قطعا همه می‌دانید که سرعت وبسایت و برنامه چقدر مهم هست؛ پس زیاده گویی نمی‌کنیم و می‌رویم سراغ اصل مطلب.
یکی از کارهایی که میتوانیم برای افزایش سرعت برنامه انجام دهیم، استفاده از Cache هست. بطور خیلی ساده، Cache یعنی قرار دادن دیتای پرکاربرد، در یک حافظه‌ی نزدیک‌تر از دیتابیس که هروقت به آن نیاز داشتیم، به آن دسترسی سریعی داشته باشیم و سرعت واکشی اطلاعات، از سرعتی که دیتابیس به ما می‌دهد، بیشتر باشد تا درخواست‌های ما با پاسخ سریع‌تری همراه شوند.
این حافظه، Ram هست و عمل Caching به اینصورت خواهد بود که هر وقت دیتای مورد نظر یکبار از دیتابیس واکشی شود، از دفعات بعد، آن دیتا را در Ram ذخیره میکند و برای درخواست‌های بعدی به دیتابیس Query نمیزند و دیتای مورد نیازش را از Ram میگیرد.
این امر در کنار مزایایی که دارد ، حساسیت بالایی هم بهمراه خواهد داشت؛ چرا که حافظه مورد استفاده Ram، یک حافظه محدود هست همچنین میتواند برای هر سخت افزاری متفاوت باشد. پس پیاده سازی این سیستم نیاز به دو دو تا چهارتا و ساختار درست دارد؛ در غیر اینصورت Cache کردن دیتای غلط میتواند به تنهایی وب‌سایتتان را Down کند؛ پس خیلی باید به این موضوع دقت داشت.

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

آیا تمام اطلاعات را میتوان کش کرد؟
خیر.
  • سخت افزاری که برای کش استفاده میکنیم یعنی Ram، بسیار گران‌تر از دیتابیس برای ما تمام میشود؛ چرا که محدود است.
  • اگر همه دیتاهارا کش کنید، عمل سرچ میان آن زمان بیشتری خواهد برد.
پس اکنون میدانید که میتوانیم داده‌های بی نهایتی را در دیتابیس ذخیره کنیم و فقط با ارزش‌ترین‌ها و پر مصرف‌ترین هارا در حافظه کش، ذخیره میکنیم.

عملیات Cache در Asp.Net Core توسط اینترفیس‌های IMemoryCache و IDistributedCache مدیریت میشود و میتوانید با تزریق این اینترفیس‌ها براحتی از متدهایشان استفاده کنید؛ اما قبل از استفاده لازم است با عملکرد هر یک از آن‌ها آشنا شویم.

روش اول : In-memory Caching (Local Caching)
معمول‌ترین و ابتدایی‌ترین روش برای کش کردن اطلاعات، روش Local Caching و بصورت In-Memory است که اطلاعات را در حافظه Ram همان سروری که برنامه در آن اجرا میشود، کش میکند.

این روش تا زمانیکه برنامه‌ی ما برای اجرا شدن، تنها از یک سرور استفاده کند، بهترین انتخاب خواهد بود؛ چرا که به دلیل نزدیک بودن، سریع‌ترین بازخورد را نیز به درخواست‌ها ارائه میدهد.


اما شرایطی را فرض کنید که برنامه از چندین سرور برای اجرا شدن استفاده میکند و به طبع هر سرور درخواست‌های خودش را داراست که ما باید برای هر یک بصورت جداگانه‌ای یک کش In-Memory را در حافظه Ram هرکدام ایجاد کنیم. 

فرض کنید دیتای ما 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 باشد. بخشی از دیتا در Server 1 کش میشود (1 , 3 , 5 , 9) و بخشی دیگر در Server 2 کش خواهد شد (2 , 4 , 6 ,7 , 8 , 10).


در اینجا مشکلات و ضعف هایی به وجود خواهد آمد :


  • برای مثال اگر Server 1 به هر دلیلی از بین برود یا Down شود، اطلاعات کش درون آن نیز پاک خواهد شد و بعد از راه اندازی باید همه آن را دوباره از دیتابیس بخواند.
  • هر کدام از سرور‌ها کش‌های جدایی دارند و باهم Sync نیستند و امکان وجود یک داده‌ی حیاتی در یکی و عدم وجود آن در دیگری، بالاست. فرض کنید برنامه برای هر درخواست، نیاز به اطلاعات دسترسی کاربری را دارد. دسترسی‌های کاربر، در Server 1 کش شده، اما در Server 2 موجود نیست. در Server 2 به دلیل عدم وجود این کش، برنامه برای درخواست‌های معمول خود و چک کردن دسترسی کاربر یا باید هربار به دیتابیس درخواستی را ارسال کند که این برخلاف خواسته ماست و یا باید دیتای مربوط به دسترسی‌های کاربر را بعد از یکبار درخواست، از دیتابیس در خودش کش کند که این‌هم دوباره کاری به حساب میاید و دوبار کش کردن یک دیتا، امر مطلوبی نخواهد بود.

روش هایی وجود دارد که بتوان از سیستم Local Caching در حالت چند سروری هم استفاده کرد و این مشکلات را از بین برد، اما روش استاندارد در حالت چند سروری، استفاده از Distributed Cache‌ها است.


روش دوم : Distributed Caching

در این روش برنامه‌ی ما برای اجرا شدن از چندین سرور شبکه شده به هم، در حال استفاده هست و Cache برنامه، توسط سرورها به اشتراک گذاشته شده. 

در این حالت سرور‌های ما از یک کش عمومی استفاده میکنند که مزایای آن شامل :

■ درخواست‌ها به چندین سرور مختلف از هم ارسال شده، اما دیتای کش بصورت منسجم در هریک وجود خواهد داشت.

■ با خراب شدن یا Down شدن یک سرور، کش موجود در سرور‌های دیگر پاک نمیشود و کماکان قابل استفاده است.

■ به حافظه Ram یک سرور محدود نیست و مشکلات زیادی همچون کمبود سخت افزاری و محدودیت‌های حافظه‌ی Ram را تا حد معقولی کاهش میدهد.


طریقه استفاده از Cache در Asp.Net Core :

  • بر خلاف ASP.NET web forms و ASP.NET MVC در نسخه‌های Core به بعد، Cache بصورت از پیش ثبت شده، وجود ندارد. کش در Asp.Net Core با فراخوانی سرویس‌های مربوطه‌ی آن قابل استفاده است و نیاز است قبل از استفاده، سرویس آن را در کلاس Startup برنامه فراخوانی کنید. 
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.AddMemoryCache();
}

  • اینترفیس IMemoryCache از سیستم تزریق وابستگی‌ها در Core استفاده میکند و برای استفاده از اینترفیس آن، پس از اضافه کردن MemoryCache به Startup ، باید در کنترلر، عمل تزریق وابستگی (DI) را انجام دهید؛ سپس متد‌های مورد نیاز برای کش، در دسترس خواهد بود. 
public class HomeController : Controller
{
    private readonly IMemoryCache  _cache;
    public HomeController(IMemoryCache  cache)
    {
        _cache = cache;
    }
    ....
}

  • برای ذخیره‌ی کش میتوانید از متد Set موجود در این اینترفیس استفاده کنید. 
public IActionResult Set()
{
  _cache.Set("CacheKey", data , TimeSpan.FromDays(1));
  return View();
}

در پارامتر اول این متد (CacheKey)، یک کلید، برای اطلاعاتی که میخواهیم کش کنیم قرار میدهیم. دقت کنید که این کلید، شناسه‌ی دیتای شماست و باید طوری آن را در نظر گرفت که با صدا زدن این کلید از سرویس کش، همان دیتای مورد نظر را برگشت دهد (هر Object دیتا، باید کلید Unique خود را داشته باشد).


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


  • برای دسترسی به اطلاعات کش شده میتوانید از متد Get استفاده کنید. 
public IActionResult Get()
{
  string data = _cache.Get("CacheKey");
  return View(data);
}

تنها پارامتر ورودی این متد، کلید از قبل نسبت داده شده به اطلاعات کش هست که با استفاده از یکسان بودن کلید در ورودی این متد و کلید Set شده از قبل در حافظه Ram، دیتا مربوط به آن را برگشت میدهد.


  • متد TryGetValue برای بررسی وجود یا عدم وجود یک کلید در حافظه کش هست و یک Boolean را خروجی میدهد. 
public IActionResult Set()
  {
        DateTime data;
       // Look for cache key.
       if (!_cache.TryGetValue( "CacheKey" , out data))
       {
              // Key not in cache, so get data.
              data= DateTime.Now;

            // Save data in cache and set the relative expiration time to one day
             _cache.Set( "CacheKey" , data, TimeSpan.FromDays(1));
        }
        return View(data);
  }

این متد ابتدا بررسی میکند که کلیدی با نام "CacheKey" وجود دارد یا خیر؟ در صورت عدم وجود، آن را میسازد و دیتای مورد نظر را به آن نسبت میدهد.


  • با استفاده از متد GetOrCreate میتوانید کار متد‌های Get و Set را باهم انجام دهید و در یک متد، وجود یا عدم وجود کش را بررسی و در صورت وجود، مقداری را return و در صورت عدم وجود، ابتدا ایجاد کش و بعد return مقدار کش شده را انجام دهید. 
 public IActionResult GetOrCreate()
{
         var data = _cache.GetOrCreate( "CacheKey" , entry =>
         entry.SlidingExpiration = TimeSpan.FromSeconds(3);
         return View(data);
});
    return View(data);
}

  • برای مدیریت حافظه‌ی Ram شما باید یک Expiration Time را برای کش‌های خود مشخص کنید؛ تا هم حافظه Ram را حجیم نکنید و هم در هر بازه‌ی زمانی، دیتای بروز را از دیتابیس بخوانید. برای این کار option‌های متفاوتی از جمله absolute expiration و sliding expiration وجود دارند.

در اینجا absolute expiration به این معنا است که یک زمان قطعی را برای منقضی شدن کش‌ها مشخص میکند؛ به عبارتی میگوییم کش با کلید فلان، در تاریخ و ساعت فلان حذف شود. اما در sliding expiration یک بازه زمانی برای منقضی شدن کش‌ها مشخص میکنیم؛ یعنی میگوییم بعد از گذشت فلان دقیقه از ایجاد کش، آن را حذف کن و اگر در طی این مدت مجددا خوانده شد، طول مدت زمان آن تمدید خواهد شد.

این تنظیمات را میتوانید در قالب یک option زمان Set کردن یک کش، به آن بدهید. 

MemoryCacheEntryOptions options = new MemoryCacheEntryOptions();
options.AbsoluteExpiration = DateTime.Now.AddMinutes(1);
options.SlidingExpiration = TimeSpan.FromMinutes(1);
_cache.Set("CacheKey", data, options );

در مثال بالا هردو option اضافه شده یک کار را انجام میدهند؛ با این تفاوت که absolute expiration تاریخ now را گرفته و یک دقیقه بعد را به آن اضافه کرده و تاریخ انقضای کش را با آن تاریخ set میکند. اما sliding expiration از حالا بمدت یک دقیقه اعتبار دارد.


  • یکی از روش‌های مدیریت حافظه Ram در کش‌ها این است که برای حذف شدن کش‌ها از حافظه، اولویت بندی‌هایی را تعریف کنید. اولویت‌ها در چهار سطح قابل دسترسی است: 

  1.  NeverRemove = 3
  2.  High = 2
  3. Normal = 1
  4.  Low = 0 

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

MemoryCacheEntryOptions options = new MemoryCacheEntryOptions();
// Low / Normal / High / NeverRemove
options.Priority = CacheItemPriority.High;
cache.Set("CacheKey", data, options);

به این صورت میتوانید الویت‌های متفاوت را در قالب option به کش‌های خود اختصاص دهید. 

در این مقاله سعی شد مفاهیم اولیه Cache، طوری گفته شود، تا برای افرادی که میخواهند به تازگی این سیستم را بیاموزند و در پروژه‌های خود استفاده کنند، کاربردی باشد و درک نسبی را نسبت به مزایا و محدودیت‌های این سیستم بدست آورند.


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

نظرات مطالب
شروع به کار با EF Core 1.0 - قسمت 11 - بررسی رابطه‌ی Self Referencing
آیا روش زیر برای پیاده‌سازی Self Referencing در حالی که هر عضو فقط صفر یا یک عضو فرزند می‌تواند داشته باشد، صحیح است؟
به نوعی پیاده‌سازی Self Referencing و  One-to-Zero-or-One  با هم:
public class EventItem
{
    public int Id { get; set; }

    public int? ParentId { get; set; }
    public virtual EventItem Parent { get; set; }

    public virtual EventItem Alternative { get; set; }
}
public class EventItemConfiguration : IEntityTypeConfiguration<EventItem>
{
    public void Configure(EntityTypeBuilder<EventItem> builder)
    {
        builder.HasIndex(e => e.ParentId);
        builder.HasOne(e => e.Parent)
            .WithOne(e => e.Alternative)
            .HasForeignKey<EventItem>(e => e.ParentId);
    }
}

مطالب
نمایش فرم‌های مودال Ajax ایی در ASP.NET MVC به کمک Twitter Bootstrap
اصول نمایش اطلاعات مودال به کمک bootstrap در مطلب «استفاده از modal dialogs مجموعه Twitter Bootstrap برای گرفتن تائید از کاربر» بررسی شدند.
در این قسمت قصد داریم یک فرم Ajaxایی را در ASP.NET MVC به همراه تمام مسایل اعتبارسنجی، پردازش اطلاعات و غیره را به کمک Twitter Bootstrap و jQuery Ajax پیاده سازی کنیم.


تهیه افزونه jquery.bootstrap-modal-ajax-form.js

از این جهت که مباحث مرتبط با نمایش و پردازش فرم‌های مودال Ajaxایی به کمک Twitter Bootstrap اندکی نکته دار و طولانی هستند، بهتر است این موارد را به شکل یک افزونه، کپسوله کنیم. کدهای کامل افزونه jquery.bootstrap-modal-ajax-form.js را در ادامه ملاحظه می‌کنید:
// <![CDATA[
(function ($) {
    $.bootstrapModalAjaxForm = function (options) {
        var defaults = {
            renderModalPartialViewUrl: null,
            renderModalPartialViewData: null,
            postUrl: '/',
            loginUrl: '/login',
            beforePostHandler: null,
            completeHandler: null,
            errorHandler: null
        };
        var options = $.extend(defaults, options);

        var validateForm = function (form) {
            //فعال سازی دستی اعتبار سنجی جی‌کوئری
            var val = form.validate();
            val.form();
            return val.valid();
        };

        var enableBootstrapStyleValidation = function () {
            $.validator.setDefaults({
                highlight: function (element, errorClass, validClass) {
                    if (element.type === 'radio') {
                        this.findByName(element.name).addClass(errorClass).removeClass(validClass);
                    } else {
                        $(element).addClass(errorClass).removeClass(validClass);
                        $(element).closest('.control-group').removeClass('success').addClass('error');
                    }
                    $(element).trigger('highlited');
                },
                unhighlight: function (element, errorClass, validClass) {
                    if (element.type === 'radio') {
                        this.findByName(element.name).removeClass(errorClass).addClass(validClass);
                    } else {
                        $(element).removeClass(errorClass).addClass(validClass);
                        $(element).closest('.control-group').removeClass('error').addClass('success');
                    }
                    $(element).trigger('unhighlited');
                }
            });
        }

        var enablePostbackValidation = function () {
            $('form').each(function () {
                $(this).find('div.control-group').each(function () {
                    if ($(this).find('span.field-validation-error').length > 0) {
                        $(this).addClass('error');
                    }
                });
            });
        }

        var processAjaxForm = function (dialog) {
            $('form', dialog).submit(function (e) {
                e.preventDefault();

                if (!validateForm($(this))) {
                    //اگر فرم اعتبار سنجی نشده، اطلاعات آن ارسال نشود
                    return false;
                }

                //در اینجا می‌توان مثلا دکمه‌ای را غیرفعال کرد
                if (options.beforePostHandler)
                    options.beforePostHandler();

                //اطلاعات نباید کش شوند
                $.ajaxSetup({ cache: false });
                $.ajax({
                    url: options.postUrl,
                    type: "POST",
                    data: $(this).serialize(),
                    success: function (result) {
                        if (result.success) {
                            $('#dialogDiv').modal('hide');
                            if (options.completeHandler)
                                options.completeHandler();
                        } else {
                            $('#dialogContent').html(result);
                            if (options.errorHandler)
                                options.errorHandler();
                        }
                    }
                });
                return false;
            });
        };

        var mainContainer = "<div id='dialogDiv' class='modal hide fade in'><div id='dialogContent'></div></div>";
        enableBootstrapStyleValidation(); //اعمال نکات خاص بوت استرپ جهت اعتبارسنجی یکپارچه با آن
        $.ajaxSetup({ cache: false });
        $.ajax({
            type: "POST",
            url: options.renderModalPartialViewUrl,
            data: options.renderModalPartialViewData,
            contentType: "application/json; charset=utf-8",
            dataType: "json",
            complete: function (xhr, status) {
                var data = xhr.responseText;
                var data = xhr.responseText;
                if (xhr.status == 403) {
                    window.location = options.loginUrl; //در حالت لاگین نبودن شخص اجرا می‌شود
                }
                else if (status === 'error' || !data) {
                    if (options.errorHandler)
                        options.errorHandler();
                }
                else {
                    var dialogContainer = "#dialogDiv";
                    $(dialogContainer).remove();
                    $(mainContainer).appendTo('body');

                    $('#dialogContent').html(data); // دریافت پویای اطلاعات مودال دیالوگ
                    $.validator.unobtrusive.parse("#dialogContent"); // فعال سازی اعتبارسنجی فرمی که با ایجکس بارگذاری شده                            
                    enablePostbackValidation();
                    // و سپس نمایش آن به صورت مودال
                    $('#dialogDiv').modal({
                        backdrop: 'static', //با کلیک کاربر روی صفحه، صفحه مودال بسته نمی‌شود
                        keyboard: true
                    }, 'show');
                    // تحت نظر قرار دادن این فرم اضافه شده
                    processAjaxForm('#dialogContent');
                }
            }
        });
    };
})(jQuery);
// ]]>
توضیحات:
- توابع enableBootstrapStyleValidation و enablePostbackValidation در مطلب «اعمال کلاس‌های ویژه اعتبارسنجی Twitter bootstrap به فرم‌های ASP.NET MVC» بررسی شدند.
- این افزونه با توجه به مقدار renderModalPartialViewUrl، یک partial view را از برنامه ASP.NET MVC درخواست می‌کند.
- سپس این partial view را به صورت خودکار به صفحه اضافه کرده و آن‌را به صورت modal نمایش می‌دهد.
- پس از افزودن فرم Ajaxایی دریافتی، مسایل اعتبارسنجی را به آن اعمال کرده و سپس دکمه submit آن‌را تحت کنترل قرار می‌دهد.
- در زمان submit، ابتدا بررسی می‌کند که آیا فرم معتبر است و اعتبارسنجی آن بدون مشکل است؟ اگر اینچنین است، اطلاعات فرم را به آدرس postUrl به صورت Ajaxایی ارسال می‌کند.


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

namespace Mvc4TwitterBootStrapTest.Models
{
    public class User
    {
        public int Id { set; get; }

        [DisplayName("نام")]
        [Required(ErrorMessage="لطفا نام را تکمیل کنید")]
        public string Name { set; get; }

        [DisplayName("نام خانوادگی")]
        [Required(ErrorMessage = "لطفا نام خانوادگی را تکمیل کنید")]
        public string LastName { set; get; }
    }
}
در اینجا یک مدل ساده‌را به همراه ویژگی‌های اعتبارسنجی و نام‌های نمایشی خواص ملاحظه می‌کنید.


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

using System.Web.Mvc;
using Mvc4TwitterBootStrapTest.Models;

namespace Mvc4TwitterBootStrapTest.Controllers
{
    public class ModalFormAjaxController : Controller
    {
        [HttpGet]
        public ActionResult Index()
        {
            return View(); //نمایش صفحه اولیه
        }

        [HttpPost] //برای این حالت امن‌تر است
        //[AjaxOnly]
        public ActionResult RenderModalPartialView()
        {
            //رندر پارشال ویوو صفحه مودال به همراه اطلاعات مورد نیاز آن
            return PartialView(viewName: "_ModalPartialView", model: new User { Name = "", LastName = "" });
        }

        [HttpPost]
        //[AjaxOnly]
        public ActionResult Index(User user) //ذخیره سازی اطلاعات
        {
            if (this.ModelState.IsValid)
            {
                //todo: SaveChanges;
                return Json(new { success = true });
            }

            this.ModelState.AddModelError("", "خطایی رخ داده است");
            return PartialView("_ModalPartialView", user);
        }
    }
}
کدهای کنترلر برنامه در این حالت از سه قسمت تشکیل می‌شود:
الف) متد Index حالت HttpGet که صفحه ابتدایی را نمایش خواهد داد.
ب) متد RenderModalPartialView یک partial view اضافه شده به برنامه به نام _ModalPartialView.cshtml را بازگشت می‌دهد. این partial view در حقیقت همان فرمی است که قرار است به صورت مودال نمایش داده شود و پردازش آن نیز Ajaxایی باشد.
ج) متد Index حالت HttpPost که نهایتا اطلاعات فرم مودال را دریافت خواهد کرد. اگر پردازش موفقیت آمیز بود، نیاز است همانند کدهای فوق return Json صورت گیرد. در غیراینصورت مجددا همان partial view را بازگشت دهید.


کدهای Index.cshtml

@{
    ViewBag.Title = "Index";
    var renderModalPartialViewUrl = Url.Action("RenderModalPartialView", "ModalFormAjax");
    var postDataUrl = Url.Action("Index", "ModalFormAjax");
}
<h2>
    Index</h2>
<a href="#" class="btn btn-primary" id="btnCreate">ثبت اطلاعات</a>

@section JavaScript
{
    <script type="text/javascript">
        $(function () {           
            $('#btnCreate').click(function (e) {
                e.preventDefault(); //می‌خواهیم لینک به صورت معمول عمل نکند

                $.bootstrapModalAjaxForm({
                    postUrl: '@postDataUrl',
                    renderModalPartialViewUrl: '@renderModalPartialViewUrl',
                    renderModalPartialViewData: {},
                    loginUrl: '/login',
                    beforePostHandler: function () {                       
                    },
                    completeHandler: function () {
                        // Refresh: برای حالتیکه نیاز به به روز رسانی کامل صفحه زیرین باشد
                        // location.reload();
                    },
                    errorHandler: function () {
                    }
                });
            });
        });             
    </script>
}
این کدها متناظر هستند با کدهای view اکشن متد Index در حالت Get.
- در اینجا یک لینک ساده در صفحه قرار گرفته و به کمک کلاس btn مجموعه bootstrap به شکل یک دکمه مزین شده است.
- در ادامه نحوه استفاده از افزونه‌ای را که در ابتدای بحث طراحی کردیم، ملاحظه می‌کنید. کار با آن بسیار ساده است و تنها باید مسیرهای ارسال اطلاعات نهایی به سرور یا postDataUrl و مسیر دریافت partial view رندر شده یا renderModalPartialViewUrl به آن معرفی شود. سایر مسایل آن خودکار است.


کدهای _ModalPartialView.cshtml یا همان فرم مودال برنامه

@model Mvc4TwitterBootStrapTest.Models.User
<div class="modal-header">
    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">
        &times;</button>
    <h5>
        افزودن کاربر جدید</h5>
</div>
@using (Html.BeginForm("Index", " ModalFormAjax", FormMethod.Post, new { @class = "modal-form" }))
{
    <div class="modal-body">
        @Html.ValidationSummary(true, null, new { @class = "alert alert-error alert-block" })
        <fieldset class="form-horizontal">
            <legend>مشخصات کاربر</legend>
            <div class="control-group">
                @Html.LabelFor(model => model.Name, new { @class = "control-label" })
                <div class="controls">
                    @Html.EditorFor(model => model.Name)
                    @Html.ValidationMessageFor(model => model.Name, null, new { @class = "help-inline" })
                </div>
            </div>
            <div class="control-group">
                @Html.LabelFor(model => model.LastName, new { @class = "control-label" })
                <div class="controls">
                    @Html.EditorFor(model => model.LastName)
                    @Html.ValidationMessageFor(model => model.LastName, null, new { @class = "help-inline" })
                </div>
            </div>
        </fieldset>
    </div>
        
    <div class="modal-footer">
        <button class="btn btn-primary" type="submit">
            ارسال</button>
        <button class="btn" data-dismiss="modal" aria-hidden="true">
            انصراف</button>
    </div>
}
در اینجا اطلاعات فرمی را ملاحظه می‌کنید که قرار است به صورت مودال نمایش داده شود. نحوه طراحی آن بر اساس نکات form-horizontal است. همچنین divهای modal-header، modal-body و modal-footer نیز به این فرم ویژه اضافه شده‌اند تا به خوبی توسط bootstrap پردازش گردد.
حاصل نهایی این مبحث را در دو شکل ذیل ملاحظه می‌کنید. صفحه index نمایش دهنده یک دکمه و در ادامه باز شدن یک فرم مودال، پس از کلیک بر روی دکمه ثبت اطلاعات.