اشتراک‌ها
سورس نگارش کامل دات نت

The referencesource repository contains sources from Microsoft .NET Reference Source that represent a subset of the .NET Framework. This subset contains similar functionality to the class libraries that are being developed in .NET Core. We intend to consult the referencesource repository as we develop .NET Core. It is also for the community to leverage to enable more scenarios for .NET developers. 

سورس نگارش کامل دات نت
مطالب
امکان تغییر رشته‌ی اتصالی به بانک اطلاعاتی در EF Core در زمان اجرای برنامه
تغییر پویای رشته‌ی اتصالی به بانک اطلاعاتی در نگارش‌های پیشین EF، مشکل بودند که نمونه‌هایی از آن را پیشتر در مطالب زیر مشاهده کرده‌اید:
- «تنظیم رشته اتصالی Entity Framework به بانک اطلاعاتی به وسیله کد»
- «استفاده از چندین بانک اطلاعاتی به صورت همزمان در EF Code First»

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


نیاز به تغییر رشته‌ی اتصالی به بانک اطلاعاتی در زمان اجرا

دلایل نیاز به امکان تغییر رشته‌ی اتصالی در زمان اجرا شامل موارد زیر هستند:
- در برنامه‌هایی کمی پیچیده‌تر و سابقه دار، ممکن است عملیات تجاری یکسال را در بانک اطلاعاتی سال 98 و دیگری را در بانک اطلاعاتی سال 99 ثبت کنید. در این حالت کاربران باید بتوانند در زمان اجرا به هر بانک اطلاعاتی که پیشتر با آن کار کرده‌اند، متصل شده و از آن استفاده کنند.
- یکی از روش‌های پیاده سازی برنامه‌های چند مستاجری، داشتن یک بانک اطلاعاتی مجزا، به ازای هر مستاجر است. در این حالت نیز تک برنامه‌ی ما باید بتواند بر اساس Id مشتری، بانک اطلاعاتی متناظری را در زمان اجرا انتخاب کند.
- نیاز به داشتن چندین context در برنامه و کار با بانک‌های اطلاعاتی متفاوت در زمان اجرا؛ مانند کار با SQL Server، اوراکل و یا SQLite


روش تغییر رشته‌ی اتصالی به بانک اطلاعاتی در EF Core در زمان اجرای برنامه

اگر به روش ثبت متداول سرویس DbContext برنامه و پروایدر آن دقت کنیم:
services.AddDbContext<ApplicationDbContext>(options =>
      options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")
));
یک action delegate قابل مشاهده‌است. کار این اکشن، تنظیم پروایدر و تمام نیازهای یک رشته‌ی اتصالی به بانک اطلاعاتی، جهت شروع به کار با Context برنامه است. نکته‌ی مهمی که در اینجا وجود دارد، فراخوانی هرباره‌ی این action، به ازای هر اتصال تشکیل شده‌است. یعنی کدهای داخل این action delegate کش نمی‌شوند و همین مساله امکان تغییر پویای آن‌ها را میسر می‌کند.

یک نکته: چون این اطلاعات کش نمی‌شوند، اگر رشته‌ی اتصالی شما ثابت است (و نیازی به تغییر آن در زمان اجرای برنامه نیست)، محل تامین آن‌را به پیش از سطر services.AddDbContext انتقال دهید و فقط نتیجه‌ی محاسبه شده‌ی نهایی را استفاده کنید تا کارآیی برنامه افزایش یابد؛ در غیراینصورت فراخوانی Configuration.GetConnectionString مدام تکرار خواهد شد.


دریافت یک قالب قابل تغییر از تنظیمات برنامه و تغییر آن با هدرهای درخواست رسیده‌ی به آن

فرض کنید قالب رشته‌ی اتصالی برنامه در فایل appsettings.json به صورت زیر است:
"ConnectionStrings": {
    "ConnectionTemplate": "Data Source=.;Initial Catalog={db_Name};Integrated Security=True",
}
و db_Name آن قرار است برای مثال از یک query string، سشن، کوکی و یا فیلد خاصی در هدر HTTP رسیده تامین شود. برای مثال سال مالی انتخابی و یا شماره مستاجر انتخابی به صورت یک فیلد خاص HTTP به سمت برنامه ارسال می‌شوند.
بنابراین اکنون نیاز است به ازای هر درخواست رسیده بتوان به سرویس IHttpContextAccessor و شیء HttpContext.Request جاری دسترسی یافت و سپس از هدرهای رسیده، برای مثال هدر ویژه‌ی tenantId و یا year را پردازش کرد؛ اما در تعریف services.AddDbContext فوق چگونه می‌توان اینکار را انجام داد؟
خوشبختانه متد services.AddDbContext، دارای یک overload دیگر نیز هست که امکان دسترسی به تمام سرویس‌های جاری سیستم را میسر می‌کند:
services.AddDbContext<ApplicationDbContext>((serviceProvider, dbContextBuilder) =>
{
   var connectionStringTemplate = Configuration.GetConnectionString("ConnectionTemplate");
   var httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>();
   var dbName = httpContextAccessor.HttpContext.Request.Headers["tenantId"].First();
   var connectionString = connectionStringTemplate.Replace("{db_Name}", dbName);
   dbContextBuilder.UseSqlServer(connectionString);
});
همانطور که مشاهده می‌کنید، overload دوم متد services.AddDbContext، امکان ارسال serviceProvider را نیز به این action delegate دارد. پس از آن می‌توان توسط متد GetRequiredService آن به هر سرویس مدنظری که در سیستم ثبت شده‌است، دسترسی یافت و برای مثال در اینجا فیلد هدر سفارشی tenantId را از آن استخراج نمود و در قالب رشته‌ی اتصالی به بانک اطلاعاتی، در زمان اجرا به صورت پویایی جایگزین کرد.
همچنین در صورت نیاز می‌توان UseSqlServer آن‌را نیز در این action delegate به هر پروایدر دیگری در زمان اجرا تغییر داد و از این لحاظ محدودیتی وجود ندارد.

یک نکته: البته برنامه نباید هر tenantId ای را پردازش کند و این خودش می‌تواند تبدیل به یک نقیصه‌ی امنیتی شود. به همین جهت برای مثال می‌توان tenantId را در یک JWT قرار داد و در حین تعیین اعتبار آن و کاربر جاری، این مقدار را نیز بررسی کرد.
اشتراک‌ها
مخزن آموزشی Net. Core با مثالهای پیاده سازی شده

در این مخزن روش‌های بهینه و توصیه شده جهت ساخت برنامه‌های تحت وب با استفاده از Net Core. در قالب 12 پروژه پیاده سازی شده است که منبع خوبی جهت الگو برداری است.

Boilerplate for ASP.NET Core reference application with Entity Framework Core, demonstrating a layered application architecture with DDD best practices. Implements NLayer Hexagonal architecture (Core, Application, Infrastructure and Presentation Layers) and Domain Driven Design (Entities, Repositories, Domain/Application Services, DTO's...) and aimed to be a Clean Architecture, with applying SOLID principles in order to use for a project template. Also implements best practices like loosely-coupled, dependency-inverted architecture and using design patterns such as Dependency Injection, logging, validation, exception handling, localization and so on. 

مخزن آموزشی Net. Core با مثالهای پیاده سازی شده
مطالب
به روز رسانی اطلاعات 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 مورد نظر برای اعتبارسنجی ورودی متد سرویس نیز در نظر گرفته شده است.


منابع تکمیلی

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

کدهای کامل مطلب جاری را  از اینجا می‌توانید دریافت کنید.
مطالب
استفاده از EF در اپلیکیشن های N-Tier : قسمت چهارم
در قسمت قبل تشخیص تغییرات توسط Web API را بررسی کردیم. در این قسمت نگاهی به پیاده سازی Change-tracking در سمت کلاینت خواهیم داشت.


ردیابی تغییرات در سمت کلاینت توسط Web API

فرض کنید می‌خواهیم از سرویس‌های REST-based برای انجام عملیات CRUD روی یک Object graph استفاده کنیم. همچنین می‌خواهیم رویکردی در سمت کلاینت برای بروز رسانی کلاس موجودیت‌ها پیاده سازی کنیم که قابل استفاده مجدد (reusable) باشد. علاوه بر این دسترسی داده‌ها توسط مدل Code-First انجام می‌شود.

در مثال جاری یک اپلیکیشن کلاینت (برنامه کنسول) خواهیم داشت که سرویس‌های ارائه شده توسط پروژه Web API را فراخوانی می‌کند. هر پروژه در یک Solution مجزا قرار دارد، با این کار یک محیط n-Tier را شبیه سازی می‌کنیم.

مدل زیر را در نظر بگیرید.

همانطور که می‌بینید مدل مثال جاری مشتریان و شماره تماس آنها را ارائه می‌کند. می‌خواهیم مدل‌ها و کد دسترسی به داده‌ها را در یک سرویس Web API پیاده سازی کنیم تا هر کلاینتی که به HTTP دسترسی دارد بتواند از آن استفاده کند. برای ساخت سرویس مذکور مراحل زیر را دنبال کنید.

  • در ویژوال استودیو پروژه جدیدی از نوع ASP.NET Web Application بسازید و قالب پروژه را Web API انتخاب کنید. نام پروژه را به Recipe4.Service تغییر دهید.
  • کنترلر جدیدی با نام CustomerController به پروژه اضافه کنید.
  • کلاسی با نام BaseEntity ایجاد کنید و کد آن را مطابق لیست زیر تغییر دهید. تمام موجودیت‌ها از این کلاس پایه مشتق خواهند شد که خاصیتی بنام TrackingState را به آنها اضافه می‌کند. کلاینت‌ها هنگام ویرایش آبجکت موجودیت‌ها باید این فیلد را مقدار دهی کنند. همانطور که می‌بینید این خاصیت از نوع TrackingState enum مشتق می‌شود. توجه داشته باشید که این خاصیت در دیتابیس ذخیره نخواهد شد. با پیاده سازی enum وضعیت ردیابی موجودیت‌ها بدین روش، وابستگی‌های EF را برای کلاینت از بین می‌بریم. اگر قرار بود وضعیت ردیابی را مستقیما از EF به کلاینت پاس دهیم وابستگی‌های بخصوصی معرفی می‌شدند. کلاس DbContext اپلیکیشن در متد OnModelCreating به EF دستور می‌دهد که خاصیت TrackingState را به جدول موجودیت نگاشت نکند.
public abstract class BaseEntity
{
    protected BaseEntity()
    {
        TrackingState = TrackingState.Nochange;
    }

    public TrackingState TrackingState { get; set; }
}

public enum TrackingState
{
    Nochange,
    Add,
    Update,
    Remove,
}
  • کلاس‌های موجودیت Customer و PhoneNumber را ایجاد کنید و کد آنها را مطابق لیست زیر تغییر دهید.
public class Customer : BaseEntity
{
    public int CustomerId { get; set; }
    public string Name { get; set; }
    public string Company { get; set; }
    public virtual ICollection<Phone> Phones { get; set; }
}

public class Phone : BaseEntity
{
    public int PhoneId { get; set; }
    public string Number { get; set; }
    public string PhoneType { get; set; }
    public int CustomerId { get; set; }
    public virtual Customer Customer { get; set; }
}
  • با استفاده از NuGet Package Manager کتابخانه Entity Framework 6 را به پروژه اضافه کنید.
  • کلاسی با نام Recipe4Context ایجاد کنید و کد آن را مطابق لیست زیر تغییر دهید. در این کلاس از یکی از قابلیت‌های جدید EF 6 بنام "Configuring Unmapped Base Types" استفاده کرده ایم. با استفاده از این قابلیت جدید هر موجودیت را طوری پیکربندی می‌کنیم که خاصیت TrackingState را نادیده بگیرند. برای اطلاعات بیشتر درباره این قابلیت EF 6 به این لینک مراجعه کنید.
public class Recipe4Context : DbContext
{
    public Recipe4Context() : base("Recipe4ConnectionString") { }
    public DbSet<Customer> Customers { get; set; }
    public DbSet<Phone> Phones { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        // Do not persist TrackingState property to data store
        // This property is used internally to track state of
        // disconnected entities across service boundaries.
        // Leverage the Custom Code First Conventions features from Entity Framework 6.
        // Define a convention that performs a configuration for every entity
        // that derives from a base entity class.
        modelBuilder.Types<BaseEntity>().Configure(x => x.Ignore(y => y.TrackingState));
        modelBuilder.Entity<Customer>().ToTable("Customers");
        modelBuilder.Entity<Phone>().ToTable("Phones");
}
}
  • فایل Web.config پروژه را باز کنید و رشته اتصال زیر را به قسمت ConnectionStrings اضافه نمایید.
<connectionStrings>
  <add name="Recipe4ConnectionString"
    connectionString="Data Source=.;
    Initial Catalog=EFRecipes;
    Integrated Security=True;
    MultipleActiveResultSets=True"
    providerName="System.Data.SqlClient" />
</connectionStrings>
  • فایل Global.asax را باز کنید و کد زیر را به متد Application_Start اضافه نمایید. این کد بررسی Entity Framework Model Compatibility را غیرفعال می‌کند و به JSON serializer دستور می‌دهد که self-referencing loop خواص پیمایشی را نادیده بگیرد. این حلقه بدلیل رابطه bidirectional بین موجودیت‌های Customer و PhoneNumber بوجود می‌آید.
protected void Application_Start()
{
    // Disable Entity Framework Model Compatibilty
    Database.SetInitializer<Recipe1Context>(null);
    // The bidirectional navigation properties between related entities
    // create a self-referencing loop that breaks Web API's effort to
    // serialize the objects as JSON. By default, Json.NET is configured
    // to error when a reference loop is detected. To resolve problem,
    // simply configure JSON serializer to ignore self-referencing loops.
    GlobalConfiguration.Configuration.Formatters.JsonFormatter
        .SerializerSettings.ReferenceLoopHandling =
            Newtonsoft.Json.ReferenceLoopHandling.Ignore;
    ...
}
  • کلاسی با نام EntityStateFactory بسازید و کد آن را مطابق لیست زیر تغییر دهید. این کلاس مقدار خاصیت TrackingState که به کلاینت‌ها ارائه می‌شود را به مقادیر متناظر کامپوننت‌های ردیابی EF تبدیل می‌کند.
public static EntityState Set(TrackingState trackingState)
{
    switch (trackingState)
    {
        case TrackingState.Add:
            return EntityState.Added;
        case TrackingState.Update:
            return EntityState.Modified;
        case TrackingState.Remove:
            return EntityState.Deleted;
        default:
            return EntityState.Unchanged;
    }
}
  • در آخر کد کنترلر CustomerController را مطابق لیست زیر بروز رسانی کنید.
public class CustomerController : ApiController
{
    // GET api/customer
    public IEnumerable<Customer> Get()
    {
        using (var context = new Recipe4Context())
        {
            return context.Customers.Include(x => x.Phones).ToList();
        }
    }

    // GET api/customer/5
    public Customer Get(int id)
    {
        using (var context = new Recipe4Context())
        {
            return context.Customers.Include(x => x.Phones).FirstOrDefault(x => x.CustomerId == id);
        }
    }

    [ActionName("Update")]
    public HttpResponseMessage UpdateCustomer(Customer customer)
    {
        using (var context = new Recipe4Context())
        {
            // Add object graph to context setting default state of 'Added'.
            // Adding parent to context automatically attaches entire graph
            // (parent and child entities) to context and sets state to 'Added'
            // for all entities.
            context.Customers.Add(customer);
            foreach (var entry in context.ChangeTracker.Entries<BaseEntity>())
            {
                entry.State = EntityStateFactory.Set(entry.Entity.TrackingState);
                if (entry.State == EntityState.Modified)
                {
                    // For entity updates, we fetch a current copy of the entity
                    // from the database and assign the values to the orginal values
                    // property from the Entry object. OriginalValues wrap a dictionary
                    // that represents the values of the entity before applying changes.
                    // The Entity Framework change tracker will detect
                    // differences between the current and original values and mark
                    // each property and the entity as modified. Start by setting
                    // the state for the entity as 'Unchanged'.
                    entry.State = EntityState.Unchanged;
                    var databaseValues = entry.GetDatabaseValues();
                    entry.OriginalValues.SetValues(databaseValues);
                }
            }

        context.SaveChanges();
    }

    return Request.CreateResponse(HttpStatusCode.OK, customer);
}

    [HttpDelete]
    [ActionName("Cleanup")]
    public HttpResponseMessage Cleanup()
    {
        using (var context = new Recipe4Context())
        {
            context.Database.ExecuteSqlCommand("delete from phones");
            context.Database.ExecuteSqlCommand("delete from customers");
            return Request.CreateResponse(HttpStatusCode.OK);
        }
    }
}
حال اپلیکیشن کلاینت (برنامه کنسول) را می‌سازیم که از این سرویس استفاده می‌کند.

  • در ویژوال استودیو پروژه جدیدی از نوع Console Application بسازید و نام آن را به Recipe4.Client تغییر دهید.
  • فایل program.cs را باز کنید و کد آن را مطابق لیست زیر تغییر دهید.
internal class Program
{
    private HttpClient _client;
    private Customer _bush, _obama;
    private Phone _whiteHousePhone, _bushMobilePhone, _obamaMobilePhone;
    private HttpResponseMessage _response;

    private static void Main()
    {
        Task t = Run();
        t.Wait();
        Console.WriteLine("\nPress <enter> to continue...");
        Console.ReadLine();
    }

    private static async Task Run()
    {
        var program = new Program();
        program.ServiceSetup();
        // do not proceed until clean-up completes
        await program.CleanupAsync();
        program.CreateFirstCustomer();
        // do not proceed until customer is added
        await program.AddCustomerAsync();
        program.CreateSecondCustomer();
        // do not proceed until customer is added
        await program.AddSecondCustomerAsync();
        // do not proceed until customer is removed
        await program.RemoveFirstCustomerAsync();
        // do not proceed until customers are fetched
        await program.FetchCustomersAsync();
    }

    private void ServiceSetup()
    {
        // set up infrastructure for Web API call
        _client = new HttpClient { BaseAddress = new Uri("http://localhost:62799/") };
        // add Accept Header to request Web API content negotiation to return resource in JSON format
        _client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue
        ("application/json"));
    }
    private async Task CleanupAsync()
    {
        // call the cleanup method from the service
        _response = await _client.DeleteAsync("api/customer/cleanup/");
    }

    private void CreateFirstCustomer()
    {
        // create customer #1 and two phone numbers
        _bush = new Customer
        {
            Name = "George Bush",
            Company = "Ex President",
            // set tracking state to 'Add' to generate a SQL Insert statement
            TrackingState = TrackingState.Add,
        };
        _whiteHousePhone = new Phone
        {
            Number = "212 222-2222",
            PhoneType = "White House Red Phone",
            // set tracking state to 'Add' to generate a SQL Insert statement
            TrackingState = TrackingState.Add,
        };
        _bushMobilePhone = new Phone
        {
            Number = "212 333-3333",
            PhoneType = "Bush Mobile Phone",
            // set tracking state to 'Add' to generate a SQL Insert statement
            TrackingState = TrackingState.Add,
        };
        _bush.Phones.Add(_whiteHousePhone);
        _bush.Phones.Add(_bushMobilePhone);
    }

    private async Task AddCustomerAsync()
    {
        // construct call to invoke UpdateCustomer action method in Web API service
        _response = await _client.PostAsync("api/customer/updatecustomer/", _bush, new JsonMediaTypeFormatter());
        if (_response.IsSuccessStatusCode)
        {
            // capture newly created customer entity from service, which will include
            // database-generated Ids for all entities
            _bush = await _response.Content.ReadAsAsync<Customer>();
            _whiteHousePhone = _bush.Phones.FirstOrDefault(x => x.CustomerId == _bush.CustomerId);
            _bushMobilePhone = _bush.Phones.FirstOrDefault(x => x.CustomerId == _bush.CustomerId);
            Console.WriteLine("Successfully created Customer {0} and {1} Phone Numbers(s)",
            _bush.Name, _bush.Phones.Count);
            foreach (var phoneType in _bush.Phones)
            {
                Console.WriteLine("Added Phone Type: {0}", phoneType.PhoneType);
            }
        }
        else
            Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase);
    }

    private void CreateSecondCustomer()
    {
        // create customer #2 and phone numbers
        _obama = new Customer
        {
            Name = "Barack Obama",
            Company = "President",
            // set tracking state to 'Add' to generate a SQL Insert statement
            TrackingState = TrackingState.Add,
        };
        _obamaMobilePhone = new Phone
        {
            Number = "212 444-4444",
            PhoneType = "Obama Mobile Phone",
            // set tracking state to 'Add' to generate a SQL Insert statement
            TrackingState = TrackingState.Add,
        };
        // set tracking state to 'Modifed' to generate a SQL Update statement
        _whiteHousePhone.TrackingState = TrackingState.Update;
        _obama.Phones.Add(_obamaMobilePhone);
        _obama.Phones.Add(_whiteHousePhone);
    }

    private async Task AddSecondCustomerAsync()
    {
        // construct call to invoke UpdateCustomer action method in Web API service
        _response = await _client.PostAsync("api/customer/updatecustomer/", _obama, new JsonMediaTypeFormatter());
        if (_response.IsSuccessStatusCode)
        {
            // capture newly created customer entity from service, which will include
            // database-generated Ids for all entities
            _obama = await _response.Content.ReadAsAsync<Customer>();
            _whiteHousePhone = _bush.Phones.FirstOrDefault(x => x.CustomerId == _obama.CustomerId);
            _bushMobilePhone = _bush.Phones.FirstOrDefault(x => x.CustomerId == _obama.CustomerId);
            Console.WriteLine("Successfully created Customer {0} and {1} Phone Numbers(s)",
            _obama.Name, _obama.Phones.Count);
            foreach (var phoneType in _obama.Phones)
            {
                Console.WriteLine("Added Phone Type: {0}", phoneType.PhoneType);
            }
        }
        else
            Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase);
    }

    private async Task RemoveFirstCustomerAsync()
    {
        // remove George Bush from underlying data store.
        // first, fetch George Bush entity, demonstrating a call to the
        // get action method on the service while passing a parameter
        var query = "api/customer/" + _bush.CustomerId;
        _response = _client.GetAsync(query).Result;

        if (_response.IsSuccessStatusCode)
        {
            _bush = await _response.Content.ReadAsAsync<Customer>();
            // set tracking state to 'Remove' to generate a SQL Delete statement
            _bush.TrackingState = TrackingState.Remove;
            // must also remove bush's mobile number -- must delete child before removing parent
            foreach (var phoneType in _bush.Phones)
            {
                // set tracking state to 'Remove' to generate a SQL Delete statement
                phoneType.TrackingState = TrackingState.Remove;
            }
            // construct call to remove Bush from underlying database table
            _response = await _client.PostAsync("api/customer/updatecustomer/", _bush, new JsonMediaTypeFormatter());
            if (_response.IsSuccessStatusCode)
            {
                Console.WriteLine("Removed {0} from database", _bush.Name);
                foreach (var phoneType in _bush.Phones)
                {
                    Console.WriteLine("Remove {0} from data store", phoneType.PhoneType);
                }
            }
            else
                Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase);
        }
        else
        {
            Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase);
        }
    }

    private async Task FetchCustomersAsync()
    {
        // finally, return remaining customers from underlying data store
        _response = await _client.GetAsync("api/customer/");
        if (_response.IsSuccessStatusCode)
        {
            var customers = await _response.Content.ReadAsAsync<IEnumerable<Customer>>();
            foreach (var customer in customers)
            {
                Console.WriteLine("Customer {0} has {1} Phone Numbers(s)",
                customer.Name, customer.Phones.Count());
                foreach (var phoneType in customer.Phones)
                {
                    Console.WriteLine("Phone Type: {0}", phoneType.PhoneType);
                }
            }
        }
        else
        {
            Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase);
        }
    }
}

  • در آخر کلاس‌های Customer, Phone و BaseEntity را به پروژه کلاینت اضافه کنید. چنین کدهایی بهتر است در لایه مجزایی قرار گیرند و بین لایه‌های مختلف اپلیکیشن به اشتراک گذاشته شوند.

اگر اپلیکیشن کلاینت را اجرا کنید با خروجی زیر مواجه خواهید شد.








شرح مثال جاری

با اجرای اپلیکیشن Web API شروع کنید. این اپلیکیشن یک MVC Web Controller دارد که پس از اجرا شما را به صفحه خانه هدایت می‌کند. در این مرحله سایت در حال اجرا است و سرویس‌ها قابل دسترسی هستند.

سپس اپلیکیشن کنسول را باز کنید و روی خط اول کد فایل program.cs یک breakpoint قرار داده و آن را اجرا کنید. ابتدا آدرس سرویس را نگاشت می‌کنیم و از سرویس درخواست می‌کنیم که اطلاعات را با فرمت JSON بازگرداند.

سپس توسط متد DeleteAsync که روی آبجکت HttpClient تعریف شده است اکشن متد Cleanup را روی سرویس فراخوانی می‌کنیم. این فراخوانی تمام داده‌های پیشین را حذف می‌کند.

در قدم بعدی یک مشتری بهمراه دو شماره تماس می‌سازیم. توجه کنید که برای هر موجودیت مشخصا خاصیت TrackingState را مقدار دهی می‌کنیم تا کامپوننت‌های Change-tracking در EF عملیات لازم SQL برای هر موجودیت را تولید کنند.

سپس توسط متد PostAsync که روی آبجکت HttpClient تعریف شده اکشن متد UpdateCustomer را روی سرویس فراخوانی می‌کنیم. اگر به این اکشن متد یک breakpoint اضافه کنید خواهید دید که موجودیت مشتری را بعنوان یک پارامتر دریافت می‌کند و آن را به context جاری اضافه می‌نماید. با اضافه کردن موجودیت به کانتکست جاری کل object graph اضافه می‌شود و EF شروع به ردیابی تغییرات آن می‌کند. دقت کنید که آبجکت موجودیت باید Add شود و نه Attach.

قدم بعدی جالب است، هنگامی که از خاصیت DbChangeTracker استفاده می‌کنیم. این خاصیت روی آبجکت context تعریف شده و یک <IEnumerable<DbEntityEntry را با نام Entries ارائه می‌کند. در اینجا بسادگی نوع پایه EntityType را تنظیم میکنیم. این کار به ما اجازه می‌دهد که در تمام موجودیت هایی که از نوع BaseEntity هستند پیمایش کنیم. اگر بیاد داشته باشید این کلاس، کلاس پایه تمام موجودیت‌ها است. در هر مرحله از پیمایش (iteration) با استفاده از کلاس EntityStateFactory مقدار خاصیت TrackingState را به مقدار متناظر در سیستم ردیابی EF تبدیل می‌کنیم. اگر کلاینت مقدار این فیلد را به Modified تنظیم کرده باشد پردازش بیشتری انجام می‌شود. ابتدا وضعیت موجودیت را از Modified به Unchanged تغییر می‌دهیم. سپس مقادیر اصلی را با فراخوانی متد GetDatabaseValues روی آبجکت Entry از دیتابیس دریافت می‌کنیم. فراخوانی این متد مقادیر موجود در دیتابیس را برای موجودیت جاری دریافت می‌کند. سپس مقادیر بدست آمده را به کلکسیون OriginalValues اختصاص می‌دهیم. پشت پرده، کامپوننت‌های EF Change-tracking بصورت خودکار تفاوت‌های مقادیر اصلی و مقادیر ارسالی را تشخیص می‌دهند و فیلدهای مربوطه را با وضعیت Modified علامت گذاری می‌کنند. فراخوانی‌های بعدی متد SaveChanges تنها فیلدهایی که در سمت کلاینت تغییر کرده اند را بروز رسانی خواهد کرد و نه تمام خواص موجودیت را.

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

متد UpdateCustomer در سرویس ما مقادیر TrackingState را به مقادیر متناظر EF تبدیل می‌کند و آبجکت‌ها را به موتور change-tracking ارسال می‌کند که نهایتا منجر به تولید دستورات لازم SQL می‌شود.

نکته: در اپلیکیشن‌های واقعی بهتر است کد دسترسی داده‌ها و مدل‌های دامنه را به لایه مجزایی منتقل کنید. همچنین پیاده سازی فعلی change-tracking در سمت کلاینت می‌تواند توسعه داده شود تا با انواع جنریک کار کند. در این صورت از نوشتن مقادیر زیادی کد تکراری جلوگیری خواهید کرد و از یک پیاده سازی می‌توانید برای تمام موجودیت‌ها استفاده کنید.

نظرات مطالب
VS Code برای توسعه دهندگان ASP.NET Core - قسمت دوم - ایجاد و اجرای اولین برنامه
نکته‌ای در مورد دیباگ برنامه‌های NET Core 2.0.

اگر فایل vscode\launch.json. پیشتر برای یک پروژه‌ی NET Core 1.x. تشکیل شده باشد، پس از ارتقاء برنامه به NET Core 2.0.، با شروع دیباگر در VSCode، خطای ذیل را مشاهده خواهید کرد:
The specified framework 'Microsoft.NETCore.App', version '1.1.2' was not found.
علت اینجا است که در فایل launch.json، مسیر dll اصلی برنامه هنوز به netcoreapp1.0 تنظیم است و باید به صورت ذیل به netcoreapp2.0 اصلاح شود:
"program": "${workspaceRoot}/src/ASPNETCoreIdentitySample/bin/Debug/netcoreapp2.0/ASPNETCoreIdentitySample.dll",
نظرات مطالب
EF Code First #6
در مورد ASP.NET Web API و UpShot و اینها که از EF 4.3 تو خودشون استفاده کردند چی ؟
متاسفانه دیگه نمی‌شه با assembly binding بشون بگیم که از EF 5 استفاده کنید
چون Runtime خطا می‌دن و مثلا می‌گن که System.ComponentMode.DataAnnotation.ForeignKey در Entity Framework 5 نیست !
بله نیست، چون رفته به DLL مربوط به Component Model.Data Annotation
راه حلی جز گرفتن سورس کد upshot و Build مجددش هست ؟
و غیر از عقب گرد به EF 3
ممنون