مطالب
تزریق وابستگی‌ها در پروفایل‌های AutoMapper در برنامه‌های ASP.NET Core
Profileهای AutoMapper، قابلیت تزریق وابستگی‌ها را در سازنده‌ی خود ندارند؛ به همین جهت در این مطلب، دو راه حل را جهت رفع این محدودیت بررسی می‌کنیم.


مثال: نیاز به نگاشت کلمه‌ی عبور، به کلمه‌ی عبور هش شده

فرض کنید موجودیت کاربری که قرار است در بانک اطلاعاتی ذخیره شود، چنین ساختاری را دارد:
namespace AutoMapperInjection.Entities
{
    public class User
    {
        public int Id { set; get; }

        public string HashedPassword { set; get; }
    }
}
در اینجا کلمه‌ی عبور، به صورت معمولی ذخیره نمی‌شود و معادل هش شده‌ی آن ذخیره خواهد شد. اما اطلاعاتی را که از کاربر دریافت می‌کنیم:
namespace AutoMapperInjection.Models
{
    public class UserDto
    {
        public string Password { set; get; }
    }
}
حاوی کلمه‌ی عبور معمولی است که باید در حین نگاشت UserDto به User، هش شود. این اطلاعات نیز به صورت زیر توسط اکشن متد RegisterUser دریافت می‌شوند که توسط متد mapper.Map، قرار است به یک نمونه از شیء User تبدیل شود:
namespace AutoMapperInjection.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class HomeController : ControllerBase
    {
        private readonly IMapper _mapper;

        public HomeController(IMapper mapper)
        {
            _mapper = mapper ?? throw new NullReferenceException(nameof(mapper));
        }

        [HttpPost("[action]")]
        public IActionResult RegisterUser(UserDto model)
        {
            var user = _mapper.Map<User>(model);

            // TODO: Save user

            return Ok();
        }
    }
}
کار این هش شدن نیز برای مثال توسط سرویس زیر انجام می‌شود:
using System;
using System.Security.Cryptography;
using System.Text;

namespace AutoMapperInjection.Services
{
    public interface IPasswordHasherService
    {
        string GetSha256Hash(string input);
    }

    public class PasswordHasherService : IPasswordHasherService
    {
        public string GetSha256Hash(string input)
        {
            using var hashAlgorithm = new SHA256CryptoServiceProvider();
            var byteValue = Encoding.UTF8.GetBytes(input);
            var byteHash = hashAlgorithm.ComputeHash(byteValue);
            return Convert.ToBase64String(byteHash);
        }
    }
}
به همین جهت در حین تعریف نگاشت‌های AutoMaper، نیاز خواهیم داشت تا بتوانیم از این سرویس استفاده کنیم.


روش اول: IValueResolver‌ها قابلیت تزریق وابستگی را در سازنده‌ی خود دارند

توسط یک IValueResolver سفارشی، می‌توانیم مشخص کنیم که برای مثال اطلاعات یک خاصیت خاص، چگونه باید از منبع دریافتی تامین شود:
        public class HashedPasswordResolver : IValueResolver<UserDto, User, string>
        {
            private readonly IPasswordHasherService _hasher;

            public HashedPasswordResolver(IPasswordHasherService hasher)
            {
                _hasher = hasher ?? throw new ArgumentNullException(nameof(hasher));
            }

            public string Resolve(UserDto source, User destination, string destMember, ResolutionContext context)
            {
                return _hasher.GetSha256Hash(source.Password);
            }
        }
همانطور که مشاهده می‌کنید، در اینجا می‌توان سرویس مدنظر را به سازنده‌ی این کلاس تزریق کرد و سپس از آن جهت تامین مقدار هش شده‌ی کلمه‌ی عبور استفاده کرد. IValueResolverها تنها تامین کننده‌ی مقدار یک خاصیت، در حین نگاشت هستند.

پس از آن، روش استفاده‌ی از این تامین کننده‌ی مقدار سفارشی، به صورت زیر است:
    public class UserDtoMappingsProfile : Profile
    {
        public UserDtoMappingsProfile()
        {
            // Map from User (entity) to UserDto, and back
            this.CreateMap<User, UserDto>()
                .ReverseMap()
                .ForMember(user => user.HashedPassword, exp => exp.MapFrom<HashedPasswordResolver>());
        }
    }
با این تعاریف، هر زمانیکه قرار است کار var user = _mapper.Map<User>(model) انجام شود، مقدار خاصیت HashedPassword، از طریق HashedPasswordResolver تامین می‌شود که در اینجا کار تزریق وابستگی‌های آن نیز به صورت خودکار توسط AutoMapper مدیریت می‌شود. البته بدیهی است که سرویس IPasswordHasherService باید به نحو زیر پیشتر به سیستم معرفی شده باشد:
namespace AutoMapperInjection
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IPasswordHasherService, PasswordHasherService>();
            services.AddAutoMapper(typeof(UserDtoMappingsProfile).Assembly);
            services.AddControllers();
        }


روش دوم: IMappingAction‌ها قابلیت تزریق وابستگی را در سازنده‌ی خود دارند

می‌توان پیش و یا پس از عملیات نگاشت، منطقی را توسط یک IMappingAction سفارشی بر روی آن اجرا کرد که در اینجا نیز امکان تزریق وابستگی در سازنده‌ی IMappingActionهای پیاده سازی شده، وجود دارد:
        public class UserDtoMappingsAction : IMappingAction<UserDto, User>
        {
            private readonly IPasswordHasherService _hasher;

            public UserDtoMappingsAction(IPasswordHasherService hasher)
            {
                _hasher = hasher ?? throw new ArgumentNullException(nameof(hasher));
            }

            public void Process(UserDto source, User destination, ResolutionContext context)
            {
                destination.HashedPassword = _hasher.GetSha256Hash(source.Password);
            }
        }
در اینجا می‌خواهیم مقدار یک یا چندین خاصیت از مقصد نگاشت را (شیء User در اینجا) پس از نگاشت ابتدایی از طریق مقدار دریافتی از کاربر، با منطق خاصی تغییر دهیم. برای مثال کلمه‌ی عبور ساده‌ی دریافتی را هش کنیم و به خاصیت خاصی نسبت دهیم (و یا حتی مقدار خواص دیگری را نیز پس از نگاشت، تغییر دهیم).

در این حالت روش استفاده‌ی از کلاس UserDtoMappingsAction به صورت زیر است:
    public class UserDtoMappingsProfile : Profile
    {
        public UserDtoMappingsProfile()
        {
            // Map from User (entity) to UserDto, and back
            this.CreateMap<User, UserDto>()
                .ReverseMap()
                .AfterMap<UserDtoMappingsAction>();
        }
    }


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: AutoMapperInjection.zip
مطالب
پیاده سازی یک تامین کننده MySQL برای ASP.NET Identity
در این مقاله جایگزینی پیاده سازی پیش فرض ASP.NET Identity را بررسی می‌کنیم. در ادامه خواهید خواند:

  • جزئیات نحوه پیاده سازی یک Storage Provider برای ASP.NET Identity
  • تشریح اینترفیس هایی که باید پیاده سازی شوند، و نحوه استفاده از آنها در ASP.NET Identity
  • ایجاد یک دیتابیس MySQL روی Windows Azure
  • نحوه استفاده از یک ابزار کلاینت (MySQL Workbench) برای مدیریت دیتابیس مذکور
  • نحوه جایگزینی پیاده سازی سفارشی با نسخه پیش فرض در یک اپلیکیشن ASP.NET MVC
در انتهای این مقاله یک اپلیکیشن ASP.NET MVC خواهیم داشت که از ASP.NET Identity و تامین کننده سفارشی جدید استفاده می‌کند. دیتابیس اپلیکیشن MySQL خواهد بود و روی Windows Azure میزبانی می‌شود. سورس کد کامل این مثال را هم می‌توانید از این لینک دریافت کنید.


پیاده سازی یک Storage Provider سفارشی برای ASP.NET Identity

ASP.NET Identity سیستم توسعه پذیری است که می‌توانید بخش‌های مختلف آن را جایگزین کنید.در این سیستم بناهای سطح بالایی مانند Managers و Stores وجود دارند.
Managers کلاس‌های سطح بالایی هستند که توسعه دهندگان از آنها برای اجرای عملیات مختلف روی ASP.NET Identity استفاده می‌کنند. مدیریت کننده‌های موجود عبارتند از UserManager و RoleManager. کلاس UserManager برای اجرای عملیات مختلف روی کاربران استفاده می‌شود، مثلا ایجاد کاربر جدید یا حذف آنها. کلاس RoleManager هم برای اجرای عملیات مختلف روی نقش‌ها استفاده می‌شود.

Stores کلاس‌های سطح پایین‌تری هستند که جزئیات پیاده سازی را در بر می‌گیرند، مثلا اینکه موجودیت‌های کاربران و نقش‌ها چگونه باید ذخیره و بازیابی شوند. این کلاس‌ها با مکانیزم ذخیره و بازیابی تلفیق شده اند. مثلا Microsoft.AspNet.Identity.EntityFramework کلاسی با نام UserStore دارد که برای ذخیره و بازیابی User‌ها و داده‌های مربوطه توسط EntityFramework استفاده می‌شود.

Managers از Stores تفکیک شده اند و هیچ وابستگی ای به یکدیگر ندارند. این تفکیک بدین منظور انجام شده که بتوانید مکانیزم ذخیره و بازیابی را جایگزین کنید، بدون اینکه اپلیکیشن شما از کار بیافتد یا نیاز به توسعه بیشتر داشته باشد. کلاس‌های Manager می‌توانند با هر Store ای ارتباط برقرار کنند. از آنجا که شما از API‌های سطح بالای UserManager برای انجام عملیات CRUD روی کاربران استفاده می‌کنید، اگر UserStore را با پیاده سازی دیگری جایگزین کنید، مثلا AzureTable Storage یا MySql، نیازی به بازنویسی اپلیکیشن نیست.

در مثال جاری پیاده سازی پیش فرض Entity Framework را با یک  تامین کننده MySQL جایگزین می‌کنیم.

پیاده سازی کلاس‌های Storage
برای پیاده سازی تامین کننده‌های سفارشی، باید کلاس هایی را پیاده سازی کنید که همتای آنها در Microsoft.AspNet.Identity.EntityFramework وجود دارند:
  • <UserStore<TUser
  • IdentityUser
  • <RoleStore<TRole
  • IdentityRole
پیاده سازی پیش فرض Entity Framework را در تصاویر زیر مشاهده می‌کنید.
Users

Roles

در مخزن پیش فرض ASP.NET Identity EntityFramework کلاس‌های بیشتری برای موجودیت‌ها مشاهده می‌کنید.

  • IdentityUserClaim
  • IdentityUserLogin
  • IdentityUserRole
همانطور که از نام این کلاس‌ها مشخص است، اختیارات، نقش‌ها و اطلاعات ورود کاربران توسط این کلاس‌ها معرفی می‌شوند. در مثال جاری این کلاس‌ها را پیاده سازی نخواهیم کرد، چرا که بارگذاری اینگونه رکوردها از دیتابیس به حافظه برای انجام عملیات پایه (مانند افزودن و حذف اختیارات کاربران) سنگین است. در عوض کلاس‌های backend store اینگونه عملیات را بصورت مستقیم روی دیتابیس اجرا خواهند کرد. بعنوان نمونه متد ()UserStore.GetClaimsAsync را در نظر بگیرید. این متد به نوبه خود متد (userClaimTable.FindByUserId(user.Id را فراخوانی می‌کند که یک کوئری روی جدول مربوطه اجرا می‌کند و لیستی از اختیارات کاربر را بر می‌گرداند.
public Task<IList<Claim>> GetClaimsAsync(IdentityUser user)
{
    ClaimsIdentity identity = userClaimsTable.FindByUserId(user.Id);
    return Task.FromResult<IList<Claim>>(identity.Claims.ToList());
}
برای پیاده سازی یک تامین کننده سفارشی MySQL مراحل زیر را دنبال کنید.
1. کلاس کاربر را ایجاد کنید، که اینترفیس IUser را پیاده سازی می‌کند.
public class IdentityUser : IUser
{
    public IdentityUser(){...}

    public IdentityUser(string userName) (){...}

    public string Id { get; set; }

    public string UserName { get; set; }

    public string PasswordHash { get; set; }

    public string SecurityStamp { get; set; }
}
2. کلاس User Store را ایجاد کنید، که اینترفیس‌های IUserStore, IUserClaimStore, IUserLoginStore, IUserRoleStore و IUserPasswordStore را پیاده سازی می‌کند. توجه کنید که تنها اینترفیس IUserStore را باید پیاده سازی کنید، مگر آنکه بخواهید از امکاناتی که دیگر اینترفیس‌ها ارائه می‌کنند هم استفاده کنید.
public class UserStore : IUserStore<IdentityUser>,
                         IUserClaimStore<IdentityUser>,
                         IUserLoginStore<IdentityUser>,
                         IUserRoleStore<IdentityUser>,
                         IUserPasswordStore<IdentityUser>
{
    public UserStore(){...}

    public Task CreateAsync(IdentityUser user){...}

    public Task<IdentityUser> FindByIdAsync(string userId){...}   
...
}
3. کلاس Role را ایجاد کنید که اینترفیس IRole را پیاده سازی می‌کند.
public class IdentityRole : IRole
{
    public IdentityRole(){...}

    public IdentityRole(string roleName) (){...}

    public string Id { get; set; }

    public string Name { get; set; }
}
4. کلاس Role Store را ایجاد کنید که اینترفیس IRoleStore را پیاده سازی می‌کند. توجه داشته باشید که پیاده سازی این مخزن اختیاری است و در صورتی لازم است که بخواهید از نقش‌ها در سیستم خود استفاده کنید.
public class RoleStore : IRoleStore<IdentityRole>                        
{
    public RoleStore(){...}

    public Task CreateAsync(IdentityRole role){...}

    public Task<IdentityRole> FindByIdAsync(string roleId){...}   
....
}
کلاس‌های بیشتری هم وجود دارند که مختص پیاده سازی مثال جاری هستند.
  • MySQLDatabase: این کلاس اتصال دیتابیس MySql و کوئری‌ها را کپسوله می‌کند. کلاس‌های UserStore و RoleStore توسط نمونه ای از این کلاس وهله سازی می‌شوند.
  • RoleTable: این کلاس جدول Roles و عملیات CRUD مربوط به آن را کپسوله می‌کند.
  • UserClaimsTable: این کلاس جدول UserClaims و عملیات CRUD مربوط به آن را کپسوله می‌کند.
  • UserLoginsTable: این کلاس جدول UserLogins و عملیات CRUD مربوط به آن را کپسوله می‌کند.
  • UserRolesTable: این کلاس جدول UserRoles و عملیات CRUD مربوطه به آن را کپسوله می‌کند.
  • UserTable: این کلاس جدول Users و عملیات CRUD مربوط به آن را کپسوله می‌کند.

ایجاد یک دیتابیس MySQL روی Windows Azure

1. به پورتال مدیریتی Windows Azure وارد شوید.
2. در پایین صفحه روی NEW+ کلیک کنید و گزینه STORE را انتخاب نمایید.

در ویزارد Choose Add-on به سمت پایین اسکرول کنید و گزینه ClearDB MySQL Database را انتخاب کنید. سپس به مرحله بعد بروید.

4. راهکار Free بصورت پیش فرض انتخاب شده، همین گزینه را انتخاب کنید و نام دیتابیس را به IdentityMySQLDatabase تغییر دهید. نزدیک‌ترین ناحیه (region) به خود را انتخاب کنید و به مرحله بعد بروید.

5. روی علامت checkmark کلیک کنید تا دیتابیس شما ایجاد شود. پس از آنکه دیتابیس شما ساخته شد می‌توانید از قسمت ADD-ONS آن را مدیریت کنید.

6. همانطور که در تصویر بالا می‌بینید، می‌توانید اطلاعات اتصال دیتابیس (connection info) را از پایین صفحه دریافت کنید.

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


ایجاد جداول ASP.NET Identity در یک دیتابیس MySQL

ابتدا ابزار MySQL Workbench را نصب کنید.
1. ابزار مذکور را از اینجا دانلود کنید.
2. هنگام نصب، گزینه Setup Type: Custom را انتخاب کنید.
3. در قسمت انتخاب قابلیت ها، گزینه‌های Applications و MySQLWorkbench را انتخاب کنید و مراحل نصب را به اتمام برسانید.
4. اپلیکیشن را اجرا کرده و روی MySQLConnection کلیک کنید تا رشته اتصال جدیدی تعریف کنید. رشته اتصالی که در مراحل قبل از Azure MySQL Database کپی کردید را اینجا استفاده کنید. بعنوان مثال:
 Connection Name: AzureDB; Host Name: us-cdbr-azure-west-b.cleardb.com; Username: <username>; Password: <password>; Default Schema: IdentityMySQLDatabase 
5. پس از برقراری ارتباط با دیتابیس، یک برگ Query جدید باز کنید. فرامین زیر را برای ایجاد جداول مورد نیاز کپی کنید.
CREATE TABLE `IdentityMySQLDatabase`.`users` (
  `Id` VARCHAR(45) NOT NULL,
  `UserName` VARCHAR(45) NULL,
  `PasswordHash` VARCHAR(100) NULL,
  `SecurityStamp` VARCHAR(45) NULL,
  PRIMARY KEY (`id`));

CREATE TABLE `IdentityMySQLDatabase`.`roles` (
  `Id` VARCHAR(45) NOT NULL,
  `Name` VARCHAR(45) NULL,
  PRIMARY KEY (`Id`));

CREATE TABLE `IdentityMySQLDatabase`.`userclaims` (
  `Id` INT NOT NULL AUTO_INCREMENT,
  `UserId` VARCHAR(45) NULL,
  `ClaimType` VARCHAR(100) NULL,
  `ClaimValue` VARCHAR(100) NULL,
  PRIMARY KEY (`Id`),
  FOREIGN KEY (`UserId`)
    REFERENCES `IdentityMySQLDatabase`.`users` (`Id`) on delete cascade);

CREATE TABLE `IdentityMySQLDatabase`.`userlogins` (
  `UserId` VARCHAR(45) NOT NULL,
  `ProviderKey` VARCHAR(100) NULL,
  `LoginProvider` VARCHAR(100) NULL,
  FOREIGN KEY (`UserId`)
    REFERENCES `IdentityMySQLDatabase`.`users` (`Id`) on delete cascade);

CREATE TABLE `IdentityMySQLDatabase`.`userroles` (
  `UserId` VARCHAR(45) NOT NULL,
  `RoleId` VARCHAR(45) NOT NULL,
  PRIMARY KEY (`UserId`, `RoleId`),
  FOREIGN KEY (`UserId`)
    REFERENCES `IdentityMySQLDatabase`.`users` (`Id`) 
on delete cascade
on update cascade,
  FOREIGN KEY (`RoleId`)
    REFERENCES `IdentityMySQLDatabase`.`roles` (`Id`)
on delete cascade
on update cascade);
6. حالا تمام جداول لازم برای ASP.NET Identity را در اختیار دارید، دیتابیس ما MySQL است و روی Windows Azure میزبانی شده.


ایجاد یک اپلیکیشن ASP.NET MVC و پیکربندی آن برای استفاده از MySQL Provider

2. در گوشه سمت راست پایین صفحه روی دکمه Download Zip کلیک کنید تا کل پروژه را دریافت کنید.
3. محتوای فایل دریافتی را در یک پوشه محلی استخراج کنید.
4. پروژه AspNet.Identity.MySQL را باز کرده و آن را کامپایل (build) کنید.
5. روی نام پروژه کلیک راست کنید و گزینه Add, New Project را انتخاب نمایید. پروژه جدیدی از نوع ASP.NET Web Application بسازید و نام آن را به IdentityMySQLDemo تغییر دهید.

6. در پنجره New ASP.NET Project قالب MVC را انتخاب کنید و تنظیمات پیش فرض را بپذیرید.

7. در پنجره Solution Explorer روی پروژه IdentityMySQLDemo کلیک راست کرده و Manage NuGet Packages را انتخاب کنید. در قسمت جستجوی دیالوگ باز شده عبارت "Identity.EntityFramework" را وارد کنید. در لیست نتایج این پکیج را انتخاب کرده و آن را حذف (Uninstall) کنید. پیغامی مبنی بر حذف وابستگی‌ها باید دریافت کنید که مربوط به پکیج EntityFramework است، گزینه Yes را انتخاب کنید. از آنجا که کاری با پیاده سازی فرض نخواهیم داشت، این پکیج‌ها را حذف می‌کنیم.

8. روی پروژه IdentityMySQLDemo کلیک راست کرده و Add, Reference, Solution, Projects را انتخاب کنید. در دیالوگ باز شده پروژه AspNet.Identity.MySQL را انتخاب کرده و OK کنید.

9. در پروژه IdentityMySQLDemo پوشه Models را پیدا کرده و کلاس IdentityModels.cs را حذف کنید.

10. در پروژه IdentityMySQLDemo تمام ارجاعات ";using Microsoft.AspNet.Identity.EntityFramework" را با ";using AspNet.Identity.MySQL" جایگزین کنید.

11. در پروژه IdentityMySQLDemo تمام ارجاعات به کلاس "ApplicationUser" را با "IdentityUser" جایگزین کنید.

12. کنترلر Account را باز کنید و متد سازنده آنرا مطابق لیست زیر تغییر دهید.

public AccountController() : this(new UserManager<IdentityUser>(new UserStore(new MySQLDatabase())))
{

}

13. فایل web.config را باز کنید و رشته اتصال DefaultConnection را مطابق لیست زیر تغییر دهید.

<add name="DefaultConnection" connectionString="Database=IdentityMySQLDatabase;Data Source=<DataSource>;User Id=<UserID>;Password=<Password>" providerName="MySql.Data.MySqlClient" />

مقادیر <DataSource>, <UserId> و <Password> را با اطلاعات دیتابیس خود جایگزین کنید.


اجرای اپلیکیشن و اتصال به دیتابیس MySQL

1. روی پروژه IdentityMySQLDemo کلیک راست کرده و Set as Startup Project را انتخاب کنید.
2. اپلیکیشن را با Ctrl + F5 کامپایل و اجرا کنید.
3. در بالای صفحه روی Register کلیک کنید.
4. حساب کاربری جدیدی بسازید.

5. در این مرحله کاربر جدید باید ایجاد شده و وارد سایت شود.

6. به ابزار MySQL Workbench بروید و محتوای جداول IdentityMySQLDatabase را بررسی کنید. جدول users را باز کنید و اطلاعات کاربر جدید را بررسی نمایید.

برای ساده نگاه داشتن این مقاله از بررسی تمام کدهای لازم خودداری شده، اما اگر مراحل را دنبال کنید و سورس کد نمونه را دریافت و بررسی کنید خواهید دید که پیاده سازی تامین کنندگان سفارشی برای ASP.NET Identity کار نسبتا ساده ای است.

نظرات مطالب
ASP.NET MVC #12
- ابتدا نیاز است با مفهوم ViewModel آشنا باشید.
- اگر لیستی قرار است در تمام صفحات نمایش داده شود و محل آن هم باید در layout باشد، یعنی باید این لیست در هر بار نمایش و یا تولید هر View (تمام Viewهای سایت)، تولید شود. بنابراین در BaseViewModelایی که عنوان شد، تعریف خاصیت این لیست را قرار دهید و در layout از آن استفاده کنید.
// در پایه مدل‌ها
public abstract class BaseViewModel
{
    public IList<Post> Posts { get; set; } // خاصیت عمومی که قرار است در فایل مستر قابل دسترسی باشد
}

// در اکشن متد
 return View(model: new HomeViewModel { Posts = .... });

// در اینجا ویوومدل ارسالی از پایه مدل‌ها مشتق می‌شود
public class HomeViewModel : BaseViewModel
- و اگر نمی‌خواهید به ازای هر Viewایی که در سایت تولید می‌شود یکبار این لیست را مقدار دهی کنید، بهتر است از روش Html.RenderAction استفاده کنید (بحث «نمایش اطلاعات از کنترلر‌های مختلف در یک صفحه» در مطلب فوق). مباحث Caching را هم برای لیست‌های عمومی فراموش نکنید.
مطالب دوره‌ها
لغو Lazy Loading در حین کار با AutoMapper و Entity Framework
پیشنیازها
- مطالعه‌ی مطالب گروه AutoMapper در سایت، دید خوبی را برای شروع به کار با آن فراهم می‌کنند و در اینجا قصد تکرار این مباحث پایه‌ای را نخواهیم داشت. هدف بیشتر بررسی یک سری نکات پیشرفته‌تر و عمیق‌تر است از کار با AutoMapper.
- آشنایی با Lazy loading و Eager loading در حین کار با EF


ساختار و پیشنیازهای برنامه‌ی مطلب جاری

جهت سهولت پیگیری مطلب و تمرکز بیشتر بر روی مفاهیم اصلی مورد بحث، یک برنامه‌ی کنسول را آغاز کرده و سپس بسته‌های نیوگت ذیل را به آن اضافه کنید:
PM> install-package AutoMapper
PM> install-package EntityFramework
به این ترتیب بسته‌های AutoMapper و EF به پروژه‌ی جاری اضافه خواهند شد.


آشنایی با ساختار مدل‌های برنامه

در اینجا ساختار جداول مطالب یک بلاگ را به همراه نویسندگان آن‌ها، مشاهده می‌کنید:
public class BlogPost
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
 
    [ForeignKey("UserId")]
    public virtual User User { get; set; }
    public int UserId { get; set; }
}

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
 
    public virtual ICollection<BlogPost> BlogPosts { get; set; }
}
هر کاربر می‌تواند تعدادی مطلب تهیه کند و هر مطلب توسط یک کاربر نوشته شده‌است.


هدف از این مثال

فرض کنید اطلاعاتی که قرار است به کاربر نمایش داده شوند، توسط ViewModel ذیل تهیه می‌شود:
public class UserViewModel
{
    public int Id { set; get; }
    public string Name { set; get; }
 
    public ICollection<BlogPost> BlogPosts { get; set; }
}
در اینجا می‌خواهیم اولین کاربر ثبت شده را یافته و سپس لیست مطالب آن‌را نمایش دهیم. همچنین می‌خواهیم این کوئری تهیه شده به صورت خودکار اطلاعاتش را بر اساس ساختار ViewModel ایی که مشخص کردیم (و این ViewModel الزاما تمام عناصر آن با عناصر مدل اصلی یکی نیست)، بازگشت دهیم.


تهیه نگاشت‌های AutoMapper

برای مدیریت بهتر نگاشت‌های AutoMapper توصیه شده‌است که کلاس‌های Profile ایی را به شکل ذیل تهیه کنیم:
public class TestProfile : Profile
{
    protected override void Configure()
    {
        this.CreateMap<User, UserViewModel>();
    }
 
    public override string ProfileName
    {
        get { return this.GetType().Name; }
    }
}
کار با ارث بری از کلاس پایه Profile کتابخانه‌ی AutoMapper شروع می‌شود. سپس باید متد Configure آن‌را بازنویسی کنیم. در اینجا می‌توان با استفاده از متدی مانند Create مشخص کنیم که قرار است اطلاعاتی با ساختار شیء User، به اطلاعاتی با ساختار از نوع شیء UserViewModel به صورت خودکار نگاشت شوند.


ثبت و معرفی پروفایل‌های AutoMapper

پس از تهیه‌ی پروفایل مورد نیاز، در ابتدای برنامه با استفاده از متد Mapper.Initialize، کار ثبت این تنظیمات صورت خواهد گرفت:
Mapper.Initialize(cfg => // In Application_Start()
{
    cfg.AddProfile<TestProfile>();
});


روش متداول کار با AutoMapper جهت نگاشت اطلاعات User به ViewModel آن

در ادامه به نحو متداولی، ابتدا اولین کاربر ثبت شده را یافته و سپس با استفاده از متد Mapper.Map اطلاعات این شیء user به ViewModel آن نگاشت می‌شود:
using (var context = new MyContext())
{
    var user1 = context.Users.FirstOrDefault();
    if (user1 != null)
    {
        var uiUser = new UserViewModel();
        Mapper.Map(source: user1, destination: uiUser);
 
        Console.WriteLine(uiUser.Name);
        foreach (var post in uiUser.BlogPosts)
        {
            Console.WriteLine(post.Title);
        }
    }
}
تا اینجا اگر برنامه را اجرا کنید، مشکلی را مشاهده نخواهید کرد، اما این کدها سبب اجرای حداقل دو کوئری خواهند شد:
الف) یافتن اولین کاربر
ب) واکشی لیست مطالب او در یک کوئری دیگر


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

در حالت متداول کار با EF، با استفاده از متد Include می‌توان این Lazy loading را لغو کرد و در همان اولین کوئری، مطالب کاربر یافت شده را نیز دریافت نمود:
 var user1 = context.Users.Include(user => user.BlogPosts).FirstOrDefault();
و سپس این اطلاعات را توسط AutoMapper نگاشت کرد.
در این حالت، AutoMapper برای ساده سازی این مراحل، متدهای Project To را معرفی کرده‌است:
 var uiUser = context.Users.Project().To<UserViewModel>().FirstOrDefault();
در اینجا نیز Lazy loading لغو شده و به صورت خودکار جوینی به جدول مطالب کاربران ایجاد خواهد شد.
بنابراین با استفاده از متد‌های Project To می‌توان از ذکر Includeهای EF صرفنظر کرد و همچنین دیگر نیازی به نوشتن متد Select جهت نگاشت دستی خواص مورد نظر به خواص ViewModel نیست.

کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید:
AM_Sample01.zip
مطالب
Asp.Net Identity #3
در مقاله‌ی  پیشین  نگاهی داشتیم به نحوه‌ی برپایی سیستم Identity. در این مقاله به نحوه‌ی استفاده از این سیستم به منظور طراحی یک سیستم مدیریت کاربران خواهیم پرداخت و انشالله در مقاله‌های بعدی این سیستم را تکمیل خواهیم نمود. کار را با اضافه کردن یک کنترلر جدید به پروژه آغاز می‌کنیم.
using System.Web;
using System.Web.Mvc;
using Microsoft.AspNet.Identity.Owin;
using Users.Infrastructure;

namespace Users.Controllers
{
    public class HomeController : Controller
    {
        private AppUserManager UserManager
        {
            get { return HttpContext.GetOwinContext().GetUserManager<AppUserManager>(); }
        }
        // GET: Home
        public ActionResult Index()
        {
            return View(UserManager.Users);

        }

}
در خط 10 یک پروپرتی از نوع AppUserManager (کلاسی که مدیریت کاربران را برعهده دارد) ایجاد می‌کنیم. اسمبلی Microsoft.Owin.Host.SystemWeb یک سری متدهای الحاقی را به کلاس HttpContext اضافه می‌کند که یکی از آنها متد GetOwinContext می‌باشد. این متد یک شیء Per-Request Context را از طریق رابط IOwinContext به OwinApi ارسال می‌کند؛ با استفاده از متد الحاقی <GetUserManager<T که T همان کلاس AppUserManager می‌باشد. حال که نمونه‌ای از کلاس AppUserManager را بدست آوردیم، می‌توانیم درخواستهایی را به جداول کاربران بدهیم. مثلا در خط 17 با استفاده از پروپرتی Users میتوانیم لیست کاربران موجود را بدست آورده و آن را به ویو پاس دهیم.
@using Users.Models
@model IEnumerable<AppUser>
@{
    ViewBag.Title = "Index";
}
<div class="panel panel-primary">
    <div class="panel-heading">
        User Accounts
    </div>
    <table class="table table-striped">
        <tr><th>ID</th><th>Name</th><th>Email</th></tr>
        @if (!Model.Any())
        {
            <tr><td colspan="3" class="text-center">No User Accounts</td></tr>
        }
        else
        {
            foreach (AppUser user in Model)
            {
                <tr>
                    <td>@user.Id</td>
                    <td>@user.UserName</td>
                    <td>@user.Email</td>
                </tr>
            }
        }
    </table>
</div>
@Html.ActionLink("Create", "CreateUser", null, new { @class = "btn btn-primary" })

نحوه‌ی ساخت یک کاربر جدید
ابتدا در پوشه Models یک کلاس ایجاد کنید : 
 namespace Users.Models
    {
        public class CreateModel
        {
            [Required]
            public string Name { get; set; }
            [Required]
            public string Email { get; set; }
            [Required]
            public string Password { get; set; }
        }
    }
فقط دوستان توجه داشته باشید که در پروژه‌های حرفه‌ای و تجاری هرگز اطلاعات مهم مربوط به مدل‌ها را در پوشه‌ی Models قرار ندهید. ما در اینجا صرف آموزش و برای جلوگیری از پیچیدگی مثال این کار را انجام میدهیم. برای اطلاعات بیشتر به این مقاله مراجعه کنید.
حال در کنترلر برنامه کدهای زیر را اضافه می‌کنیم:
 public ActionResult CreateUser()
        {
            return View();
        }

        [HttpPost]
        public async Task<ActionResult> CreateUser(CreateModel model)
        {
            if (!ModelState.IsValid)
                return View(model);

            var user = new AppUser { UserName = model.Name, Email = model.Email };
            var result = await UserManager.CreateAsync(user, model.Password);

            if (result.Succeeded)
            {
                return RedirectToAction("Index");
            }

            foreach (var error in result.Errors)
            {
                ModelState.AddModelError("", error);
            }
            return View(model);
        }
در اکشن CreateUser ابتدا یک شیء از کلاس AppUser ساخته و پروپرتی‌های مدل را به پروپرتی‌های کلاس AppUser انتساب می‌دهیم. در مرحله‌ی بعد یک شیء از کلاس IdentityResult به نام result ایجاد کرده و نتیجه‌ی متد CreateAsync را درون آن قرار می‌دهیم. متد CreateAsync از طریق پروپرتی از نوع AppUserManager قابل دسترسی است و دو پارامتر را دریافت می‌کند. پارامتر اول یک شیء از کلاس AppUser و پارامتر دوم یک رشته‌ی حاوی Password می‌باشد و خروجی متد یک شیء از کلاس IdentityResult است. در مرحله‌ی بعد چک می‌کنیم اگر Result، مقدار Succeeded را داشته باشد (یعنی نتیجه موفقیت آمیز بود) آن‌وقت ... در غیر اینصورت خطاهای موجود را به ModelState اضافه نموده و به View می‌فرستیم.
@model Users.ViewModels.CreateModel

@Html.ValidationSummary(false)

@using (Html.BeginForm())
{
    <div class="form-group">
        <label>Name</label>
        @Html.TextBoxFor(x => x.UserName, new { @class = "form-control" })
    </div>
    <div class="form-group">
        <label>Email</label>
        @Html.TextBoxFor(x => x.Email, new { @class = "form-control" })
    </div>

    <div class="form-group">
        <label>Password</label>
        @Html.PasswordFor(x => x.Password, new { @class = "form-control" })
    </div>
    <button type="submit" class="btn btn-primary">Create</button>
    @Html.ActionLink("Cancel", "Index", null, new { @class = "btn btn-default" })
}

اعتبار سنجی رمز
عمومی‌ترین و مهمترین نیازمندی برای هر برنامه‌ای، اجرای سیاست رمزگذاری می‌باشد؛ یعنی ایجاد یک سری محدودیتها برای ایجاد رمز است. مثلا رمز نمی‌تواند از 6 کاراکتر کمتر باشد و یا باید حاوی حروف بزرگ و کوچک باشد و ... . برای اجرای سیاست‌های رمزگذاری از کلاس PasswordValidator استفاده میشود. کلاس PasswordValidator برای اجرای سیاستهای رمزگذاری از پروپرتی‌های زیر استفاده می‌کند.

var manager = new AppUserManager(new UserStore<AppUser>(db))
            {
                PasswordValidator = new PasswordValidator
                {
                    RequiredLength = 6,
                    RequireNonLetterOrDigit = false,
                    RequireDigit = false,
                    RequireLowercase = true,
                    RequireUppercase = true
                }
            };

فقط دوستان توجه داشته باشید که کد بالا را در متد Create از کلاس AppUserManager استفاده کنید.


اعتبار سنجی نام کاربری

برای اعبارسنجی نام کاربری از کلاس UserValidator به صورت زیر استفاده می‌کنیم:

manager.UserValidator = new UserValidator<AppUser>(manager)
            {
                AllowOnlyAlphanumericUserNames = true,
                RequireUniqueEmail = true
            };

کد بالا را نیز در متد Create  از کلاس AppUserManager قرار می‌دهیم.

مطالب
شروع به کار با EF Core 1.0 - قسمت 7 - بررسی رابطه‌ی One-to-Many
در مطلب «شروع به کار با EF Core 1.0 - قسمت 4 - کار با بانک‌های اطلاعاتی از پیش موجود»، نحوه‌ی مهندسی معکوس ساختار جداول و ارتباطات یک بانک اطلاعاتی از پیش موجود را به روش Code First بررسی کردیم. با توجه به رسمی بودن این ابزار، می‌توان از آن برای یافتن معادل‌های سمت بانک اطلاعاتی، در EF Core نیز استفاده کرد. برای مثال بررسی کرد، درک EF Core از بانک اطلاعاتی طراحی شده چیست و هر چند در آن مطلب عنوان شد که می‌توان با پارامتر data-annotations-- ، خروجی نهایی را بر اساس روش data-annotations، بجای Fluent API به دست آورد، اما در مطلب «شروع به کار با EF Core 1.0 - قسمت 5 - استراتژهای تعیین کلید اصلی جداول و ایندکس‌ها» مشاهده کردیم که بسیاری از تنظیمات پیشرفته‌ی EF Core، اساسا معادل data-annotation ایی ندارند. بنابراین بهتر است این پارامتر را فعال سازی نکنید.


تنظیمات روابط یک به چند در EF Core

همان اسکریپت ابتدای مطلب «شروع به کار با EF Core 1.0 - قسمت 4 - کار با بانک‌های اطلاعاتی از پیش موجود» را درنظر بگیرید. رابطه‌ی تعریف شده‌ی در آن از نوع one-to-many است: یک بلاگ که می‌تواند چندین مطلب را داشته باشد.


اگر EF Core را وادار به تولید نگاشت‌های Code First معادل آن کنیم، به این خروجی‌ها خواهیم رسید:
الف) با استفاده از روش Fluent API
دستور استفاده شده برای مهندسی معکوس بانک اطلاعاتی نمونه:
 dotnet ef dbcontext scaffold "Data Source=(local);Initial Catalog=BloggingCore2016;Integrated Security = true" Microsoft.EntityFrameworkCore.SqlServer -o Entities --context MyDBDataContext --verbose
با خروجی:
using System;
using System.Collections.Generic;

namespace Core1RtmEmptyTest.Entities
{
    public partial class Blog
    {
        public Blog()
        {
            Post = new HashSet<Post>();
        }

        public int BlogId { get; set; }
        public string Url { get; set; }

        public virtual ICollection<Post> Post { get; set; }
    }
}

using System;
using System.Collections.Generic;

namespace Core1RtmEmptyTest.Entities
{
    public partial class Post
    {
        public int PostId { get; set; }
        public string Content { get; set; }
        public string Title { get; set; }

        public virtual Blog Blog { get; set; }
        public int BlogId { get; set; }
    }
}

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;

namespace Core1RtmEmptyTest.Entities
{
    public partial class MyDBDataContext : DbContext
    {
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"Data Source=(local);Initial Catalog=BloggingCore2016;Integrated Security = true");
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Blog>(entity =>
            {
                entity.Property(e => e.Url).IsRequired();
            });

            modelBuilder.Entity<Post>(entity =>
            {
                entity.HasOne(d => d.Blog)
                    .WithMany(p => p.Post)
                    .HasForeignKey(d => d.BlogId);
            });
        }

        public virtual DbSet<Blog> Blog { get; set; }
        public virtual DbSet<Post> Post { get; set; }
    }
}

نحوه‌ی تشخیص خودکار روابط

EF Core به صورت پیش فرض، روابط را بر اساس ارجاعات بین کلاس‌ها تشخیص می‌دهد. در اینجا به خاصیت Blog نام navigation property را می‌دهند:
 public virtual Blog Blog { get; set; }
و به خاصیت Post نیز Collection navigation property می‌گویند:
 public virtual ICollection<Post> Post { get; set; }
در اینجا اگر تنها دو navigation property، در کلاس‌های به هم مرتبط شده، یافت شوند، به صورت خودکار به عنوان دو سر رابطه تنظیم می‌شوند. اگر بیشتر از یک navigation property در کلاسی وجود داشت، هیچ رابطه‌ای به صورت خودکار تشکیل نشده و باید ابتدا و انتهای روابط را به صورت دستی مشخص نمود.


نحوه‌ی تشخیص خودکار کلیدهای خارجی
اگر در یک طرف رابطه‌ی تشخیص داده شده، خاصیتی با یکی از سه نام زیر وجود داشت:
<primary key property name>
<navigation property name><primary key property name>
<principal entity name><primary key property name>
آنگاه این خاصیت به صورت خودکار به عنوان کلید خارجی تنظیم می‌شود. در رابطه‌ی فوق Blog از نوع principal است (پدر رابطه) و Post از نوع dependent (فرزند رابطه).
برای مثال در رابطه‌ی فوق، نام خاصیت BlogId دقیقا بر اساس همان الگوی <primary key property name> طرف دیگر رابطه‌است:
  public virtual Blog Blog { get; set; }
  public int BlogId { get; set; }
بنابراین به صورت خودکار به عنوان کلید خارجی درنظر گرفته می‌شود.

تا اینجا اگر مطلب را دنبال کرده باشید به این نتیجه خواهید رسید که دو کلاس فوق، اساسا نیازی به هیچ نوع تنظیم Fluent و یا Data annotations ایی برای برقراری ارتباط یک به چند ندارند. چون روابط بین آن‌ها بر اساس خواص راهبری (navigation property) و همچنین الگوی <primary key property name>، به صورت خودکار قابل تشخیص و تنظیم است. به علاوه ... در هر طرف رابطه، فقط یک navigation property وجود دارد و نیازی به تنظیم دستی سر دیگر رابطه نیست.


استفاده از Fluent API برای تنظیم رابطه‌ی One-to-Many

در تنظیمات فوق، در متد OnModelCreating، ذکر صریح این روابط را صرفا جهت از بین بردن هرگونه ابهامی مشاهده می‌کنید:
modelBuilder.Entity<Post>(entity =>
{
    entity.HasOne(d => d.Blog)
             .WithMany(p => p.Post)
             .HasForeignKey(d => d.BlogId);
});
از هر طرفی که شروع می‌کنید، متدهای HasOne و یا HasMany، مشخص کننده‌ی navigation property هستند که در سمت موجودیت معرفی شده قرار دارند. در اینجا چون کار با موجودیت Post شروع شده‌است، متد HasOne به خاصیت راهبری در همان سمت و به خاصیت Blog آن اشاره می‌کند.
مرحله‌ی بعد، مشخص کردن سر دیگر رابطه (inverse navigation) است. این‌کار توسط یکی از متدهای WithOne و یا WithMany انجام می‌شود.
متدهایی که اسامی فرد دارند مانند HasOne/WithOne به یک navigation property ساده اشاره می‌کنند.
متدهایی که اسامی جمع دارند مانند HasMany/WithMany به collection navigation properties اشاره خواهند کرد.
متد HasForeignKey نیز برای ذکر صریح کلید خارجی بکار رفته‌است.


ب) با استفاده از روش data-annotations
دستور استفاده شده برای مهندسی معکوس بانک اطلاعاتی نمونه:
 dotnet ef dbcontext scaffold "Data Source=(local);Initial Catalog=BloggingCore2016;Integrated Security = true" Microsoft.EntityFrameworkCore.SqlServer -o Entities --context MyDBDataContext --verbose -a
با خروجی:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace Core1RtmEmptyTest.Entities
{
    public partial class Blog
    {
        public Blog()
        {
            Post = new HashSet<Post>();
        }

        public int BlogId { get; set; }

        [Required]
        public string Url { get; set; }

        [InverseProperty("Blog")]
        public virtual ICollection<Post> Post { get; set; }
    }
}

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace Core1RtmEmptyTest.Entities
{
    public partial class Post
    {
        public int PostId { get; set; }
        public string Content { get; set; }
        public string Title { get; set; }

        [ForeignKey("BlogId")]
        [InverseProperty("Post")]
        public virtual Blog Blog { get; set; }
        public int BlogId { get; set; }
    }
}

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;

namespace Core1RtmEmptyTest.Entities
{
    public partial class MyDBDataContext : DbContext
    {
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"Data Source=(local);Initial Catalog=BloggingCore2016;Integrated Security = true");
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
        }

        public virtual DbSet<Blog> Blog { get; set; }
        public virtual DbSet<Post> Post { get; set; }
    }
}
همانطور که در توضیحات روش Fluent API عنوان شد، این مدل خاص، چون دقیقا بر اساس پیش فرض‌های EF Core طراحی شده‌است، نیازی به هیچگونه تنظیم اضافه‌تری ندارد. اما اگر کلید خارجی، مطابق سه الگویی که عنوان شد، قابل تشخیص نباشد، باید آن‌را در روش data annotations توسط ویژگی ForeignKey، به نحو صریحی مشخص کرد:
  [ForeignKey("BlogId")]
  [InverseProperty("Post")]
  public virtual Blog Blog { get; set; }
  public int BlogId { get; set; }
همچنین اگر بیش از یک خاصیت راهبری (navigation property) وجود داشت، ذکر InverseProperty نیز ضروری است تا مشخص شود سر دیگر این رابطه دقیقا کدام است.
در این حالت (داشتن بیش از یک خاصیت راهبری)، باید ویژگی InverseProperty را نیز به سر دوم رابطه، اعمال کرد.
   [InverseProperty("Blog")]
  public virtual ICollection<Post> Post { get; set; }

مطالب تکمیلی

علت virtual بودن خواص راهبری تولید شده

اگر دقت کنید، EF Core کدی را که تولید کرده‌است، به همراه خاصیت‌هایی virtual است:
public virtual Blog Blog { get; set; }
در اینجا تمام خاصیت‌های راهبری virtual تعریف شده‌اند. علت آن، به پیاده سازی مباحث AOP بر می‌گردد. زمانیکه خاصیتی به صورت virtual تعریف می‌شود، EF core می‌تواند آن‌را توسط یک شیء پروکسی شفاف احاطه کند. این پروکسی‌ها دو هدف را دنبال می‌کند:
الف) پیاده سازی lazy loading (بارگذاری خودکار اعضای مرتبط (همان خواص راهبری) با اولین دسترسی به آن‌ها)
ب) پیاده سازی change tracking

مبحث lazy loading فعلا در EF Core 1.0 پشتیبانی نمی‌شود. اما change tracking آن فعال است.
بنابراین اگر مشاهده کردید خواص راهبری به صورت virtual تعریف شده‌اند، علت آن فعال سازی lazy loading است و اگر سایر خواص به صورت virtual تعریف شده‌اند، هدف اصلی آن بهبود عملکرد سیستم change tracking است.
همچنین اگر دقت کرده باشید، نوع مجموعه‌ها نیز ICollection ذکر شده‌است. این مورد نیز یکی دیگر از پیش فرض‌های توکار EF Core است؛ در جهت تشکیل پروکسی‌ها بر روی خواص راهبری مجموعه‌ای (علاوه بر virtual تعریف کردن آن‌ها). عنوان شده‌است که اگر برای مثال از List استفاده کنید (پیاده سازی اینترفیس) یا هر اینترفیس دیگری که از ICollection  مشتق شده‌است، این پروکسی‌ها تشکیل نخواهند شد.


واکشی اعضای به هم مرتبط

همانطور که عنوان شد، نگارش اول EF Core برخلاف EF 6.x از Lazy loading پشتیبانی نمی‌کند. البته این مساله در کل مورد مثبتی است؛ خصوصا در برنامه‌های وب! چون استفاده‌ی نادرست از Lazy loading که به select n+1 نیز مشهور است، سبب رفت و برگشت‌های بی‌شماری به بانک اطلاعاتی می‌شود و عموم برنامه نویس‌های وب باید مدام توسط برنامه‌های Profiler بررسی کنند که آیا این مساله رخ داده‌است یا خیر. فعلا EF Core از این مشکل در امان است!
اما ... اگر به روش کار EF 6.x عادت کرده باشید، قطعه کد ذیل:
 var firstPost = context.Post.First();
Console.WriteLine(firstPost.Blog.Url);
چنین خطایی را صادر می‌کند:
 System.NullReferenceException
Object reference not set to an instance of an object.
علت اینجا است که چون Lazy loading غیرفعال است (هنوز در EF Core 1.0 پیاده سازی نشده‌است)، اولین دسترسی به شیء Blog، سبب وهله سازی خودکار آن نشده و این شیء نال است. به همین جهت استثنای فوق را مشاهده می‌کنیم.
برای رفع این مشکل باید توسط متد Include، سبب لغو عملیات Lazy loading و واکشی صریح Blog مرتبط شویم که اصطلاحا به آن eager loading می‌گویند:
 var firstPost = context.Post.Include(x => x.Blog).First();
Console.WriteLine(firstPost.Blog.Url);

نکته‌ای در مورد سطوح بارگذاری اعضای به هم مرتبط در EF Core

متد Include ایی را که تا اینجا مشاهده کردید، با EF 6.x تفاوتی ندارد. برای مثال اگر شیء Blog حاوی خواص راهبری Posts و همچنین Owner باشد، برای بارگذاری این اعضای مرتبط، می‌توان همانند قبل، متدهای Include را پشت سر هم ذکر کرد:
var blogs = context.Blogs
                              .Include(blog => blog.Posts)
                              .Include(blog => blog.Owner)
                              .ToList();
اما فرض کنید خاصیت Post، دارای یک خاصیت راهبری دیگری به نام Author نیز باشد و می‌خواهیم این خاصیت هم بارگذاری شود:
var blogs = context.Blogs
                              .Include(blog => blog.Posts)
                                      .ThenInclude(post => post.Author)
                              .ToList();
روش انجام چنین کاری در EF Core، توسط متد الحاقی جدید ThenInclude است. ابتدا لیست Blogها عنوان شده‌است. سپس در این لیست علاقمند به واکشی تمام مطالب این بلاگ‌ها هم بوده‌ایم. به علاوه در این مطالب، نیاز است خاصیت Author آن‌ها نیز از پیش مقدار دهی شده و قابل دسترسی باشد. به همین جهت برای دسترسی به چندین سطح مختلف از متد ThenInclude کمک گرفته شده‌است.
همچنین در اینجا امکان ذکر زنجیروار متدهای ThenInclude هم هست:
var blogs = context.Blogs
                              .Include(blog => blog.Posts)
                                 .ThenInclude(post => post.Author)
                                        .ThenInclude(author => author.Photo)
                              .ToList();
در این مثال یک سطح دیگر جلو رفته و شیء Photo مربوط به شیء Author را هم واکشی کرده‌ایم.
به علاوه امکان ذکر چندین ریشه و چندین زیر ریشه هم وجود دارند:
var blogs = context.Blogs
                              .Include(blog => blog.Posts)
                                  .ThenInclude(post => post.Author)
                                      .ThenInclude(author => author.Photo)
                              .Include(blog => blog.Owner)
                                    .ThenInclude(owner => owner.Photo)
                              .ToList();

یک نکته: متد Include تنها زمانی درنظر گرفته خواهد شد که نوع خروجی نهایی کوئری، دقیقا از نوع موجودیتی باشد که با آن شروع به کار کرده‌ایم. برای مثال اگر در این بین یک Select اضافه شود و فقط تنها تعدادی از خواص Blog واکشی شوند، از تمام Includeهای ذکر شده صرفنظر می‌شود؛ مانند کوئری ذیل:
var blogs = context.Blogs
                              .Include(blog => blog.Posts)
                              .Select(blog => new
                               {
                                  Id = blog.BlogId,
                                  Url = blog.Url
                               })
                               .ToList();


تنظیمات حذف آبشاری در رابطه‌ی one-to-many

زمانیکه در رابطه‌ی one-to-many قسمت principal (والد رابطه) و یا همان Blog در مثال جاری حذف می‌شود، سه اتفاق برای فرزندان آن میسر خواهند بود:
الف) Cascade : در این حالت ردیف‌های فرزندان وابسته نیز حذف خواهند شد.
باید دقت داشت که حالت Cascade فقط برای موجودیت‌هایی اعمال می‌شود که توسط Context بارگذاری شده و در آن وجود دارند. اگر می‌خواهید سایر موجودیت‌های مرتبط نیز با این روش حذف شوند، باید در سمت دیتابیس نیز تنظیماتی مانند ON DELETE CASCADE زیر نیز وجود داشته باشند:
 CONSTRAINT [FK_Post_Blog_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blog] ([BlogId]) ON DELETE CASCADE
و اگر با EF Core بانک اطلاعاتی خود را ایجاد می‌کنید (مباحث مهاجرت‌ها)، این تنظیم به صورت خودکار اعمال خواهد شد؛ اگر DeleteBehavior را به نحو ذیل مشخص کرده باشید:
modelBuilder.Entity<Post>()
                    .HasOne(p => p.Blog)
                    .WithMany(b => b.Posts)
                    .OnDelete(DeleteBehavior.Cascade);
ب) SetNull: در این حالت فرزندان وابسته حذف نمی‌شوند و تنها کلید خارجی آن‌ها به نال تنظیم می‌شود.
ج) Restrict: هیچ تغییری بر روی فرزندان رابطه رخ نمی‌دهد.

یک نکته: به صورت پیش فرض اگر رابطه‌ی one-to-many، به Required تنظیم شود، حالت حذف آن cascade خواهد بود. در غیراینصورت برای حالت‌های Optional، حالت SetNull تنظیم می‌گردد:
modelBuilder.Entity<Post>()
                    .HasOne(p => p.Blog)
                    .WithMany(b => b.Posts)
                    .IsRequired();
در اینجا ذکر صریح متد IsRequired به این معنا است که مقدار دهی کلید خارجی سر دیگر رابطه، اجباری است.
به علاوه باید دقت داشت، همان مباحث «تعیین اجباری بودن یا نبودن ستون‌ها در EF Core» در قسمت قبل، در اینجا هم صادق است. برای مثال چون BlogId (کلید خارجی در کلاس Post) از نوع int است و نال پذیر نیست، بنابراین از دیدگاه EF Core یک فیلد اجباری درنظر گرفته می‌شود. به همین جهت است که در کدهای تولید شده‌ی توسط EF Core در ابتدای بحث، ذکر متد IsRequired و یا OnDelete را مشاهده نمی‌کنید.
بنابراین اگر می‌خواهید حالت SetNull را فعال کنید، باید این کلید خارجی را نیز نال پذیر و به صورت int? BlogId ذکر کنید تا optional درنظر گرفته شود.
مطالب
Minimal API's در دات نت 6 - قسمت دوم - ایجاد مدل‌ها و Context برنامه
در قسمت قبل، ساختار ابتدایی یک پروژه‌ی Minimal API's مبتنی بر معماری Vertical slices را تشکیل دادیم. در ادامه موجودیت‌ها و DbContext آن‌را تشکیل می‌دهیم.


ایجاد مدل‌ها و موجودیت‌های برنامه‌ی Minimal Blog

در مثال این سری، هر نویسنده می‌تواند بلاگ خاص خودش را داشته باشد و در هر بلاگ، تعدادی مقاله منتشر کند. هر مقاله هم می‌تواند تعدادی تگ یا گروه مرتبط را داشته باشد.

ساختار ابتدایی موجودیت نویسندگان مطالب بلاگ

این موجودیت‌ها در پوشه‌ی جدیدی به نام Model پروژه‌ی MinimalBlog.Domain اضافه خواهند شد:
namespace MinimalBlog.Domain.Model;

public class Author
{
    public int Id { get; set; }
    public string FirstName { get; set; } = default!;
    public string LastName { get; set; } = default!;

    public string FullName => FirstName + " " + LastName;
    public DateTime DateOfBirth { get; set; }
    public string? Bio { get; set; }
}
در اینجا تعاریفی مانند !default را هم مشاهده می‌کنید که مرتبط است با فعال بودن nullable reference types در این پروژه که در فایل Directory.Build.props به صورت سراسری به تمام پروژه‌ها اعمال می‌شود. با تعریف !default به کامپایلر اعلام می‌کنیم که این خواص هیچگاه نال نخواهند بود. هدف اصلی از nullable reference types، بالا بردن قابلیت پیش بینی نال بودن، یا نبودن آن‌ها است؛ تا برنامه نویس در قسمت‌های مختلف برنامه بداند که آیا واقعال نیاز است هنگام کار با خاصیت FirstName، نال بودن آن‌را پیش از استفاده بررسی کند یا خیر؟

ساختار ابتدایی موجودیت مقالات بلاگ
namespace MinimalBlog.Domain.Model;

public class Article
{
    public Article()
    {
        Categories = new List<Category>();
    }

    public int Id { get; set; }
    public string Title { get; set; } = default!;
    public string? Subtitle { get; set; }
    public string Body { get; set; } = default!;

    public int AuthorId { get; set; }
    public Author Author { get; set; } = default!;
    public DateTime DateCreated { get; set; }
    public DateTime LastUpdated { get; set; }
    public int NumberOfLikes { get; set; }
    public int NumberOfShares { get; set; }
    public ICollection<Category> Categories { get; set; }
}

ساختار ابتدایی بلاگ‌های اختصاصی قابل تعریف
namespace MinimalBlog.Domain.Model;

public class Blog
{
    public Blog()
    {
        Contributors = new List<Author>();
    }

    public int Id { get; set; }
    public string Name { get; set; } = default!;
    public string Description { get; set; } = default!;
    public DateTime CreatedDate { get; set; }

    public int AuthorId { get; set; }
    public Author Owner { get; set; } = default!;

    public ICollection<Author> Contributors { get; }
}

ساختار ابتدایی گروه‌های مقالات بلاگ
namespace MinimalBlog.Domain.Model;

public class Category
{
    public Category()
    {
        Articles = new List<Article>();
    }

    public int Id { get; set; }
    public string Name { get; set; } = default!;
    public string Description { get; set; } = default!;
    public ICollection<Article> Articles { get; set; }
}


معرفی موجودیت‌های تعریف شده به لایه‌ی Dal

لایه‌ی Dal جایی است که DbContext برنامه تعریف می‌شود و موجودیت‌ها فوق توسط DbSetها در معرض دید EF-Core قرار می‌گیرند. به همین جهت ابتدا فایل MinimalBlog.Dal.csproj را به صورت زیر تغییر می‌دهیم:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="..\MinimalBlog.Domain\MinimalBlog.Domain.csproj" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.2" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.2" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.2">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>
</Project>
در اینجا در ابتدا ارجاعی به پروژه‌ی MinimalBlog.Domain.csproj اضافه شده‌است. سپس بسته‌های مورد نیاز از EF-Core نیز جهت تعریف DbSetها و اجرای Migrations، اضافه شده‌اند.
به علاوه تعاریفی مانند ImplicitUsings و Nullable را هم مشاهده می‌کنید که با توجه به استفاده‌ی از فایل Directory.Build.props غیرضروری و تکراری هستند؛ چون این فایل مخصوص به همراه این تعاریف سراسری نیز هست.

سپس به پروژه‌ی Dal، کلاس جدید MinimalBlogDbContext را به صورت زیر اضافه می‌کنیم تا مدل‌های برنامه را در معرض دید EF-Core قرار دهد:
using Microsoft.EntityFrameworkCore;
using MinimalBlog.Domain.Model;

namespace MinimalBlog.Dal;

public class MinimalBlogDbContext : DbContext
{
    public MinimalBlogDbContext(DbContextOptions options) : base(options)
    {
    }

    public DbSet<Article> Articles { get; set; } = default!;
    public DbSet<Category> Categories { get; set; } = default!;
    public DbSet<Author> Authors { get; set; } = default!;
    public DbSet<Blog> Blogs { get; set; } = default!;
}
در اینجا نیز تعاریف !default را مشاهده می‌کنید که خاصیت راهنمای کامپایلر را در حالت فعال بودن <Nullable>enable</Nullable> دارند و هدف از آن، اعلام نال نبودن این خواص، در حین استفاده‌ی از آن‌ها در قسمت‌های مختلف برنامه است.


ایجاد و اجرای Migrations

پس از تعریف MinimalBlogDbContext، اکنون نوبت به ایجاد کلاس‌های Migrations و اجرای آن‌ها جهت ایجاد بانک اطلاعاتی متناظر با مدل‌های برنامه است. برای این منظور در ابتدا به پروژه‌ی Api مراجعه کرده و فایل appsettings.json را به صورت زیر تکمیل می‌کنیم:
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "Default": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=MinimalBlog;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False"
  }
}
در اینجا یک رشته‌ی اتصالی جدید را از نوع SQL Server LocalDB، تعریف کرده‌ایم. سپس نیاز است تا این رشته‌ی اتصالی را به پروایدر SQL Server مخصوص EF-Core اضافه کنیم. برای اینکار ابتدا باید تغییرات زیر را به فایل MinimalBlog.Api.csproj اعمال کنیم:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.2" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.2" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.2">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="..\MinimalBlog.Domain\MinimalBlog.Domain.csproj" />
    <ProjectReference Include="..\MinimalBlog.Dal\MinimalBlog.Dal.csproj" />
  </ItemGroup>
</Project>
که در آن ارجاعاتی به پروژه‌های Domain و Dal اضافه شده‌است و همچنین بسته‌های نیوگت EF-Core نیز افزوده شده‌اند تا بتوان در فایل Program.cs، تنظیم زیر را اضافه کرد:
using Microsoft.EntityFrameworkCore;
using MinimalBlog.Dal;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var connectionString = builder.Configuration.GetConnectionString("Default");
builder.Services.AddDbContext<MinimalBlogDbContext>(opt => opt.UseSqlServer(connectionString));
در اینجا با استفاده از متد الحاقی AddDbContext، کلاس Context برنامه به مجموعه‌ی سرویس‌های آن اضافه شده و همچنین رشته‌ی اتصالی با کلید Default هم از تنظیمات برنامه، دریافت و به متد UseSqlServer جهت استفاده ارسال شده‌است.

پس از این تنظیمات به پوشه‌ی MinimalBlog.Dal وارد شده و فایل _01-add_migrations.cmd را اجرا می‌کنیم (این فایل به همراه پیوست قسمت اول، ارائه شده‌است). محتوای این فایل به صورت زیر است:
For /f "tokens=2-4 delims=/ " %%a in ('date /t') do (set mydate=%%c_%%a_%%b)
For /f "tokens=1-2 delims=/:" %%a in ("%TIME: =0%") do (set mytime=%%a%%b)
dotnet tool update --global dotnet-ef --version 6.0.2
dotnet build
dotnet ef migrations --startup-project ../MinimalBlog.Api/ add V%mydate%_%mytime% --context MinimalBlogDbContext
pause
ابتدا، آخرین نگارش ابزار dotnet-ef را نصب کرده و سپس یکبار هم پروژه را build می‌کند تا اگر مشکلی باشد، در همینجا با اطلاعات بیشتری نمایش داده شود. سپس با اجرای دستور dotnet ef migrations، کلاس‌های Migrations در پوشه‌ای به همین نام تشکیل می‌شوند.
البته چون کدهای تولید شده‌ی به صورت خودکار الزاما با Roslyn analyzers برنامه سازگاری ندارند، آن‌ها را توسط فایل مخصوص editorconfig. قرار گرفته‌ی در پوشه‌ی MinimalBlog.Dal\Migrations، از لیست آنالیزهای برنامه خارج می‌کنیم. محتوای این فایل ویژه به صورت زیر است:
[*.cs]
generated_code = true
که سبب خواهد شد تا این پوشه‌ی ویژه به عنوان کدهای به صورت خودکار تولید شده تشخیص داده شود و دیگر آنالیز نشود؛ چون کیفیت آن، مشکل ما نیست!

پس از ایجاد کلاس‌های Migration، اکنون نوبت به اجرای آن‌ها بر روی بانک اطلاعاتی است که در رشته‌ی اتصالی مشخص کردیم. برای اینکار فایل MinimalBlog.Dal\_02-update_db.cmd را اجرا می‌کنیم که چنین محتویاتی دارد:
dotnet tool update --global dotnet-ef --version 6.0.2
dotnet build
dotnet ef --startup-project ../MinimalBlog.Api/ database update --context MinimalBlogDbContext
pause
در اینجا نیز ابتدا از نصب آخرین نگارش ابزارهای EF اطمینان حاصل می‌شود. یکبار دیگر نیز پروژه بر اساس فایل‌های جدیدی که به آن اضافه شده، کامپایل شده و سپس کلاس‌های مهاجرت‌ها، اجرا و اعمال می‌شوند.
مطالب
ایجاد یک DbContext مشترک بین entityهای پروژه‌های متفاوت
فر ض کنید پروژه بزرگی دارید که هر قسمت را به یک برنامه نویس می‌سپارید تا آن قسمت را در پروژه مجزایی طراحی و برنامه نویسی کند. هر برنامه نویس Entity‌های خاص خود را در لایه‌های مربوط به پروژه خود تعریف می‌کند و از آنها استفاده می‌کند. حال یکی از برنامه نویس‌ها می‌خواهد از Entity های پروژه دیگر استفاده کند. در این صورت اگر از دو Context شیء‌ایی را بسازد و آنها را با یکدیگر Join  بزند، خطایی مربوط به تعلق داشتن دو  Entity به دو Context متفاوت را می‌گیرد.

در پروژه‌های کوچک، کل تیم بر روی ماژول‌های مختلف یک پروژه کار می‌کنند و یک DbContext مشترک دارند. اما راه حل این مشکل در پروژه‌های بزرگ چیست؟ 
یکی از راه‌های پیشنهادی، استفاده از یک کلاس DbContextBase است که همه پروژه‌ها بایستی Context خود را از این کلاس به ارث ببرند که در این صورت باز هم مشکل ساخت چند DbContext وجود خواهد داشت که فقط می‌توان از Entity‌های موجود در DbContextBase و DbContext پروژه جاری استفاده کرد. اما در شرکت‌های بزرگ که پروژه‌هایی مانندERP دارند، روش دیگری استفاده می‌شود که در ادامه خواهیم دید.
روش مورد استفاده به این صورت است که در زمان اجرا یک DbContext برای همه Entity‌های پروژه‌های مختلف ساخته می‌شود. اجازه بدهید همراه با مثال، این پروژه را پیش برویم. فرض کنید دو تیم برنامه نویسی داریم که هر کدام بر روی پروژه‌های مجزای SampleProject1 و SampleProject2 کار میکنند که Entity‌های هر کدام در لایه‌های Common قرار گرفته‌اند.

در SampleProject1 مدل Product را داریم:

public partial class Product : Entity
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public Nullable<byte> ProductTypeId { get; set; }
    }
و در SampleProject2، مدل ProductType را داریم که هر دو Entity از کلاس Entity ارث بری می‌کند: 
 public partial class ProductType : Entity
    {
        public byte Id { get; set; }
        public string Name { get; set; }
    }
همه پروژه‌ها را در پروژه‌ی SampleProject1.Console، به عنوان رفرنس اضافه می‌کنیم؛ بجز SampleProject2.Console و Output path همه پروژه‌ها را به یک پوشه مشترک هدایت می‌کنیم. در ادامه برای بدست آوردن Entity‌ها از کد زیر استفاده می‌کنیم:
            List<Assembly> allAssemblies = new List<Assembly>();
            string path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);

            foreach (string dll in Directory.GetFiles(path, "*.Common.dll"))
                allAssemblies.Add(Assembly.LoadFile(dll));

            var type = typeof(Entity);
      
            List<Type> types = allAssemblies
             .SelectMany(s => s.GetTypes())
             .Where(p => type.IsAssignableFrom(p)).ToList();

            List<string> entities = new List<string>();
            foreach (var item in types)
            {
                entities.Add(item.Name);
            }

            types.Add(typeof(Entity));
و سپس برای Generate کردن کلاس DbContext از کلاس زیر استفاده می‌کنیم:
public class ContextGenerator
    {
        public void Generate(List<string> entities, params Type[] types)
        {
            StringBuilder code = new StringBuilder();

            code.AppendLine(@"
           using System.Data.Entity;
           using System.Data.Entity.Core.EntityClient;
           using SampleProject1.Common.Models;
           using SampleProject1.Common.Models.Mapping;
           using SampleProject2.Common.Models;
           using SampleProject2.Common.Models.Mapping;

           namespace DbContextGenerator
           {
                public partial class TestContext : DbContext
                {
                    static TestContext()
                    {
                        Database.SetInitializer<TestContext>(null);
                    }

                    public TestContext()
                        : base(""Data Source=.;Initial Catalog=Test;Integrated Security=True;MultipleActiveResultSets=True"")
                    {
                        }
                ");

            var pluralizeHelper = new PluralizeHelper();

            foreach (var entity in entities)
            {
                code.AppendLine($@"public DbSet<{entity}> {pluralizeHelper.Pluralize(entity)} {{ get; set; }}");
            }

            code.AppendLine(@"protected override void OnModelCreating(DbModelBuilder modelBuilder)");
            code.AppendLine(@"{");

            foreach (var entity in entities)
            {
                code.AppendLine($@"modelBuilder.Configurations.Add(new {entity}Map());");
            }
            code.AppendLine(@"}");
            code.AppendLine(@"}");
            code.AppendLine(@"}");
           
            CSharpCodeProvider provider = new CSharpCodeProvider();
            CompilerParameters parameters = new CompilerParameters();

            parameters.ReferencedAssemblies.Add("System.Drawing.dll");
            parameters.ReferencedAssemblies.Add("System.Data.dll");
            parameters.ReferencedAssemblies.Add("System.Data.Entity.dll");
            parameters.ReferencedAssemblies.Add("System.ComponentModel.dll");

            foreach (var type in types)
            {
                parameters.ReferencedAssemblies.Add(type.Assembly.Location);
            }

            parameters.ReferencedAssemblies.Add(typeof(DbSet).Assembly.Location);
            parameters.ReferencedAssemblies.Add(typeof(DbContext).Assembly.Location);
            parameters.ReferencedAssemblies.Add(typeof(IQueryable).Assembly.Location);
            parameters.ReferencedAssemblies.Add(typeof(IQueryable<>).Assembly.Location);
            parameters.ReferencedAssemblies.Add(typeof(System.ComponentModel.IListSource).Assembly.Location);

            parameters.GenerateExecutable = false;
            parameters.GenerateInMemory = false;
            parameters.OutputAssembly = "ProjectContext.dll";

            CompilerResults results = provider.CompileAssemblyFromSource(parameters, code.ToString());

            if (results.Errors.HasErrors)
            {
                StringBuilder sb = new StringBuilder();

                foreach (CompilerError error in results.Errors)
                {
                    sb.AppendLine(String.Format("Error ({0}): {1}", error.ErrorNumber, error.ErrorText));
                }

                throw new InvalidOperationException(sb.ToString());
            }
        }

    }
و نحوه فراخوانی آن:
 new ContextGenerator().Generate(entities, types.ToArray()); // generate dbContext
همانطور که مشاهده می‌کنید، برای تولید کد، از کلاس CSharpCodeProvider استفاده میکنیم که نتیجه اجرای کد بالا، ساخت DLLی به نام ProjectContext.dll است. با مشاهده DLL ساخته شده توسط نرم افزار ILSpy، کد جنریت شده به صورت زیر خواهد بود: 

حال برای استفاده از Context تولید شده، به صورت زیر شیءایی را ساخته:

 static DbContext _dbContext=null;
        public static DbContext GetDbContextInstance()
        {
            if (_dbContext == null)
            {
                string path = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
                var dllversionAssm = Assembly.LoadFile(path + "\\ProjectContext.dll");
                Type type = dllversionAssm.GetType("DbContextGenerator.TestContext");
                _dbContext = (DbContext)Activator.CreateInstance(type);
            }
            return _dbContext;
        }

و سپس برای ساخت DbSet از هر Entity به کد زیر نیاز خواهیم داشت:

public static System.Data.Entity.DbSet<T> Get<T>() where T : class
        {
            var set = GetDbContextInstance().Set<T>();
            return set;
        }

هم اکنون می‌توان رکوردهای Entity‌ها را واکشی کرده و یا آن‌ها را با یکدیگر Join بزنیم:

            var products = Get<Product>().ToList();

            var productTypes = Get<ProductType>().ToList();


            var query = from p in Get<Product>()
                        join pt in Get<ProductType>() on p.ProductTypeId equals pt.Id
                        select new
                        {
                            Id = p.Id,
                            Name = p.Name,
                            ProductType = pt.Name

                        };

            var JoinResult = query.ToList();

و نتیجه واکشی ها 


کد کامل این پروژه  

مطالب
نحوه کاهش مصرف حافظه EF Code first حین گزارشگیری از اطلاعات
تمام ORMهای خوب، دارای سطح اول کش هستند. از این سطح جهت نگهداری اطلاعات تغییرات صورت گرفته روی اشیاء و سپس اعمال نهایی آن‌ها در پایان یک تراکنش استفاده می‌شود. بدیهی است جمع آوری این اطلاعات اندکی بر روی سرعت انجام کار و همچنین بر روی میزان مصرف حافظه برنامه تاثیرگذار است. به علاوه یک سری از اعمال مانند گزارشگیری نیازی به این سطح اول کش ندارند. اطلاعات مورد استفاده در آن‌ها مانند نمایش لیستی از اطلاعات در یک گرید، حالت فقط خواندنی دارد. در EF Code first برای یک چنین مواردی استفاده از متد الحاقی AsNoTracking تدارک دیده شده است که سبب خاموش شدن سطح اول کش می‌شود. در ادامه در طی یک مثال، اثر این متد را بر روی سرعت و میزان مصرف حافظه برنامه بررسی خواهیم کرد.

کدهای کامل این مثال را در ذیل ملاحظه می‌کنید:

using System;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Diagnostics;
using System.Linq;

namespace EFGeneral
{
    public class User
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

    public class MyContext : DbContext
    {
        public DbSet<User> Users { get; set; }
    }

    public class Configuration : DbMigrationsConfiguration<MyContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = true;
            AutomaticMigrationDataLossAllowed = true;
        }

        protected override void Seed(MyContext context)
        {            
            for (int i = 0; i < 21000; i++)
            {
                context.Users.Add(new User { Name = "name " + i });
                if (i % 1000 == 0)
                    context.SaveChanges();
            }
            base.Seed(context);
        }
    }

    public class PerformanceHelper
    {
        public static string RunActionMeasurePerformance(Action action)
        {
            GC.Collect();
            long initMemUsage = Process.GetCurrentProcess().WorkingSet64;

            var stopwatch = new Stopwatch();
            stopwatch.Start();

            action();

            stopwatch.Stop();

            var currentMemUsage = Process.GetCurrentProcess().WorkingSet64;
            var memUsage = currentMemUsage - initMemUsage;
            if (memUsage < 0) memUsage = 0;

            return string.Format("Elapsed time: {0}, Memory Usage: {1:N2} KB", stopwatch.Elapsed, memUsage / 1024);
        }
    }

    public static class Test
    {
        public static void RunTests()
        {
            Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, Configuration>());
            StartDb();            

            for (int i = 0; i < 3; i++)
            {
                Console.WriteLine("\nRun {0}", i + 1);

                var memUsage = PerformanceHelper.RunActionMeasurePerformance(() => LoadWithTracking());
                Console.WriteLine("LoadWithTracking:\n{0}", memUsage);

                memUsage = PerformanceHelper.RunActionMeasurePerformance(() => LoadWithoutTracking());
                Console.WriteLine("LoadWithoutTracking:\n{0}", memUsage);
            }
        }

        private static void StartDb()
        {
            using (var ctx = new MyContext())
            {
                var user = ctx.Users.Find(1);
                if (user != null)
                {
                    // keep the object in memory
                }
            }
        }

        private static void LoadWithTracking()
        {
            using (var ctx = new MyContext())
            {
                var list = ctx.Users.ToList();
                if (list.Any())
                {
                    // keep the list in memory
                }
            }
        }

        private static void LoadWithoutTracking()
        {
            using (var ctx = new MyContext())
            {
                var list = ctx.Users.AsNoTracking().ToList();
                if (list.Any())
                {
                    // keep the list in memory
                }
            }
        }
    }
}

توضیحات:
مدل برنامه یک کلاس ساده کاربر است به همراه id و نام او.
سپس این کلاس توسط Context برنامه در معرض دید EF Code first قرار می‌گیرد.
در کلاس Configuration تعدادی رکورد را در ابتدای کار برنامه در بانک اطلاعاتی ثبت خواهیم کرد. قصد داریم میزان مصرف حافظه بارگذاری این اطلاعات را بررسی کنیم.
کلاس PerformanceHelper معرفی شده، دو کار اندازه گیری میزان مصرف حافظه برنامه در طی اجرای یک فرمان خاص و همچنین مدت زمان سپری شدن آن‌را اندازه‌گیری می‌کند.
در کلاس Test فوق چندین متد به شرح زیر وجود دارند:
متد StartDb سبب می‌شود تا تنظیمات ابتدایی برنامه به بانک اطلاعاتی اعمال شوند. تا زمانیکه کوئری خاصی به بانک اطلاعاتی ارسال نگردد، EF Code first بانک اطلاعاتی را آغاز نخواهد کرد.
در متد LoadWithTracking اطلاعات تمام رکوردها به صورت متداولی بارگذاری شده است.
در متد LoadWithoutTracking نحوه استفاده از متد الحاقی AsNoTracking را مشاهده می‌کنید. در این متد سطح اول کش به این ترتیب خاموش می‌شود.
و متد RunTests، این متدها را در سه بار متوالی اجرا کرده و نتیجه عملیات را نمایش خواهد داد.

برای نمونه این نتیجه در اینجا حاصل شده است:


همانطور که ملاحظه کنید، بین این دو حالت، تفاوت بسیار قابل ملاحظه است؛ چه از لحاظ مصرف حافظه و چه از لحاظ سرعت.

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

 
مطالب
مقدار دهی اولیه‌ی بانک اطلاعاتی توسط Entity framework Core
قابلیت مقدار دهی اولیه‌ی بانک اطلاعاتی (data seeding) توسط اجرای کدهای Migrations و متد DbMigration­Configuration.Seed آن، در حین انتقال از EF 6x به EF Core ناپدید شده بود که مجددا با ارائه‌ی EF Core 2.1 به نحو کاملا متفاوتی توسط یک Fluent API، در متد OnModelCreating قابل تعریف و استفاده‌است.


کلاس‌های موجودیت‌های مثال جاری

برای توضیح قابلیت جدید مقدار دهی اولیه‌ی بانک اطلاعاتی در +EF Core 2.1، از کلاس‌های موجودیت‌های ذیل استفاده خواهیم کرد:
public class Magazine
{
  public int MagazineId { get; set; }
  public string Name { get; set; }
  public string Publisher { get; set; }

  public List<Article> Articles { get; set; }
}

public class Article
{
  public int ArticleId { get; set; }
  public string Title { get; set; }
  public DateTime PublishDate { get;  set; }

  public int MagazineId { get; set; }

  public Author Author { get; set; }
  public int? AuthorId { get; set; }
}

public class Author
{
  public int AuthorId { get; set; }
  public string Name { get; set; }

  public List<Article> Articles { get; set; }
}


روش مقدار دهی اولیه‌ی تک موجودیت‌ها

اکنون فرض کنید قصد داریم جدول مجلات را مقدار دهی اولیه کنیم. برای اینکار خواهیم داشت:
protected override void OnModelCreating (ModelBuilder modelBuilder)
{
   modelBuilder.Entity<Magazine>().HasData(new Magazine { MagazineId = 1, Name = "DNT Magazine" });
}
چند نکته در اینجا حائز اهمیت هستند:
- ذکر صریح مقدار Id یک رکورد (هرچند نوع Id آن auto-increment است).
- عدم ذکر مقدار Publisher.

اکنون اگر توسط دستورات Migrations مانند dotnet ef migrations add init، کار تولید کدهای متناظر به روز رسانی بانک اطلاعاتی را بر اساس این کدها تولید کنیم، در قسمتی از آن، یک چنین خروجی را دریافت خواهیم کرد:
migrationBuilder.InsertData(
  table: "Magazines",
  columns: new[] { "MagazineId", "Name", "Publisher" },
  values: new object[] { 1, "DNT Magazine", null });
در ادامه اگر از روی این کلاس‌های مهاجرت‌ها، اسکریپت معادل نهایی اعمالی به بانک اطلاعاتی را توسط دستور dotnet ef migrations script تولید کنیم، یک چنین خروجی حاصل می‌شود:
set IDENTITY_INSERT ON
INSERT INTO "Magazines" ("MagazineId", "Name", "Publisher") VALUES (1, 'DNT Magazine', NULL);
همانطور که مشاهده می‌کنید، اگر نوع بانک اطلاعاتی ما SQL Server باشد، ابتدا ثبت دستی فیلدهای IDENTITY تنظیم می‌شود و سپس Id رکورد جدید را بر اساس مقداری که مشخص کرده‌ایم، درج می‌کند.

توسط متد HasData امکان درج چندین رکورد با هم نیز وجود دارد:
modelBuilder.Entity<Magazine>()
           .HasData(new Magazine{ MagazineId=2, Name="This Mag" },
                    new Magazine{ MagazineId=3, Name="That Mag" }
           );

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

در مورد داده‌های نال‌نپذیر چطور؟
در مثال فوق اگر تنظیمات خاصیت Publisherای را که نال وارد کردیم، نال‌نپذیر تعریف کنیم:
modelBuilder.Entity<Magazine>().Property(m=>m.Publisher).IsRequired();
و مجددا دستورات تولید کلاس‌های Migrations را صادر کنیم، اینبار خطای واضح زیر حاصل خواهد شد:
 "The seed entity for entity type 'Magazine' cannot be added because there was no value provided for the required property 'Publisher'."
همین پیام خطا با عدم ذکر صریح مقدار Id نیز تولید می‌شود. هرچند Id، یک فیلد auto-increment است، اما چون شرط IsRequired در مورد آن برقرار است، شامل بررسی فیلدهای نال‌نپذیر نیز می‌شود. به همین جهت ذکر آن در متد HasData اجباری است.


امکان استفاده‌ی از Anonymous Types در متد HasData

فرض کنید برای کلاس موجودیت خود یک سازنده را نیز تعریف کرده‌اید:
public Magazine(string name, string publisher)
{
  Name=name;
  Publisher=publisher;
}
چون در متد HasData ذکر Id موجودیت، اجباری است، دیگر نمی‌توان یک چنین تعاریفی را ارائه داد:
modelBuilder.Entity<Magazine>().HasData(new Magazine("DNT Magazine", "1105 Media"));
برای رفع یک چنین مشکلاتی، امکان استفاده‌ی از anonymous types نیز در متد HasData پیش‌بینی شده‌است. در این حالت می‌توان بجای وهله سازی مستقیم شیء Magazine، یک anonymous type را وهله سازی کرد و در آن MagazineId را نیز ذکر کرد؛ بدون اینکه نگران این باشیم آیا این خاصیت عمومی است، خصوصی است و یا ... حتی تعریف شده‌است یا خیر!
modelBuilder.Entity<Magazine>().HasData(new {MagazineId=1, Name="DNT Mag", Publisher="1105 Media"});
که حاصل آن تولید یک چنین کد مهاجرتی است:
migrationBuilder.InsertData(
                table: "Magazines",
                columns: new[] { "MagazineId", "Name", "Publisher" },
                values: new object[] { 1, "DNT Mag", "1105 Media" });
و سبب درج صحیح مقادیر فیلدهای یک رکورد جدول Magazines می‌شود.

حالت دیگر استفاده‌ی از این قابلیت، کار با خواصی هستند که private set می‌باشند. فرض کنید کلاس موجودیت Magazine را به صورت زیر تغییر داده‌اید:
public class Magazine
{
  public Magazine(string name, string publisher)
  {
    Name=name;
    Publisher=publisher;
    MagazineId=Guid.NewGuid();
  }

  public Guid MagazineId { get; private set; }
  public string Name { get; private set; }
  public string Publisher { get; private set; }
  public List<Article> Articles { get; set; }
}
که در آن Id اینبار از نوع Guid است و در سازنده‌ی کلاس مقدار دهی می‌شود و همچنین خواص این موجودیت به صورت private set تعریف شده‌اند. در این حالت اگر متد HasData این موجودیت را به صورت زیر تعریف کنیم:
modelBuilder.Entity<Magazine>().HasData(new Magazine("DNT Mag", "1105 Media");
هر بار که دستورات Migrations اجرا می‌شوند، یک Guid جدید به صورت خودکار ایجاد خواهد شد که سبب می‌شود، مقدار آغازین پیشین، از بانک اطلاعاتی حذف و مقدار جدید آن با یک Guid جدید، درج شود. به همین جهت نیاز است Guid را حتما به صورت دستی و مشخص، در متد HasData وارد کرد که چنین کاری با توجه به تعریف کلاس موجودیت فوق، مسیر نیست. بنابراین در اینجا نیز می‌توان از یک anonymous type استفاده کرد:
var mag1=new {MagazineId= new Guid("0483b59c-f7f8-4b21-b1df-5149fb57984e"),  Name="DNT Mag", Publisher="1105 Media"};
modelBuilder.Entity<Magazine>().HasData(mag1);


مقدار دهی اولیه‌ی اطلاعات به هم مرتبط

همانطور که پیشتر نیز ذکر شد، متد HasData تنها با یک تک موجودیت کار می‌کند و روش کار آن همانند کار با DbSetها نیست. به همین جهت نمی‌توان اشیاء به هم مرتبط را توسط آن در بانک اطلاعاتی درج کرد. بنابراین برای درج اطلاعات یک مجله و مقالات مرتبط با آن، ابتدا باید مجله را ثبت کرد و سپس بر اساس Id آن مجله، کلید خارجی مقالات را به صورت جداگانه‌ای مقدار دهی نمود:
modelBuilder.Entity<Article>().HasData(new Article { ArticleId = 1, MagazineId = 1, Title = "EF Core 2.1 Query Types"});
پیشتر یک Magazine را با Id مساوی 1 ثبت کرده بودیم. اکنون این Id را در اینجا به صورت یک کلید خارجی، جهت درج یک مقاله‌ی جدیدی استفاده می‌کنیم. حاصل آن یک چنین مهاجرتی است:
var mag1=new {MagazineId= new Guid("0483b59c-f7f8-4b21-b1df-5149fb57984e"),  Name="DNT Mag", Publisher="1105 Media"};
modelBuilder.Entity<Magazine>().HasData(mag1);
در اینجا چون PublishDate را ذکر نکرده‌ایم (و DateTime نیز یک value type است)، کمترین مقدار ممکن را برای آن تنظیم کرده‌است.


مقدار دهی اولیه‌ی Owned Entities

complex types در EF 6x با مفهوم دیگری به نام owned types در EF Core جایگزین شده‌اند:
public class Publisher
{
  public string Name { get; set; }
  public int YearFounded { get; set; }
}

public class Magazine
{ 
  public int MagazineId { get;  set; }
  public string Name { get;  set; }
  public Publisher Publisher { get;  set; }
  public List<Article> Articles { get; set; }
}
در اینجا اطلاعات مربوط به Publisher‌، در طی یک عملیات Refactoring، تبدیل به یک کلاس مستقل شده‌اند و سپس در تعریف کلاس موجودیت مجله، مورد استفاده قرار گرفته‌اند. این کلاس جدید، دارای Id نیست.
modelBuilder.Entity<Magazine>().HasData (new Magazine { MagazineId = 1, Name = "DNT Magazine" });
modelBuilder.Entity<Magazine>().OwnsOne (m => m.Publisher)
   .HasData (new { Name = "1105 Media", YearFounded = 2006, MagazineId=1 });
متد HasData تنها اجازه‌ی کار با یک نوع کلاس را می‌دهد. به همین جهت یکبار باید Magazine را بدون Publisher ثبت کرد. سپس در طی ثبتی دیگر می‌توان نوع Publisher را توسط یک anonymous type متصل به Id مجله‌ی ثبت شده، درج کرد (متد OwnsOne کار ارتباط را برقرار می‌کند). علت استفاده‌ی از anonymous type نیز درج Id ای است که در کلاس Publisher وجود خارجی ندارد.
این دو دستور، خروجی Migrations زیر را تولید می‌کنند:
migrationBuilder.InsertData(
  table: "Magazines",
  columns: new[] { "MagazineId", "Name", "Publisher_Name", "Publisher_YearFounded" },
  values: new object[] { 1, "DNT Magazine", "1105 Media", 2006 });


محل صحیح اجرای Migrations در برنامه‌های ASP.NET Core 2x

زمانیکه متد ()context.Database.Migrate را اجرا می‌کنید، تمام مهاجرت‌های اعمال نشده را به بانک اطلاعاتی اعمال می‌کند که این مورد شامل اجرای دستورات HasData نیز هست. روش فراخوانی این متد در ASP.NET Core 1x به صورت زیر در متد Configure کلاس Startup بود (و البته هنوز هم کار می‌کند):
namespace EFCoreMultipleDb.Web
{
    public class Startup
    {
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            applyPendingMigrations(app);
// ...
        }

        private static void applyPendingMigrations(IApplicationBuilder app)
        {
            var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
            using (var scope = scopeFactory.CreateScope())
            {
                var uow = scope.ServiceProvider.GetService<IUnitOfWork>();
                uow.Migrate();
            }
        }
    }
}
متد applyPendingMigrations، کار وهله سازی IUnitOfWork را انجام می‌دهد. سپس متد Migrate آن‌را اجرا می‌کند، تا تمام Migartions تولید شده، اما اعمال نشده‌ی به بانک اطلاعاتی به صورت خودکار به آن اعمال شوند. متد Migrate نیز به صورت زیر تعریف می‌شود:
namespace EFCoreMultipleDb.DataLayer.SQLite.Context
{
    public class SQLiteDbContext : DbContext, IUnitOfWork
    {
    // ... 

        public void Migrate()
        {
            this.Database.Migrate();
        }
    }
}
روش بهتر اینکار در ASP.NET Core 2x، انتقال متد applyPendingMigrations به بالاترین سطح ممکن در برنامه، در فایل program.cs و پیش از اجرای متد Configure کلاس Startup است. به این ترتیب در برنامه، قسمت‌هایی که پیش از متد Configure شروع به کار می‌کنند و نیاز به دسترسی به بانک اطلاعاتی را دارند، با صدور پیام خطایی، سبب خاتمه‌ی برنامه نخواهند شد:
public static void Main(string[] args)
{
   var host = BuildWebHost(args);
   using (var scope = host.Services.CreateScope())
   {
       var context = scope.ServiceProvider.GetRequiredService<yourDBContext>();
       context.Database.Migrate();
   }
   host.Run();
}