نظرات مطالب
EF Code First #1
من هم با مشکل شما مواجه شدم و پس از بررسی‌های زیاد مشکل رو در کانکشن پیدا کردم. کد زیر کانکشنی هست که من بصورت لوکال استفاده می‌کنم (حالت SQL Server Authentication). البته به یک Instance از SQL Server بر روی کامپیوتر خودم متصل می‌شم.
<connectionStrings>
<add name="X"
  connectionString="Data Source=Y;Initial Catalog=DataBaseName;User ID=sa; Password=1234;"
  providerName="System.Data.SqlClient" />
  </connectionStrings>
X نامی هست که از طریق سازنده کلاس Context در لایه  DataAccess مشخص کردم.
  public Context():base("X")
        {
            
        }
Y هم نام Instance ای هست که بصورت لوکال وصل می‌شوم .
مطالب
سفارشی سازی ASP.NET Core Identity - قسمت دوم - سرویس‌های پایه
در قسمت قبل کلاس‌های متناظر با جداول پایه‌ی ASP.NET Core Identity را تغییر دادیم. اما هنوز سرویس‌های پایه‌ی این فریم ورک مانند مدیریت و ذخیره‌ی کاربران و مدیریت و ذخیره‌ی نقش‌ها، اطلاعی از وجود آن‌ها ندارند. در ادامه این سرویس‌ها را نیز سفارشی سازی کرده و سپس به ASP.NET Core Identity معرفی می‌کنیم.

سفارشی سازی RoleStore

ASP.NET Core Identity دو سرویس را جهت کار با نقش‌های کاربران پیاده سازی کرده‌است:
- سرویس RoleStore: کار آن دسترسی به ApplicationDbContext ایی است که در قسمت قبل سفارشی سازی کردیم و سپس ارائه‌ی زیر ساختی به سرویس RoleManager جهت استفاده‌ی از آن برای ذخیره سازی و یا تغییر و حذف نقش‌های سیستم.
وجود Storeها از این جهت است که اگر علاقمند بودید، بتوانید از سایر ORMها و یا زیرساخت‌های ذخیره سازی اطلاعات برای کار با بانک‌های اطلاعاتی استفاده کنید. در اینجا از همان پیاده سازی پیش‌فرض آن که مبتنی بر EF Core است استفاده می‌کنیم. به همین جهت است که وابستگی ذیل را در فایل project.json مشاهده می‌کنید:
   "Microsoft.AspNetCore.Identity.EntityFrameworkCore": "1.1.0",
- سرویس RoleManager: این سرویس از سرویس RoleStore و تعدادی سرویس دیگر مانند اعتبارسنج نام نقش‌ها و نرمال ساز نام نقش‌ها، جهت ارائه‌ی متدهایی برای یافتن، افزودن و هر نوع عملیاتی بر روی نقش‌ها استفاده می‌کند.
برای سفارشی سازی سرویس‌های پایه‌ی ASP.NET Core Identity‌، ابتدا باید سورس این مجموعه را جهت یافتن نگارشی از سرویس مدنظر که کلاس‌های موجودیت را به صورت آرگومان‌های جنریک دریافت می‌کند، پیدا کرده و سپس از آن ارث بری کنیم:
public class ApplicationRoleStore :
     RoleStore<Role, ApplicationDbContext, int, UserRole, RoleClaim>,
     IApplicationRoleStore
تا اینجا مرحله‌ی اول تشکیل کلاس ApplicationRoleStore سفارشی به پایان می‌رسد. نگارش جنریک RoleStore پایه را یافته و سپس موجودیت‌های سفارشی سازی شده‌ی خود را به آن معرفی می‌کنیم.
این ارث بری جهت تکمیل، نیاز به بازنویسی سازنده‌ی RoleStore پایه را نیز دارد:
public ApplicationRoleStore(
   IUnitOfWork uow,
   IdentityErrorDescriber describer = null)
   : base((ApplicationDbContext)uow, describer)
در اینجا پارامتر اول آن‌را به IUnitOfWork بجای DbContext متداول تغییر داده‌ایم؛ چون IUnitOfWork دقیقا از همین نوع است و همچنین امکان دسترسی به متدهای ویژه‌ی آن‌را نیز فراهم می‌کند.
در نگارش 1.1 این کتابخانه، بازنویسی متد CreateRoleClaim نیز اجباری است تا در آن مشخص کنیم، نحوه‌ی تشکیل کلید خارجی و اجزای یک RoleClaim به چه نحوی است:
        protected override RoleClaim CreateRoleClaim(Role role, Claim claim)
        {
            return new RoleClaim
            {
                RoleId = role.Id,
                ClaimType = claim.Type,
                ClaimValue = claim.Value
            };
        }
در نگارش 2.0 آن، این new RoleClaim به صورت خودکار توسط کتابخانه صورت خواهد گرفت و سفارشی کردن آن ساده‌تر می‌شود.

در ادامه اگر به انتهای تعریف امضای کلاس دقت کنید، یک اینترفیس IApplicationRoleStore را نیز مشاهده می‌کنید. برای تشکیل آن بر روی نام کلاس سفارشی خود کلیک راست کرده و با استفاده از ابزارهای Refactoring کار Extract interface را انجام می‌دهیم؛ از این جهت که در سایر لایه‌های برنامه نمی‌خواهیم از تزریق مستقیم کلاس ApplicationRoleStore استفاده کنیم. بلکه می‌خواهیم اینترفیس IApplicationRoleStore را در موارد ضروری، به سازنده‌های کلاس‌های تزریق نمائیم.
پس از تشکیل این اینترفیس، مرحله‌ی بعد، معرفی آن‌ها به سیستم تزریق وابستگی‌های ASP.NET Core است. چون تعداد تنظیمات مورد نیاز ما زیاد هستند، یک کلاس ویژه به نام IdentityServicesRegistry تشکیل شده‌است تا به عنوان رجیستری تمام سرویس‌های سفارشی سازی شده‌ی ما عمل کند و تنها با فراخوانی متد AddCustomIdentityServices آن در کلاس آغازین برنامه، کار ثبت یکجای تمام سرویس‌های ASP.NET Core Identity انجام شود و بی‌جهت کلاس آغازین برنامه شلوغ نگردد.
ثبت ApplicationDbContext طبق روش متداول آن در کلاس آغازین برنامه انجام شده‌است. سپس معرفی سرویس IUnitOfWork را که از ApplicationDbContext تامین می‌شود، در کلاس IdentityServicesRegistry مشاهده می‌کنید.
 services.AddScoped<IUnitOfWork, ApplicationDbContext>();
در ادامه RoleStore سفارشی ما نیاز به دو تنظیم جدید را خواهد داشت:
services.AddScoped<IApplicationRoleStore, ApplicationRoleStore>();
services.AddScoped<RoleStore<Role, ApplicationDbContext, int, UserRole, RoleClaim>, ApplicationRoleStore>();
ابتدا مشخص کرده‌ایم که اینترفیس IApplicationRoleStore، از طریق کلاس سفارشی ApplicationRoleStore تامین می‌شود.
سپس RoleStore توکار ASP.NET Core Identity را نیز به ApplicationRoleStore خود هدایت کرده‌ایم. به این ترتیب هر زمانیکه در کدهای داخلی این فریم ورک وهله‌ای از RoleStore جنریک آن درخواست می‌شود، دیگر از همان پیاده سازی پیش‌فرض خود استفاده نخواهد کرد و به پیاده سازی ما هدایت می‌شود.

این روشی است که جهت سفارشی سازی تمام سرویس‌های پایه‌ی ASP.NET Core Identity بکار می‌گیریم:
1) ارث بری از نگارش جنریک سرویس پایه‌ی موجود و معرفی موجودیت‌های سفارشی سازی شده‌ی خود به آن
2) سفارشی سازی سازنده‌ی این کلاس‌ها با سرویس‌هایی که تهیه کرده‌ایم (بجای سرویس‌های پیش فرض).
3) تکمیل متدهایی که باید به اجبار پس از این ارث بری پیاده سازی شوند.
4) استخراج یک اینترفیس از کلاس نهایی تشکیل شده (توسط ابزارهای Refactoring).
5) معرفی اینترفیس و همچنین نمونه‌ی توکار این سرویس به سیستم تزریق وابستگی‌های ASP.NET Core جهت استفاده‌ی از این سرویس جدید سفارشی سازی شده.


سفارشی سازی RoleManager

در اینجا نیز همان 5 مرحله‌ای را که عنوان کردیم باید جهت تشکیل کلاس جدید ApplicationRoleManager پیگیری کنیم.
1) ارث بری از نگارش جنریک سرویس پایه‌ی موجود و معرفی موجودیت‌های سفارشی سازی شده‌ی خود به آن
public class ApplicationRoleManager :
    RoleManager<Role>,
    IApplicationRoleManager
در اینجا نگارشی از RoleManager را انتخاب کرده‌ایم که بتواند Role سفارشی خود را به سیستم معرفی کند.

2) سفارشی سازی سازنده‌ی این کلاس با سرویسی که تهیه کرده‌ایم (بجای سرویس پیش فرض).
public ApplicationRoleManager(
            IApplicationRoleStore store,
            IEnumerable<IRoleValidator<Role>> roleValidators,
            ILookupNormalizer keyNormalizer,
            IdentityErrorDescriber errors,
            ILogger<ApplicationRoleManager> logger,
            IHttpContextAccessor contextAccessor,
            IUnitOfWork uow) :
            base((RoleStore<Role, ApplicationDbContext, int, UserRole, RoleClaim>)store, roleValidators, keyNormalizer, errors, logger, contextAccessor)
در این سفارشی سازی دو مورد را تغییر داده‌ایم:
الف) ذکر IApplicationRoleStore بجای RoleStore آن
ب) ذکر IUnitOfWork بجای ApplicationDbContext

3) تکمیل متدهایی که باید به اجبار پس از این ارث بری پیاده سازی شوند.
RoleManager پایه نیازی به پیاده سازی اجباری متدی ندارد.

4) استخراج یک اینترفیس از کلاس نهایی تشکیل شده (توسط ابزارهای Refactoring).
محتوای این اینترفیس را در IApplicationRoleManager‌ مشاهده می‌کنید.

5) معرفی اینترفیس و همچنین نمونه‌ی توکار این سرویس به سیستم تزریق وابستگی‌های ASP.NET Core جهت استفاده‌ی از این سرویس جدید سفارشی سازی شده.
services.AddScoped<IApplicationRoleManager, ApplicationRoleManager>();
services.AddScoped<RoleManager<Role>, ApplicationRoleManager>();
در کلاس IdentityServicesRegistry، یکبار اینترفیس و یکبار اصل سرویس توکار RoleManager را به سرویس جدید و سفارشی سازی شده‌ی ApplicationRoleManager خود هدایت کرده‌ایم.


سفارشی سازی UserStore

در مورد مدیریت کاربران نیز دو سرویس Store و Manager را مشاهده می‌کنید. کار کلاس Store، کپسوله سازی لایه‌ی دسترسی به داده‌ها است تا کتابخانه‌های ثالث، مانند وابستگی Microsoft.AspNetCore.Identity.EntityFrameworkCore بتوانند آن‌را پیاده سازی کنند و کار کلاس Manager، استفاده‌ی از این Store است جهت کار با بانک اطلاعاتی.

5 مرحله‌ای را که باید جهت تشکیل کلاس جدید ApplicationUserStore پیگیری کنیم، به شرح زیر هستند:
1) ارث بری از نگارش جنریک سرویس پایه‌ی موجود و معرفی موجودیت‌های سفارشی سازی شده‌ی خود به آن
public class ApplicationUserStore :
   UserStore<User, Role, ApplicationDbContext, int, UserClaim, UserRole, UserLogin, UserToken, RoleClaim>,
   IApplicationUserStore
از بین نگارش‌های مختلف UserStore، نگارشی را انتخاب کرده‌ایم که بتوان در آن موجودیت‌های سفارشی سازی شده‌ی خود را معرفی کنیم.

2) سفارشی سازی سازنده‌ی این کلاس با سرویسی که تهیه کرده‌ایم (بجای سرویس پیش فرض).
public ApplicationUserStore(
   IUnitOfWork uow,
   IdentityErrorDescriber describer = null)
   : base((ApplicationDbContext)uow, describer)
در این سفارشی سازی یک مورد را تغییر داده‌ایم:
الف) ذکر IUnitOfWork بجای ApplicationDbContext

3) تکمیل متدهایی که باید به اجبار پس از این ارث بری پیاده سازی شوند.
در اینجا نیز تکمیل 4 متد از کلاس پایه UserStore جنریک انتخاب شده، جهت مشخص سازی نحوه‌ی انتخاب کلیدهای خارجی جداول سفارشی سازی شده‌ی مرتبط با جدول کاربران ضروری است:
        protected override UserClaim CreateUserClaim(User user, Claim claim)
        {
            var userClaim = new UserClaim { UserId = user.Id };
            userClaim.InitializeFromClaim(claim);
            return userClaim;
        }

        protected override UserLogin CreateUserLogin(User user, UserLoginInfo login)
        {
            return new UserLogin
            {
                UserId = user.Id,
                ProviderKey = login.ProviderKey,
                LoginProvider = login.LoginProvider,
                ProviderDisplayName = login.ProviderDisplayName
            };
        }

        protected override UserRole CreateUserRole(User user, Role role)
        {
            return new UserRole
            {
                UserId = user.Id,
                RoleId = role.Id
            };
        }

        protected override UserToken CreateUserToken(User user, string loginProvider, string name, string value)
        {
            return new UserToken
            {
                UserId = user.Id,
                LoginProvider = loginProvider,
                Name = name,
                Value = value
            };
        }
در نگارش 2.0 آن، این new‌ها به صورت خودکار توسط خود فریم ورک صورت خواهد گرفت و سفارشی کردن آن ساده‌تر می‌شود.

4) استخراج یک اینترفیس از کلاس نهایی تشکیل شده (توسط ابزارهای Refactoring).
محتوای این اینترفیس را در IApplicationUserStore‌ مشاهده می‌کنید.

5) معرفی اینترفیس و همچنین نمونه‌ی توکار این سرویس به سیستم تزریق وابستگی‌های ASP.NET Core جهت استفاده‌ی از این سرویس جدید سفارشی سازی شده.
services.AddScoped<IApplicationUserStore, ApplicationUserStore>();
services.AddScoped<UserStore<User, Role, ApplicationDbContext, int, UserClaim, UserRole, UserLogin, UserToken, RoleClaim>, ApplicationUserStore>();
در کلاس IdentityServicesRegistry، یکبار اینترفیس و یکبار اصل سرویس توکار UserStore را به سرویس جدید و سفارشی سازی شده‌ی ApplicationUserStore خود هدایت کرده‌ایم.


سفارشی سازی UserManager

5 مرحله‌ای را که باید جهت تشکیل کلاس جدید ApplicationUserManager پیگیری کنیم، به شرح زیر هستند:
1) ارث بری از نگارش جنریک سرویس پایه‌ی موجود و معرفی موجودیت‌های سفارشی سازی شده‌ی خود به آن
public class ApplicationUserManager :
   UserManager<User>,
   IApplicationUserManager
از بین نگارش‌های مختلف UserManager، نگارشی را انتخاب کرده‌ایم که بتوان در آن موجودیت‌های سفارشی سازی شده‌ی خود را معرفی کنیم.

2) سفارشی سازی سازنده‌ی این کلاس با سرویسی که تهیه کرده‌ایم (بجای سرویس پیش فرض).
public ApplicationUserManager(
            IApplicationUserStore store,
            IOptions<IdentityOptions> optionsAccessor,
            IPasswordHasher<User> passwordHasher,
            IEnumerable<IUserValidator<User>> userValidators,
            IEnumerable<IPasswordValidator<User>> passwordValidators,
            ILookupNormalizer keyNormalizer,
            IdentityErrorDescriber errors,
            IServiceProvider services,
            ILogger<ApplicationUserManager> logger,
            IHttpContextAccessor contextAccessor,
            IUnitOfWork uow,
            IUsedPasswordsService usedPasswordsService)
            : base((UserStore<User, Role, ApplicationDbContext, int, UserClaim, UserRole, UserLogin, UserToken, RoleClaim>)store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
در این سفارشی سازی چند مورد را تغییر داده‌ایم:
الف) ذکر IUnitOfWork بجای ApplicationDbContext (البته این مورد، یک پارامتر اضافی است که بر اساس نیاز این سرویس سفارشی، اضافه شده‌است)
تمام پارامترهای پس از logger به دلیل نیاز این سرویس اضافه شده‌اند و جزو پارامترهای سازنده‌ی کلاس پایه نیستند.
ب) استفاده‌ی از IApplicationUserStore بجای UserStore پیش‌فرض

3) تکمیل متدهایی که باید به اجبار پس از این ارث بری پیاده سازی شوند.
UserManager پایه نیازی به پیاده سازی اجباری متدی ندارد.

4) استخراج یک اینترفیس از کلاس نهایی تشکیل شده (توسط ابزارهای Refactoring).
محتوای این اینترفیس را در IApplicationUserManager‌ مشاهده می‌کنید.

5) معرفی اینترفیس و همچنین نمونه‌ی توکار این سرویس به سیستم تزریق وابستگی‌های ASP.NET Core جهت استفاده‌ی از این سرویس جدید سفارشی سازی شده.
services.AddScoped<IApplicationUserManager, ApplicationUserManager>();
services.AddScoped<UserManager<User>, ApplicationUserManager>();
در کلاس IdentityServicesRegistry، یکبار اینترفیس و یکبار اصل سرویس توکار UserManager را به سرویس جدید و سفارشی سازی شده‌ی ApplicationUserManager خود هدایت کرده‌ایم.


سفارشی سازی SignInManager

سرویس پایه SignInManager از سرویس UserManager جهت فراهم آوردن زیرساخت لاگین کاربران استفاده می‌کند.
5 مرحله‌ای را که باید جهت تشکیل کلاس جدید ApplicationSignInManager پیگیری کنیم، به شرح زیر هستند:
1) ارث بری از نگارش جنریک سرویس پایه‌ی موجود و معرفی موجودیت‌های سفارشی سازی شده‌ی خود به آن
public class ApplicationSignInManager :
    SignInManager<User>,
    IApplicationSignInManager
از بین نگارش‌های مختلف SignInManager، نگارشی را انتخاب کرده‌ایم که بتوان در آن موجودیت‌های سفارشی سازی شده‌ی خود را معرفی کنیم.

2) سفارشی سازی سازنده‌ی این کلاس با سرویسی که تهیه کرده‌ایم (بجای سرویس پیش فرض).
public ApplicationSignInManager(
            IApplicationUserManager userManager,
            IHttpContextAccessor contextAccessor,
            IUserClaimsPrincipalFactory<User> claimsFactory,
            IOptions<IdentityOptions> optionsAccessor,
            ILogger<ApplicationSignInManager> logger)
            : base((UserManager<User>)userManager, contextAccessor, claimsFactory, optionsAccessor, logger)
در این سفارشی سازی یک مورد را تغییر داده‌ایم:
الف) استفاده‌ی از IApplicationUserManager بجای UserManager پیش‌فرض

3) تکمیل متدهایی که باید به اجبار پس از این ارث بری پیاده سازی شوند.
SignInManager پایه نیازی به پیاده سازی اجباری متدی ندارد.

4) استخراج یک اینترفیس از کلاس نهایی تشکیل شده (توسط ابزارهای Refactoring).
محتوای این اینترفیس را در IApplicationSignInManager‌ مشاهده می‌کنید.

5) معرفی اینترفیس و همچنین نمونه‌ی توکار این سرویس به سیستم تزریق وابستگی‌های ASP.NET Core جهت استفاده‌ی از این سرویس جدید سفارشی سازی شده.
services.AddScoped<IApplicationSignInManager, ApplicationSignInManager>();
services.AddScoped<SignInManager<User>, ApplicationSignInManager>();
در کلاس IdentityServicesRegistry، یکبار اینترفیس و یکبار اصل سرویس توکار مدیریت لاگین را به سرویس جدید و سفارشی سازی شده‌ی ApplicationSignInManager خود هدایت کرده‌ایم.


معرفی نهایی سرویس‌های سفارشی سازی شده به ASP.NET Identity Core

تا اینجا سرویس‌های پایه‌ی این فریم ورک را جهت معرفی موجودیت‌های سفارشی سازی شده‌ی خود سفارشی سازی کردیم و همچنین آن‌ها را به سیستم تزریق وابستگی‌های ASP.NET Core نیز معرفی نمودیم. مرحله‌ی آخر، ثبت این سرویس‌ها در رجیستری ASP.NET Core Identity است:
 services.AddIdentity<User, Role>(identityOptions =>
{
}).AddUserStore<ApplicationUserStore>()
  .AddUserManager<ApplicationUserManager>()
  .AddRoleStore<ApplicationRoleStore>()
  .AddRoleManager<ApplicationRoleManager>()
  .AddSignInManager<ApplicationSignInManager>()
  // You **cannot** use .AddEntityFrameworkStores() when you customize everything
  //.AddEntityFrameworkStores<ApplicationDbContext, int>()
  .AddDefaultTokenProviders();
اگر منابع را مطالعه کنید، تمام آن‌ها از AddEntityFrameworkStores و سپس معرفی ApplicationDbContext به آن استفاده می‌کنند. با توجه به اینکه ما همه چیز را در اینجا سفارشی سازی کرده‌ایم، فراخوانی متد افزودن سرویس‌های EF این فریم ورک، تمام آن‌ها را بازنویسی کرده و به حالت اول و پیش فرض آن بر می‌گرداند. بنابراین نباید از آن استفاده شود.
در اینجا متد AddIdentity یک سری  تنظیم‌های پیش فرض‌ها این فریم ورک مانند اندازه‌ی طول کلمه‌ی عبور، نام کوکی و غیره را در اختیار ما قرار می‌دهد به همراه ثبت تعدادی سرویس پایه مانند نرمال ساز نام‌ها و ایمیل‌ها. سپس توسط متدهای AddUserStore، AddUserManager و ... ایی که مشاهده می‌کنید، سبب بازنویسی سرویس‌های پیش فرض این فریم ورک به سرویس‌های سفارشی خود خواهیم شد.

در این مرحله‌است که اگر Migration را اجرا کنید، کار می‌کند و خطای تبدیل نشدن کلاس‌ها به یکدیگر را دریافت نخواهید کرد.


تشکیل مرحله استفاده‌ی از ASP.NET Core Identity و ثبت اولین کاربر در بانک اطلاعاتی به صورت خودکار

روال متداول کار با امکانات کتابخانه‌های نوشته شده‌ی برای ASP.NET Core، ثبت سرویس‌های پایه‌ی آن‌ها توسط متدهای Add است که نمونه‌ی services.AddIdentity فوق دقیقا همین کار را انجام می‌دهد. مرحله‌ی بعد به app.UseIdentity می‌رسیم که کار ثبت میان‌افزارهای این فریم ورک را انجام می‌دهد. متد UseCustomIdentityServices کلاس IdentityServicesRegistry این‌کار را انجام می‌دهد که از آن در کلاس آغازین برنامه استفاده شده‌است.
        public static void UseCustomIdentityServices(this IApplicationBuilder app)
        {
            app.UseIdentity();

            var identityDbInitialize = app.ApplicationServices.GetService<IIdentityDbInitializer>();
            identityDbInitialize.Initialize();
            identityDbInitialize.SeedData();
        }
در اینجا یک مرحله‌ی استفاده‌ی از سرویس IIdentityDbInitializer را نیز مشاهده می‌کنید. کلاس IdentityDbInitializer‌ این اهداف را برآورده می‌کند:
الف) متد Initialize آن، متد context.Database.Migrate را فراخوانی می‌کند. به همین جهت دیگر نیاز به اعمال دستی حاصل Migrations، به بانک اطلاعاتی نخواهد بود. متد Database.Migrate هر مرحله‌ی اعمال نشده‌ای را که باقی مانده باشد، به صورت خودکار اعمال می‌کند.
ب) متد SeedData آن، به نحو صحیحی یک Scope جدید را ایجاد کرده و توسط آن به ApplicationDbContext دسترسی پیدا می‌کند تا نقش Admin و کاربر Admin را به سیستم اضافه کند. همچنین به کاربر Admin، نقش Admin را نیز انتساب می‌دهد. تنظیمات این کاربران را نیز از فایل appsettings.json و مدخل AdminUserSeed آن دریافت می‌کند.


کدهای کامل این سری را در مخزن کد DNT Identity می‌توانید ملاحظه کنید.
مطالب
خروجی Excel با حجم بالا در برنامه‌های ‌ASP.NET Core با استفاده از MiniExcel

امکان خروجی اکسل از گزارشات سیستم، یکی از بایدهای بیشتر سیستم‌های اطلاعاتی می‌باشد؛ یکی از چالش‌های اصلی در تولید این نوع خروجی، افزایش مصرف حافظه متناسب با افزایش حجم دیتا می‌باشد. از آنجایی‌که بیشتر راهکارهای موجود از جمله ClosedXml یا Epplus کل ساختار را ابتدا تولید کرده و اصطلاحا خروجی مورد نظر را بافر می‌کنند، برای حجم بالای اطلاعات مناسب نخواهند بود. راهکار برای خروجی CSV به عنوان مثال خیلی سرراست می‌باشد و می‌توان با چند خط کد، به نتیجه دلخواه از طریق مکانیزم Streaming رسید؛ ولی ساختار Excel به سادگی فرمت CSV نیست و برای مثال فرمت Excel Workbook با پسوند xlsx یک بسته Zip شده‌ای از فایل‌های XML می‌باشد.

معرفی MiniExcel

MiniExcel یک کتابخانه سورس باز با هدف به حداقل رساندن مصرف حافظه در زمان پردازش فایل‌های Excel در دات نت می‌باشد. در مقایسه با Aspose از منظر امکانات شاید حرفی برای گفتن نداشته باشد، ولی از جهت خواندن اطلاعات فایل‌های Excel با قابلیت پشتیبانی از ‌LINQ و Deferred Execution در کنار مصرف کم حافظه و جلوگیری از مشکل OOM خیلی خوب عمل می‌کند. در تصویر زیر مشخص است که برای عمده عملیات پیاده‌سازی شده، از استریم‌ها بهره برده شده است.

همچنین در زیر مقایسه‌ای روی خروجی ۱ میلیون رکورد با تعداد ۱۰ ستون در هر ردیف انجام شده‌است که قابل توجه می‌باشد:

Logic : create a total of 10,000,000 "HelloWorld" excel
LibraryMethodMax Memory UsageMean
MiniExcel'MiniExcel Create Xlsx'15 MB11.53181 sec
Epplus'Epplus Create Xlsx'1,204 MB22.50971 sec
OpenXmlSdk'OpenXmlSdk Create Xlsx'2,621 MB42.47399 sec
ClosedXml'ClosedXml Create Xlsx'7,141 MB140.93992 sec

به شدت API خوش دستی برای استفاده دارد و شاید مطالعه سورس کد آن از جهت طراحی نیز درس آموزی داشته باشد. در ادامه چند مثال از مستندات آن را می‌توانید ملاحظه کنید:

var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.xlsx");
MiniExcel.SaveAs(path, new[] {
    new { Column1 = "MiniExcel", Column2 = 1 },
    new { Column1 = "Github", Column2 = 2}
});

// DataReader export multiple sheets (recommand by Dapper ExecuteReader)

using (var cnn = Connection)
{
    cnn.Open();
    var sheets = new Dictionary<string,object>();
    sheets.Add("sheet1", cnn.ExecuteReader("select 1 id"));
    sheets.Add("sheet2", cnn.ExecuteReader("select 2 id"));
    MiniExcel.SaveAs("Demo.xlsx", sheets);
}

طراحی یک ActionResult سفارشی برای استفاده از MiniExcel

برای این منظور نیاز است تا Stream مربوط به Response درخواست جاری را در اختیار این کتابخانه قرار دهیم و از سمت دیگر دیتای مورد نیاز را به نحوی که بافر نشود و از طریق مکانیزم Streaming در EF (استفاده از Deferred Execution و Enumerableها) مهیا کنیم. برای امکان تعویض پذیری (این سناریو در پروژه واقعی و باتوجه به جهت وابستگی‌ها می‌تواند ضروری باشد) از دو واسط زیر استفاده خواهیم کرد:

public interface IExcelDocumentFactory
{
    ILargeExcelDocument CreateLargeDocument(IEnumerable<ExcelColumn> headers, Stream stream);
}


public interface ILargeExcelDocument : IAsyncDisposable, IDisposable
{
    Task Write<T>(
        PaginatedEnumerable<T> items,
        int count,
        int sizeLimit,
        CancellationToken cancellationToken = default) where T : notnull;
}

متد CreateLargeDocument یک وهله از ILargeExcelDocument را در اختیار مصرف کننده قرار می‌دهد که قابلیت نوشتن روی آن از طریق متد Write را خواهد داشت. روش واکشی دیتا از طریق Delegate تعریف شده با نام PaginatedEnumerable به مصرف کننده محول شده‌است که در ادامه امضای آن را می‌توانید مشاهده کنید:

public delegate IEnumerable<T> PaginatedEnumerable<out T>(int page, int pageSize);

در ادامه پیاده‌سازی واسط ILargeExcelDocument برای MiniExcel به شکل زیر خواهد بود:

internal sealed class MiniExcelDocument(Stream stream, IEnumerable<ExcelColumn> columns) : ILargeExcelDocument
{
    private const int SheetLimit = 1_048_576;
    private bool _disposedValue;

    public async Task Write<T>(
        PaginatedEnumerable<T> items,
        int count,
        int sizeLimit,
        CancellationToken cancellationToken = default)
        where T : notnull
    {
        ThrowIfDisposed();
        
        // TODO: apply sizeLimit
        var properties = FastReflection.Instance.GetProperties(typeof(T))
            .ToDictionary(p => p.Name, StringComparer.OrdinalIgnoreCase);

        var sheets = new Dictionary<string, object>();
        var index = 1;
        while (count > 0)
        {
            cancellationToken.ThrowIfCancellationRequested();

            IEnumerable<Dictionary<string, object>> reader = items(index, SheetLimit)
                .Select(item =>
                {
                    cancellationToken.ThrowIfCancellationRequested();
                    return columns.ToDictionary(h => h.Title, h => ValueOf(item, h.Name, properties));
                });

            sheets.Add($"sheet_{index}", reader);
            count -= SheetLimit;
            index++;
        }

        // This part is forward-only, and we are pretty sure that streaming will happen without buffering.
        await stream.SaveAsAsync(sheets, cancellationToken: cancellationToken);
    }

    private void Dispose(bool disposing)
    {
        if (!_disposedValue)
        {
            if (disposing)
            {
                // TODO: dispose managed state (managed objects)
            }

            // TODO: free unmanaged resources (unmanaged objects) and override finalizer
            // TODO: set large fields to null
            _disposedValue = true;
        }
    }

    ~MiniExcelDocument()
    {
        Dispose(disposing: false);
    }

    public void Dispose()
    {
        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }

    public async ValueTask DisposeAsync()
    {
        Dispose();
        await ValueTask.CompletedTask;
    }

    private void ThrowIfDisposed()
    {
        if (!_disposedValue) return;
        
        throw new ObjectDisposedException(nameof(MiniExcelDocument));
    }
    private static object ValueOf<T>(T record, string prop, IDictionary<string, FastPropertyInfo> properties)
        where T : notnull
    {
        var property = properties[prop] ??
                       throw new InvalidOperationException($"There is no property with given name [{prop}]");

        return NormalizeValue(property.GetValue?.Invoke(record));
    }

    private static object NormalizeValue(object? value)
    {
        if (value == null) return null!;

        return value switch
        {
            DateTime dateTime => dateTime.ToShortPersianDateTimeString(),
            TimeSpan time => time.ToString(@"hh\:mm\:ss"),
            DateOnly dateTime => dateTime.ToShortPersianDateString(false),
            TimeOnly time => time.ToString(@"hh\:mm\:ss"),
            bool boolean => boolean ? "بلی" : "خیر",
            IEnumerable<object> values => string.Join(',', values.Select(NormalizeValue).ToList()),
            Enum enumField => enumField.GetEnumStringValue(),
            _ => value
        };
    }
}

در بدنه متد Write باتوجه به تعداد کل رکوردها، یک کوئری برای هر شیت از طریق فراخوانی متد منتسب به پارامتر items اجرا خواهد شد؛ توجه کنید که اجرای این کوئری مشخصا به تعویق افتاده و تا زمان اولین MoveNext، اجرایی صورت نخواهد گرفت (مفهوم Deferred Execution). به این ترتیب باقی کارها از جمله فرمت کردن مقادیر در سمت برنامه و از طریق Linq To Object انجام خواهد شد. همچنین پیاده‌سازی Factory مرتبط با آن به شکل زیر خواهد بود:

internal sealed class ExcelDocumentFactory : IExcelDocumentFactory
{
    public ILargeExcelDocument CreateLargeDocument(IEnumerable<ExcelColumn> columns, Stream stream)
    {
        return new MiniExcelDocument(stream, columns);
    }
}

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

public class ExcelExportResult<T>(PaginatedEnumerable<T> items, int count, ExportMetadata metadata) : ActionResult
    where T : notnull
{
    private const string ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
    private const string Extension = ".xlsx";
    private const int SizeLimit = int.MaxValue;

    private readonly IReadOnlyList<FastPropertyInfo> _properties = FastReflection.Instance.GetProperties(typeof(T));

    public override async Task ExecuteResultAsync(ActionContext context)
    {
        var sp = context.HttpContext.RequestServices;
        var factory = sp.GetRequiredService<IExcelDocumentFactory>();

        var disposition = new ContentDispositionHeaderValue(DispositionTypeNames.Attachment);
        disposition.SetHttpFileName(MakeFilename());

        context.HttpContext.Response.Headers[HeaderNames.ContentDisposition] = disposition.ToString();
        context.HttpContext.Response.Headers.Append(HeaderNames.ContentType, ContentType);
        context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;

        //TODO: deal with exception, because our global exception handling cannot take into account while the response is started.

        await using var bodyStream = context.HttpContext.Response.BodyWriter.AsStream();
        await context.HttpContext.Response.StartAsync(context.HttpContext.RequestAborted);
        await using (var document = factory.CreateLargeDocument(MakeColumns(), bodyStream))
        {
            await document.Write(items, count, SizeLimit, context.HttpContext.RequestAborted);
        }

        await context.HttpContext.Response.CompleteAsync();
    }

    private string MakeFilename()
    {
        return
            $"{metadata.Title} - {DateTime.UtcNow.ToEpochSeconds()}{Extension}";
    }

    private IEnumerable<ExcelColumn> MakeColumns()
    {
        var types = _properties.ToDictionary(p => p.Name, p => p.PropertyType, StringComparer.OrdinalIgnoreCase);
        return metadata.Fields.Select(f =>
        {
            var type = types[f.Name];

            type = Nullable.GetUnderlyingType(type) ?? type;

            if (type.IsEnum ||
                type == typeof(DateOnly) ||
                type == typeof(TimeOnly) ||
                type == typeof(bool) ||
                type == typeof(TimeSpan) ||
                type == typeof(DateTime))
            {
                type = typeof(string);
            }

            return new ExcelColumn(f.Name, f.Title, type);
        });
    }
}

در اینجا از طریق ExportMetadata که از سمت کاربر تعیین می‌شود، مشخص خواهد شد که کدام فیلدها در فایل نهایی حضور داشته باشند. در بدنه متد ExecuteResultAsync یکسری هدر مرتبط با کار با فایل‌ها تنظیم شده‌است و سپس از طریق BodyWriter و متد AsStream به استریم مورد نظر دست یافته و در اختیار متد Write مربوط به document ایجاد شده، قرار داده‌ایم. یک نمونه استفاده از آن برای موجودیت فرضی مشتری می تواند به شکل زیر باشد:

[ApiController, Route("api/customers")]
public class CustomersController(IDbContext dbContext) : ControllerBase
{
    [HttpGet("export")]
    public async Task<ActionResult> ExportCustomers([FromQuery] ExportMetadata metadata,
        CancellationToken cancellationToken)
    {
        var count = await dbContext.Set<Customer>().CountAsync(cancellationToken);
        return this.Export(
            (page, pageSize) => dbContext.Set<Customer>()
                .OrderBy(c => c.Id)
                .Skip((page - 1) * pageSize)
                .Take(pageSize)
                .AsNoTracking()
                .AsEnumerable(), // Enable streaming instead of buffering through deferred execution
            count,
            metadata);
    }
}

در اینجا از طریق Extension Method مهیا شده روش کوئری کردن برای هر شیت را مشخص کرده‌ایم؛ نکته مهم در ایجاد استفاده از ‌متد AsEnumerable می باشد که در عمل یک Type Casting انجام می دهد که باقی متدهای استفاده شده روی خروجی، از طریق Linq To Object اعمال شود و همچنین نیاز به استفاده از ToList و یا موارد مشابه را نخواهیم داشت. نمونه درخواست GET برای این API می تواند به شکل زیر باشد:

http://localhost:5118/api/customers/export?Title=Test&Fields[0].Name=FirstName&Fields[0].Title=First name&Fields[1].Name=LastName&Fields[1].Title=Last name&Fields[2].Name=BirthDate&Fields[2].Title=BirthDate

سورس کد مثال قابل اجرا از طریق مخزن زیر قابل دسترس می باشد:

https://github.com/rabbal/large-excel-streaming

در این مثال در زمان آغاز برنامه، ۱۰ میلیون رکورد در جدول Customer ثبت خواهد شد که در ادامه می توان از آن خروجی Excel تهیه کرد.

نکته مهم: توجه داشته باشید که استفاده از این روش قابلیت از سرگیری مجدد برای دانلود را نخواهد داشت و شاید بهتر است این فرآیند را از طریق یک Job انجام داده و با استفاده از قابلیت‌های Multipart Upload مربوط به یک BlobStroage مانند Minio، خروجی مورد نظر از قبل ذخیره کرده و لینک دانلودی را در اختیار کاربر قرار دهید.

مطالب
C# 7 - More Expression-Bodied Members
یکی از امکانات جالب سی‌شارپ که در نسخه 6 معرفی شد، قابلیت Expression-Bodied Members بود. در نسخه 7 سی‌شارپ، امکانات جدیدتری اضافه شده است؛ به عنوان مثال اکنون می‌توان برای constructors, finalizers و همچنین get and set برای پراپرتی‌ها و ایندکسرها نیز از این قابلیت استفاده کرد.

 
استفاده از expression body برای constructors 
public class Person
{
    public string FirstName { get; set; }
    public Person(string firstName)
    {
        this.FirstName = firstName;
    }
}
به عنوان مثال اکنون سازنده‌ی کلاس فوق را می‌توانیم از روش block body متداول، به روش expression body، به صورت خلاصه‌تری بنویسیم:
public class Person
{
     public string FirstName { get; set; }
     public Person(string firstName) => this.FirstName = firstName;
}
البته محدودیت این روش این است که تنها برای یک پارامتر می‌توانیم به اینصورت عمل کنیم؛ اما در نسخه‌ 7.1  قرار است قابلیت استفاده از expression body برای بیشتر از یک پارامتر نیز اضافه شود:
public class Person
{
    public string Name { get; }
    public int Age { get; }

    public Person(string name, int age) => (Name, Age) = (name, age);
}

اما اگر نیاز داشتید برای بیشتر از دو متغیر از expression body استفاده کنید می‌توانید از Tuple برای شبیه‌سازی آن استفاده کنید(+):
public class Person
{
    private readonly (string name, int age) _tuple;    

    public string Name => _tuple.name;
    public int Age => _tuple.age;

    public Person(string name, int age) => _tuple = (name, age);
}

استفاده از expression body برای destructors 
public class Resource
{
    ~Resource() => Console.WriteLine("destructor");
}


 استفاده از expression body در get / set accessors 
 در سی‌شارپ 7 برای accessors نیز می‌توانیم از سینتکس جدید expression body استفاده کنیم. به عنوان مثال کد زیر را در نظر بگیرید:
private int _x;
public int X 
{
    get
    {
        return _x;
    }
    set
    {
        _x = value;
    }
}
کد فوق را می‌توانیم در سی‌شارپ 7 به صورت خلاصه‌تری بنویسیم:
private int _x;
public int X 
{
    get => _x;
    set => _x = value;
}

در ویژوال‌استودیوی 2017 نیز با قرار دادن ماوس بر روی پراپرتی x_، استفاده‌ی از سینتکس expression body به شما پیشنهاد داده خواهد شد:


همچنین برای Event Accessors نیز می‌توانیم از این قابلیت استفاده کنیم:

private EventHandler _someEvent;
public event EventHandler SomeEvent
{
    add => _someEvent += value;
    remove => _someEvent -= value;
}


مطالب
توصیه‌هایی در مورد overloading متدها

مطلب زیر چکیده‌ای است از کتاب Framework Design Guidelines در مورد سربارگذاری توابع

الف) از یک نوع خروجی استفاده کنید

مثال زیر را در نظر بگیرید:

public User[] GetGroupMembers(int groupId)
public List<User> GetGroupMembers(string groupName)

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

ب) نام‌های آرگومان‌های توابع سربارگذاری شده شما باید یکسان باشند

در مثال زیر، از اولین آرگومان جهت دریافت شناسه‌ی یک گروه استفاده می‌شود:

public List<User> GetGroupMembers(int groupId)
public List<User> GetGroupMembers(int id, int pageIndex, int pageSize)

اما همانطور که ملاحظه می‌کنید در یکی از groupId استفاده شده و در دیگری از نام id . این روش مزموم است! از آن پرهیز کنید! (هر دو یا باید groupId باشند و یا id . این هماهنگی و یکسان بودن باید در توابع سربارگذاری شده‌ی شما حفظ شود)

ج) ترتیب آرگومان‌ها را در توابع سربارگذاری شده حفظ و رعایت نمائید

لطفا به مثال زیر دقت کنید:

public List<User> GetGroupMembers(int groupId)
public List<User> GetGroupMembers(int pageIndex, int pageSize, int groupId)

در اینجا شناسه‌ی گروه یکبار به عنوان آرگومان اول معرفی شده و بار دیگر به عنوان آرگومان سوم. این کار نیز مزموم است، زیرا برنامه نویس‌ها از روی عادت به اولین آرگومان توابع سربارگذاری شده‌ی شما، همان شناسه‌ی گروه را ارسال خواهند کرد و این مورد سبب بروز باگ‌هایی خواهد شد که ردیابی آن مشکل است. بنابراین اگر شناسه‌ی گروه به عنوان اولین آرگومان معرفی شده، در تمامی overload های این تابع نیز اولین آرگومان باید همان شناسه‌ی گروه باشد.

د) از call forwarding استفاده کنید

جهت توضیح بهتر call forwarding به مثال زیر دقت نمائید:
public List<User> GetGroupMembers(int groupId)
{
return GetGroupMembers(groupId, 0, 10);
}

public List<User> GetGroupMembers(int groupId, int pageIndex, int pageSize)
{
return GetGroupMembers(groupId, pageIndex, pageSize, SortOrder.Ascending);
}

public List<User> GetGroupMembers(int groupId, int pageIndex, int pageSize, SortOrder sortOrder)
{
var query = new GroupQuery();
query.GroupID = groupId;
query.PageIndex = pageIndex;
query.PageSize = pageSize;
query.SortOrder = sortOrder;

return GetGroupMembers(query);
}

public List<User> GetGroupMembers(GroupQuery groupQuery)
{
// Actual implementation to get group members goes here
}

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

ه) زیاده‌ روی نکنید!

آخرین موردی که توصیه شده این است که در تهیه توابع سربارگذاری شده زیاده روی نکنید. در این نوع موارد باید قانون 80/20 را در نظر گرفت. 2 تا 3 تابع سربارگذاری شده ارائه دهید که 80 درصد کار را انجام می‌دهند و سپس یک تابع سربارگذاری شده‌ی دیگر ارائه دهید که 20 درصد باقیمانده را پوشش دهد.


پ.ن.
در سی شارپ 4 ، با معرفی optional parameters ، شاید کمتر به سربارگذاری نیاز باشد.

مطالب
C# 7 - Pattern matching and switch expressions
هرچند کار کردن با کلاس‌ها و اینترفیس‌های strongly typed ساده‌تر است، اما گاهی از اوقات نیاز است تا با نوع object کار کرد. به علاوه حتی در حین کار کردن با کلاس‌ها و اینترفیس‌ها هم نیاز است تا نوع خاصی از کلاس‌های مشتق شده را جهت فراخوانی متدی ویژه، بررسی کرد. به همین جهت مفهوم «pattern matching» به C# 7 اضافه شده‌است تا بتوان با سلسله مراتب اشیاء، ساده‌تر کار کرد. برای این منظور اپراتور is و عبارت switch، با الگوهای const ،var و type بهبود و تکامل بخشیده شده‌اند.


استفاده از اپراتور is به همراه pattern matching

اپراتور is از اولین نگارش #C مهیا بوده‌است و هدف آن بررسی تطابق شیءایی خاص، با نوعی مفروض است. برای مثال آیا این نوع مورد بررسی، اینترفیس خاصی را پیاده سازی می‌کند و یا اینکه آیا از کلاسی خاص مشتق شده‌است یا خیر؟ حاصل این بررسی هم true یا false است.
با بهبودهای حاصل شده‌ی در C# 7، اکنون می‌توان از اپراتور is جهت بررسی الگوها نیز استفاده کرد.


الگوی const

در مثال ذیل، آرایه‌ای از اشیاء، شامل یک نال، یک عدد و دو شیء کاربر، تعریف شده‌اند:
public class User
{
    public User(string name)
    {
        Name = name;
    }
 
    public string Name { get; }
}


object[] data = { null, 42, new User("User 1"), new User("User 2") };
foreach (var item in data)
{
    if (item is null) Console.WriteLine("it's a const pattern");
    if (item is 42) Console.WriteLine("it's 42");
}
اولین الگوی مهیای در C# 7، با نام «const pattern» شناخته می‌شود که نمونه‌ای از آن‌را در بدنه‌ی حلقه‌ی فوق مشاهده می‌کنید.
در C# 7 می‌توان اپراتور is را بر روی یک عدد ثابت مانند 42 و یا یک null بکار گرفت. پیش از C# 7 برای بررسی نال بودن یک شیء، تنها از پراتور == می‌شد استفاده کرد.


الگوی Type

دومین الگوی مهیای در C# 7، «الگوی نوع» نام دارد و هدف آن بررسی تطابق یک شیء، با شیءایی دیگر است. مهم‌ترین تفاوت آن با نگارش‌های پیشین سی شارپ این است که اگر اکنون تطابقی تشخیص داده شود، شیء، به متغیر جدید تعریف شده، انتساب داده می‌شود:
object[] data = { null, 42, new User("User 1"), new User("User 2") };
foreach (var item in data)
{
    if (item is int i) Console.WriteLine($"it's a type pattern with an int and the value {i}");
    if (item is User p) Console.WriteLine($"it's a person: {p.Name}");
    if (item is User p2 && p2.Name.StartsWith("U"))
    {
        Console.WriteLine($"it's a person starting with U {p2.Name}");
    }
}
همانطور که ملاحظه می‌کنید اینبار می‌توان پس از اپراتور is، یک متغیر جدید را هم تعریف کرد و در صورت تطابق، این متغیر به صورت خودکار مقدار دهی می‌گردد. به علاوه در اینجا امکان ترکیب شرط‌ها نیز پس از is، مانند سومین if نوشته شده، میسر است.

و یا اکنون قطعه کد قدیمی ذیل را
object obj1 = "Hello, World!";
var str1 = obj1 as string;
if (str1 != null)
{
   Console.WriteLine(str1);
}
می‌توان با pattern matching و استفاده از «الگوی نوع»، به نحو ذیل خلاصه کرد:
object obj2 = "Hello, World!";
if (obj2 is string str2)
{
   Console.WriteLine(str2);
}


الگوی Var

سومین الگوی مهیای در C# 7، الگوی var نام دارد و در این حالت می‌توان بجای ذکر صریح نوع تطابق داده شده، از var استفاده کرد.
بدیهی است این الگو همواره با موفقیت روبرو می‌شود؛ چون var به همان نوع شیء مفروض اشاره می‌کند:
object[] data = { null, 42, new User("User 1"), new User("User 2") };
foreach (var item in data)
{
    if (item is var x) Console.WriteLine($"it's a var pattern with the type {x?.GetType()?.Name}");
}
مهم‌ترین مزیت آن این است که متغیر تعریف شده‌ی پس از var دقیقا دارای همان مقدار و نوع اصلی شیء است و پس از فراخوانی GetType می‌توان به خواص آن دسترسی یافت؛ مانند خاصیت Name ذکر شده‌ی در مثال فوق.
در این حالت اگر item دقیقا null باشد، برای بررسی آن می‌توان از null conditional operator معرفی شده‌ی در C# 6 استفاده کرد.


استفاده از عبارت switch به همراه pattern matching

در C# 7، عبارت switch نیز تکامل یافته‌است. در اینجا الگوهای const ،var و type را نیز می‌توان پس از ذکر case بکار گرفت:
public static void SwitchPattern(object o)
{
    switch (o)
    {
        case null:
            Console.WriteLine("it's a constant pattern");
            break;
        case int i:
            Console.WriteLine("it's an int");
            break;
        case User p when p.Name.StartsWith("U"):
            Console.WriteLine($"a U person {p.Name}");
            break;
        case User p:
            Console.WriteLine($"any other person {p.Name}");
            break;
        case var x:
            Console.WriteLine($"it's a var pattern with the type {x?.GetType().Name} ");
            break;
        default:
            break;
    }
}
الگوهایی را که در اینجا مشاهده می‌کنید دقیقا همان‌هایی هستند که پیشتر بررسی کردیم. الگوی const برای بررسی نال و یک عدد. الگوی type برای بررسی تطابق با یک شیء خاص و سپس استفاده‌ی از آن شیء و الگوی var برای دسترسی به نام نوع مفروض.
تنها نکته‌ی جدید در اینجا، استفاده از واژه‌ی کلیدی when است برای ترکیب شرط‌ها (case User p when p.Name.StartsWith). بنابراین در C# 7 امکان نوشتن case null میسر است؛ به همراه نوشتن شرط‌ها توسط when، در حین تعاریف caseها. به علاوه اینبار عبارت switch محدود به نوع‌های پایه مانند اعداد، رشته‌ها و enums نیست و در اینجا می‌توان یک شیء را نیز مشخص کرد.


شبیه سازی switch موجود در ویژوال بیسیک در C# 7

ویژوال بیسیک از نگارش‌های ابتدایی آن دارای case‌های پیشرفته‌تری است نسبت به #C. برای نمونه در اینجا امکان تعریف تعدادی عدد، استفاده از To و استفاده‌ی از =< را هم مشاهده می‌کنید:
Select Case age
  Case 50
    ageBlock = "the big five-oh"
  Case 80, 81, 82, 83, 84, 85, 86, 87, 88, 89
    ageBlock = "octogenarian"
  Case 90 To 99
    ageBlock = "nonagenarian"
  Case Is >= 100
    ageBlock = "centenarian"
  Case Else
    ageBlock = "just old"
End Select

اکنون در C# 7 می‌توان یک چنین توانمندی را با pattern matching هم پیاده سازی کرد:
string ageBlock;
var age = 40;
switch (age)
{
    case 50:
        ageBlock = "the big five-oh";
        break;
    case var testAge when (new List<int> { 80, 81, 82, 83, 84, 85, 86, 87, 88, 89 }).Contains(testAge):
        ageBlock = "octogenarian";
        break;
    case var testAge when ((testAge >= 90) && (testAge <= 99)):
        ageBlock = "nonagenarian";
        break;
    case var testAge when (testAge >= 100):
        ageBlock = "centenarian";
        break;
    default:
        ageBlock = "just old";
        break;
}
در این مثال یکی از کاربردهای عملی الگوی var را مشاهده می‌کنید؛ یا همان دسترسی به مقدار و نوع وارد شده و سپس اعمال شرط بر روی آن.
همانطور که مشاهده می‌کنید، در قسمت when نیز می‌توان توسط && و || نیز شرط‌ها را ترکیب کرد و یا متدی را با خروجی bool (مانند Contains) بر روی مقدار دریافتی اعمال کرد.
مطالب
آشنایی با NHibernate - قسمت هشتم

معرفی الگوی Repository

روش متداول کار با فناوری‌های مختلف دسترسی به داده‌ها عموما بدین شکل است:
الف) یافتن رشته اتصالی رمزنگاری شده به دیتابیس از یک فایل کانفیگ (در یک برنامه اصولی البته!)
ب) باز کردن یک اتصال به دیتابیس
ج) ایجاد اشیاء Command برای انجام عملیات مورد نظر
د) اجرا و فراخوانی اشیاء مراحل قبل
ه) بستن اتصال به دیتابیس و آزاد سازی اشیاء

اگر در برنامه‌های یک تازه کار به هر محلی از برنامه او دقت کنید این 5 مرحله را می‌توانید مشاهده کنید. همه جا! قسمت ثبت، قسمت جستجو، قسمت نمایش و ...
مشکلات این روش:
1- حجم کارهای تکراری انجام شده بالا است. اگر قسمتی از فناوری دسترسی به داده‌ها را به اشتباه درک کرده باشد، پس از مطالعه بیشتر و مشخص شدن نحوه‌ی رفع مشکل، قسمت عمده‌ای از برنامه را باید اصلاح کند (زیرا کدهای تکراری همه جای آن پراکنده‌اند).
2- برنامه نویس هر بار باید این مراحل را به درستی انجام دهد. اگر در یک برنامه بزرگ تنها قسمت آخر در یکی از مراحل کاری فراموش شود دیر یا زود برنامه تحت فشار کاری بالا از کار خواهد افتاد (و متاسفانه این مساله بسیار شایع است).
3- برنامه منحصرا برای یک نوع دیتابیس خاص تهیه خواهد شد و تغییر این رویه جهت استفاده از دیتابیسی دیگر (مثلا کوچ برنامه از اکسس به اس کیوال سرور)، نیازمند بازنویسی کل برنامه می‌باشد.
و ...

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

این روزها تشکیل این لایه دسترسی به داده‌ها (data access layer یا DAL) نیز مزموم است! و دلایل آن در مباحث چرا به یک ORM نیازمندیم برشمرده شده است. جهت کار با ORM ها نیز نیازمند یک لایه دیگر می‌باشیم تا یک سری اعمال متداول با آن‌هارا کپسوله کرده و از حجم کارهای تکراری خود بکاهیم. برای این منظور قبل از اینکه دست به اختراع بزنیم، بهتر است به الگوهای طراحی برنامه نویسی شیء گرا رجوع کرد و از رهنمودهای آن استفاده نمود.

الگوی Repository یکی از الگوهای برنامه‌ نویسی با مقیاس سازمانی است. با کمک این الگو لایه‌ای بر روی لایه نگاشت اشیاء برنامه به دیتابیس تشکیل شده و عملا برنامه را مستقل از نوع ORM مورد استفاه می‌کند. به این صورت هم از تشکیل یک سری کدهای تکراری در سطح برنامه جلوگیری شده و هم از وابستگی بین مدل برنامه و لایه دسترسی به داده‌ها (که در اینجا همان NHibernate می‌باشد) جلوگیری می‌شود. الگوی Repository (مخزن)، کار ثبت،‌ حذف، جستجو و به روز رسانی داده‌ها را با ترجمه آن‌ها به روش‌های بومی مورد استفاده توسط ORM‌ مورد نظر، کپسوله می‌کند. به این شکل شما می‌توانید یک الگوی مخزن عمومی را برای کارهای خود تهیه کرده و به سادگی از یک ORM به ORM دیگر کوچ کنید؛ زیرا کدهای برنامه شما به هیچ ORM خاصی گره نخورده و این عملیات بومی کار با ORM توسط لایه‌ای که توسط الگوی مخزن تشکیل شده، صورت گرفته است.

طراحی کلاس مخزن باید شرایط زیر را برآورده سازد:
الف) باید یک طراحی عمومی داشته باشد و بتواند در پروژه‌های متعددی مورد استفاده مجدد قرار گیرد.
ب) باید با سیستمی از نوع اول طراحی و کد نویسی و بعد کار با دیتابیس، سازگاری داشته باشد.
ج) باید امکان انجام آزمایشات واحد را سهولت بخشد.
د) باید وابستگی کلاس‌های دومین برنامه را به زیر ساخت ORM مورد استفاده قطع کند (اگر سال بعد به این نتیجه رسیدید که ORM ایی به نام XYZ برای کار شما بهتر است، فقط پیاده سازی این کلاس باید تغییر کند و نه کل برنامه).
ه) باید استفاده از کوئری‌هایی از نوع strongly typed را ترویج کند (مثل کوئری‌هایی از نوع LINQ).


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

مدل این قسمت (برنامه NHSample4 از نوع کنسول با همان ارجاعات متداول ذکر شده در قسمت‌های قبل)، از نوع many-to-many می‌باشد. در اینجا یک واحد درسی توسط چندین دانشجو می‌تواند اخذ شود یا یک دانشجو می‌تواند چندین واحد درسی را اخذ نماید که برای نمونه کلاس دیاگرام و کلاس‌های متشکل آن به شکل زیر خواهند بود:



using System.Collections.Generic;

namespace NHSample4.Domain
{
public class Course
{
public virtual int Id { get; set; }
public virtual string Teacher { get; set; }
public virtual IList<Student> Students { get; set; }

public Course()
{
Students = new List<Student>();
}
}
}


using System.Collections.Generic;

namespace NHSample4.Domain
{
public class Student
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual IList<Course> Courses { get; set; }

public Student()
{
Courses = new List<Course>();
}
}
}

کلاس کانفیگ برنامه جهت ایجاد نگاشت‌ها و سپس ساخت دیتابیس متناظر

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"))
)
.Mappings(
m => m.AutoMappings.Add(
new AutoPersistenceModel()
.Where(x => x.Namespace.EndsWith("Domain"))
.AddEntityAssembly(typeof(NHSample4.Domain.Course).Assembly))
.ExportTo(System.Environment.CurrentDirectory)
);
}

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

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<!--NHSessionManager-->
<add name="DbConnectionString"
connectionString="Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true"/>
</connectionStrings>
</configuration>

ب) در NHibernate سنتی (!) کار ساخت نگاشت‌ها توسط یک سری فایل xml صورت می‌گیرد که با معرفی فریم ورک Fluent NHibernate و استفاده از قابلیت‌های Auto Mapping آن، این‌کار با سهولت و دقت هر چه تمام‌تر قابل انجام است که توضیحات نحوه‌ی انجام ‌آن‌را در قسمت‌های قبل مطالعه فرمودید. اگر نیاز بود تا این فایل‌های XML نیز جهت بررسی شخصی ایجاد شوند، تنها کافی است از متد ExportTo آن همانگونه که در متد GetConfig استفاده شده، کمک گرفته شود. به این صورت پس از ایجاد خودکار نگاشت‌ها، فایل‌های XML متناظر نیز در مسیری که به عنوان آرگومان متد ExportTo مشخص گردیده است، تولید خواهند شد (دو فایل NHSample4.Domain.Course.hbm.xml و NHSample4.Domain.Student.hbm.xml را در پوشه‌ای که محل اجرای برنامه است خواهید یافت).

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



پیاده سازی الگوی مخزن

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

using System;
using System.Linq;
using System.Linq.Expressions;

namespace NHSample4.NHRepository
{
//Repository Interface
public interface IRepository<T>
{
T Get(object key);

T Save(T entity);
T Update(T entity);
void Delete(T entity);

IQueryable<T> Find();
IQueryable<T> Find(Expression<Func<T, bool>> predicate);
}
}

سپس پیاده سازی آن با توجه به کلاس SingletonCore ایی که در قسمت قبل تهیه کردیم (جهت مدیریت صحیح سشن فکتوری)، به صورت زیر خواهد بود.
این کلاس کار آغاز و پایان تراکنش‌ها را نیز مدیریت کرده و جهت سهولت کار اینترفیس IDisposable را نیز پیاده سازی می‌کند :

using System;
using System.Linq;
using NHSessionManager;
using NHibernate;
using NHibernate.Linq;

namespace NHSample4.NHRepository
{
public class Repository<T> : IRepository<T>, IDisposable
{
private ISession _session;
private bool _disposed = false;

public Repository()
{
_session = SingletonCore.SessionFactory.OpenSession();
BeginTransaction();
}

~Repository()
{
Dispose(false);
}

public T Get(object key)
{
if (!isSessionSafe) return default(T);

return _session.Get<T>(key);
}

public T Save(T entity)
{
if (!isSessionSafe) return default(T);

_session.Save(entity);
return entity;
}

public T Update(T entity)
{
if (!isSessionSafe) return default(T);

_session.Update(entity);
return entity;
}

public void Delete(T entity)
{
if (!isSessionSafe) return;

_session.Delete(entity);
}

public IQueryable<T> Find()
{
if (!isSessionSafe) return null;

return _session.Linq<T>();
}

public IQueryable<T> Find(System.Linq.Expressions.Expression<Func<T, bool>> predicate)
{
if (!isSessionSafe) return null;

return Find().Where(predicate);
}

void Commit()
{
if (!isSessionSafe) return;

if (_session.Transaction != null &&
_session.Transaction.IsActive &&
!_session.Transaction.WasCommitted &&
!_session.Transaction.WasRolledBack)
{
_session.Transaction.Commit();
}
else
{
_session.Flush();
}
}

void Rollback()
{
if (!isSessionSafe) return;

if (_session.Transaction != null && _session.Transaction.IsActive)
{
_session.Transaction.Rollback();
}
}

private bool isSessionSafe
{
get
{
return _session != null && _session.IsOpen;
}
}

void BeginTransaction()
{
if (!isSessionSafe) return;

_session.BeginTransaction();
}


public void Dispose()
{
Dispose(true);
// tell the GC that the Finalize process no longer needs to be run for this object.
GC.SuppressFinalize(this);
}

protected virtual void Dispose(bool disposeManagedResources)
{
if (_disposed) return;
if (!disposeManagedResources) return;
if (!isSessionSafe) return;

try
{
Commit();
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
Rollback();
}
finally
{
if (isSessionSafe)
{
_session.Close();
_session.Dispose();
}
}

_disposed = true;
}
}
}
اکنون جهت استفاده از این کلاس مخزن به شکل زیر می‌توان عمل کرد:

using System;
using System.Collections.Generic;
using NHSample4.Domain;
using NHSample4.NHRepository;

namespace NHSample4
{
class Program
{
static void Main(string[] args)
{
//ایجاد دیتابیس در صورت نیاز
//NHSessionManager.Config.CreateDb();


//ابتدا یک دانشجو را اضافه می‌کنیم
Student student = null;
using (var studentRepo = new Repository<Student>())
{
student = studentRepo.Save(new Student() { Name = "Vahid" });
}

//سپس یک واحد را اضافه می‌کنیم
using (var courseRepo = new Repository<Course>())
{
var course = courseRepo.Save(new Course() { Teacher = "Shams" });
}

//اکنون یک واحد را به دانشجو انتساب می‌دهیم
using (var courseRepo = new Repository<Course>())
{
courseRepo.Save(new Course() { Students = new List<Student>() { student } });
}

//سپس شماره دروس استادی خاص را نمایش می‌دهیم
using (var courseRepo = new Repository<Course>())
{
var query = courseRepo.Find(t => t.Teacher == "Shams");

foreach (var course in query)
Console.WriteLine(course.Id);
}

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}

همانطور که ملاحظه می‌کنید در این سطح دیگر برنامه هیچ درکی از ORM مورد استفاده ندارد و پیاده سازی نحوه‌ی تعامل با NHibernate در پس کلاس مخزن مخفی شده است. کار آغاز و پایان تراکنش‌ها به صورت خودکار مدیریت گردیده و همچنین آزاد سازی منابع را نیز توسط اینترفیس IDisposable مدیریت می‌کند. به این صورت امکان فراموش شدن یک سری از اعمال متداول به حداقل رسیده، میزان کدهای تکراری برنامه کم شده و همچنین هر زمانیکه نیاز بود، صرفا با تغییر پیاده سازی کلاس مخزن می‌توان به ORM دیگری کوچ کرد؛ بدون اینکه نیازی به بازنویسی کل برنامه وجود داشته باشد.

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

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


مطالب
Kendo UI MVVM
پیشنیازها
- «استفاده از Kendo UI templates »
- «اعتبار سنجی ورودی‌های کاربر در Kendo UI»
- «فعال سازی عملیات CRUD در Kendo UI Grid» جهت آشنایی با نحوه‌ی تعریف DataSource ایی که می‌تواند اطلاعات را ثبت، حذف و یا ویرایش کند.


در این مطلب قصد داریم به یک چنین صفحه‌ای برسیم که در آن در ابتدای نمایش، لیست ثبت نام‌های موجود، از سرور دریافت و توسط یک Kendo UI template نمایش داده می‌شود. سپس امکان ویرایش و حذف هر ردیف، وجود خواهد داشت، به همراه امکان افزودن ردیف‌های جدید. در این بین مدیریت نمایش لیست ثبت نام‌ها توسط امکانات binding توکار فریم ورک MVVM مخصوص Kendo UI صورت خواهد گرفت. همچنین کلیه اعمال مرتبط با هر ردیف نیز توسط data binding دو طرفه مدیریت خواهد شد.



Kendo UI MVVM

الگوی MVVM یا Model-View-ViewModel که برای اولین بار جهت کاربردهای WPF و Silverlight معرفی شد، برای ساده سازی اتصال تغییرات کنترل‌های برنامه به خواص ViewModel یک View کاربرد دارد. برای مثال با تغییر عنصر انتخابی یک DropDownList در یک View، بلافاصله خاصیت متصل به آن که در ViewModel برنامه تعریف شده‌است، مقدار دهی و به روز خواهد شد. هدف نهایی آن نیز جدا سازی منطق کدهای UI، از کدهای جاوا اسکریپتی سمت کاربر است. برای این منظور کتابخانه‌هایی مانند Knockout.js به صورت اختصاصی برای این کار تهیه شده‌اند؛ اما Kendo UI نیز جهت یکپارچگی هرچه تمامتر اجزای آن، دارای یک فریم ورک MVVM توکار نیز می‌باشد. طراحی آن نیز بسیار شبیه به Knockout.js است؛ اما با سازگاری 100 درصد با کل مجموعه.
پیاده سازی الگوی MVVM از 4 قسمت تشکیل می‌شود:
- Model که بیانگر خواص متناظر با اشیاء رابط کاربری است.
- View همان رابط کاربری است که به کاربر نمایش داده می‌شود.
- ViewModel واسطی است بین Model و View. کار آن انتقال داده‌ها و رویدادها از View به مدل است و در حالت binding دوطرفه، عکس آن نیز صحیح می‌باشد.
- Declarative data binding جهت رهایی برنامه نویس‌ها از نوشتن کدهای هماهنگ سازی اطلاعات المان‌های View و خواص ViewModel کاربرد دارد.

در ادامه این اجزا را با پیاده سازی مثالی که در ابتدای بحث مطرح شد، دنبال می‌کنیم.


تعریف Model و ViewModel

در سمت سرور، مدل ثبت نام برنامه چنین شکلی را دارد:
namespace KendoUI07.Models
{
    public class Registration
    {
        public int Id { set; get; }
        public string UserName { set; get; }
        public string CourseName { set; get; }
        public int Credit { set; get; }
        public string Email { set; get; }
        public string Tel { set; get; }
    }
}
در سمت کاربر، این مدل را به نحو ذیل می‌توان تعریف کرد:
    <script type="text/javascript">
        $(function () {
            var model = kendo.data.Model.define({
                id: "Id",
                fields: {
                    Id: { type: 'number' }, // leave this set to 0 or undefined, so Kendo knows it is new.
                    UserName: { type: 'string' },
                    CourseName: { type: 'string' },
                    Credit: { type: 'number' },
                    Email: { type: 'string' },
                    Tel: { type: 'string' }
                }
            });
        });
    </script>
و ViewModel برنامه در ساده‌ترین شکل آن اکنون چنین تعریفی را خواهد یافت:
    <script type="text/javascript">
        $(function () {
            var viewModel = kendo.observable({
                accepted: false,
                course: new model()
            });
        });
    </script>
یک viewModel در Kendo UI به صورت یک observable object تعریف می‌شود که می‌تواند دارای تعدادی خاصیت و متد دلخواه باشد. هر خاصیت آن به یک عنصر HTML متصل خواهد شد. در اینجا این اتصال دو طرفه است؛ به این معنا که تغییرات UI به خواص viewModel و برعکس منتقل و منعکس می‌شوند.


اتصال ViewModel به View برنامه

تعریف فرم ثبت نام را در اینجا ملاحظه می‌کنید. فیلدهای مختلف آن بر اساس نکات اعتبارسنجی HTML 5 با ویژگی‌های خاص آن، مزین شده‌اند. جزئیات آن‌را در مطلب «اعتبار سنجی ورودی‌های کاربر در Kendo UI» پیشتر بررسی کرده‌ایم.
اگر به تعریف هر فیلد دقت کنید، ویژگی data-bind جدیدی را هم ملاحظه خواهید کرد:
    <div id="coursesSection" class="k-rtl k-header">
        <div class="box-col">
            <form id="myForm" data-role="validator" novalidate="novalidate">
                <h3>ثبت نام</h3>
                <ul>
                    <li>
                        <label for="Id">Id</label>
                        <span id="Id" data-bind="text:course.Id"></span>
                    </li>
                    <li>
                        <label for="UserName">نام</label>
                        <input type="text" id="UserName" name="UserName" class="k-textbox"
                               data-bind="value:course.UserName"
                               required />
                    </li>
                    <li>
                        <label for="CourseName">دوره</label>
                        <input type="text" dir="ltr" id="CourseName" name="CourseName" required
                               data-bind="value:course.CourseName" />
                        <span class="k-invalid-msg" data-for="CourseName"></span>
                    </li>
                    <li>
                        <label for="Credit">مبلغ پرداختی</label>
                        <input id="Credit" name="Credit" type="number" min="1000" max="6000"
                               required data-max-msg="عددی بین 1000 و 6000" dir="ltr"
                               data-bind="value:course.Credit"
                               class="k-textbox k-input" />
                        <span class="k-invalid-msg" data-for="Credit"></span>
                    </li>
                    <li>
                        <label for="Email">پست الکترونیک</label>
                        <input type="email" id="Email" dir="ltr" name="Email"
                               data-bind="value:course.Email"
                               required class="k-textbox" />
                    </li>
                    <li>
                        <label for="Tel">تلفن</label>
                        <input type="tel" id="Tel" name="Tel" dir="ltr" pattern="\d{8}"
                               required class="k-textbox"
                               data-bind="value:course.Tel"
                               data-pattern-msg="8 رقم" />
                    </li>
                    <li>
                        <input type="checkbox" name="Accept"
                               data-bind="checked:accepted"
                               required />
                        شرایط دوره را قبول دارم.
                        <span class="k-invalid-msg" data-for="Accept"></span>
                    </li>
                    <li>
                        <button class="k-button"
                                data-bind="enabled: accepted, click: doSave"
                                type="submit">
                            ارسال
                        </button>
                        <button class="k-button" data-bind="click: resetModel">از نو</button>
                    </li>
                </ul>
                <span id="doneMsg"></span>
            </form>
        </div>
برای اتصال ViewModel تعریف شده به ناحیه‌ی مشخص شده با DIV ایی با Id مساوی coursesSection، می‌توان از متد kendo.bind استفاده کرد.
    <script type="text/javascript">
        $(function () {
            var model = kendo.data.Model.define({
            // ...
            });

            var viewModel = kendo.observable({
            // ...
            });

            kendo.bind($("#coursesSection"), viewModel);
        });
    </script>
به این ترتیب Kendo UI به بر اساس تعریف data-bind یک فیلد، برای مثال تغییرات خواص course.UserName را به text box نام کاربر منتقل می‌کند و همچنین اگر کاربر اطلاعاتی را در این text box وارد کند، بلافاصله این تغییرات در خاصیت course.UserName منعکس خواهند شد.
<input type="text" id="UserName" name="UserName" class="k-textbox"
       data-bind="value:course.UserName"
       required />

بنابراین تا اینجا به صورت خلاصه، مدلی را توسط متد kendo.data.Model.define، معادل مدل سمت سرور خود ایجاد کردیم. سپس وهله‌ای از این مدل را به صورت یک خاصیت جدید دلخواهی در ViewModel تعریف شده توسط متد kendo.observable در معرض دید View برنامه قرار دادیم. در ادامه اتصال ViewModel و View، با فراخوانی متد kendo.bind انجام شد. اکنون برای دریافت تغییرات کنترل‌های برنامه، تنها کافی است ویژگی‌های data-bind ایی را به آن‌ها اضافه کنیم.
در ناحیه‌ی تعریف شده توسط متد kendo.bind، کلیه خواص ViewModel در دسترس هستند. برای مثال اگر به تعریف ViewModel دقت کنید، یک خاصیت دیگر به نام accepted با مقدار false نیز در آن تعریف شده‌است (این خاصیت چون صرفا کاربرد UI داشت، در model برنامه قرار نگرفت). از آن برای اتصال checkbox تعریف شده، به button ارسال اطلاعات، استفاده کرده‌ایم:
<input type="checkbox" name="Accept"
       data-bind="checked:accepted"
       required />

<button class="k-button"
        data-bind="enabled: accepted, click: doSave"
        type="submit">
       ارسال
</button>
برای مثال اگر کاربر این checkbox را انتخاب کند، مقدار خاصیت accepted، مساوی true خواهد شد. تغییر مقدار این خاصیت، توسط ViewModel بلافاصله در کل ناحیه coursesSection منتشر می‌شود. به همین جهت ویژگی enabled: accepted که به معنای مقید بودن فعال یا غیرفعال بودن دکمه بر اساس مقدار خاصیت accepted است، دکمه را فعال می‌کند، یا برعکس و برای انجام این عملیات نیازی نیست کدنویسی خاصی را انجام داد. در اینجا بین checkbox و button یک سیم کشی برقرار است.


ارسال داده‌های تغییر کرده‌ی ViewModel به سرور

تا اینجا 4 جزء اصلی الگوی MVVM که در ابتدای بحث عنوان شد، تکمیل شده‌اند. مدل اطلاعات فرم تعریف گردید. ViewModel ایی که این خواص را به المان‌های فرم متصل می‌کند نیز در ادامه اضافه شده‌است. توسط ویژگی‌های data-bind کار Declarative data binding انجام می‌شود.
در ادامه نیاز است تغییرات ViewModel را به سرور، جهت ثبت، به روز رسانی و حذف نهایی منتقل کرد.
    <script type="text/javascript">
        $(function () {
            var model = kendo.data.Model.define({
                //...
            });

            var dataSource = new kendo.data.DataSource({
                type: 'json',
                transport: {
                    read: {
                        url: "api/registrations",
                        dataType: "json",
                        contentType: 'application/json; charset=utf-8',
                        type: 'GET'
                    },
                    create: {
                        url: "api/registrations",
                        contentType: 'application/json; charset=utf-8',
                        type: "POST"
                    },
                    update: {
                        url: function (course) {
                            return "api/registrations/" + course.Id;
                        },
                        contentType: 'application/json; charset=utf-8',
                        type: "PUT"
                    },
                    destroy: {
                        url: function (course) {
                            return "api/registrations/" + course.Id;
                        },
                        contentType: 'application/json; charset=utf-8',
                        type: "DELETE"
                    },
                    parameterMap: function (data, type) {
                        // Convert to a JSON string.  Without this step your content will be form encoded.
                        return JSON.stringify(data);
                    }
                },
                schema: {
                    model: model
                },
                error: function (e) {
                    alert(e.errorThrown);
                },
                change: function (e) {
                    // فراخوانی در زمان دریافت اطلاعات از سرور و یا تغییرات محلی
                    viewModel.set("coursesDataSourceRows", new kendo.data.ObservableArray(this.view()));
                }
            });

            var viewModel = kendo.observable({
                //...
            });

            kendo.bind($("#coursesSection"), viewModel);
            dataSource.read(); // دریافت لیست موجود از سرور در آغاز کار
        });
    </script>
در اینجا تعریف DataSource کار با منبع داده راه دور ASP.NET Web API را مشاهده می‌کنید. تعاریف اصلی آن با تعاریف مطرح شده در مطلب «فعال سازی عملیات CRUD در Kendo UI Grid» یکی هستند. هر قسمت آن مانند read، create، update و destory به یکی از متدهای کنترلر ASP.NET Web API اشاره می‌کنند. حالت‌های update و destroy بر اساس Id ردیف انتخابی کار می‌کنند. این Id را باید در قسمت model مربوط به اسکیمای تعریف شده، دقیقا مشخص کرد. عدم تعریف فیلد id، سبب خواهد شد تا عملیات update نیز در حالت create تفسیر شود.


متصل کردن DataSource به ViewModel

تا اینجا DataSource ایی جهت کار با سرور تعریف شده‌است؛ اما مشخص نیست که اگر رکوردی اضافه شد، چگونه باید اطلاعات خودش را به روز کند. برای این منظور خواهیم داشت:
    <script type="text/javascript">
        $(function () {
            $("#coursesSection").kendoValidator({
                // ...
            });

            var model = kendo.data.Model.define({
                // ...
            });

            var dataSource = new kendo.data.DataSource({
                // ...
            });

            var viewModel = kendo.observable({
                accepted: false,
                course: new model(),
                doSave: function (e) {
                    e.preventDefault();
                    console.log("this", this.course);
                    var validator = $("#coursesSection").data("kendoValidator");
                    if (validator.validate()) {
                        if (this.course.Id == 0) {
                            dataSource.add(this.course);
                        }
                        dataSource.sync(); // push to the server
                        this.set("course", new model()); // reset controls
                    }
                },
                resetModel: function (e) {
                    e.preventDefault();
                    this.set("course", new model());
                }            
             });

            kendo.bind($("#coursesSection"), viewModel);
            dataSource.read(); // دریافت لیست موجود از سرور در آغاز کار
        });
    </script>
همانطور که در تعاریف تکمیلی viewModel مشاهده می‌کنید، اینبار دو متد جدید دلخواه doSave و resetModel را اضافه کرده‌ایم.
در متد doSave، ابتدا بررسی می‌کنیم آیا اعتبارسنجی فرم با موفقیت انجام شده‌است یا خیر. اگر بله، توسط متد add منبع داده، اطلاعات فرم جاری را توسط شیء course که هم اکنون به تمامی فیلدهای آن متصل است، اضافه می‌کنیم. در اینجا بررسی شده‌است که آیا Id این اطلاعات صفر است یا خیر. از آنجائیکه از همین متد برای به روز رسانی نیز در ادامه استفاده خواهد شد، در حالت به روز رسانی، Id شیء ثبت شده، از طرف سرور دریافت می‌گردد. بنابراین غیر صفر بودن این Id به معنای عملیات به روز رسانی است و در این حالت نیازی نیست کار بیشتری را انجام داد؛ زیرا شیء متناظر با آن پیشتر به منبع داده اضافه شده‌است.
استفاده از متد add صرفا به معنای مطلع کردن منبع داده محلی از وجود رکوردی جدید است. برای ارسال این تغییرات به سرور، از متد sync آن می‌توان استفاده کرد. متد sync بر اساس متد add یک درخواست POST، بر اساس شیءایی که Id غیر صفر دارد، یک درخواست PUT و با فراخوانی متد remove بر روی منبع داده، یک درخواست DELETE را به سمت سرور ارسال می‌کند.
متد دلخواه  resetModel سبب مقدار دهی مجدد شیء course با یک وهله‌ی جدید از شیء model می‌شود. همینقدر برای پاک کردن تمامی کنترل‌های صفحه کافی است.

تا اینجا دو متد جدید را در ViewModel برنامه تعریف کرده‌ایم. در مورد نحوه‌ی اتصال آن‌ها به View، به کدهای دو دکمه‌ی موجود در فرم دقت کنید:
<button class="k-button"
        data-bind="enabled: accepted, click: doSave"
        type="submit">
       ارسال
</button>
<button class="k-button" data-bind="click: resetModel">از نو</button>
این متدها نیز توسط ویژگی‌های data-bind به هر دکمه نسبت داده شده‌اند. به این ترتیب برای مثال با کلیک کاربر بر روی دکمه‌ی submit، متد doSave موجود در ViewModel فراخوانی می‌شود.


مدیریت سمت سرور ثبت، ویرایش و حذف اطلاعات

در حالت ثبت، متد Post توسط آدرس مشخص شده در قسمت create منبع داده، فراخوانی می‌گردد. نکته‌ی مهمی که در اینجا باید به آن دقت داشت، نحوه‌ی بازگشت Id رکورد جدید ثبت شده‌است.  اگر این تنظیم صورت نگیرد، Id رکورد جدید را در لیست، مساوی صفر مشاهده خواهید کرد و منبع داده این رکورد را همواره به عنوان یک رکورد جدید، مجددا به سرور ارسال می‌کند.
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using KendoUI07.Models;

namespace KendoUI07.Controllers
{
    public class RegistrationsController : ApiController
    {
        public HttpResponseMessage Delete(int id)
        {
            var item = RegistrationsDataSource.LatestRegistrations.FirstOrDefault(x => x.Id == id);
            if (item == null)
                return Request.CreateResponse(HttpStatusCode.NotFound);

            RegistrationsDataSource.LatestRegistrations.Remove(item);
            return Request.CreateResponse(HttpStatusCode.OK, item);
        }

        public IEnumerable<Registration> Get()
        {
            return RegistrationsDataSource.LatestRegistrations;
        }

        public HttpResponseMessage Post(Registration registration)
        {
            if (!ModelState.IsValid)
                return Request.CreateResponse(HttpStatusCode.BadRequest);

            var id = 1;
            var lastItem = RegistrationsDataSource.LatestRegistrations.LastOrDefault();
            if (lastItem != null)
            {
                id = lastItem.Id + 1;
            }
            registration.Id = id;
            RegistrationsDataSource.LatestRegistrations.Add(registration);

            // ارسال آی دی مهم است تا از ارسال رکوردهای تکراری جلوگیری شود
            return Request.CreateResponse(HttpStatusCode.Created, registration);
        }

        [HttpPut] // Add it to fix this error: The requested resource does not support http method 'PUT'
        public HttpResponseMessage Update(int id, Registration registration)
        {
            var item = RegistrationsDataSource.LatestRegistrations
                                        .Select(
                                            (prod, index) =>
                                                new
                                                {
                                                    Item = prod,
                                                    Index = index
                                                })
                                        .FirstOrDefault(x => x.Item.Id == id);
            if (item == null)
                return Request.CreateResponse(HttpStatusCode.NotFound);


            if (!ModelState.IsValid || id != registration.Id)
                return Request.CreateResponse(HttpStatusCode.BadRequest);

            RegistrationsDataSource.LatestRegistrations[item.Index] = registration;
            return Request.CreateResponse(HttpStatusCode.OK);
        }
    }
}
در اینجا بیشتر امضای این متدها مهم هستند، تا منطق پیاده سازی شده در آن‌ها. همچنین بازگشت Id رکورد جدید، توسط متد Post نیز بسیار مهم است و سبب می‌شود تا DataSource بداند با فراخوانی متد sync آن، باید عملیات Post یا create انجام شود یا Put و update.


نمایش آنی اطلاعات ثبت شده در یک لیست

ردیف‌های اضافه شده به منبع داده را می‌توان بلافاصله در همان سمت کلاینت توسط Kendo UI Template که قابلیت کار با ViewModelها را دارد، نمایش داد:
    <div id="coursesSection" class="k-rtl k-header">
        <div class="box-col">
            <form id="myForm" data-role="validator" novalidate="novalidate">
                           <!--فرم بحث شده در ابتدای مطلب-->
            </form>
        </div>
        <div id="results">
            <table class="metrotable">
                <thead>
                    <tr>
                        <th>Id</th>
                        <th>نام</th>
                        <th>دوره</th>
                        <th>هزینه</th>
                        <th>ایمیل</th>
                        <th>تلفن</th>
                        <th></th>
                        <th></th>
                    </tr>
                </thead>
                <tbody data-template="row-template" data-bind="source: coursesDataSourceRows"></tbody>
                <tfoot data-template="footer-template" data-bind="source: this"></tfoot>
            </table>
            <script id="row-template" type="text/x-kendo-template">
                <tr>
                    <td data-bind="text: Id"></td>
                    <td data-bind="text: UserName"></td>
                    <td dir="ltr" data-bind="text: CourseName"></td>
                    <td>
                        #: kendo.toString(get("Credit"), "c0") #
                    </td>
                    <td data-bind="text: Email"></td>
                    <td data-bind="text: Tel"></td>
                    <td><button class="k-button" data-bind="click: deleteCourse">حذف</button></td>
                    <td><button class="k-button" data-bind="click: editCourse">ویرایش</button></td>
                </tr>
            </script>
            <script id="footer-template" type="text/x-kendo-template">
                <tr>
                    <td colspan="3"></td>
                    <td>
                        جمع کل: #: kendo.toString(totalPrice(), "c0") #
                    </td>
                    <td colspan="2"></td>
                    <td></td>
                    <td></td>
                </tr>
            </script>
        </div>
    </div>
در ناحیه‌ی coursesSection که توسط متد kendo.bind به viewModel برنامه متصل شده‌است، یک جدول را برای نمایش ردیف‌های ثبت شده توسط کاربر اضافه کرده‌ایم. thead آن بیانگر سر ستون جدول است. قسمت tbody و tfoot این جدول توسط دو Kendo UI Template مقدار دهی شد‌ه‌اند. هر کدام نیز منبع داده‌اشان را از view model دریافت می‌کنند. در row-template معادل خواص شیء course را مشاهده می‌کنید. در footer-template متد totalPrice برای نمایش جمع ستون هزینه اضافه شده‌است. بنابراین مطابق این قسمت از View، به یک خاصیت جدید coursesDataSourceRows و سه متد deleteCourse، editCourse و totalPrice نیاز است:
    <script type="text/javascript">
        $(function () {
            // ...
            var viewModel = kendo.observable({
                accepted: false,
                course: new model(),
                coursesDataSourceRows: new kendo.data.ObservableArray([]),
                doSave: function (e) {
                       // ...
                },
                resetModel: function (e) {
                      // ...
                },
                totalPrice: function () {
                    var sum = 0;
                    $.each(this.get("coursesDataSourceRows"), function (index, item) {
                        sum += item.Credit;
                    });
                    return sum;
                },
                deleteCourse: function (e) {
                    // the current data item is passed as the "data" field of the event argument
                    var course = e.data;
                    dataSource.remove(course);
                    dataSource.sync(); // push to the server
                },
                editCourse: function(e) {
                    // the current data item is passed as the "data" field of the event argument
                    var course = e.data;
                    this.set("course", course);
                }
            });

            kendo.bind($("#coursesSection"), viewModel);
            dataSource.read(); // دریافت لیست موجود از سرور در آغاز کار
        });
    </script>
نحوه‌ی اتصال خاصیت جدید coursesDataSourceRows که به عنوان منبع داده ردیف‌های row-template عمل می‌کند، به این صورت است:
- ابتدا خاصیت دلخواه coursesDataSourceRows به viewModel اضافه می‌شود تا در ناحیه‌ی coursesSection در دسترس قرار گیرد.
- سپس اگر به انتهای تعریف DataSource دقت کنید، داریم:
    <script type="text/javascript">
        $(function () {
            var dataSource = new kendo.data.DataSource({
                //...
                change: function (e) {
                    // فراخوانی در زمان دریافت اطلاعات از سرور و یا تغییرات محلی
                    viewModel.set("coursesDataSourceRows", new kendo.data.ObservableArray(this.view()));
                }
            });
        });
    </script>
متد change آن، هر زمانیکه اطلاعاتی در منبع داده تغییر کنند یا اطلاعاتی به سمت سرور ارسال یا دریافت گردد، فراخوانی می‌شود. در همینجا فرصت خواهیم داشت تا خاصیت coursesDataSourceRows را جهت نمایش اطلاعات موجود در منبع داده، مقدار دهی کنیم. همین مقدار دهی ساده سبب اجرای row-template برای تولید ردیف‌های جدول می‌شود. استفاده از new kendo.data.ObservableArray سبب خواهد شد تا اگر اطلاعاتی در فرم برنامه تغییر کند، این اطلاعات بلافاصله در لیست گزارش برنامه نیز منعکس گردد.



کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید:
KendoUI07.zip
مطالب دوره‌ها
انتقال خواص محاسباتی Entity Framework به ViewModelها توسط AutoMapper
در طی دو مطلب (^ و ^) با نحوه‌ی قرار دادن خواص محاسباتی، درون کلاس‌های مدل‌های بانک اطلاعاتی مورد استفاده‌ی توسط Entity Framework آشنا شدیم. در اینجا قصد داریم این خواص محاسباتی را از کلاس‌های اصلی مدل‌های بانک اطلاعاتی خود خارج و به ViewModelها منتقل کنیم؛ چون اساسا هدف از این نوع خواص ویژه، ارائه اطلاعات نمایشی است به کاربر و نه ذخیره سازی آن‌ها در بانک اطلاعاتی.


مدل‌ها و تنظیمات برنامه

مدل‌ها و تنظیمات مورد استفاده‌ی در مثال جاری، با مدل‌های مطلب «لغو Lazy Loading در حین کار با AutoMapper و Entity Framework» یکی است. فقط ViewModel مورد استفاده اینبار یک‌چنین ساختاری را دارد:
public class UserViewModel
{
    public int Id { set; get; }
    public string CustomName { set; get; }
    public int PostsCount { set; get; }
}
در  اینجا می‌خواهیم در حین نگاشت اطلاعات جدول کاربران بانک اطلاعاتی به UserViewModel :
 - خاصیت CustomName از جمع نام و سن شخص تشکیل شود.
 - خاصیت PostsCount بیانگر جمع مطالب ارسالی آن شخص باشد.


نگاشت‌های AutoMapper می‌توانند حاوی توابع تجمعی نیز باشند

برای حل مساله‌ی فوق تنها کافی است نگاشت ذیل را تهیه کنیم:
public class TestProfile : Profile
{
    protected override void Configure()
    {
        this.CreateMap<User, UserViewModel>()
            .ForMember(dest => dest.CustomName,
                       opt => opt.MapFrom(src => src.Name + "[" + src.Age + "]"))
             .ForMember(dest => dest.PostsCount,
                        opt => opt.MapFrom(src => src.BlogPosts.Count()));
    }
 
    public override string ProfileName
    {
        get { return this.GetType().Name; }
    }
}
در این نگاشت عنوان شده‌است که اطلاعات CustomName را مطابق فرمول خاص جمع نام شخص و سن او تهیه کن. همچنین مقدار PostsCount، باید از جمع تعداد مطالب ارسالی او تشکیل شود.


کوئری نهایی استفاده کننده از تنظیمات نگاشت تهیه شده

در ادامه متدهای Project To را جهت استفاده‌ی از تنظیمات نگاشت فوق بکار می‌گیریم:
using (var context = new MyContext())
{
    var user1 = context.Users
                       .Project()
                       .To<UserViewModel>()
                       .FirstOrDefault();
 
    if (user1 != null)
    {
        Console.Write(user1.CustomName);
        Console.Write(user1.PostsCount);
    }
}
این کوئری یک چنین خروجی SQL ایی را به همراه دارد:
                SELECT
                    [Limit1].[Id] AS [Id],
                    [Limit1].[C1] AS [C1],
                    [Limit1].[C2] AS [C2]
                    FROM ( SELECT TOP (1)
                        [Project1].[Id] AS [Id],
                        CASE WHEN ([Project1].[Name] IS NULL) THEN N'' ELSE [Project1].[Name] END
                     + N'[' +  CAST( [Project1].[Age] AS nvarchar(max)) + N']' AS [C1],
                        [Project1].[C1] AS [C2]
                        FROM ( SELECT
                            [Extent1].[Id] AS [Id],
                            [Extent1].[Name] AS [Name],
                            [Extent1].[Age] AS [Age],
                            (SELECT
                                COUNT(1) AS [A1]
                                FROM [dbo].[BlogPosts] AS [Extent2]
                                WHERE [Extent1].[Id] = [Extent2].[UserId]) AS [C1]
                            FROM [dbo].[Users] AS [Extent1]
                        )  AS [Project1]
                    )  AS [Limit1]
همانطور که مشاهده می‌کنید، تنظیمات نگاشت تهیه شده (نحوه‌ی تهیه‌ی نام و جمع تعداد مطالب شخص) به SQL ترجمه شده‌اند.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید.
مطالب
آشنایی با CLR: قسمت دوازدهم
متادیتاها شامل بلوکی از داده‌های باینری هستند که شامل چندین جدول شده و جدول‌ها نیز به سه دسته تقسیم می‌شوند:
  1. جداول تعاریف Definition Table
  2. جداول ارجاع References Table
  3. جداول manifest

جداول تعریف

جدول زیر تعدادی از جداول تعریف‌ها را توضیح می‌دهد:
 ModuleDef  شامل آدرس یا مدخلی است که ماژول در آن تعریف شده است. این آدرس شامل نام ماژول به همراه پسوند آن است؛ بدون ذکر مسیر. در صورتی که کامپایل به صورت GUID انجام گرفته باشد، Version ID ماژول هم همراه آن‌ها خواهد بود. در صورتیکه نام فایل تغییر کند، این جدول باز نام اصلی ماژول را به همراه خواهد داشت. هر چند تغییر نام فایل به شدت رد شده و ممکن است باعث شود CLR نتواند در زمان اجرا آن را پیدا کند.
 TypeDef  شامل یک مدخل ورودی برای هر نوعی است که تعریف شده است. هر آدرس ورودی شامل نام نوع ، پرچمها (همان مجوز‌های public و private و ...) می‌باشد. همچنین شامل اندیس هایی به متدها است که شامل جدول MethodDef می‌باشند یا فیلدهایی که شامل جدول FieldDef می‌باشند و الی آخر...
 MethodDef  شامل آدرسی برای هر متد تعریف شده در ماژول است که شامل  نام متد و پرچم هاست. همچنین شامل امضای متد و نقطه‌ی آغاز کد IL آن در ماژول هم می‌شود و آن آدرس هم میتواند ارجاعی به جدول ParamDef جهت شناسایی پارامترها باشد.
 FieldDef  شامل اطلاعاتی در مورد فیلدهاست که این اطلاعات ، پرچم، نام و نوع فیلد را مشخص می‌کنند.
 ParamDef  حاوی اطلاعات پارامتر متدهاست که این اطلاعات شامل پرچم‌ها (in , out ,retval) ، نوع و نام است.
 PropertyDef   برای هر پراپرتی یا خصوصیت، شامل یک آدرس است که شامل نام، نوع و پرچم می‌شود.
 EventDef  برای هر رویداد شامل یک آدرس است که این آدرس شامل نام و نوع است.

جداول ارجاعی
موقعی که کد شما کامپایل می‌شود، اگر شما به اسمبلی دیگری ارجاع داشته باشید، از جداول ارجاع کمک گرفته می‌شود که در جدول زیر تعدادی از این جداول فهرست شده‌اند:
 AssemblyRef  شامل آدرس اسمبلی است که ماژولی به آن ارجاع داده است و این آدرس شامل اطلاعات ضروری جهت اتصال به اسمبلی می‌شود و این اطلاعات شامل نام اسمبلی (بدون ذکر پسوند و مسیر)، شماره نسخه اسمبلی، سیستم فرهنگی و منطقه‌ای تعیین شده اسمبلی culture و یک کلید عمومی که عموما توسط ناشر ایجاد می‌گردد که هویت ناشر آن اسمبلی را مشخص می‌کند. هر آدرس شامل یک پرچم و یک کد هش هست که بری ارزیابی از صحت و بی خطا بودن بیت‌های اسمبلی ارجاع شده Checksum استفاده می‌شود.
 ModuleRef  شامل یک آدرس ورودی به هدر PE ماژول است به نوع‌های پیاده سازی شده آن ماژول در آن اسمبلی. هر آدرس شامل نام فایل و پسوند آن بدون ذکر مسیر است. این جدول برای اتصال به نوع‌هایی استفاده می‌شود که در یک ماژول متفاوت از ماژول اسمبلی صدا زده شده پیاده سازی شده است.
 TypeRef  شامل یک آدرس یا ورودی برای هر نوعی است که توسط ماژول ارجاع داده شده است. هر آدرس شامل نام نوع و آدرسی است که نوع در آن جا قرار دارد. اگر این نوع داخل نوع دیگری پیاده سازی شود، ارجاعات به سمت یک جدول TypeDef خواهد بود. اگر نوع داخل همان ماژول تعریف شده باشد، ارجاع به سمت جدول ModuleDef خواهد بود و اگر نوع در ماژول دیگری از آن اسمبلی پیاده سازی شده باشد، ارجاع به سمت یک جدول ModuleRef خواهد بود و اگر نوع در یک اسمبلی جداگانه تعریف شده باشد، ارجاع به جدول AssemblyRef خواهد بود.
 MemberRef  شامل یک آدرس ورودی برای هر عضو (فیلد و متدها و حتی پراپرتی و رویدادها) است که توسط آن آن ماژول ارجاع شده باشد. هر آدرس شامل نام عضو، امضاء و یک اشاره‌گر به جدول TypeRef است، برای نوع‌هایی که به تعریف عضو پرداخته‌اند. 
البته جداولی که در بالا هستند فقط تعدادی از آن جداول هستند، ولی قصد ما تنها یک آشنایی کلی با جداول هر قسمت بود. در مورد جداول manifest بعد‌تر صحبت می‌کنیم.
ابزارهای متنوع و زیادی هستند که برای بررسی و آزمایش متادیتاها استفاده می‌شوند. یکی از این ابزارها ILDasm.exe می‌باشد. برای دیدن متادیتاهای یک فایل اجرایی فرمان زیر را صادر کنید:
ILDasm Program.exe
صدور فرمان بالا باعث اجرای ILDasm و بارگزاری اسمبلی‌های program.exe می‌شود. برای مشاهده‌ی اطلاعات جداول متا به صورت شکیل و قابل خواندن برای انسان، در منوی برنامه، مسیر زیر را طی کنید:
View/MetaInfo/Show
با طی کردن گزینه‌های بالا، اطلاعات به صورت زیر نمایش داده می‌شوند:
===========================================================
ScopeName : Program.exe
MVID : {CA73FFE8­0D42­4610­A8D3­9276195C35AA}
===========================================================
Global functions
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
Global fields
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
Global MemberRefs
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
TypeDef #1 (02000002)
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
TypDefName: Program (02000002)
Flags : [Public] [AutoLayout] [Class] [Sealed] [AnsiClass]
[BeforeFieldInit] (00100101)
Extends : 01000001 [TypeRef] System.Object
Method #1 (06000001) [ENTRYPOINT]
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
MethodName: Main (06000001)
Flags : [Public] [Static] [HideBySig] [ReuseSlot] (00000096)
RVA : 0x00002050
ImplFlags : [IL] [Managed] (00000000)
CallCnvntn: [DEFAULT]
ReturnType: Void
No arguments.
Method #2 (06000002)
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
MethodName: .ctor (06000002)
Flags : [Public] [HideBySig] [ReuseSlot] [SpecialName]
[RTSpecialName] [.ctor] (00001886)
RVA : 0x0000205c
ImplFlags : [IL] [Managed] (00000000)
CallCnvntn: [DEFAULT]
hasThis
ReturnType: Void
No arguments.
TypeRef #1 (01000001)
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
Token: 0x01000001
ResolutionScope: 0x23000001
TypeRefName: System.Object
MemberRef #1 (0a000004)
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
Member: (0a000004) .ctor:
CallCnvntn: [DEFAULT]
hasThis
ReturnType: Void
No arguments.
TypeRef #2 (01000002)
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
Token: 0x01000002
ResolutionScope: 0x23000001
TypeRefName: System.Runtime.CompilerServices.CompilationRelaxationsAttribute
MemberRef #1 (0a000001)
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
Member: (0a000001) .ctor:
CallCnvntn: [DEFAULT]
hasThis
ReturnType: Void
1 Arguments
Argument #1: I4
TypeRef #3 (01000003)
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
Token: 0x01000003
ResolutionScope: 0x23000001
TypeRefName: System.Runtime.CompilerServices.RuntimeCompatibilityAttribute
MemberRef #1 (0a000002)
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
Member: (0a000002) .ctor:
CallCnvntn: [DEFAULT]
hasThis
ReturnType: Void
No arguments.
TypeRef #4 (01000004)
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
Token: 0x01000004
ResolutionScope: 0x23000001
TypeRefName: System.Console
MemberRef #1 (0a000003)
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
Member: (0a000003) WriteLine:
CallCnvntn: [DEFAULT]
ReturnType: Void
1 Arguments
Argument #1: String
Assembly
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
Token: 0x20000001
Name : Program
Public Key :
Hash Algorithm : 0x00008004
Version: 0.0.0.0
Major Version: 0x00000000
Minor Version: 0x00000000
Build Number: 0x00000000
Revision Number: 0x00000000
Locale: <null>
Flags : [none] (00000000)
CustomAttribute #1 (0c000001)
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
CustomAttribute Type: 0a000001
CustomAttributeName:
System.Runtime.CompilerServices.CompilationRelaxationsAttribute ::
instance void .ctor(int32)
Length: 8
Value : 01 00 08 00 00 00 00 00 > <
ctor args: (8)
CustomAttribute #2 (0c000002)
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
CustomAttribute Type: 0a000002
CustomAttributeName: System.Runtime.CompilerServices.RuntimeCompatibilityAttribute ::
instance void .ctor()
Length: 30
Value : 01 00 01 00 54 02 16 57 72 61 70 4e 6f 6e 45 78 > T WrapNonEx<
: 63 65 70 74 69 6f 6e 54 68 72 6f 77 73 01 >ceptionThrows <
ctor args: ()
AssemblyRef #1 (23000001)
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
Token: 0x23000001
Public Key or Token: b7 7a 5c 56 19 34 e0 89
Name: mscorlib
Version: 4.0.0.0
Major Version: 0x00000004
Minor Version: 0x00000000
Build Number: 0x00000000
Revision Number: 0x00000000
Locale: <null>
HashValue Blob:
Flags: [none] (00000000)
User Strings
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
70000001 : ( 2) L"Hi"
Coff symbol name overhead: 0
لازم نیست که تمامی اطلاعات بالا را به طور کامل بفهمید. همین که متوجه شوید برنامه شامل  TypeDef است که نام آن Program است و این نوع به صورت یک کلاس عمومی sealed است که از نوع system.object ارث بری کرده است (یک نوع ارجاع از اسمبلی دیگر) و برنامه شامل دو متد main و یک سازنده ctor. است، کافی هست.
متد Main یک متد عمومی و ایستا static است که شامل کد IL است و هیچ خروجی ندارد و هیچ آرگومانی را نمی‌پزیرد. متد سازنده عمومی است و شامل کد IL است، سازنده هیچ نوع خروجی ندارد و هیچ آرگومانی هم نمی‌پذیرد و یک اشاره‌گر که به یک object در حافظه که موقع صدا زدن ساخته خواهد شد.
ابزار ILDasm امکاناتی بیشتری از آنچه که دیدید ارائه می‌کند. به عنوان نمونه اگر مسیر زیر را در منوها طی کنید:
View/statistics
اطلاعات آماری زیر نمایش داده می‌شود:
File size : 3584
PE header size : 512 (496 used) (14.29%)
PE additional info : 1411 (39.37%)
Num.of PE sections : 3
CLR header size : 72 ( 2.01%)
CLR meta­data size : 612 (17.08%)
CLR additional info : 0 ( 0.00%)
CLR method headers : 2 ( 0.06%)
Managed code : 20 ( 0.56%)
Data : 2048 (57.14%)
Unaccounted : ­1093 (­30.50%)
Num.of PE sections : 3
.text ­ 1024
.rsrc ­ 1536
.reloc ­ 512
CLR meta­data size : 612
Module ­ 1 (10 bytes)
TypeDef ­ 2 (28 bytes) 0 interfaces, 0 explicit layout
TypeRef ­ 4 (24 bytes)
MethodDef ­ 2 (28 bytes) 0 abstract, 0 native, 2 bodies
MemberRef ­ 4 (24 bytes)
CustomAttribute­ 2 (12 bytes)
Assembly ­ 1 (22 bytes)
AssemblyRef ­ 1 (20 bytes)
Strings ­ 184 bytes
Blobs ­ 68 bytes
UserStrings ­ 8 bytes
Guids ­ 16 bytes
Uncategorized ­ 168 bytes
CLR method headers : 2
Num.of method bodies ­ 2
Num.of fat headers ­ 0
Num.of tiny headers ­ 2
Managed code : 20
Ave method size ­ 10
اطلاعات بالا شامل نمایش حجم فایل به بایت و سایر قسمت‌های تشکیل دهنده فایل است...
توجه: ILDasm یک باگ دارد که بر نمایش اندازه‌ی فایل تاثیر می‌گذارد و باعث می‌شود شما نتوانید به اطلاعات ثبت شده اعتماد داشته باشید.