مطالب
به روز رسانی اطلاعات Master-Detail یا Master-Detail-DetailOfDetail با استفاده از EF Core

یکی از چالش‌هایی که در طراحی زیرساخت برای Domain هایی که تعداد زیادی عملیات CRUD را در back office سیستم خود دارند، داشتن مکانیزمی برای ذخیره سازی اطلاعات Master-Detail یا چه بسا Master-Detail-DetailOfDetail می‌باشد. در ادامه نحوه برخورد با چنین سناریوهایی را در EF Core و همچنین با استفاده از AutoMapper و FluentValidation بررسی خواهیم کرد.


موجودیت‌های فرضی

public abstract class Entity : IHaveTrackingState
{
    public long Id { get; set; }
    [NotMapped] public TrackingState TrackingState { get; set; }
}

public class Master : Entity
{
    public string Title { get; set; }
    public ICollection<Detail> Details { get; set; }
}

public class Detail : Entity
{
    public string Title { get; set; }

    public ICollection<DetailOfDetail> Details { get; set; }
    public Master Master { get; set; }
    public long MasterId { get; set; }
}

public class DetailOfDetail : Entity
{
    public string Title { get; set; }
    public Detail Detail { get; set; }
    public long DetailId { get; set; }
}

DbContext برنامه

public class ProjectDbContext : DbContext
{
    public DbSet<Master> Masters { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        base.OnConfiguring(optionsBuilder);

        optionsBuilder.UseInMemoryDatabase("SharedDatabaseName");
    }
}

واسط IHaveTrackingState
public interface IHaveTrackingState
{
    TrackingState TrackingState { get; set; }
    //ICollection<string> ModifiedProperties { get; set; }
}

public enum TrackingState
{
    Unchanged = 0,
    Added = 1,
    Modified = 2,
    Deleted = 3
}

با استفاده از پراپرتی TrackingState بالا، امکان مشخص کردن صریح State رکورد ارسالی توسط کلاینت مهیا می‌شود. قبلا نیز مطلبی در راستای STE یا همان Self-Tracking Entity تهیه شده است؛ و همچنین نظرات ارسالی این مطلب نیز می‌تواند مفید واقع شود. 


DTO‌های متناظر با موجودیت‌های فرضی

public abstract class Model : IHaveTrackingState
{
    public long Id { get; set; }
    public TrackingState TrackingState { get; set; }
}

public class MasterModel : Model
{
    public string Title { get; set; }
    public ICollection<DetailModel> Details { get; set; }
}

public class DetailModel : Model
{
    public string Title { get; set; }
    public ICollection<DetailOfDetailModel> Details { get; set; }
}

public class DetailOfDetailModel : Model
{
    public string Title { get; set; }
}

تنظیمات نگاشت موجودیت‌ها و DTOها
Mapper.Initialize(expression =>
{
    expression.CreateMap<MasterModel, Master>(MemberList.None).ReverseMap();
    expression.CreateMap<DetailModel, Detail>(MemberList.None).ReverseMap();
    expression.CreateMap<DetailOfDetailModel, DetailOfDetail>(MemberList.None).ReverseMap();
});

البته بهتر است این تنظیمات در درون Profile‌های مرتبط با AutoMapper کپسوله شوند و در زمان مورد نیاز نیز برای انجام نگاشت‌ها، واسط IMapper تزریق شده و استفاده شود.


تهیه داده ارسالی فرضی توسط کلاینت

var masterModel = new MasterModel
    {
        Title = "Master-Title",
        TrackingState = TrackingState.Added,
        Details = new List<DetailModel>
        {
            new DetailModel
            {
                Title = "Detail-Title",
                TrackingState = TrackingState.Added,
                Details = new List<DetailOfDetailModel>
                {
                    new DetailOfDetailModel
                    {
                        Title = "DetailOfDetail-Title",
                        TrackingState = TrackingState.Added,
                    }
                }
            }
        }
    };

ذخیره سازی اطلاعات

در EF Core، متد جدید context.ChangeTracker.TrackGraph برای به روز رسانی وضعیت یک گراف از اشیاء مشابه به اطلاعات ارسالی ذکر شده در بالا، اضافه شده است. این مکانیزم مفهوم کاملا جدیدی در EF Core می‌باشد که امکان کنترل نهایی برروی اشیایی را که قرار است توسط Context ردیابی شوند، مهیا می‌کند. با پیمایش یک گراف، امکان اجرای عملیات مورد نظر شما را برروی تک تک اشیاء، مهیا می‌سازد. 

using (var context = new ProjectDbContext())
{
    Console.WriteLine("################ Create Master and Details and DetailsOfDetail ##################");
    Print(masterModel);

    var masterEntity = Mapper.Map<Master>(masterModel);

    context.ChangeTracker.TrackGraph(
        masterEntity,
        n =>
        {
            var entity = (IHaveTrackingState) n.Entry.Entity;
            n.Entry.State = entity.TrackingState.ToEntityState();
        });

    context.SaveChanges();
}

در تکه کد بالا، پس از انجام عملیات نگاشت، توسط متد TrackGraph به صورت صریح، وضعیت موجودیت‌ها مشخص شده است؛ این کار با تغییر State ارسالی توسط کلاینت به State قابل فهم توسط EF انجام شده‌است. برای این منظور دو متد الحاقی زیر را می‌توان در نظر گرفت:

public static class TrackingStateExtensions
{
    public static EntityState ToEntityState(this TrackingState trackingState)
    {
        switch (trackingState)
        {
            case TrackingState.Added:
                return EntityState.Added;

            case TrackingState.Modified:
                return EntityState.Modified;

            case TrackingState.Deleted:
                return EntityState.Deleted;

            case TrackingState.Unchanged:
                return EntityState.Unchanged;

            default:
                return EntityState.Unchanged;
        }
    }

    public static TrackingState ToTrackingState(this EntityState state)
    {
        switch (state)
        {
            case EntityState.Added:
                return TrackingState.Added;

            case EntityState.Modified:
                return TrackingState.Modified;

            case EntityState.Deleted:
                return TrackingState.Deleted;

            case EntityState.Unchanged:
                return TrackingState.Unchanged;

            default:
                return TrackingState.Unchanged;
        }
    }
}

شبیه سازی عملیات ویرایش
//GetForEditAsync
var masterModel = context.Masters
    .ProjectTo<MasterModel>()
    .AsNoTracking().Single(a => a.Id == 1);

//Client
var detail1 = masterModel.Details.First();
detail1.Title = "Details-EditedTitle";
detail1.TrackingState = TrackingState.Modified;

foreach (var detail in detail1.Details)
{
    detail.TrackingState = TrackingState.Deleted;
    //detail.Title = "DetailOfDetails-EditedTitle";
}

متدی تحت عنوان GetForEditAsync که یک MasterModel را بازگشت می‌دهد، در نظر بگیرید؛ کلاینت از طریق API، این Object Graph را دریافت می‌کند و تغییرات خود را اعمال کرده و همانطور که مشخص می‌باشد به دلیل اینکه تنظیمات نگاشت بین Detail و DetailModel در ابتدای بحث نیز انجام شده است، این بار دیگر نیاز به استفاده از متد Include نمی‌باشد و این عملیات توسط متد ProjectTo خودکار می‌باشد. در نهایت داده ارسالی توسط کلاینت را دریافت کرده و به شکل زیر عملیات به روز رسانی انجام می‌شود:

using (var context = new ProjectDbContext())
{
    Console.WriteLine(
        "################ Unchanged Master and Modified Details and Deleted DetailsOfDetail ##################");
    Print(masterModel);

    var masterEntity = Mapper.Map<Master>(masterModel);

    context.ChangeTracker.TrackGraph(
        masterEntity,
        n =>
        {
            var entity = (IHaveTrackingState) n.Entry.Entity;
            n.Entry.State = entity.TrackingState.ToEntityState();
        });

    context.SaveChanges();
}

با خروجی زیر:

برای بحث اعتبارسنجی هم می‌توان به شکل زیر عمل کرد:

public class MasterValidator : AbstractValidator<MasterModel>
{
    public MasterValidator()
    {
        RuleFor(a => a.Title).NotEmpty();
        RuleForEach(a => a.Details).SetValidator(new DetailValidator());
    }
}

public class DetailValidator : AbstractValidator<DetailModel>
{
    public DetailValidator()
    {
        RuleFor(a => a.Title).NotEmpty();
        RuleForEach(a => a.Details).SetValidator(new DetailOfDetailValidator());
    }
}

public class DetailOfDetailValidator : AbstractValidator<DetailOfDetailModel>
{
    public DetailOfDetailValidator()
    {
        RuleFor(a => a.Title).NotEmpty();
    }
}

با استفاده از متد RuleForEach و SetValidator موجود در کتابخانه FluentValidation، امکان مشخص کردن اعتبارسنج برای Detail موجود در شیء Master را خواهیم داشت.

همچنین با توجه به این که برای عملیات Create و Edit از یک مدل (DTO) استفاده خواهیم کرد، شاید لازم باشد اعتبارسنجی خاصی را فقط در زمان ویرایش لازم داشته باشیم، که در این صورت می‌توان از امکانات RuleSet استفاده کنید. در مطلب «طراحی و پیاده سازی ServiceLayer به همراه خودکارسازی Business Validationها» با استفاده ValidateWithRuleAttribute امکان مشخص کردن RuleSet مورد نظر برای اعتبارسنجی ورودی متد سرویس نیز در نظر گرفته شده است.


منابع تکمیلی

کتابخانه کمکی

کدهای کامل مطلب جاری را  از اینجا می‌توانید دریافت کنید.
مطالب
توصیف فیلدها توسط Tag Helper و Data annotation

همه ما با DisplayAttribute در DataAnnotaion آشنا هستیم. چیزی شبیه زیر برای یک موجودیت:

public class Student{
    [Display(Name="نام خانوادگی")]
    public string FamilyName { get; set;}
}

با استفاده از tag helper ای به نام asp-for می‌توان متادیتای Name را به کاربر، در سمت رابط کاربری نشان داد؛ برای مثال:

<label asp-for="FamilyName"></label>

و یا موقع اعتبارسنجی می‌توان به جای نشان دادن نام FamilyName از نام مفهوم‌تری مانند نام خانوادگی استفاده نمود.

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

[Display(
     Name = "نام خانوادگی",
     Description = "بهتر است فقط در اینجا نام خانوادگی شخص وارد شود")]
public string FamilyName{ get; set; }

بتوان از tag helper ای مانند زیر استفاده نمود:

<span asp-description-for="FamilyName"></span>

که در نهایت چنین خروجی html ای داشته باشیم:

<span>بهتر است فقط در اینجا نام خانوادگی شخص وارد شود</span>

برای این منظور می‌توان از کلاس زیر بهره برد:

using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;

[HtmlTargetElement("div", Attributes = ForAttributeName)]
[HtmlTargetElement("p", Attributes = ForAttributeName)]
[HtmlTargetElement("span", Attributes = ForAttributeName)]
public sealed class DescriptionForTagHelper : TagHelper
{
    private const string ForAttributeName = "asp-description-for";

    [HtmlAttributeName(ForAttributeName)] 
    public ModelExpression For { get; set; } = default!;

    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (output == null)
        {
            throw new ArgumentNullException(nameof(output));
        }

        var description = For.Metadata.Description;
        if (description != null)
        {
            // Do not update the content if another tag helper
            // targeting this element has already done so.
            if (!output.IsContentModified)
            {
                var childContent = await output.GetChildContentAsync();
                if (childContent.IsEmptyOrWhiteSpace)
                {
                    output.Content.SetHtmlContent(description);
                }
                else
                {
                    output.Content.SetHtmlContent(childContent);
                }
            }
        }
    }
}

کلاس DescriptionForTagHelper از کلاس پایه TagHelper ارث بری نموده است و متد ProcessAsync آن به نحوی که  asp-description-for را بپذیرد override شده است.

حوزه اعمال این tag helper به span، p و div محدود شده است؛ اما می‌توان با گذاشتن یک ستاره (*) آن را به کل المان‌های html اعمال کرد.

اشتراک‌ها
Bulk delete و Bulk update در Entity framework
در Entity framework به کمک امکانات LINQ، نمی‌توان چندین رکورد را با هم (یکجا) حذف کرد یا به روز رسانی نمود. برای مثال دستوراتی مانند Delete from tbl1 where id>3 و یا update tbl1 set f1=2 where id>3 معادلی در LINQ to EF ندارند. برای انجام اینگونه امور یا باید رویه ذخیره شده نوشت و سپس آن‌را در EF فراخوانی کرد؛ یا دستورات SQL خام را مستقیما بر روی بانک اطلاعاتی اجرا کرد و یا باید حلقه‌ای را تشکیل داد و موارد موجود را یک به یک ویرایش یا حذف نمود. مورد آخر خصوصا از لحاظ کارآیی به هیچ عنوان پذیرفتنی نیست.
البته اکثر ORMهای موجود از Bulk operations پشتیبانی نمی‌کنند و این مورد منحصر به EF نیست. علت آن هم به مشکل بودن هماهنگ سازی Context و بانک اطلاعاتی، پس از Bulk operations بر می‌گردد. برای مثال در یک کوئری حذف پیچیده، هماهنگ سازی سطح اول کش یا Tracking API با رکوردهای باقی مانده در بانک اطلاعاتی کار ساده‌ای نیست و اگر اینکار به نحو صحیحی صورت نگیرد، کاربران در حین ذخیره سازی اطلاعات ممکن است با خطاهای همزمانی متوقف شوند (چون هنوز رکوردهای حذف شده در سطح اول کش یا Context وجود دارند، ORM تصور می‌کند کاربر دیگری اطلاعات را حذف کرده است و یک استثنای همزمانی را صادر خواهد کرد).


معرفی Entity Framework Extended Library

کتابخانه «Entity Framework Extended Library» تعدادی متد الحاقی را به EF جهت اعمال Bulk delete و Bulk update سازگار با کوئری‌های تهیه شده به وسیله امکانات LINQ to EF، اضافه می‌کند. البته این کتابخانه امکانات دیگری مانند Future Queries (یکی کردن چند کوئری متفاوت LINQ جهت اجرا در یک رفت و برگشت به بانک اطلاعاتی)، Query Result Cache (سطح دوم کش) را نیز به همراه دارد.
Bulk delete و Bulk update در Entity framework
مطالب
راهنمای تغییر بخش احراز هویت و اعتبارسنجی کاربران سیستم مدیریت محتوای IRIS به ASP.NET Identity – بخش دوم
در بخش اول، کارهایی که انجام دادیم به طور خلاصه عبارت بودند از:
1-  حذف کاربرانی که نام کاربری و ایمیل تکراری داشتند
2-  تغییر نام فیلد Password به PasswordHash در جدول User
 
سیستم مدیریت محتوای IRIS، برای استفاده از Entity Framework، از الگوی واحد کار (Unit Of Work) و تزریق وابستگی استفاده کرده است و اگر با نحوه‌ی پیاده سازی این الگو‌ها آشنا نیستید، خواندن مقاله EF Code First #12  را به شما توصیه می‌کنم.
برای استفاده از ASP.NET Identity نیز باید از الگوی واحد کار استفاده کرد و برای این کار، ما از مقاله اعمال تزریق وابستگی‌ها به مثال رسمی ASP.NET Identity استفاده خواهیم کرد.
نکته مهم: در ادامه اساس کار ما بر پایه‌ی مقاله اعمال تزریق وابستگی‌ها به مثال رسمی ASP.NET Identity است و چیزی که بیشتر برای ما اهمیت دارد کدهای نهایی آن هست؛ پس حتما به مخزن کد آن مراجعه کرده و کدهای آن را دریافت کنید.
 
تغییر نام کلاس User به ApplicationUser

اگر به کدهای مثال رسمی ASP.NET Identity نگاهی بیندازید، می‌بینید که کلاس مربوط به جدول کاربران ApplicationUser نام دارد، ولی در سیستم IRIS نام آن User است. بهتر است که ما هم نام کلاس خود را از User به ApplicationUser تغییر دهیم چرا که مزایای زیر را به دنبال دارد:

1- به راحتی می‌توان کدهای مورد نیاز را از مثال Identity کپی کرد.
2- در سیستم Iris، بین کلاس User متعلق به پروژه خودمان و User مربوط به HttpContext تداخل رخ می‌داد که با تغییر نام کلاس User دیگر این مشکل را نخواهیم داشت.
 
برای این کار وارد پروژه Iris.DomainClasses شده و نام کلاس User را به ApplicationUser تغییر دهید. دقت کنید که این تغییر نام را از طریق Solution Explorer انجام دهید و نه از طریق کدهای آن. پس از این تغییر ویژوال استودیو می‌پرسد که آیا نام این کلاس را هم در کل پروژه تغییر دهد که شما آن را تایید کنید.

برای آن که نام جدول Users در دیتابیس تغییری نکند، وارد پوشه‌ی Entity Configuration شده و کلاس UserConfig را گشوده و در سازنده‌ی آن کد زیر را اضافه کنید:
ToTable("Users");

نصب ASP.NET Identity

برای نصب ASP.NET Identity دستور زیر را در کنسول Nuget وارد کنید:
Get-Project Iris.DomainClasses, Iris.Datalayer, Iris.Servicelayer, Iris.Web | Install-Package Microsoft.AspNet.Identity.EntityFramework
از پروژه AspNetIdentityDependencyInjectionSample.DomainClasses کلاس‌های CustomUserRole، CustomUserLogin، CustomUserClaim و CustomRole را به پروژه Iris.DomainClasses منتقل کنید. تنها تغییری که در این کلاس‌ها باید انجام دهید، اصلاح namespace آنهاست.
همچنین بهتر است که به کلاس CustomRole، یک property به نام Description اضافه کنید تا توضیحات فارسی نقش مورد نظر را هم بتوان ذخیره کرد:

 
    public class CustomRole : IdentityRole<int, CustomUserRole>
    {
        public CustomRole() { }
        public CustomRole(string name) { Name = name; }

        public string Description { get; set; }

    }

نکته: پیشنهاد می‌کنم که اگر می‌خواهید مثلا نام CustomRole را به IrisRole تغییر دهید، این کار را از طریق find and replace انجام ندهید. با همین نام‌های پیش فرض کار را تکمیل کنید و سپس از طریق خود ویژوال استودیو نام کلاس را تغییر دهید تا ویژوال استودیو به نحو بهتری این نام‌ها را در سرتاسر پروژه تغییر دهد.
 
سپس کلاس ApplicationUser پروژه IRIS را باز کرده و تعریف آن را به شکل زیر تغییر دهید:
public class ApplicationUser : IdentityUser<int, CustomUserLogin, CustomUserRole, CustomUserClaim>

اکنون می‌توانید property‌های Id،  UserName، PasswordHash و Email را حذف کنید؛ چرا که در کلاس پایه IdentityUser تعریف شده اند.
 
تغییرات DataLayer

وارد Iris.DataLayer شده و کلاس IrisDbContext را به شکل زیر ویرایش کنید:
public class IrisDbContext : IdentityDbContext<ApplicationUser, CustomRole, int, CustomUserLogin, CustomUserRole, CustomUserClaim>,
        IUnitOfWork

اکنون می‌توانید property زیر را نیز حذف کنید چرا که در کلاس پایه تعریف شده است: 
 public DbSet<ApplicationUser> Users { get; set; }

 

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

        public IrisDbContext()
            : base("IrisDbContext")
        {
        }

همچنین درون متد OnModelCreating کدهای زیر را پس از فراخوانی متد (base.OnModelCreating(modelBuilder  جهت تعیین نام جداول دیتابیس بنویسید:
            modelBuilder.Entity<CustomRole>().ToTable("AspRoles");
            modelBuilder.Entity<CustomUserClaim>().ToTable("UserClaims");
            modelBuilder.Entity<CustomUserRole>().ToTable("UserRoles");
            modelBuilder.Entity<CustomUserLogin>().ToTable("UserLogins");
از این جهت نام جدول CustomRole را در دیتابیس AspRoles انتخاب کردم تا با نام جدول Roles نقش‌های کنونی سیستم Iris تداخلی پیش نیاید.
اکنون دستور زیر را در کنسول Nuget وارد کنید تا کدهای مورد نیاز برای مهاجرت تولید شوند:
Add-Migration UpdateDatabaseToAspIdentity
public partial class UpdateDatabaseToAspIdentity : DbMigration
{
        public override void Up()
        {
            CreateTable(
                "dbo.UserClaims",
                c => new
                    {
                        Id = c.Int(nullable: false, identity: true),
                        UserId = c.Int(nullable: false),
                        ClaimType = c.String(),
                        ClaimValue = c.String(),
                        ApplicationUser_Id = c.Int(),
                    })
                .PrimaryKey(t => t.Id)
                .ForeignKey("dbo.Users", t => t.ApplicationUser_Id)
                .Index(t => t.ApplicationUser_Id);
            
            CreateTable(
                "dbo.UserLogins",
                c => new
                    {
                        LoginProvider = c.String(nullable: false, maxLength: 128),
                        ProviderKey = c.String(nullable: false, maxLength: 128),
                        UserId = c.Int(nullable: false),
                        ApplicationUser_Id = c.Int(),
                    })
                .PrimaryKey(t => new { t.LoginProvider, t.ProviderKey, t.UserId })
                .ForeignKey("dbo.Users", t => t.ApplicationUser_Id)
                .Index(t => t.ApplicationUser_Id);
            
            CreateTable(
                "dbo.UserRoles",
                c => new
                    {
                        UserId = c.Int(nullable: false),
                        RoleId = c.Int(nullable: false),
                        ApplicationUser_Id = c.Int(),
                    })
                .PrimaryKey(t => new { t.UserId, t.RoleId })
                .ForeignKey("dbo.Users", t => t.ApplicationUser_Id)
                .ForeignKey("dbo.AspRoles", t => t.RoleId, cascadeDelete: true)
                .Index(t => t.RoleId)
                .Index(t => t.ApplicationUser_Id);
            
            CreateTable(
                "dbo.AspRoles",
                c => new
                    {
                        Id = c.Int(nullable: false, identity: true),
                        Description = c.String(),
                        Name = c.String(nullable: false, maxLength: 256),
                    })
                .PrimaryKey(t => t.Id)
                .Index(t => t.Name, unique: true, name: "RoleNameIndex");
            
            AddColumn("dbo.Users", "EmailConfirmed", c => c.Boolean(nullable: false));
            AddColumn("dbo.Users", "SecurityStamp", c => c.String());
            AddColumn("dbo.Users", "PhoneNumber", c => c.String());
            AddColumn("dbo.Users", "PhoneNumberConfirmed", c => c.Boolean(nullable: false));
            AddColumn("dbo.Users", "TwoFactorEnabled", c => c.Boolean(nullable: false));
            AddColumn("dbo.Users", "LockoutEndDateUtc", c => c.DateTime());
            AddColumn("dbo.Users", "LockoutEnabled", c => c.Boolean(nullable: false));
            AddColumn("dbo.Users", "AccessFailedCount", c => c.Int(nullable: false));
        }
        
        public override void Down()
        {
            DropForeignKey("dbo.UserRoles", "RoleId", "dbo.AspRoles");
            DropForeignKey("dbo.UserRoles", "ApplicationUser_Id", "dbo.Users");
            DropForeignKey("dbo.UserLogins", "ApplicationUser_Id", "dbo.Users");
            DropForeignKey("dbo.UserClaims", "ApplicationUser_Id", "dbo.Users");
            DropIndex("dbo.AspRoles", "RoleNameIndex");
            DropIndex("dbo.UserRoles", new[] { "ApplicationUser_Id" });
            DropIndex("dbo.UserRoles", new[] { "RoleId" });
            DropIndex("dbo.UserLogins", new[] { "ApplicationUser_Id" });
            DropIndex("dbo.UserClaims", new[] { "ApplicationUser_Id" });
            DropColumn("dbo.Users", "AccessFailedCount");
            DropColumn("dbo.Users", "LockoutEnabled");
            DropColumn("dbo.Users", "LockoutEndDateUtc");
            DropColumn("dbo.Users", "TwoFactorEnabled");
            DropColumn("dbo.Users", "PhoneNumberConfirmed");
            DropColumn("dbo.Users", "PhoneNumber");
            DropColumn("dbo.Users", "SecurityStamp");
            DropColumn("dbo.Users", "EmailConfirmed");
            DropTable("dbo.AspRoles");
            DropTable("dbo.UserRoles");
            DropTable("dbo.UserLogins");
            DropTable("dbo.UserClaims");
        }
}

بهتر است که در کدهای تولیدی فوق، اندکی متد Up را با کد زیر تغییر دهید: 
AddColumn("dbo.Users", "EmailConfirmed", c => c.Boolean(nullable: false, defaultValue:true));

چون در سیستم جدید احتیاج به تایید ایمیل به هنگام ثبت نام است، بهتر است که ایمیل‌های قبلی موجود در سیستم نیز به طور پیش فرض تایید شده باشند.
در نهایت برای اعمال تغییرات بر روی دیتابیس دستور زیر را در کنسول Nuget وارد کنید:
Update-Database
 
تغییرات ServiceLayer

ابتدا دستور زیر را در کنسول Nuget  وارد کنید: 
Get-Project Iris.Servicelayer, Iris.Web | Install-Package Microsoft.AspNet.Identity.Owin
سپس از فولدر Contracts پروژه AspNetIdentityDependencyInjectionSample.ServiceLayer فایل‌های IApplicationRoleManager، IApplicationSignInManager، IApplicationUserManager، ICustomRoleStore و ICustomUserStore را در فولدر Interfaces پروژه Iris.ServiceLayer کپی کنید. تنها کاری هم که نیاز هست انجام بدهید اصلاح namespace هاست.

باز از پروژه AspNetIdentityDependencyInjectionSample.ServiceLayer کلاس‌های ApplicationRoleManager، ApplicationSignInManager،  ApplicationUserManager، CustomRoleStore، CustomUserStore، EmailService و SmsService را به پوشه EFServcies پروژه‌ی Iris.ServiceLayer کپی کنید.
نکته: پیشنهاد می‌کنم که EmailService را به IdentityEmailService تغییر نام دهید چرا که در حال حاضر سیستم Iris دارای کلاسی به نامی EmailService هست.
 
تنظیمات StructureMap برای تزریق وابستگی ها
پروژه Iris.Web  را باز کرده، به فولدر DependencyResolution بروید و به کلاس IoC کدهای زیر را اضافه کنید:
                x.For<IIdentity>().Use(() => (HttpContext.Current != null && HttpContext.Current.User != null) ? HttpContext.Current.User.Identity : null);

                x.For<IUnitOfWork>()
                    .HybridHttpOrThreadLocalScoped()
                    .Use<IrisDbContext>();

                x.For<IrisDbContext>().HybridHttpOrThreadLocalScoped()
                   .Use(context => (IrisDbContext)context.GetInstance<IUnitOfWork>());
                x.For<DbContext>().HybridHttpOrThreadLocalScoped()
                   .Use(context => (IrisDbContext)context.GetInstance<IUnitOfWork>());

                x.For<IUserStore<ApplicationUser, int>>()
                    .HybridHttpOrThreadLocalScoped()
                    .Use<CustomUserStore>();

                x.For<IRoleStore<CustomRole, int>>()
                    .HybridHttpOrThreadLocalScoped()
                    .Use<RoleStore<CustomRole, int, CustomUserRole>>();

                x.For<IAuthenticationManager>()
                      .Use(() => HttpContext.Current.GetOwinContext().Authentication);

                x.For<IApplicationSignInManager>()
                      .HybridHttpOrThreadLocalScoped()
                      .Use<ApplicationSignInManager>();

                x.For<IApplicationRoleManager>()
                      .HybridHttpOrThreadLocalScoped()
                      .Use<ApplicationRoleManager>();

                // map same interface to different concrete classes
                x.For<IIdentityMessageService>().Use<SmsService>();
                x.For<IIdentityMessageService>().Use<IdentityEmailService>();

                x.For<IApplicationUserManager>().HybridHttpOrThreadLocalScoped()
                   .Use<ApplicationUserManager>()
                   .Ctor<IIdentityMessageService>("smsService").Is<SmsService>()
                   .Ctor<IIdentityMessageService>("emailService").Is<IdentityEmailService>()
                   .Setter<IIdentityMessageService>(userManager => userManager.SmsService).Is<SmsService>()
                   .Setter<IIdentityMessageService>(userManager => userManager.EmailService).Is<IdentityEmailService>();

                x.For<ApplicationUserManager>().HybridHttpOrThreadLocalScoped()
                   .Use(context => (ApplicationUserManager)context.GetInstance<IApplicationUserManager>());

                x.For<ICustomRoleStore>()
                      .HybridHttpOrThreadLocalScoped()
                      .Use<CustomRoleStore>();

                x.For<ICustomUserStore>()
                      .HybridHttpOrThreadLocalScoped()
                      .Use<CustomUserStore>();

اگر ()HttpContext.Current.GetOwinContext شناسایی نمی‌شود دلیلش این است که متد GetOwinContext یک متد الحاقی است که برای استفاده از آن باید پکیج نیوگت زیر را نصب کنید:
Install-Package Microsoft.Owin.Host.SystemWeb

تغییرات Iris.Web
در ریشه پروژه‌ی Iris.Web  یک کلاس به نام Startup  بسازید و کدهای زیر را در آن بنویسید:
using System;
using Iris.Servicelayer.Interfaces;
using Microsoft.AspNet.Identity;
using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.DataProtection;
using Owin;
using StructureMap;

namespace Iris.Web
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            configureAuth(app);
        }

        private static void configureAuth(IAppBuilder app)
        {
            ObjectFactory.Container.Configure(config =>
            {
                config.For<IDataProtectionProvider>()
                      .HybridHttpOrThreadLocalScoped()
                      .Use(() => app.GetDataProtectionProvider());
            });

            //ObjectFactory.Container.GetInstance<IApplicationUserManager>().SeedDatabase();

            // Enable the application to use a cookie to store information for the signed in user
            // and to use a cookie to temporarily store information about a user logging in with a third party login provider
            // Configure the sign in cookie
            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
                LoginPath = new PathString("/Account/Login"),
                Provider = new CookieAuthenticationProvider
                {
                    // Enables the application to validate the security stamp when the user logs in.
                    // This is a security feature which is used when you change a password or add an external login to your account.
                    OnValidateIdentity = ObjectFactory.Container.GetInstance<IApplicationUserManager>().OnValidateIdentity()
                }
            });
            app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

            // Enables the application to temporarily store user information when they are verifying the second factor in the two-factor authentication process.
            app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));

            // Enables the application to remember the second login verification factor such as phone or email.
            // Once you check this option, your second step of verification during the login process will be remembered on the device where you logged in from.
            // This is similar to the RememberMe option when you log in.
            app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);

            app.CreatePerOwinContext(
               () => ObjectFactory.Container.GetInstance<IApplicationUserManager>());

            // Uncomment the following lines to enable logging in with third party login providers
            //app.UseMicrosoftAccountAuthentication(
            //    clientId: "",
            //    clientSecret: "");

            //app.UseTwitterAuthentication(
            //   consumerKey: "",
            //   consumerSecret: "");

            //app.UseFacebookAuthentication(
            //   appId: "",
            //   appSecret: "");

            //app.UseGoogleAuthentication(
            //    clientId: "",
            //    clientSecret: "");

        }
    }
}

تا به این جای کار اگر پروژه را اجرا کنید نباید هیچ مشکلی مشاهده کنید. در بخش بعدی کدهای مربوط به کنترلر‌های ورود، ثبت نام، فراموشی کلمه عبور و ... را با سیستم Identity پیاده سازی می‌کنیم.
نظرات مطالب
تشخیص اصالت ردیف‌های یک بانک اطلاعاتی در EF Core
با تشکر از شما؛ قابلیت RowIntegrity به زیرساخت DNTFrameworkCore اضافه شد.

public interface IHasRowIntegrity
{
    string Hash { get; set; }
}
internal sealed class RowIntegrityHook : PostActionHook<IHasRowIntegrity>
{
    public override string Name => HookNames.RowIntegrity;
    public override int Order => int.MaxValue;
    public override EntityState HookState => EntityState.Unchanged;

    protected override void Hook(IHasRowIntegrity entity, HookEntityMetadata metadata, IUnitOfWork uow)
    {
        metadata.Entry.Property(EFCore.Hash).CurrentValue = uow.EntityHash(entity);
    }
}
//DbContextCore : IUnitOfWork

public string EntityHash<TEntity>(TEntity entity) where TEntity : class
{
    var row = Entry(entity).ToDictionary(p => p.Metadata.Name != EFCore.Hash &&
                                              !p.Metadata.ValueGenerated.HasFlag(ValueGenerated.OnUpdate) &&
                                              !p.Metadata.IsShadowProperty());
    return EntityHash<TEntity>(row);
}

protected virtual string EntityHash<TEntity>(Dictionary<string, object> row) where TEntity : class
{
    var json = JsonConvert.SerializeObject(row, Formatting.Indented);
    using (var hashAlgorithm = SHA256.Create())
    {
        var byteValue = Encoding.UTF8.GetBytes(json);
        var byteHash = hashAlgorithm.ComputeHash(byteValue);
        return Convert.ToBase64String(byteHash);
    }
}

با توجه به محدودیت‌هایی (منفصل بودن اشیاء از کانتکست) که در استفاده از TrackGraph در زیرساخت وجود داشت، در اینجا از خواص سایه‌ای چشم پوشی شده است.
روش فعال‌سازی آن نیز در نسخه‌های جدید به شکل زیر می‌باشد:
services.AddEFCore<ProjectDbContext>()
    .WithTrackingHook<long>()
    .WithDeletedEntityHook()
    .WithRowLevelSecurityHook<long>()
    .WithRowIntegrityHook()
    .WithNumberingHook(options =>
    {
        options.NumberedEntityMap[typeof(Task)] = new NumberedEntityOption
        {
            Prefix = "Task",
            FieldNames = new[] {nameof(Task.BranchId)}
        };
    });

مطالب
شروع به کار با 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 درنظر گرفته شود.
مطالب
ویژگی های کمتر استفاده شده در NET. - بخش هفتم

DebuggerStepThroughAttribute

ویژگی DebuggerStepThroughAttribute باعث می‌شود که در زمان دیباگ کردن کد، با کلید F11، متدهایی که این ویژگی را دارند، بدون رفتن به داخل متد (همانند دیباگ با کلید F10 عمل می‌کند، به جز زمانی که در داخل متد break point گذاشته باشید) ، تنها اجرا می‌شوند.
به مثال زیر توجه کنید:
class Program
{
    public static void Main(string[] args)
    {
        DebuggerStepThroughMethod1();
    }

    [DebuggerStepThrough]
    public static void DebuggerStepThroughMethod1()
    {
        Console.WriteLine( "Method 1" );
        DebuggerStepThroughMethod2();
    }

    [DebuggerStepThrough]
    public static void DebuggerStepThroughMethod2()
    {
        Console.WriteLine( "Method 2" );
    }
}
و نتیجه دیباگ با استفاده از F11 به صورت زیر می‌شود:

همانطور که مشاهده می‌کنید برنامه را با کلید F11 اجرا کردم. بعد از ورود به Method1، با زدن کلید F11 دستور بعدی، break point درون Method2 است.

ConditionalAttribute

شما با استفاده از Conditional می توانید اجرای یک متد را به شناساننده پیش پردازشی ( pre-processing identifier ) وابسته کنید. ConditionalAttribute می‌تواند بر روی یک کلاس یا یک متد بکار برده شود.
class Program
{
    public static void Main(string[] args)
    {
        DebugMode();
    }

    [Conditional("DEBUG")]
    public static void DebugMode()
    {
        Console.WriteLine( "Debug mode" );
    }
}
در صورتی که مثال بالا را در حالت Debug اجرا کنید، خروجی کنسول پیام Debug mode است و در صورتی که در حالت Release اجرا کنید، متد DebugMode اجرا نخواهد شد.
نکته: شما می‌توانید با استفاده از دستور define# (در بیرون از فضای نام) مقدار سفارشی خود را تعریف کنید.
#define ReleaseMode


Flags Enum Attribute

ویژگی Flags برای پوشش فیلدهای بیتی و انجام مقایسه بیتی استفاده می‌شود. از این ویژگی باید برای زمانیکه یک داده شمارشی می‌تواند چندین مقدار را به صورت همزمان داشته باشد، استفاده کرد.
[System.Flags]
public enum Permission
{
    View = 1,
    Insert = 2,
    Update = 4,
    Delete = 8
}
این نکته خیلی مهم است که Flags به صورت خودکار، مقادیر enum را به توان دو نمی‌رساند و شما باید به صورت دستی این مقادیر را تعیین کنید. در صورتیکه مقادیر عددی را تعیین نکنید، enum در عملیات بیتی به درستی کار نخواهد کرد، چرا که مقدار enum از 0 شروع می‌شود و افزایش پیدا می‌کند.  
public static void Main( string[] args )
{
    var permission = ( Permission.View | Permission.Insert ).ToString();
    Console.WriteLine( permission ); // Displays ‘View, Insert’

    var userPermission = Permission.View | Permission.Insert | Permission.Update | Permission.Delete;
    // To retrieve the value from property you can do this
    if ( ( userPermission & Permission.Delete ) == Permission.Delete )
    {
        Console.WriteLine( "کاربر دارای مجوز دسترسی به عملیات حذف می‌باشد" );
    }

    // In .NET 4 and later
    Console.WriteLine( userPermission.HasFlag( Permission.Delete )
                            ? "کاربر دارای مجوز دسترسی به عملیات حذف می‌باشد"
                            : "کاربر مجوز دسترسی به عملیات حذف را ندارد");
}

نکته: در صورتیکه مقداری را برای enum تعریف کرده باشید، نمی‌توانید آن را با مقدار 0 مشخص کنید (در زمانی که ویژگی flags را بر روی enum اضافه کرده باشید)، چرا که با استفاده از عملیات بیتی AND نمی‌توانید دارا بودن آن مقدار را تست کنید و همیشه نتیجه صفر خواهد بود.


Dynamically Compile and Execute C# Code

CodeDOM

با استفاده از CodeDOM می‌توانید یک سورس کد را به صورت یک فایل اسمبلی کامپایل و ذخیره کنید.
public static void Main( string[] args )
{
    var sourceCode = @"class DotNetTips
                        {
                            public void Print()
                            {
                                System.Console.WriteLine("".Net Tips"");
                            }
                        }";
    var compiledAssembly = CompileSourceCodeDom( sourceCode );
    ExecuteFromAssembly( compiledAssembly );
}

static Assembly CompileSourceCodeDom( string sourceCode )
{
    CodeDomProvider csharpCodeProvider = new CSharpCodeProvider();
    var cp = new CompilerParameters
                {
                    GenerateExecutable = false
                };
    cp.ReferencedAssemblies.Add( "System.dll" );
    var cr = csharpCodeProvider.CompileAssemblyFromSource( cp,
                                                            sourceCode );
    return cr.CompiledAssembly;
}
همانطور که در مثال بالا مشاهده می‌کنید، متغیر sourceCode حاوی کد مربوط به یک کلاس #C می‌باشد که یک متد Print در آن تعریف شده است.


Roslyn

سکوی کامپایلر دات نت " Roslyn "،  کامپایلرهای متن باز #C و  VB.NET را به همراه APIهای تجزیه و تحلیل کد ارائه کرده است که با استفاده از این APIها می‌توان ابزارهای آنالیز کد جهت استفاده در ویژوال استودیو را ایجاد کرد.

برای استفاده از Roslyn باید این کتابخانه را نصب کنید

Install-Package Microsoft.CodeAnalysis

حال مثال قبل را با استفاده از Roslyn بازنویسی می‌کنیم:

public static void Main(string[] args)
{
    var sourceCode = @"class DotNetTips
                        {
                            public void Print()
                            {
                                System.Console.WriteLine("".Net Tips"");
                            }
                        }";
    var compiledAssembly = CompileSourceRoslyn( sourceCode );
    ExecuteFromAssembly( compiledAssembly );
}

private static Assembly CompileSourceRoslyn(string sourceCode)
{
    using ( var memoryStream = new MemoryStream() )
    {
        var assemblyFileName = string.Concat( Guid.NewGuid().ToString(),
                                                ".dll" );
        var compilation = CSharpCompilation.Create( assemblyFileName,
                                                    new[]
                                                    {
                                                        CSharpSyntaxTree.ParseText( sourceCode )
                                                    },
                                                    new[]
                                                    {
                                                        MetadataReference.CreateFromFile( typeof( object ).Assembly.Location )
                                                    },
                                                    new CSharpCompilationOptions( OutputKind.DynamicallyLinkedLibrary ) );
        compilation.Emit( memoryStream );
        var assembly = Assembly.Load( memoryStream.GetBuffer() );
        return assembly;
    }
}

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

static void ExecuteFromAssembly( Assembly assembly )
{
    var helloKittyPrinterType = assembly.GetType( "DotNetTips" );
    var printMethod = helloKittyPrinterType.GetMethod( "Print" );
    var kitty = assembly.CreateInstance( "DotNetTips" );
    printMethod.Invoke( kitty,
                        BindingFlags.InvokeMethod,
                        null,
                        null,
                        CultureInfo.CurrentCulture );
}


مطالب
ASP.NET MVC #11

بررسی نکات تکمیلی Model binder در ASP.NET MVC

یک برنامه خالی جدید ASP.NET MVC را شروع کنید و سپس مدل زیر را به پوشه Models آن اضافه نمائید:

using System;

namespace MvcApplication7.Models
{
public class User
{
public int Id { set; get; }
public string Name { set; get; }
public string Password { set; get; }
public DateTime AddDate { set; get; }
public bool IsAdmin { set; get; }
}
}

از این مدل چند مقصود ذیل دنبال می‌شوند:
استفاده از Id به عنوان primary key برای edit و update رکوردها. استفاده از DateTime برای اینکه اگر کاربری اطلاعات بی ربطی را وارد کرد چگونه باید این مشکل را در حالت model binding خودکار تشخیص داد و استفاده از IsAdmin برای یادآوری یک نکته امنیتی بسیار مهم که اگر حین model binding خودکار به آن توجه نشود، سایت را با مشکلات حاد امنیتی مواجه خواهد کرد. سیستم پیشرفته است. می‌تواند به صورت خودکار ورودی‌های کاربر را تبدیل به یک شیء حاضر و آماده کند ... اما باید حین استفاده از این قابلیت دلپذیر به یک سری نکات امنیتی هم دقت داشت تا سایت ما به نحو دلپذیری هک نشود!

در ادامه یک کنترلر جدید به نام UserController را به پوشه کنترلرهای پروژه اضافه نمائید. همچنین نام کنترلر پیش فرض تعریف شده در قسمت مسیریابی فایل Global.asax.cs را هم به User تغییر دهید تا در هربار اجرای برنامه در VS.NET، نیازی به تایپ آدرس‌های مرتبط با UserController نداشته باشیم.
یک منبع داده تشکیل شده در حافظه را هم برای نمایش لیستی از کاربران، به نحو زیر به پروژه اضافه خواهیم کرد:

using System;
using System.Collections.Generic;

namespace MvcApplication7.Models
{
public class Users
{
public IList<User> CreateInMemoryDataSource()
{
return new[]
{
new User { Id = 1, Name = "User1", Password = "123", IsAdmin = false, AddDate = DateTime.Now },
new User { Id = 2, Name = "User2", Password = "456", IsAdmin = false, AddDate = DateTime.Now },
new User { Id = 3, Name = "User3", Password = "789", IsAdmin = true, AddDate = DateTime.Now }
};
}
}
}

در اینجا فعلا هدف آشنایی با زیر ساخت‌های ASP.NET MVC است و درک صحیح نحوه کارکرد آن. مهم نیست از EF استفاده می‌کنید یا NH یا حتی ADO.NET کلاسیک و یا از Micro ORMهایی که پس از ارائه دات نت 4 مرسوم شده‌اند. تهیه یک ToList یا Insert و Update با این فریم ورک‌ها خارج از بحث جاری هستند.

سورس کامل کنترلر User به شرح زیر است:

using System;
using System.Linq;
using System.Web.Mvc;
using MvcApplication7.Models;

namespace MvcApplication7.Controllers
{
public class UserController : Controller
{
[HttpGet]
public ActionResult Index()
{
var usersList = new Users().CreateInMemoryDataSource();
return View(usersList); // Shows the Index view.
}

[HttpGet]
public ActionResult Details(int id)
{
var user = new Users().CreateInMemoryDataSource().FirstOrDefault(x => x.Id == id);
if (user == null)
return View("Error");
return View(user); // Shows the Details view.
}

[HttpGet]
public ActionResult Create()
{
var user = new User { AddDate = DateTime.Now };
return View(user); // Shows the Create view.
}

[HttpPost]
public ActionResult Create(User user)
{
if (this.ModelState.IsValid)
{
// todo: Add record
return RedirectToAction("Index");
}
return View(user); // Shows the Create view again.
}

[HttpGet]
public ActionResult Edit(int id)
{
var user = new Users().CreateInMemoryDataSource().FirstOrDefault(x => x.Id == id);
if (user == null)
return View("Error");
return View(user); // Shows the Edit view.
}

[HttpPost]
public ActionResult Edit(User user)
{
if (this.ModelState.IsValid)
{
// todo: Edit record
return RedirectToAction("Index");
}
return View(user); // Shows the Edit view again.
}

[HttpPost]
public ActionResult Delete(int id)
{
// todo: Delete record
return RedirectToAction("Index");
}
}
}

توضیحات:

ایجاد خودکار فرم‌های ورود اطلاعات

در قسمت قبل برای توضیح دادن نحوه ایجاد فرم‌ها در ASP.NET MVC و همچنین نحوه نگاشت اطلاعات آن‌ها به اکشن متدهای کنترلرها، فرم‌های مورد نظر را دستی ایجاد کردیم.
اما باید درنظر داشت که برای ایجاد Viewها می‌توان از ابزار توکار خود VS.NET نیز استفاده کرد و سپس اطلاعات و فرم‌های تولیدی را سفارشی نمود. این سریع‌ترین راه ممکن است زمانیکه مدل مورد استفاده کاملا مشخص است و می‌خواهیم Strongly typed views را ایجاد کنیم.
برای نمونه بر روی متد Index کلیک راست کرده و گزینه Add view را انتخاب کنید. در اینجا گزینه‌ی create a strongly typed view را انتخاب کرده و سپس از لیست مدل‌ها، User را انتخاب نمائید. Scaffold template را هم بر روی حالت List قرار دهید.
برای متد Details هم به همین نحو عمل نمائید.
برای ایجاد View متناظر با متد Create در حالت HttpGet، تمام مراحل یکی است. فقط Scaffold template انتخابی را بر روی Create قرار دهید تا فرم ورود اطلاعات، به صورت خودکار تولید شود.
متد Create در حالت HttpPost نیازی به View اضافی ندارد. چون صرفا قرار است اطلاعاتی را از سرور دریافت و ثبت کند.
برای ایجاد View متناظر با متد Edit در حالت HttpGet، باز هم مراحل مانند قبل است با این تفاوت که Scaffold template انتخابی را بر روی گزینه Edit قرار دهید تا فرم ویرایش اطلاعات کاربر به صورت خودکار به پروژه اضافه شود.
متد Edit در حالت HttpPost نیازی به View اضافی ندارد و کارش تنها دریافت اطلاعات از سرور و به روز رسانی بانک اطلاعاتی است.
به همین ترتیب متد Delete نیز، نیازی به View خاصی ندارد. در اینجا بر اساس primary key دریافتی، می‌توان یک کاربر را یافته و حذف کرد.



سفارشی سازی Viewهای خودکار تولیدی

با کمک امکانات Scaffolding نامبرده شده، حجم قابل توجهی کد را در اندک زمانی می‌توان تولید کرد. بدیهی است حتما نیاز به سفارشی سازی کدهای تولیدی وجود خواهد داشت. مثلا شاید نیازی نباشد فیلد پسود کاربر، در حین نمایش لیست کاربران، نمایش داده شود. می‌شود کلا این ستون را حذف کرد و از این نوع مسایل.
یک مورد دیگر را هم در Viewهای تولیدی حتما نیاز است که ویرایش کنیم. آن هم مرتبط است به لینک حذف اطلاعات یک کاربر در صفحه Index.cshtml:

@Html.ActionLink("Delete", "Delete", new { id=item.Id }

در قسمت قبل هم عنوان شد که اعمال حذف باید بر اساس HttpPost محدود شوند تا بتوان میزان امنیت برنامه را بهبود داد. متد Delete هم در کنترلر فوق تنها به حالت HttpPost محدود شده است. بنابراین ActionLink پیش فرض را حذف کرده و بجای آن فرم و دکمه زیر را قرار می‌دهیم تا اطلاعات به سرور Post شوند:

@using (Html.BeginForm(actionName: "Delete", controllerName: "User", routeValues: new { id = item.Id }))
{
<input type="submit" value="Delete"
onclick="return confirm ('Do you want to delete this record?');" />
}

در اینجا نحوه ایجاد یک فرم، که id رکورد متناظر را به سرور ارسال می‌کند، مشاهده می‌کنید.



علت وجود دو متد، به ازای هر Edit یا Create

به ازای هر کدام از متدهای Edit و Create دو متد HttpGet و HttpPost را ایجاد کرده‌ایم. کار متدهای HttpGet نمایش View‌های متناظر به کاربر هستند. بنابراین وجود آن‌ها ضروری است. در این حالت چون از دو Verb متفاوت استفاده شده، می‌توان متدهای هم نامی را بدون مشکل استفاده کرد. به هر کدام از افعال Get و Post و امثال آن، یک Http Verb گفته می‌شود.



بررسی معتبر بودن اطلاعات دریافتی

کلاس پایه Controller که کنترلرهای برنامه از آن مشتق می‌شوند، شامل یک سری خواص و متدهای توکار نیز هست. برای مثال توسط خاصیت this.ModelState.IsValid می‌توان بررسی کرد که آیا Model دریافتی معتبر است یا خیر. برای بررسی این مورد، یک breakpoint را بر روی سطر this.ModelState.IsValid در متد Create قرار دهید. سپس به صفحه ایجاد کاربر جدید مراجعه کرده و مثلا بجای تاریخ روز، abcd را وارد کنید. سپس فرم را به سرور ارسال نمائید. در این حالت مقدار خاصیت this.ModelState.IsValid مساوی false می‌باشد که حتما باید به آن پیش از ثبت اطلاعات دقت داشت.



شبیه سازی عملکرد ViewState در ASP.NET MVC

در متدهای Create و Edit در حالت Post، اگر اطلاعات Model معتبر نباشند، مجددا شیء User دریافتی، به View بازگشت داده می‌شود. چرا؟
صفحات وب، زمانیکه به سرور ارسال می‌شوند، تمام اطلاعات کنترل‌های خود را از دست خواهد داد (صفحه پاک می‌شود، چون مجددا یک صفحه خالی از سرور دریافت خواهد شد). برای رفع این مشکل در ASP.NET Web forms، از مفهومی به نام ViewState کمک می‌گیرند. کار ViewState ذخیره موقت اطلاعات فرم جاری است برای استفاده مجدد پس از Postback. به این معنا که پس از ارسال فرم به سرور، اگر کاربری در textbox اول مقدار abc را وارد کرده بود، پس از نمایش مجدد فرم، مقدار abc را در همان textbox مشاهده خواهد کرد (شبیه سازی برنامه‌های دسکتاپ در محیط وب). بدیهی است وجود ViewState برای ذخیره سازی این نوع اطلاعات، حجم صفحه را بالا می‌برد (بسته به پیچیدگی صفحه ممکن است به چند صد کیلوبایت هم برسد).
در ASP.NET MVC بجای استفاده از ترفندی به نام ViewState، مجددا اطلاعات همان مدل متناظر با View را بازگشت می‌دهند. در این حالت پس از ارسال صفحه به سرور و نمایش مجدد صفحه ورود اطلاعات، تمام کنترل‌ها با همان مقادیر قبلی وارد شده توسط کاربر قابل مشاهده خواهند بود (مدل مشخص است، View ما هم از نوع strongly typed می‌باشد. در این حالت فریم ورک می‌داند که اطلاعات را چگونه به کنترل‌های قرار گرفته در صفحه نگاشت کند).
در مثال فوق، اگر اطلاعات وارد شده صحیح باشند، کاربر به صفحه Index هدایت خواهد شد. در غیراینصورت مجددا همان View جاری با همان اطلاعات model قبلی که کاربر تکمیل کرده است به او برای تصحیح، نمایش داده می‌شود. این مساله هم جهت بالا بردن سهولت کاربری برنامه بسیار مهم است. تصور کنید که یک فرم خالی با پیغام «تاریخ وارد شده معتبر نیست» مجدا به کاربر نمایش داده شود و از او درخواست کنیم که تمام اطلاعات دیگر را نیز از صفر وارد کند چون اطلاعات صفحه پس از ارسال به سرور پاک شده‌اند؛ که ... اصلا قابل قبول نیست و فوق‌العاده برنامه را غیرحرفه‌ای نمایش می‌دهد.



خطاهای نمایش داده شده به کاربر

به صورت پیش فرض خطایی که به کاربر نمایش داده می‌شود، استثنایی است که توسط فریم ورک صادر شده است. برای مثال نتوانسته است abcd را به یک تاریخ معتبر تبدیل کند. می‌توان توسط this.ModelState.AddModelError خطایی را نیز در اینجا اضافه کرد و پیغام بهتری را به کاربر نمایش داد. یا توسط یک سری data annotations هم کار اعتبار سنجی را سفارشی کرد که بحث آن به صورت جداگانه در یک قسمت مستقل بررسی خواهد شد.
ولی به صورت خلاصه اگر به فرم‌های تولید شده توسط VS.NET دقت کنید، در ابتدای هر فرم داریم:

@Html.ValidationSummary(true)

در اینجا خطاهای عمومی در سطح مدل نمایش داده می‌شوند. برای اضافه کردن این نوع خطاها، در متد AddModelError، مقدار key را خالی وارد کنید:

ModelState.AddModelError(string.Empty, "There is something wrong with model.");

همچنین در این فرم‌ها داریم:
@Html.EditorFor(model => model.AddDate)
@Html.ValidationMessageFor(model => model.AddDate)

EditorFor سعی می‌کند اندکی هوش به خرج دهد. یعنی اگر خاصیت دریافتی مثلا از نوع bool بود، خودش یک checkbox را در صفحه نمایش می‌دهد. همچنین بر اساس متادیتا یک خاصیت نیز می‌تواند تصمیم گیری را انجام دهد. این متادیتا منظور attributes و data annotations ایی است که به خواص یک مدل اعمال می‌شود. مثلا اگر ویژگی HiddenInput را به یک خاصیت اعمال کنیم، به شکل یک فیلد مخفی در صفحه ظاهر خواهد شد.
یا متد Html.DisplayFor، اطلاعات را به صورت فقط خواندنی نمایش می‌دهد. اصطلاحا به این نوع متدها، Templated Helpers هم گفته می‌شود. بحث بیشتر درباره‌ای این موارد به قسمتی مجزا و مستقل موکول می‌گردد. برای نمونه کل فرم ادیت برنامه را حذف کنید و بجای آن بنویسید Html.EditorForModel و سپس برنامه را اجرا کنید. یک فرم کامل خودکار ویرایش اطلاعات را مشاهده خواهید کرد (و البته نکات سفارشی سازی آن به یک قسمت کامل نیاز دارند).
در اینجا متد ValidationMessageFor کار نمایش خطاهای اعتبارسنجی مرتبط با یک خاصیت مشخص را انجام می‌دهد. بنابراین اگر قصد ارائه خطایی سفارشی و مخصوص یک فیلد مشخص را داشتید، در متد AddModelError، مقدار پارامتر اول یا همان key را مساوی نام خاصیت مورد نظر قرار دهید.


مقابله با مشکل امنیتی Mass Assignment در حین کار با Model binders

استفاده از Model binders بسیار لذت بخش است. یک شیء را به عنوان پارامتر اکشن متد خود معرفی می‌کنیم. فریم ورک هم در ادامه سعی می‌کند تا اطلاعات فرم را به خواص این شیء نگاشت کند. بدیهی است این روش نسبت به روش ASP.NET Web forms که باید به ازای تک تک کنترل‌های موجود در صفحه یکبار کار دریافت اطلاعات و مقدار دهی خواص یک شیء را انجام داد، بسیار ساده‌تر و سریعتر است.
اما اگر همین سیستم پیشرفته جدید ناآگاهانه مورد استفاده قرار گیرد می‌تواند منشاء حملات ناگواری شود که به نام «Mass Assignment» شهرت یافته‌اند.
همان صفحه ویرایش اطلاعات را درنظر بگیرید. چک باکس IsAdmin قرار است در قسمت مدیریتی برنامه تنظیم شود. اگر کاربری نیاز داشته باشد اطلاعات خودش را ویرایش کند، مثلا پسوردش را تغییر دهد، با یک صفحه ساده کلمه عبور قبلی را وارد کنید و دوبار کلمه عبور جدید را نیز وارد نمائید، مواجه خواهد شد. خوب ... اگر همین کاربر صفحه را جعل کند و فیلد چک باکس IsAdmin را به صفحه اضافه کند چه اتفاقی خواهد افتاد؟ بله ... مشکل هم همینجا است. در اینصورت کاربر عادی می‌تواند دسترسی خودش را تا سطح ادمین بالا ببرد، چون model binder اطلاعات IsAdmin را از کاربر دریافت کرده و به صورت خودکار به model ارائه شده، نگاشت کرده است.
برای مقابله با این نوع حملات چندین روش وجود دارند:
الف) ایجاد لیست سفید
به کمک ویژگی Bind می‌توان لیستی از خواص را جهت به روز رسانی به model binder معرفی کرد. مابقی ندید گرفته خواهند شد:

public ActionResult Edit([Bind(Include = "Name, Password")] User user)

در اینجا تنها خواص Name و Password توسط model binder به خواص شیء User نگاشت می‌شوند.
به علاوه همانطور که در قسمت قبل نیز ذکر شد، متد edit را به شکل زیر نیز می‌توان بازنویسی کرد. در اینجا متدهای توکار UpdateModel و TryUpdateModel نیز لیست سفید خواص مورد نظر را می‌پذیرند (اعمال دستی model binding):

[HttpPost]
public ActionResult Edit()
{
var user = new User();
if(TryUpdateModel(user, includeProperties: new[] { "Name", "Password" }))
{
// todo: Edit record
return RedirectToAction("Index");
}
return View(user); // Shows the Edit view again.
}


ب) ایجاد لیست سیاه
به همین ترتیب می‌توان تنها خواصی را معرفی کرد که باید صرفنظر شوند:
public ActionResult Edit([Bind(Exclude = "IsAdmin")] User user)

در اینجا از خاصیت IsAdmin صرف نظر گردیده و از مقدار ارسالی آن توسط کاربر استفاده نخواهد شد.
و یا می‌توان پارامتر excludeProperties متد TryUpdateModel را نیز مقدار دهی کرد.

لازم به ذکر است که ویژگی Bind را به کل یک کلاس هم می‌توان اعمال کرد. برای مثال:

using System;
using System.Web.Mvc;

namespace MvcApplication7.Models
{
[Bind(Exclude = "IsAdmin")]
public class User
{
public int Id { set; get; }
public string Name { set; get; }
public string Password { set; get; }
public DateTime AddDate { set; get; }
public bool IsAdmin { set; get; }
}
}

این مورد اثر سراسری داشته و قابل بازنویسی نیست. به عبارتی حتی اگر در متدی خاصیت IsAdmin را مجددا الحاق کنیم، تاثیری نخواهد داشت.
یا می‌توان از ویژگی ReadOnly هم استفاده کرد:
using System;
using System.ComponentModel;

namespace MvcApplication7.Models
{
public class User
{
public int Id { set; get; }
public string Name { set; get; }
public string Password { set; get; }
public DateTime AddDate { set; get; }

[ReadOnly(true)]
public bool IsAdmin { set; get; }
}
}

در این حالت هم خاصیت IsAdmin هیچگاه توسط model binder به روز و مقدار دهی نخواهد شد.

ج) استفاده از ViewModels
این راه حلی است که بیشتر مورد توجه معماران نرم افزار است و البته کسانی که پیشتر با الگوی MVVM کار کرده باشند این نام برایشان آشنا است؛ اما در اینجا مفهوم متفاوتی دارد. در الگوی MVVM، کلاس‌های ViewModel شبیه به کنترلرها در MVC هستند یا به عبارتی همانند رهبر یک اکستر عمل می‌کنند. اما در الگوی MVC خیر. در اینجا فقط مدل یک View هستند و نه بیشتر. هدف هم این است که بین Domain Model و View Model تفاوت قائل شد.
کار View model در الگوی MVC، شکل دادن به چندین domain model و همچنین اطلاعات اضافی دیگری که نیاز هستند، جهت استفاده نهایی توسط یک View می‌باشد. به این ترتیب View با یک شیء سر و کار خواهد داشت و همچنین منطق شکل دهی به اطلاعات مورد نیازش هم از داخل View حذف شده و به خواص View model در زمان تشکیل آن منتقل می‌شود.
مشخصات یک View model خوب به شرح زیر است:
الف) رابطه بین یک View و View model آن، رابطه‌ای یک به یک است. به ازای هر View، بهتر است یک کلاس View model وجود داشته باشد.
ب) View ساختار View model را دیکته می‌کند و نه کنترلر.
ج) View modelها صرفا یک سری کلاس POCO (کلاس‌هایی تشکیل شده از خاصیت، خاصیت، خاصیت ....) هستند که هیچ منطقی در آن‌ها قرار نمی‌گیرد.
د) View model باید حاوی تمام اطلاعاتی باشد که View جهت رندر نیاز دارد و نه بیشتر و الزامی هم ندارد که این اطلاعات مستقیما به domain models مرتبط شوند. برای مثال اگر قرار است firstName+LastName در View نمایش داده شود، کار این جمع زدن باید حین تهیه View Model انجام شود و نه داخل View. یا اگر قرار است اطلاعات عددی با سه رقم جدا کننده به کاربر نمایش داده شوند، وظیفه View Model است که یک خاصیت اضافی را برای تهیه این مورد تدارک ببیند. یا مثلا اگر یک فرم ثبت نام داریم و در این فرم لیستی وجود دارد که تنها Id عنصر انتخابی آن در Model اصلی مورد استفاده قرار می‌گیرد، تهیه اطلاعات این لیست هم کار ViewModel است و نه اینکه مدام به Model اصلی بخواهیم خاصیت اضافه کنیم.

ViewModel چگونه پیاده سازی می‌شود؟
اکثر مقالات را که مطالعه کنید، این روش را توصیه می‌کنند:

public class MyViewModel
{
    public SomeDomainModel1 Model1 { get; set; }
    public SomeDomainModel2 Model2 { get; set; }
    ...
}

یعنی اینکه View ما به اطلاعات مثلا دو Model نیاز دارد. این‌ها را به این شکل محصور و کپسوله می‌کنیم. اگر View، واقعا به تمام فیلدهای این کلاس‌ها نیاز داشته باشد، این روش صحیح است. در غیر اینصورت، این روش نادرست است (و متاسفانه همه جا هم دقیقا به این شکل تبلیغ می‌شود).
ViewModel محصور کننده یک یا چند مدل نیست. در اینجا حس غلط کار کردن با یک ViewModel را داریم. ViewModel فقط باید ارائه کننده اطلاعاتی باشد که یک View نیاز دارد و نه بیشتر و نه تمام خواص تمام کلاس‌های تعریف شده. به عبارتی این نوع تعریف صحیح است:

public class MyViewModel
{
    public string SomeExtraField1 { get; set; }
    public string SomeExtraField2 { get; set; }
public IEnumerable<SelectListItem> StateSelectList { get; set; }
// ...
    public string PersonFullName { set; set; }
}

در اینجا، View متناظری، قرار است نام کامل یک شخص را به علاوه یک سری اطلاعات اضافی که در domain model نیست، نمایش دهد. مثلا نمایش نام استان‌ها که نهایتا Id انتخابی آن قرار است در برنامه استفاده شود.
خلاصه علت وجودی ViewModel این موارد است:
الف) Model برنامه را مستقیما در معرض استفاده قرار ندهیم (عدم رعایت این نکته به مشکلات امنیتی حادی هم حین به روز رسانی اطلاعات ممکن است ختم ‌شود که پیشتر توضیح داده شد).
ب) فیلدهای نمایشی اضافی مورد نیاز یک View را داخل Model برنامه تعریف نکنیم (مثلا تعاریف عناصر یک دراپ داون لیست، جایش اینجا نیست. مدل فقط نیاز به Id عنصر انتخابی آن دارد).

با این توضیحات، اگر View به روز رسانی اطلاعات کلمه عبور کاربر، تنها به اطلاعات id آن کاربر و کلمه عبور او نیاز دارد، فقط باید همین اطلاعات را در اختیار View قرار داد و نه بیشتر:

namespace MvcApplication7.Models
{
public class UserViewModel
{
public int Id { set; get; }
public string Password { set; get; }
}
}

به این ترتیب دیگر خاصیت IsAdming اضافه‌ای وجود ندارد که بخواهد مورد حمله واقع شود.



استفاده از model binding برای آپلود فایل به سرور

برای آپلود فایل به سرور تنها کافی است یک اکشن متد به شکل زیر را تعریف کنیم. HttpPostedFileBase نیز یکی دیگر از model binderهای توکار ASP.NET MVC است:

[HttpGet]
public ActionResult Upload()
{
return View(); // Shows the upload page
}

[HttpPost]
public ActionResult Upload(System.Web.HttpPostedFileBase file)
{
string filename = Server.MapPath("~/files/somename.ext");
file.SaveAs(filename);
return RedirectToAction("Index");
}

View متناظر هم می‌تواند به شکل زیر باشد:

@{
ViewBag.Title = "Upload";
}
<h2>
Upload</h2>
@using (Html.BeginForm(actionName: "Upload", controllerName: "User",
method: FormMethod.Post,
htmlAttributes: new { enctype = "multipart/form-data" }))
{
<text>Upload a photo:</text> <input type="file" name="photo" />
<input type="submit" value="Upload" />
}

اگر دقت کرده باشید در طراحی ASP.NET MVC از anonymously typed objects زیاد استفاده می‌شود. در اینجا هم برای معرفی enctype فرم آپلود، مورد استفاده قرار گرفته است. به عبارتی هر جایی که مشخص نبوده چه تعداد ویژگی یا کلا چه ویژگی‌ها و خاصیت‌هایی را می‌توان تنظیم کرد، اجازه تعریف آن‌ها را به صورت anonymously typed objects میسر کرده‌اند. یک نمونه دیگر آن در متد routes.MapRoute فایل Global.asax.cs است که پارامتر سوم دریافت مقدار پیش فرض‌ها نیز anonymously typed object است. یا نمونه دیگر آن‌را در همین قسمت در جایی که لینک delete را به فرم تبدیل کردیم مشاهده نمودید. مقدار routeValues هم یک anonymously typed object معرفی شد.



سفارشی سازی model binder پیش فرض ASP.NET MVC

در همین مثال فرض کنید تاریخ را به صورت شمسی از کاربر دریافت می‌کنیم. خاصیت تعریف شده هم DateTime میلادی است. به عبارتی model binder حین تبدیل رشته تاریخ شمسی دریافتی به تاریخ میلادی با شکست مواجه شده و نهایتا خاصیت this.ModelState.IsValid مقدارش false خواهد بود. برای حل این مشکل چکار باید کرد؟
برای این منظور باید نحوه پردازش یک نوع خاص را سفارشی کرد. ابتدا با پیاده سازی اینترفیس IModelBinder شروع می‌کنیم. توسط bindingContext.ValueProvider می‌توان به مقداری که کاربر وارد کرده در میانه راه دسترسی یافت. آن‌را تبدیل کرده و نمونه صحیح را بازگشت داد.
نمونه‌ای از این پیاده سازی را در ادامه ملاحظه می‌کنید:

using System;
using System.Globalization;
using System.Web.Mvc;

namespace MvcApplication7.Binders
{
public class PersianDateModelBinder : IModelBinder
{

public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
var modelState = new ModelState { Value = valueResult };
object actualValue = null;
try
{
var parts = valueResult.AttemptedValue.Split('/'); //ex. 1391/1/19
if (parts.Length != 3) return null;
int year = int.Parse(parts[0]);
int month = int.Parse(parts[1]);
int day = int.Parse(parts[2]);
actualValue = new DateTime(year, month, day, new PersianCalendar());
}
catch (FormatException e)
{
modelState.Errors.Add(e);
}

bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return actualValue;
}
}
}

سپس برای معرفی PersianDateModelBinder جدید تنها کافی است سطر زیر را

ModelBinders.Binders.Add(typeof(DateTime), new PersianDateModelBinder());

به متد Application_Start قرار گرفته در فایل Global.asax.cs برنامه اضافه کرد. از این پس کاربران می‌توانند تاریخ‌ها را در برنامه شمسی وارد کنند و model binder بدون مشکل خواهد توانست اطلاعات ورودی را به معادل DateTime میلادی آن تبدیل کند و استفاده نماید.
تعریف مدل بایندر سفارشی در فایل Global.asax.cs آن‌را به صورت سراسری در تمام مدل‌ها و اکشن‌متدها فعال خواهد کرد. اگر نیاز بود تنها یک اکشن متد خاص از این مدل بایندر سفارشی استفاده کند می‌توان به روش زیر عمل کرد:

public ActionResult Create([ModelBinder(typeof(PersianDateModelBinder))] User user)

همچنین ویژگی ModelBinder را به یک کلاس هم می‌توان اعمال کرد:

[ModelBinder(typeof(PersianDateModelBinder))]
public class User
{


مطالب
QueryOver در NHibernate و تفاوت‌های آن با LINQ to NH

در NHibernate چندین و چند روش، جهت تهیه کوئری‌ها وجود دارد که QueryOver یکی از آن‌ها است (+). QueryOver نسبت به LINQ to NH سازگاری بهتری با ساز و کار درونی NHibernate دارد؛ برای مثال امکان یکپارچگی آن با سطح دوم کش. هر چند ظاهر QueryOver با LINQ یکی است، اما در عمل متفاوتند و راه و روش خاص خودش را طلب می‌کند. برای مثال در LINQ to NH می‌تواند نوشت x.Property.Contains اما در QueryOver متدی به نام contains قابل استفاده نیست (هر چند در Intellisense ظاهر می‌شود اما عملا تعریف نشده است و نباید آن‌را با LINQ اشتباه گرفت) و سعی در استفاده از آن‌ها به استثناهای زیر ختم می‌شوند:
Unrecognised method call: System.String:Boolean StartsWith(System.String)
Unrecognised method call: System.String:Boolean Contains(System.String)
برای مثال کلاس زیر را در نظر بگیرید؛ کوئری‌های مطلب جاری بر این اساس تهیه خواهند شد:
using NHibernate.Validator.Constraints;

namespace NH3Test.MappingDefinitions.Domain
{
public class Account
{
public virtual int Id { get; set; }

[NotNullNotEmpty]
[Length(Min = 3, Max = 120, Message = "طول نام باید بین 3 و 120 کاراکتر باشد")]
public virtual string Name { get; set; }

[NotNull]
public virtual int Balance { set; get; }
}
}

1) یافتن رکوردهایی که در یک مجموعه‌ی مشخص قرار دارند. برای مثال balance آن‌ها مساوی 10 و 12 است:
var list = new[]  { 12,10};
var resultList = session.QueryOver<Account>()
.WhereRestrictionOn(p => p.Balance)
.IsIn(list)
.List();

SELECT
this_.AccountId as AccountId0_0_,
this_.Name as Name0_0_,
this_.Balance as Balance0_0_
FROM
Accounts this_
WHERE
this_.Balance in (
@p0 /* = 10 */, @p1 /* = 12 */
)

2) پیاده سازی همان متد Contains ذکر شده، در QueryOver:
var accountsContianX = session.QueryOver<Account>()
.WhereRestrictionOn(x => x.Name)
.IsLike("X", NHibernate.Criterion.MatchMode.Anywhere)
.List();

SELECT
this_.AccountId as AccountId0_0_,
this_.Name as Name0_0_,
this_.Balance as Balance0_0_
FROM
Accounts this_
WHERE
this_.Name like @p0 /* = %X% */

در اینجا بر اساس مقادیر مختلف MatchMode می‌توان متدهای StartsWith (MatchMode.Start) ، EndsWith (MatchMode.End) ، Equals (MatchMode.Exact) را نیز تهیه نمود.

انجام مثال دوم راه ساده‌تری نیز دارد. قسمت WhereRestrictionOn و IsLike به صورت یک سری extension متد ویژه در فضای نام NHibernate.Criterion تعریف شده‌اند. ابتدا این فضای نام را به کلاس جاری افزوده و سپس می‌توان نوشت :
using NHibernate.Criterion;
...
var accountsContianX = session.QueryOver<Account>()
.Where(x => x.Name.IsLike("%X%"))
.List();

این فضای نام شامل چهار extension method به نام‌های IsLike ، IsInsensitiveLike ، IsIn و IsBetween است.


چگونه extension method سفارشی خود را تهیه کنیم؟

بهترین کار این است که به سورس NHibernate ، فایل‌های RestrictionsExtensions.cs و ExpressionProcessor.cs که تعاریف متد IsLike در آن‌ها وجود دارد مراجعه کرد. در اینجا می‌توان با نحوه‌ی تعریف و سپس ثبت آن در رجیستری extension methods مرتبط با QueryOver توسط متد عمومی RegisterCustomMethodCall آشنا شد. در ادامه سه کار را می‌توان انجام داد:
-متد مورد نظر را در کدهای خود (نه کدهای اصلی NH) اضافه کرده و سپس با فراخوانی RegisterCustomMethodCall آن‌را قابل استفاده نمائید.
-متد خود را به سورس اصلی NH اضافه کرده و کامپایل کنید.
-متد خود را به سورس اصلی NH اضافه کرده و کامپایل کنید (بهتر است همان روش نامگذاری بکار گرفته شده در فایل‌های ذکر شده رعایت شود). یک تست هم برای آن بنویسید (تست نویسی هم یک سری اصولی دارد (+)). سپس یک patch از آن روی آن ساخته (+) و برای تیم NH ارسال نمائید (تا جایی که دقت کردم از کلیه ارسال‌هایی که آزمون واحد نداشته باشند، صرفنظر می‌شود).

مثال:
می‌خواهیم extension متد جدیدی به نام Year را به QueryOver اضافه کنیم. این متد را هم بر اساس توابع توکار بانک‌های اطلاعاتی، تهیه خواهیم نمود. لیست کامل این نوع متدهای بومی SQL را در فایل Dialect.cs سورس‌های NH می‌توان یافت (البته به صورت پیش فرض از متد extract برای جداسازی قسمت‌های مختلف تاریخ استفاده می‌کند. این متد در فایل‌های Dialect مربوط به بانک‌های اطلاعاتی مختلف، متفاوت است و برحسب بانک اطلاعاتی جاری به صورت خودکار تغییر خواهد کرد).
using System;
using System.Linq.Expressions;
using NHibernate;
using NHibernate.Criterion;
using NHibernate.Impl;

namespace NH3Test.ConsoleApplication
{
public static class MyQueryOverExts
{
public static bool YearIs(this DateTime projection, int year)
{
throw new Exception("Not to be used directly - use inside QueryOver expression");
}

public static ICriterion ProcessAnsiYear(MethodCallExpression methodCallExpression)
{
string property = ExpressionProcessor.FindMemberExpression(methodCallExpression.Arguments[0]);
object value = ExpressionProcessor.FindValue(methodCallExpression.Arguments[1]);
return Restrictions.Eq(
Projections.SqlFunction("year", NHibernateUtil.DateTime, Projections.Property(property)),
value);
}
}

public class QueryOverExtsRegistry
{
public static void RegistrMyQueryOverExts()
{
ExpressionProcessor.RegisterCustomMethodCall(
() => MyQueryOverExts.YearIs(DateTime.Now, 0),
MyQueryOverExts.ProcessAnsiYear);
}
}
}

اکنون برای استفاده خواهیم داشت:
QueryOverExtsRegistry.RegistrMyQueryOverExts(); //یکبار در ابتدای اجرای برنامه باید ثبت شود
...
var data = session.QueryOver<Account>()
.Where(x => x.AddDate.YearIs(2010))
.List();

برای مثال اگر بانک اطلاعاتی انتخابی از نوع SQLite باشد، خروجی SQL مرتبط به شکل زیر خواهد بود:
SELECT
this_.AccountId as AccountId0_0_,
this_.Name as Name0_0_,
this_.Balance as Balance0_0_,
this_.AddDate as AddDate0_0_
FROM
Accounts this_
WHERE
strftime("%Y", this_.AddDate) = @p0 /* =2010 */


هر چند ما تابع year را در متد ProcessAnsiYear ثبت کرده‌ایم اما بر اساس فایل SQLiteDialect.cs ، تعاریف مرتبط و مخصوص این بانک اطلاعاتی (مانند متد strftime فوق) به صورت خودکار دریافت می‌گردد و کد ما مستقل از نوع بانک اطلاعاتی خواهد بود.


نکته جالب!
LINQ to NH هم قابل بسط است؛ کاری که در ORM های دیگر به این سادگی نیست. چند مثال در این زمینه:
چگونه تابع سفارشی SQL Server خود را به صورت یک extension method تعریف و استفاده کنیم: (+) ، یک نمونه دیگر: (+) و نمونه‌ای دیگر: (+).