مطالب
روشی برای مقایسه‌ی مقادیر تمام خواص دو شیء در آزمون‌های واحد
در زمان نوشتن تست‌های مختلف (Unit - Integration - UI) گاهی اوقات پیش می‌آید که بخواهید تمامی خصوصیت‌های یک شیء را تایید کنید. معمولا نوشتن اعتبارسنجی برای همه خصوصیت‌ها و همین طور پیام‌های استثناء برای هر یک در زمان عدم تایید اعتبار، کار بسیار زمانبری است. در این مقاله به شما نشان خواهم داد که چگونه با نوشتن یک اعتبارسنج عمومی از اتلاف زمان زیادی جلوگیری کنید.

با استفاده از کلاس زیر می‌توان کار اعتبارسنجی را با استفاده از Reflection به راحتی انجام داد. در اینجا برای اعتبارسنجی DateTime از کلاس DateTimeAssert استفاده کرده‌ایم.
public class PropertiesValidator<TK, T> where T : new() where TK : new()
{
    static TK _instance;

    public static TK Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = new TK();
            }
            return _instance;
        }
    }

    public void Validate(T expectedObject, T realObject, params string[] propertiesNotToCompare)
    {
        var properties = realObject.GetType().GetProperties();
        foreach (var currentRealProperty in properties)
        {
            if (!propertiesNotToCompare.Contains(currentRealProperty.Name))
            {
                var currentExpectedProperty = expectedObject.GetType().GetProperty(currentRealProperty.Name);
                var exceptionMessage = $"The property {currentRealProperty.Name} of class {currentRealProperty.DeclaringType?.Name} was not as expected.";

                if (currentRealProperty.PropertyType != typeof(DateTime) && currentRealProperty.PropertyType != typeof(DateTime?))
                {
                    Assert.AreEqual( currentExpectedProperty.GetValue( expectedObject,
                                                                        null ),
                                        currentRealProperty.GetValue( realObject,
                                                                    null ),
                                        exceptionMessage );
                }
                else
                {
                    DateTimeAssert.Validate( currentExpectedProperty.GetValue( expectedObject,
                                                                                null ) as DateTime?,
                                                currentRealProperty.GetValue( realObject,
                                                                            null ) as DateTime?,
                                                TimeSpan.FromMinutes( 5 ) );
                }
            }
        }
    }
}


طرز استفاده

فرض کنید مدلی داریم با این مشخصات:
public class ObjectToAssert
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime LastVisit { get; set; }
}
و دو نمونه از آن را ایجاد کرده ایم:
var expectedObject = new ObjectToAssert
                        {
                            FirstName = "Vahid",
                            LastName = "Mohammad Taheri",
                            LastVisit = new DateTime( 2016, 11, 14, 0, 10, 50 )
                        };
var actualObject = new ObjectToAssert
                        {
                            FirstName = "Vahid",
                            LastName = "Mohammad Taheri",
                            LastVisit = new DateTime( 2016, 11, 14, 0, 13, 50 )
                        };
کلاسی را با ارث بری از PropertiesValidator ایجاد می‌کنیم:
public class ObjectToAssertValidator : PropertiesValidator<ObjectToAssertValidator, ObjectToAssert>
{
    public void Validate(ObjectToAssert expected, ObjectToAssert actual)
    {
        this.Validate(expected, actual, "FirstName");
    }
}

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


اکنون دو نمونه ساخته شده از ObjectToAssert بالا را با فراخوانی دستور زیر اعتبارسنجی می‌کنیم:
ObjectToAssertValidator.Instance.Validate(expectedObject, actualObject);
مطالب
ردیابی تغییرات در Entity Framework، بخش اول
همان طور که می‌دانید، Entity Framework  تغییراتی را که بر روی اشیا انجام می‌دهید، ردیابی می‌کند. بدیهی است که EF از طریق ردیابی این تغییرات است که می‌تواند تغییرات انجام شده را شناسایی کند و آن‌ها را در مواقع مورد نیاز مانند ذخیره‌ی تغییرات  (DbContext.SaveChanges)، بر روی پایگاه داده اعمال  کند. شما می‌توانید به اطلاعات این ردیاب تغییر و اعمال مرتبط به آن از طریق ویژگی  DbContext.ChangeTracker دسترسی پیدا کنید. 
در این مقاله بیشتر سعی به بررسی مفاهیم ردیابی و روش هایی که EF برای ردیابی تغییرات استفاده می‌کند، بسنده می‌کنم و بررسی  API‌های مختلف آن را به مقاله ای دیگر موکول می‌کنم.
به طور کلی EF از دو روش برای ردیابی تغییرات رخ داده شده در اشیا استفاده می‌کند:
1) ردیابی تغییر عکس فوری! (Snapshot change tracking) 
2) پروکسی‌های ردیابی تغییر  (Change tracking proxies)

 ردیابی تغییر عکس فوری

به نظر من، اسم مناسبی برای این روش انتخاب کرده اند و دقیقا بیان گر کاری است که  EF انجام می‌دهد. در حالت عادی کلاس‌های دامین ما یا همان کلاس‌های POCO، هیچ منطق و کدی را برای مطلع ساختن EF از تغییراتی که در آن‌ها رخ می‌دهد پیاده سازی نکرده اند. چون هیچ راهی برای EF، برای مطلع شدن از تغییرات رخ داده وجود ندارد، EF راه جالبی را بر می‌گزیند. EF هر گاه شیئی را می‌بیند از مقادیر ویژگی‌های آن یک عکس فوری می‌گیرد! و آن‌ها را در حافظه ذخیره می‌کند.این عمل هنگامی که یک شی از پرس و جو (query) حاصل می‌شود، و یا شیئی را به DbSet اضافه می‌کنیم رخ می‌دهد.
زمانی که EF می‌خواهد بفهمد که چه تغییراتی رخ داده است، مقادیر کنونی موجود در کلیه اشیا را اسکن می‌کند و با مقادیری که در عکس فوری ذخیره کرده است مقایسه می‌کند و متوجه تغییرات رخ داده می‌شود. این فرآیند اسکن کردن کلیه اشیا زمانی رخ می‌دهد که متد DetectChanges ویژگی DbSet.ChangeTracker صدا زده شود.

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

پروکسی‌های ردیابی تغییر، مکانیزم دیگری برای ردیابی تغییرات EF است و به EF این اجازه را می‌دهد تا از تغییرات رخ داده، مطلع شود.
اگر به یاد داشته باشید در مباحث Lazy loading نیز از واژه پروکسی‌های پویا استفاده شد. پروکسی‌های ردیابی تغییر نیز با استفاده از همان مکانیزم کار می‌کنند و علاوه بر فراهم کردن Lazy loading ،این امکان را می‌دهند تا تغییرات را به Context انتقال دهند.
برای استفاده از پروکسی‌های ردیابی تغییر، شما باید ساختار کلاس‌های خود را به گونه ای تغییر دهید، تا EF بتواند در زمان اجرا، نوع پویایی را که هریک، از کلاس‌های POCO شما مشتق می‌شوند ایجاد کند، و تک تک ویژگی‌های آن‌ها را تحریف (override) کند.
این نوع پویا که به عنوان پروکسی پویا نیز شناخته می‌شود، منطقی را در ویژگی‌های تحریف شده شامل می‌شود، تا EF را از تغییرات صورت گرفته در ویژگی هایش مطلع سازد.
 برای بیان ادامه‌ی مطلب، من مدل یک دفترچه تلفن ساده را به شرح زیر در نظر گرفتم که روابط مهم و اساسی در آن در نظر گرفته شده است. 
namespace EntitySample1.DomainClasses
{
    public class Person
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public DateTime BirthDate { get; set; }
        public virtual PersonInfo PersonInfo { get; set; }
        public virtual ICollection<PhoneNumber> PhoneNumbers { get; set; }
        public virtual ICollection<Address> Addresses { get; set; }
    }
}

namespace EntitySample1.DomainClasses
{
    public class PersonInfo
    {
        public int Id { get; set; }
        public string Note { get; set; }
        public string Major { get; set; }
    }
}

namespace EntitySample1.DomainClasses
{
    public enum PhoneType
    {
        Home,
        Mobile,
        Work
    }

    public class PhoneNumber
    {
        public int Id { get; set; }
        public string Number { get; set; }
        public PhoneType PhoneType { get; set; }
        public virtual Person Person { get; set; }
    }
}

namespace EntitySample1.DomainClasses
{
    public class Address
    {
        public int Id { get; set; }
        public string City { get; set; }
        public string Street { get; set; }
        public virtual ICollection<Person> Persons { get; set; }
    }
}
طبق کلاس‌های فوق ، ما تعدادی شخص ، اطلاعات شخص ، شماره تلفن و آدرس داریم. رابطه‌ی بین شخص و اطلاعات آن شخص یک به یک، شخص و آدرس  چند به چند  و شخص با شماره تلفن یک به چند است. همچنین به این نکته توجه داشته باشید که  کلیه کلاس‌های فوق به صورت public تعریف، و کلیه خواص راهبری (navigation properties) به صورت virtual تعریف شده اند. دلیل این کار هم این است که این دو مورد، جز الزامات، برای فعال سازی Lazy loading هستند. 
تعریف کلاس context نیز به شکل زیر است:

namespace EntitySample1.DataLayer
{
    public class PhoneBookDbContext : DbContext
    {
        public DbSet<Person> Persons { get; set; }
        public DbSet<PhoneNumber> PhoneNumbers { get; set; }
        public DbSet<Address> Addresses { get; set; }

    }
}

استفاده از ردیابی تغییر عکس فوری

ردیابی تغییر عکس فوری، وابسته به این است که EF بفهمد، چه زمانی تغییرات رخ داده است. رفتار پیش فرض DbContext API ، این هست که به صورت خودکار بازرسی لازم را در نتیجه‌ی رخداد‌های DbContext انجام دهد. DetectChanges تنها اطلاعات مدیریت حالت context، که وظیفه‌ی انعکاس تغییرات صورت گرفته به پایگاه داده را دارد، به روز نمی‌کند، بلکه اصلاح رابطه(ralationship) ترکیبی از خواص راهبری مرجع ، مجموعه ای  و کلید‌های خارجی را انجام می‌دهد. این خیلی مهم خواهد بود که درک روشنی داشته باشیم از این که چگونه و چه زمانی تغییرات تشخیص داده می‌شوند،چه چیزی باید از آن انتظار داشته باشیم و چگونه کنترلش کنیم.

چه زمانی تشخیص خودکار تغییرات اجرا می‌شود؟

متد DetectChanges کلاس ObjectContext، از EF نسخه‌ی 4 به عنوان بخشی از الگوی ردیابی تغییر عکس فوری اشیای POCO ،در دسترس بوده است. تفاوتی که در مورد DataContext.ChangeTracker.DetectChanges( در حقیقت ObjectContext.DetectChanges فراخوانی می‌شود) وجود دارد این است که، رویداد‌های خیلی بیشتری وجود دارند که به صورت خودکار DetectChanges را فراخوانی می‌کنند.
لیستی از متدهایی که باعث انجام عمل تشخیص تغییرات (DetectChanges)، می‌شوند را  در ادامه مشاهده می‌کنید:
• DbSet.Add
• DbSet.Find
• DbSet.Remove
• DbSet.Local
• DbSet.SaveChanges
• فراخوانی Linq Query از DbSet
• DbSet.Attach
• DbContext.GetValidationErrors
• DbContext.Entry
• DbChangeTracker.Entries

 
کنترل زمان فراخوانی DetectChanges

بیشترین زمانی که EF احتیاج به فهمیدن تغییرات دارد، در زمان SaveChanges است، اما حالت‌های زیاد دیگری نیز هست. برای مثال، اگر ما از ردیاب تغییرات، درخواست وضعیت فعلی یک شی را بکنیم،EF احتیاج به اسکن کردن و بررسی تغییرات رخ داده را دارد. همچنین وضعیتی را در نظر بگیرید که شما از پایگاه داده یک شماره تلفن را واکشی می‌کنید و سپس آن را به مجموعه شماره تلفن‌های یک شخص جدید اضافه می‌کنید.آن شماره تلفن اکنون تغییر کرده است، چرا که انتساب آن به یک شخص جدید،خاصیت PersonId آن را تغییر داده است. ولی EF برای اینکه بفهمد تغییر رخ داده است(یا حتی نداده است) ، احتیاج به اسکن کردن همه‌ی اشیا Person دارد.
بیشتر عملیاتی که بر روی DbContext API انجام می‌دهید، موجب فراخوانی DetectChanges می‌شود. در بیشتر موارد DetectChanges به اندازه کافی سریع هست تا باعث ایجاد مشکل کارایی نشود. با این حال ممکن است ، شما تعداد خیلی زیادی اشیا در حافظه داشته باشید، و یا تعداد زیادی عملیات در DbContext ، در مدت خیلی کوتاهی انجام دهید، رفتار تشخیص خودکار تغییرات ممکن است، باعث نگرانی‌های کارایی شود. خوشبختانه گزینه ای برای خاموش کردن رفتار تشخیص خودکار تغییرات وجود دارد و هر زمانی که می‌دانید لازم است، می‌توانید آن را به صورت دستی فراخوانی کنید.
 EF بر مبنای این فرض ساخته شده است که شما ، در صورتی که در فراخوانی آخرین API، موجودیتی تغییر پیدا کرده است، قبل از فراخوانی API جدید، باید DetectChanges صدا زده شود. این شامل فراخوانی DetectChanges، قبل از اجرای هر query نیز می‌شود.اگر این عمل ناموفق یا نابجا انجام شود،ممکن است عواقب غیر منتظره ای در بر داشته باشد. DbContext انجام این وظیفه را بر عهده گرفته است و به همین دلیل به طور پیش فرض تشخیص تغییرات خودکار آن فعال است.

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

برای خاموش یا روشن کردن تشخیص خودکار تغییرات، باید متغیر بولین DbContext.Configuration.AutoDetectChangesEnabled را تنظیم کنید.
در مثال زیر، ما در متد ManualDetectChanges، تشخیص خودکار تغییرات را خاموش کرده ایم و تاثیرات آن را بررسی کرده ایم.

        private static void ManualDetectChanges()
        {
            using (var context = new PhoneBookDbContext())
            {
                context.Configuration.AutoDetectChangesEnabled = false; // turn off Auto Detect Changes

                var p1 = context.Persons.Single(p => p.FirstName == "joe");

                p1.LastName = "Brown";

                Console.WriteLine("Before DetectChanges: {0}", context.Entry(p1).State);

                context.ChangeTracker.DetectChanges(); // call detect changes manually

                Console.WriteLine("After DetectChanges: {0}", context.Entry(p1).State);
            }
        }

در کدهای بالا ابتدا تشخیص خودکار تغییرات را خاموش کرده ایم و سپس یک شخص با نام joe را از دیتابیس فراخواندیم و سپس نام خانوادگی آن را به Brown تغییر دادیم. سپس در خط بعد، وضعیت فعلی موجودیت p1 را از context جاری پرسیدیم. در خط بعدی، DetectChanges را به صورت دستی صدا زده ایم و دوباره همان پروسه را برای به دست آوردن وضیعت شی p1، انجام داده ایم. همان طور که می‌بینید ، برای به دست آوردن وضعیت فعلی شی مورد نظر از متد Entry متعلق به ChangeTracker API استفاده می‌کنیم، که در آینده مفصل در مورد آن بحث خواهد شد. اگر شما متد Main را با صدا زدن ManualDetectChanges ویرایش کنید ، خروجی زیر را مشاهده خواهید کرد:

Before DetectChanges: Unchanged
After DetectChanges: Modified

همان طور که انتظار می‌رفت، به دلیل خاموش کردن تشخیص خودکار تغییرات، context قادر به تشخیص تغییرات صورت گرفته در شی p1 نیست، تا زمانی که متد DetectChanges را به صورت دستی صدا بزنیم. دلیل این که در دفعه اول، ما نتیجه‌ی غلطی مشاهده می‌کنیم، این است که ما قانون را نقض کرده ایم و قبل از صدا زدن هر API ، متد DetectChanges را صدا نزده ایم. خوشبختانه چون ما در اینجا وضعیت یک شی را بررسی کردیم، با عوارض جانبی آن روبرو نشدیم.

نکته: به این نکته توجه داشته باشید که متد Entry به صورت خودکار، DetectChanges را فراخوانی می‌کند. برای اینکه دانسته بخواهیم این رفتار را غیر فعال کنیم، باید AutoDetectChangesEnabled را غیر فعال کنیم.
در مثال فوق ،خاموش کردن تشخیص خودکار تغییرات، برای ما مزیتی به همراه نداشت و حتی ممکن بود برای ما دردسر ساز شود. ولی حالتی را  در نظر بگیرید که ما یک سری API را فراخوانی می‌کنیم ،بدون این که در این بین ،در حالت اشیا تغییری ایجاد کنیم.در نتیجه می‌توانیم از فراخوانی‌های بی جهت DetectChanges جلوگیری کنیم.
 
در متد AddMultiplePersons مثال بعدی، این کار را نشان داده ام:

        private static void AddMultiplePerson()
        {
            using (var context = new PhoneBookDbContext())
            {
                context.Configuration.AutoDetectChangesEnabled = false;

                context.Persons.Add(new Person
                    {
                        FirstName = "brad",
                        LastName = "watson",
                        BirthDate = new DateTime(1990, 6, 8)
                    });

                context.Persons.Add(new Person
                {
                    FirstName = "david",
                    LastName = "brown",
                    BirthDate = new DateTime(1990, 6, 8)
                });

                context.Persons.Add(new Person
                {
                    FirstName = "will",
                    LastName = "smith",
                    BirthDate = new DateTime(1990, 6, 8)
                });

                context.SaveChanges();

            }
        }
در مثال بالا ما از فراخوانی چهار DetectChanges غیر ضروری که شامل DbSet.Add و SaveChanges می‌شود، جلوگیری کرده ایم.

استفاده از DetectChanges برای فراخوانی اصلاح رابطه


DetectChanges همچنین مسئولیت انجام اصلاح رابطه ، برای هر رابطه ای که تشخیص دهد تغییر کرده است را دارد.اگر شما بعضی از روابط را تغییر دادید و مایل بودید تا همه‌ی خواص راهبری و خواص کلید خارجی را منطبق کنید، DetectChanges این کار را برای شما انجام می‌دهد. این قابلیت می‌تواند برای سناریوهای data-binding که در آن ممکن است در رابط کاربری(UI) یکی از خواص راهبری (یا حتی یک کلید خارجی) تغییر کند، و شما بخواهید که خواص دیگری این رابطه به روز شوند و تغییرات را نشان دهند، مفید واقع شود.
متد DetectRelationshipChanges در مثال زیر از DetectChanges برای انجام اصلاح رابطه استفاده می‌کند.
 
        private static void DetectRelationshipChanges()
        {
            using (var context = new PhoneBookDbContext())
            {

                var phone1 = context.PhoneNumbers.Single(x => x.Number == "09351234567");

                var person1 = context.Persons.Single(x => x.FirstName == "will");

                person1.PhoneNumbers.Add(phone1);

                Console.WriteLine("Before DetectChanges: {0}", phone1.Person.FirstName);

                context.ChangeTracker.DetectChanges(); // ralationships fix-up

                Console.WriteLine("After DetectChanges: {0}", phone1.Person.FirstName);
            }
        }

در اینجا ابتدا ما شماره تلفنی را از دیتابیس لود می‌کنیم. سپس شخص دیگری را نیز با نام will از دیتابیس می‌خوانیم. قصد داریم شماره تلفن خوانده شده را به این شخص نسبت دهیم و مجموعه شماره تلفن‌های وی اضافه کنیم و ما این کار را با افزودن phone1 به مجموعه شماره تلفن‌های person1 انجام داده ایم. چون ما از اشیای POCO استفاده کرده ایم،EF  نمی‌فهمد که ما این تغییر را ایجاد کرده ایم و در نتیجه کلید خارجی PersonId شی phone1 را اصلاح نمی‌کند. ما می‌توانیم تا زمانی صبر کنیم تا متدی مثل SaveChanges، متد DetectChanges را فراخوانی کند،ولی اگر بخواهیم این عمل در همان لحظه انجام شود، می‌توانیم DetectChanges را دستی صدا بزنیم.

اگر ما متد Main را با اضافه کردن فرخوانی DetectRealtionShipsChanges تغییر بدهیم و آن را اجرا کنیم، نتیجه زیر را مشاهده می‌کنید:

Before DetectChanges: david
After DetectChanges: will

تا قبل از فراخوانی تشخیص تغییرات(DetectChanegs)، هنوز phone1 منتسب به شخص قدیمی(david) بوده، ولی پس از فراخوانی DetectChanges ، اصلاح رابطه رخ داده و همه  چیز با یکدیگر منطبق می‌شوند.

فعال سازی و کار با پروکسی‌های ردیابی تغییر

اگر پروفایلر کارایی شما، فراخوانی‌های بیش از اندازه DetectChnages را به عنوان یک مشکل شناسایی کند، و یا شما ترجیح می‌دهید که اصلاح رابطه به صورت بلادرنگ صورت گیرد ، ردیابی تغییر پروکسی‌های پویا، به عنوان گزینه ای دیگر مطرح می‌شود.فقط با چند تغییر کوچک در کلاس‌های POCO، EF قادر به ساخت پروکسی‌های پویا خواهد بود.پروکسی‌های ردیابی تغییر به EF اجازه ردیابی تغییرات در همان لحظه ای که ما تغییری در اشیای خود می‌دهیم را می‌دهند و همچنین امکان انجام اصلاح رابطه را در هر زمانی که تغییرات روابط را تشخیص دهد، دارد.
برای اینکه پروکسی ردیابی تغییر بتواند ساخته شود، باید قوانین زیر رعایت شود:
 
• کلاس باید public باشد و seald نباشد.
• همه‌ی خواص(properties) باید virtual تعریف شوند.
• همه‌ی خواص باید getter و setter با سطح دسترسی public داشته باشند.
• همه‌ی خواص راهبری مجموعه ای باید نوعشان، از نوع ICollection<T> تعریف شوند.


کلاس Person مثال خود را به گونه ای بازنویسی کرده ایم که تمام قوانین فوق را پیاده سازی کرده باشد.

نکته: توجه داشته باشید که ما دیگر در داخل سازنده کلاس ،کدی نمی‌نویسیم و منطقی  که باعث نمونه سازی اولیه خواص راهبری می‌شدند، را پیاده سازی نمی‌کنیم. این پروکسی ردیاب تغییر، همه‌ی خواص راهبری مجموعه ای را تحریف کرده و ار نوع مجموعه ای مخصوص خود(EntityCollection<T>) استفاده می‌کند. این نوع مجموعه ای، هر تغییری که در این مجموعه صورت می‌گیرد را زیر نظر گرفته و به ردیاب تغییر گزارش می‌دهد. اگر تلاش کنید تا نوع دیگری مانند List<T> که معمولا در سازنده کلاس از آن استفاده می‌کردیم را به آن انتساب دهیم، پروکسی، استثنایی را پرتاب می‌کند.

namespace EntitySample1.DomainClasses
{
    public class Person
    {
        public virtual int Id { get; set; }
        public virtual string FirstName { get; set; }
        public virtual string LastName { get; set; }
        public virtual DateTime BirthDate { get; set; }
        public virtual PersonInfo PersonInfo { get; set; }
        public virtual ICollection<PhoneNumber> PhoneNumbers { get; set; }
        public virtual ICollection<Address> Addresses { get; set; }
    }
}
همان طور که در مباحث مربوط به Lazy loading نیز مشاهده کردید،EF زمانی پروکسی‌های پویا را برای یک کلاس ایجاد می‌کند که یک یا چند خاصیت راهبری آن با virtual علامت گذاری شده باشند.آن پروکسی‌ها که از کلاس مورد نظر، مشتق شده اند، به خواص راهبری virtual امکان می‌دهند تا به صورت lazy لود شوند.پروکسی‌های ردیابی تغییر نیز به همان شکل در زمان اجرا ایجاد می‌شوند، با این تفاوت که این پروکسی ها، امکانات بیشتری دارند.
با این که احتیاجات رسیدن به پروکسی‌های ردیابی تغییر خیلی ساده هستند، اما ساده‌تر از آن ها، فراموش کردن یکی از آن هاست.حتی از این هم ساده‌تر می‌شود که در آینده تغییری در آن کلاس‌ها ایجاد کنید و ناخواسته یکی از آن قوانین را نقض کنید.به این خاطر، فکر خوبیست که یک آزمون واحد نیز اضافه کنیم تا مطمئن شویم که EF توانسته، پروکسی ردیابی تغییر را ایجاد کند یا نه.
در مثال زیر یک متد نوشته شده که این مورد را مورد آزمایش قرار می‌دهد. همچنین فراموش نکنید که فضای نام System.Data.Object.DataClasses را به using‌های خود اضافه کنید.
 
        private static void TestForChangeTrackingProxy()
        {
            using (var context = new PhoneBookDbContext())
            {
                var person = context.Persons.First();
                var isProxy = person is IEntityWithChangeTracker;
                Console.WriteLine("person is a proxy: {0}", isProxy);
            }
        }
زمانی که EF ، پروکسی پویا برای ردیابی تغییر ایجاد می‌کند، اینترفیس IentityWithChangeTrackerرا پیاده سازی  خواهد کرد.متد تست در مثال بالا، نمونه ای از Person را با دریافت آن از دیتابیس ایجاد می‌کند و سپس آن را با اینترفیس ذکر شده چک می‌کند تا مطمئن شود که Person ، توسط پروکسی ردیابی تغییر احاطه شده است. این نکته را نیز به یاد داشته باشید که چک کردن این که EF ، کلاس پروکسی ای که از کلاس ما مشتق شده است ایجاد کرده است یا نه،کفایت نمی‌کند، چرا که پروکسی‌های Lazy Loading نیز چنین کاری انجام می‌دهند. در حقیقت آن چیزی که سبب می‌شود EF به تغییرات صورت گرفته به صورت بلادرنگ گوش دهد،حضور IEntityWithChangeTracker است.
اکنون متد ManualDetectChanges را که کمی بالاتر بررسی کرده ایم را در نظر بگیرید و کد context.ChangeTracker.DetectChanges آن را حذف کنید و بار دیگر آن را فرا بخوانید  و نتیجه را مشاهده کنید:
 
Before DetectChanges: Modified
After DetectChanges: Modified
این دفعه،EF از تغییرات صورت گرفته آگاه است،حال چه DetectChanges فراخوانده شود یا نشود.

اکنون متد DetectRelationshipChanges را ویرایش کرده و برنامه را اجرا کنید:

Before DetectChanges: will
After DetectChanges: will
این بار می‌بینیم که EF، تغییر رابطه را تشخیص داده و اصلاح رابطه را بدون فراخوانی DetectChanges انجام داده است.

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

تذکر: اگر شما موجودیت هایی داشته باشید که شامل انواع پیچیده(Complex Types) می‌شوند،EF هنوز هم از ردیابی تغییر عکس فوری، برای خواص موجود در نوع پیچیده استفاده می‌کند، و از این جهت لازم است کهEF، برای نمونه‌ی نوع پیچیده، پروکسی ایجاد نمی‌کند.شما هنوز هم، تشخیص خودکار تغییرات خواصی که مستقیما درون آن موجودیت(Entity) تعریف شده اند را دارید، ولی تغییرات رخ داده درون نوع پیچیده، فقط از طریق DetectChanges قابل تشخیص است.

چگونگی اطمینان از اینکه نمونه‌های جدید ، پروکسی‌ها را دریافت خواهند کرد

EF به صورت خودکار برای نتایج حاصل از کوئری هایی که شما اجرا می‌کنید، پروکسی‌ها را ایجاد می‌کند. با این حال اگر شما فقط از سازنده‌ی کلاس POCO خود برای ایجاد نمونه‌ی جدید استفاده کنید،دیگر پروکسی‌ها ایجاد نخواهند شد.بدین منظور برای دریافت پروکسی ها، شما باید از متد DbSet.Create برای دریافت نمونه‌های جدید آن موجودیت استفاده کنید.
نکته: اگر شما، پروکسی‌های ردیابی تغییر را برای موجودیتی از مدلتان فعال کرده باشید،هنوز هم می‌توانید،نمونه‌های فاقد پروکسی آن موجودیت را ایجاد و بیافزایید.خوشبختانه EF با موجودیت‌های پروکسی و غیر پروکسی در همان مجموعه(set) کار می‌کند.شما باید آگاه باشید که ردیابی خودکار تغییرات و یا اصلاح رابطه، برای نمونه هایی که پروکسی هایی ردیابی تغییر نیستند، قابل استفاده نیستند.داشتن مخلوطی از نمونه‌های پروکسی و غیر پروکسی در همان مجموعه، می‌تواند گیج کننده باشد.بنابر این عموما توصیه می‌شود که برای ایجاد نمونه‌های جدید از DbSet.Create استفاده کنید، تا همه‌ی موجودیت‌های موجود در مجموعه، پروکسی‌های ردیابی تغییر باشند.
متد CreateNewProxies را به برنامه‌ی خود اضافه کرده و آن را اجرا کنید.

        private static void CreateNewProxies()
        {
            using (var context = new PhoneBookDbContext())
            {
                var phoneNumber = new PhoneNumber { Number = "987" };

                var davidPersonProxy = context.Persons.Create();
                davidPersonProxy.FirstName = "david";
                davidPersonProxy.PhoneNumbers.Add(phoneNumber);

                Console.WriteLine(phoneNumber.Person.FirstName);
            }
        }

خروجی مثال فوق david خواهد بود.همان طور که می‌بینید با استفاده از context.Persons.Create، نمونه‌ی ساخته شده، دیگر شی POCO نیست، بلکه davidPersonProxy، از جنس پروکسی ردیابی تغییر است و تغییرات آن به طور خودکار ردیابی شده و رابطه آن نیز به صورت خودکار اصلاح  می‌شود.در اینجا نیز با افزودن phoneNumber به شماره تلفن‌های davidPersonProxy، به طور خودکار رابطه‌ی بین phoneNumber و davidPersonPeroxy بر قرار شده است. همان طور که می‌دانید این عملیات بدون استفاده از پروکسی‌های ردیابی تغییرات امکان پذیر نیست و موجب بروز خطا می‌شود.


ایجاد نمونه‌های پروکسی برای انواع مشتق شده

اورلود جنریک دیگری برای DbSet.Create وجود  دارد که برای نمونه سازی کلاس‌های مشتق شده در مجموعه ما استفاده می‌شود .برای مثال، فراخوانی Create بر روی مجموعه‌ی Persons ،نمونه ای از کلاس Person را  بر می‌گرداند.ولی ممکن است کلاس هایی در مجموعه‌ی Persons وجود داشته باشند، که از آن مشتق شده باشند، مانند Student. برای دریافت نمونه‌ی پروکسی Student، از اورلود جنریک Create استفاده می‌کنیم.

var newStudent = context.Persons.Create<Student>();
واکشی موجودیت‌ها بدون ردیابی تغییرات

تا به این جای کار باید متوجه شده باشید که ردیابی تغییرات، فرآیندی ساده و بدیهی نیست و مقداری سربار در کار است. در بعضی از بخش‌های برنامه تان، احتمالا داده‌ها را به صورت فقط خواندنی در اختیار کاربران قرار می‌دهید و چون اطلاعات هیچ وقت تغییر نمی‌کنند، شما می‌خواهید که سربار ناشی از ردیابی تغییرات را حذف کنید.
خوشبختانه EF شامل متد AsNoTracking است که می‌توان از آن برای اجرای کوئری‌های بدون ردیابی استفاده کرد.یک کوئری بدون ردیابی، یک کوئری ساده هست که نتایج آن توسط context برای تشخیص تغییرات ردیابی نخواهد شد.

متد PrintPersonsWithoutChangeTracking را به برنامه اضافه کنید و آن را اجرا کنید: 
 
        private static void PrintPersonsWithoutChangeTracking()
        {
            using (var context = new PhoneBookDbContext())
            {
                var persons = context.Persons.AsNoTracking().ToList();

                foreach (var person in persons)
                {
                    Console.WriteLine(person.FirstName);
                }

            }
        }
در مثال بالا از متد AsNoTracking برای گرفتن کوئری فاقد ردیابی استفاده کردیم تا محتویات مجموعه Persons را دریافت کنیم. در نهایت با یک حلقه‌ی foreach، نتایج را بر روی کنسول به نمایش در آوردیم.به دلیل اینکه، این یک کوئری بدون ردیابی هست، context دیگر تغییراتی که روی Persons رخ می‌دهد را ردیابی نمی‌کند.در نتیجه اگر شما یکی از خواص یکی از Persons را تغییردهید و SaveChanges را صدا بزنید، تغییرات به دیتابیس ارسال نمی‌شوند.

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

AsNoTracking یک متد الحاقی است، که در <IQueryable<T تعریف شده است، در نتیجه شما می‌توانید از آن، در کوئری‌های LINQ نیز استفاده کنید. شما می‌توانید از AsNoTracking، در انتهای DbSet ،در خط from کوئری استفاده کنید.

var query = from p in context.Persons.AsNoTracking()
                            where p.FirstName == "joe"
                            select p;

شما همچنین از AsNoTracking می‌توانید برای تبدیل یک کوئری LINQ موجود، به یک کوئری فاقد ردیابی استفاده کنید. این نکته را به یاد داشته باشید که فقط AsNoTracking بر روی کوئری، فرانخوانده شده است، بلکه متغیر query را با نتیجه‌ی حاصل از فراخوانی AsNoTracking بازنویسی(override)  کرده است و این، از این جهت لازم است که AsNoTracking ،تغییری در کوئری ای که بر روی آن فراخوانده شده نمی‌دهد، بلکه یک کوئری جدید بر می‌گرداند. 

 
                var query = from p in context.Persons
                            where p.FirstName == "joe"
                            select p;
                query = query.AsNoTracking();
نکته: به دلیل اینکه AsNoTracking یک متد الحاقی است، شما احتیاج به افزودن فضای نام System.Data.Entity به فضا‌های نام خود دارید.

منبع: ترجمه ای آزاد از کتاب Programming Entity Framework: DbContext
مطالب
بهبود کارآیی کنترل‌های لیستی WPF در حین بارگذاری تعداد زیادی از رکوردها
کنترل‌های WPF در حالت پیش فرض و بدون اعمال قالب خاصی به آن‌ها عموما خوب عمل می‌کنند. مشکل از جایی شروع می‌شود که قصد داشته باشیم حالت پیش فرض را اندکی تغییر دهیم و یا Visual tree این کنترل‌ها اندکی پیچیده شوند. برای نمونه مدل زیر را در نظر بگیرید:
using System;

namespace WpfLargeLists.Models
{
    public class User
    {
        public int Id { set; get; }
        public string FirstName { set; get; }
        public string LastName { set; get; }
        public string Address { set; get; }
        public DateTime DateOfBirth { set; get; }
    }
}
قصد داریم فقط 1000 رکورد ساده از این مدل را به یک ListView اعمال کنیم.
                    <ListView ItemsSource="{Binding UsersTab1}" Grid.Row="1" Margin="3">
                        <ListView.View>
                            <GridView>
                                <GridViewColumn Header="Id" Width="50" DisplayMemberBinding="{Binding Id, Mode=OneTime}" />
                                <GridViewColumn Header="FirstName" Width="100" DisplayMemberBinding="{Binding FirstName, Mode=OneTime}" />
                                <GridViewColumn Header="LastName" Width="100" DisplayMemberBinding="{Binding LastName, Mode=OneTime}" />
                                <GridViewColumn Header="Address" Width="100" DisplayMemberBinding="{Binding Address, Mode=OneTime}" />
                                <GridViewColumn Header="DateOfBirth" Width="150" DisplayMemberBinding="{Binding DateOfBirth, Mode=OneTime}" />
                            </GridView>
                        </ListView.View>
                    </ListView>
در اینجا UsersTab1، لیستی حاوی فقط 1000 رکورد از شیء User است. در حالت معمولی این لیست بدون مشکل بارگذاری می‌شود. اما با اعمال مثلا قالب MahApp.Metro، بارگذاری همین لیست، حدود 5 ثانیه با CPU usage صد در صد طول می‌کشد. علت اینجا است که در این حالت WPF سعی می‌کند تا ابتدا در VisualTree، کل 1000 ردیف را کاملا ایجاد کرده و سپس نمایش دهد.


راه حل توصیه شده برای بارگذاری تعداد بالایی رکورد در WPF  : استفاده از UI Virtualization

UI Virtualization روشی است که در آن تنها المان‌هایی که توسط کاربر در حال مشاهده هستند، تولید و مدیریت خواهند شد. در این حالت اگر 1000 رکورد را به یک ListBox یا ListView ارسال کنید و کاربر بر اساس اندازه صفحه جاری خود تنها 10 رکورد را مشاهده می‌کند، WPF فقط 10 عنصر را در VisualTree مدیریت خواهد کرد. با اسکرول به سمت پایین، مواردی که دیگر نمایان نیستند dispose شده و مجموعه نمایان دیگری خلق خواهند شد. به این ترتیب می‌توان حجم بالایی از اطلاعات را در WPF با میزان مصرف پایین حافظه و همچنین مصرف CPU بسیار کم مدیریت کرد. این مجازی سازی در WPF به وسیله VirtualizingStackPanel در دسترس است.

برای اینکه WPF virtualization به درستی کار کند، نیاز است یک سری شرایط مقدماتی فراهم شوند:
- از کنترلی استفاده شود که از virtualization پشتیبانی می‌کند؛ مانند ListBox و ListView.
- ارتفاع کنترل لیستی باید دقیقا مشخص باشد؛ یا درون یک ردیف از Grid ایی باشد که ارتفاع آن مشخص است. برای نمونه اگر ارتفاع ردیف Grid ایی که ListView را دربرگرفته است به * تنظیم شده، مشکلی نیست؛ اما اگر ارتفاع این ردیف به Auto تنظیم شده، کنترل لیستی برای محاسبه vertical scroll bar خود دچار مشکل خواهد شد.
- کنترل مورد استفاده نباید در یک کنترل  ScrollViewer محصور شود؛ در غیر اینصورت virtualization رخ نخواهد داد. علاوه بر آن در خود کنترل باید خاصیت ScrollViewer.HorizontalScrollBarVisibility نیز به Disabled تنظیم گردد.
- در کنترل در حال استفاده، ScrollViewer.CanContentScroll باید به true تنظیم شود.

مورد مشخص بودن ارتفاع بسیار مهم است. برای نمونه در برنامه‌ای پس از فعال سازی مجازی سازی، کنترل لیستی کلا از کار افتاد و حرکت scroll bar آن سبب بروز CPU Usage بالایی می‌شد. این مشکل با تنظیم ارتفاع آن به شکل زیر برطرف شد:
 Height="{Binding Path=RowDefinitions[1].ActualHeight, RelativeSource={RelativeSource AncestorType=Grid}}"
در این تنظیم، ارتفاع کنترل، به ارتفاع سطر دوم گرید دربرگیرنده ListView متصل شده است.

- پس از اعمال موارد یاد شده، باید VirtualizingStackPanel کنترل را فعال کرد. ابتدا دو خاصیت زیر باید مقدار دهی شوند:
   VirtualizingStackPanel.IsVirtualizing="True"
  VirtualizingStackPanel.VirtualizationMode="Recycling"
سپس ItemsPanelTemplate کنترل باید به صورت یک VirtualizingStackPanel مقدار دهی شود. برای مثال اگر از ListBox استفاده می‌کنید، تنظیمات آن به نحو زیر است:
 <ListBox.ItemsPanel>
    <ItemsPanelTemplate>
          <VirtualizingStackPanel IsVirtualizing="True" VirtualizationMode="Recycling" />
    </ItemsPanelTemplate>
</ListBox.ItemsPanel>
و اگر از ListView استفاده می‌شود، تنظیمات آن مشابه ListBox است:
 <ListView.ItemsPanel>
    <ItemsPanelTemplate>
          <VirtualizingStackPanel
               IsVirtualizing="True"
               VirtualizationMode="Recycling" />
    </ItemsPanelTemplate>
</ListView.ItemsPanel>
با این توضیحات ListView ابتدای بحث به شکل زیر تغییر خواهد یافت تا مجازی سازی آن فعال گردد:
                    <ListView ItemsSource="{Binding UsersTab2}" Grid.Row="1" Margin="3"
                              ScrollViewer.HorizontalScrollBarVisibility="Disabled"
                              ScrollViewer.CanContentScroll="True"
                              VirtualizingStackPanel.IsVirtualizing="True"
                              VirtualizingStackPanel.VirtualizationMode="Recycling">
                        <ListView.ItemsPanel>
                            <ItemsPanelTemplate>
                                <VirtualizingStackPanel 
                                        IsVirtualizing="True" 
                                        VirtualizationMode="Recycling" />
                            </ItemsPanelTemplate>
                        </ListView.ItemsPanel>
                        <ListView.View>
                            <GridView>
                                <GridViewColumn Header="Id" Width="50" DisplayMemberBinding="{Binding Id, Mode=OneTime}" />
                                <GridViewColumn Header="FirstName" Width="100" DisplayMemberBinding="{Binding FirstName, Mode=OneTime}" />
                                <GridViewColumn Header="LastName" Width="100" DisplayMemberBinding="{Binding LastName, Mode=OneTime}" />
                                <GridViewColumn Header="Address" Width="100" DisplayMemberBinding="{Binding Address, Mode=OneTime}" />
                                <GridViewColumn Header="DateOfBirth" Width="150" DisplayMemberBinding="{Binding DateOfBirth, Mode=OneTime}" />
                            </GridView>
                        </ListView.View>
                    </ListView>
کدهای کامل مثال فوق را از اینجا می‌توانید دریافت کنید: WpfLargeLists.zip
در این مثال دو برگه را ملاحظه می‌کنید. برگه اول حالت normal ابتدای بحث است و برگه دوم پیاده سازی UI Virtualization را انجام داده است.

نظرات مطالب
C# 8.0 - Nullable Reference Types
یک نکته‌ی تکمیلی: تاثیر نوع‌های ارجاعی نال نپذیر C# 8.0 بر روی EF Core 3.0

تغییرات نحوه‌ی تعریف موجودیت‌ها در C# 8.0

تا پیش از C# 8.0، برای تعریف فیلدهای نال نپذیر و نال پذیر در موجودیت‌های EF Core، به صورت زیر عمل می‌شد:
    public class Person_BeforeCS8
    {
        [Required]
        public string FirstName { get; set; }  // NOT NULL

        public string MiddleName { get; set; } // NULL
    }
اگر رشته‌ای مزین به ویژگی Required باشد، یعنی به یک فیلد نال‌نپذیر، ترجمه و نگاشت خواهد شد و برعکس. اما پس از فعالسازی ویژگی نوع‌های ارجاعی نال نپذیر C# 8.0 در پروژه‌ی خود، کامپایلر اخطارهایی مانند «Non-nullable property 'FirstName' is uninitialized. Consider declaring the property as nullable» را به ازای تک تک خواص تعریف شده‌ی در کلاس موجودیت فوق، صادر می‌کند. برای رفع این مشکل می‌توان از bang operator که کمی بالاتر در مورد آن توضیح داده شده، استفاده کرد:
    public class Person_AfterCS8
    {
        public string FirstName { get; set; } = null!; // NOT NULL

        public string? MiddleName { get; set; } // NULL
    }
در اینجا نحوه‌ی تعریف دو فیلد نال‌نپذیر و نال پذیر را در موجودیت‌های EF Core 3.0 و C# 8.0 مشاهده می‌کنید. خاصیت اول دیگر نیازی به ویژگی Required ندارد؛ اما چون دیگر نال را نمی‌پذیرد، می‌توان مقدار دهی اولیه‌ی آن‌را توسط !null انجام داد؛ تا کامپایلر دیگر خطایی را در مورد عدم مقدار دهی اولیه‌ی آن صادر نکند (تنها کاربرد !null است). البته بهتر است !null را صرفا با EF Core و موجودیت‌های آن استفاده کنید و برای سایر کلاس‌ها، از دیگر روش‌های مطرح شده‌ی در این مطلب مانند تعریف یک سازنده، کمک بگیرید.
مزیت این روش نسبت به Person_BeforeCS8 این است که اینبار علاوه بر نال‌نپذیر تعریف شدن فیلد FirstName، نحوه‌ی استفاده‌ی از آن در برنامه (عدم انتساب نال به آن) نیز تحت کنترل کامپایلر قرار می‌گیرد که پیشتر با ویژگی Required چنین امری میسر نبود.
بنابراین در موجودیت‌های برنامه‌ی مبتنی بر C# 8.0، دیگر نیاز به استفاده‌ی از ویژگی Required نبوده و نال‌پذیری با عملگر ? مشخص می‌شود.


کار با وابستگی‌ها و ارتباط‌های نال‌پذیر

فرض کنید یک چنین کوئری را در EF Core 3.0 و C# 8.0 نوشته‌اید:
var parentPosts = db.Posts.Where(p => p.ParentPost.Id == postId).ToList();
در اینجا ParentPost می‌تواند نال باشد، اما در عمل EF Core به این موضوع اهمیتی نمی‌دهد و از آن صرفا جهت تهیه‌ی SQL نهایی استفاده می‌کند؛ اما کامپایلر C# 8.0، اخطار «Dereference of a possible null reference» را صادر می‌کند. برای رفع آن نیز می‌توان از bang operator استفاده کرد:
var parentPosts = db.Posts.Where(p => p.ParentPost!.Id == postId).ToList();
وجود عملگر ! در اینجا، به معنای اعلام صریح نال نبودن ParentPost، در شرایط کوئری فوق، به کامپایلر است.
مطالب
پشتیبانی از حذف و به‌روز رسانی دسته‌ای رکوردها در EF 7.0
همواره حذف و به روز رسانی تعداد زیادی رکورد توسط EF، بسیار غیربهینه و کند بوده‌است؛ از این جهت که یکی از روش‌های انجام اینکار، کوئری گرفتن از رکوردهای مدنظر جهت حذف، سپس بارگذاری آن‌ها در حافظه و در آخر حذف یکی یکی آن‌ها بوده‌است:
using var dbContext = new MyDbContext();
var objectToDelete = await dbContext.Objects.FirstAsync(o => o.Id == id);
dbContext.Objects.Remove(objectToDelete);
await dbContext.SaveChangesAsync();
در اینجا در ابتدا، شیء‌ای که قرار است حذف شود، از بانک اطلاعاتی کوئری گرفته می‌شود تا وارد سیستم Change Tracking شود. سپس از این سیستم ردیابی اطلاعات درون حافظه‌ای، حذف خواهد شد و در نهایت این تغییرات به بانک اطلاعاتی اعمال می‌شوند. بنابراین در این مثال ساده، حداقل دوبار رفت و برگشت به بانک اطلاعاتی وجود خواهد داشت.
البته راه دومی نیز برای انجام اینکار وجود دارد:
using var dbContext = new MyDbContext();
var objectToDelete = new MyObject { Id = id };
dbContext.Objects.Remove(objectToDelete);
await dbContext.SaveChangesAsync();
در این مثال، رفت و برگشت ابتدایی، حذف شده‌است و با فرض معلوم بودن کلید اصلی رکورد مدنظر، آن‌را وارد سیستم Change Tracking کرده و درنهایت آن‌را حذف می‌کنیم. کار متد Remove در اینجا، علامتگذاری این شیء دارای Id، به صورت EntityState.Deleted است.

اکنون می‌توان در EF 7.0، روش سومی را نیز به این لیست اضافه کرد که فقط یکبار رفت و برگشت به بانک اطلاعاتی را سبب می‌شود:
await dbContext.Objects.Where(x => x.Id == id).ExecuteDeleteAsync();


معرفی متدهای حذف و به‌روز رسانی دسته‌ای رکوردها در EF 7.0

EF 7.0 به همراه دو متد جدید ExecuteUpdate و ExecuteDelete (و همچنین نگارش‌های async آن‌ها) است که کار به‌روز رسانی و یا حذف دسته‌ای رکوردها را بدون دخالت سیستم Change tacking میسر می‌کنند. مزیت مهم این روش، عدم نیاز به کوئری گرفتن از بانک اطلاعاتی جهت بارگذاری رکوردهای مدنظر در حافظه و سپس حذف یکی یکی آن‌ها است. فقط باید دقت داشت که چون این روش خارج از سیستم Change tracking صورت می‌گیرد، نتیجه‌ی حاصل، دیگر با اطلاعات درون حافظه‌ای سمت کلاینت، هماهنگ نخواهد بود و کار به روز رسانی دستی آن‌ها به‌عهده‌ی شماست.


بررسی نحوه‌ی عملکرد ExecuteUpdate و ExecuteDelete با یک مثال

فرض کنید مدل‌های موجودیت‌های برنامه شامل کلاس‌های زیر هستند:
public class User
{
    public int Id { get; set; }
    public required string FirstName { get; set; }
    public required string LastName { get; set; }
    public virtual List<Book> Books { get; set; } = new();
    public virtual Address? Address { get; set; }
}

public class Book
{
    public int Id { get; set; }
    public required string Type { get; set; }
    public required string Name { get; set; }

    public virtual User User { get; set; } = default!;
    public int UserId { get; set; }
}

public class Address
{
    public int Id { get; set; }
    public required string Street { get; set; }
    
    public virtual User User { get; set; } = default!;
    public int UserId { get; set; }
}
که در اینجا یک کاربر می‌تواند دارای یک آدرس و چندین کتاب تعریف شده باشد؛ با این Context ابتدایی:
public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
    {
    }

    public DbSet<User> Users { get; set; } = default!;

    public DbSet<Book> Books { get; set; } = default!;

    public DbSet<Address> Addresses { get; set; } = default!;
}

مثال 1: حذف دسته‌ای تعدادی کتاب
context.Books.Where(book => book.Name.Contains("1")).ExecuteDelete();
در اینجا نحوه‌ی استفاده از متد ExecuteDelete را مشاهده می‌کنید که به انتهای LINQ Query، اضافه شده‌است. در این مثال، تمام کتاب‌هایی که در نامشان حرف 1 وجود دارد، حذف می‌شوند. این کوئری، به صورت زیر بر روی بانک اطلاعاتی اجرا می‌شود:
DELETE FROM [b]
FROM [Books] AS [b]
WHERE [b].[Name] LIKE N'%1%'
مهم‌ترین مزیت این روش، عدم نیاز به بارگذاری و یا ساخت درون حافظه‌ای لیست کتاب‌هایی است که قرار است حذف شوند. کل این عملیات در یک رفت و برگشت ساده و سریع انجام می‌شود.

یک نکته: متد ExecuteDelete، تعداد رکوردهای حذف شده را نیز بازگشت می‌دهد.


مثال 2: حذف کاربران و تمام رکوردهای وابسته به آن

فرض کنید می‌خواهیم تعدادی از کاربران را از بانک اطلاعاتی حذف کنیم:
context.Users.Where(user => user.Id <= 500).ExecuteDelete();
اگر این کوئری را با تنظیمات فعلی اجرا کنیم، با خطای زیر متوقف خواهیم شد:
DELETE FROM [u]
FROM [Users] AS [u]
WHERE [u].[Id] <= 500

The DELETE statement conflicted with the REFERENCE constraint "FK_Books_Users_UserId".
The conflict occurred in database "EF7BulkOperations", table "dbo.Books", column 'UserId'.
عنوان می‌کند که یک کاربر، دارای تعدادی کتاب و آدرسی از پیش ثبت شده‌است و نمی‌توان آن‌را بدون حذف وابستگی‌های آن، حذف کرد. اگر کاربری را حذف کنیم، کلید‌های خارجی ذکر شده‌ی در جداولی که این کلید خارجی را به همراه دارند، غیرمعتبر می‌شوند (و این کلید خارجی تعریف شده، نال پذیر هم نیست). برای رفع این مشکل، یا باید ابتدا در طی دستوراتی جداگانه، وابستگی‌های ممکن را حذف کنیم و یا می‌توان تنظیم cascade delete را به نحو زیر به تعریف جداول مرتبط اضافه کرد تا صدور یک دستور delete، به صورت خودکار سبب حذف وابستگی‌های مرتبط نیز شود:
public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
    {
    }

    public DbSet<User> Users { get; set; } = default!;

    public DbSet<Book> Books { get; set; } = default!;

    public DbSet<Address> Addresses { get; set; } = default!;

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder
            .Entity<User>()
            .HasMany(user => user.Books)
            .WithOne(book => book.User)
            .OnDelete(DeleteBehavior.Cascade);

        modelBuilder
            .Entity<User>()
            .HasOne(user => user.Address)
            .WithOne(address => address.User)
            .HasForeignKey<Address>(address => address.UserId)
            .OnDelete(DeleteBehavior.Cascade);
    }
}
همانطور که ملاحظه می‌کنید، به متد OnModelCreating تنظیم cascade delete وابستگی‌های جدول کاربران اضافه شده‌است. پس از این تنظیم، دستور مثال دوم، بدون مشکل اجرا شده و حذف یک کاربر، سبب حذف خودکار کتاب‌ها و آدرس او نیز می‌شود.


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

فرض کنید می‌خواهیم LastName تعدادی کاربر مشخص را به مقدار جدید Updated، تغییر دهیم:
context.Users.Where(user => user.Id <= 400)
   .ExecuteUpdate(p => p.SetProperty(user => user.LastName,  user => "Updated"));
برای اینکار، پس از مشخص شدن شرط کوئری در قسمت Where، کار به روز رسانی توسط متد ExecuteUpdate و سپس متد SetProperty صورت می‌گیرد. در اینجا در ابتدا مشخص می‌کنیم که کدام خاصیت قرار است به روز رسانی شود و پارامتر دوم آن، مقدار جدید را مشخص می‌کند. این کوئری به نحو زیر به بانک اطلاعاتی اعمال خواهد شد:
UPDATE [u]
SET [u].[LastName] = N'Updated'
FROM [Users] AS [u]
WHERE [u].[Id] <= 400
در اینجا می‌توان در پارامتر دوم متد SetProperty، از مقدار فعلی سایر خواص نیز استفاده کرد:
context.Users.Where(user => user.Id <= 300)
  .ExecuteUpdate(p => p.SetProperty(user => user.LastName,
      user => "Updated" + user.LastName));
که خروجی زیر را تولید می‌کند:
UPDATE [u]
SET [u].[LastName] = N'Updated' + [u].[LastName]
FROM [Users] AS [u]
WHERE [u].[Id] <= 300
همچنین می‌توان چندین متد SetProperty را نیز به صورت زنجیروار، جهت به روز رسانی چندین خاصیت و فیلد، ذکر کرد:
context.Users.Where(user => user.Id <= 800)
   .ExecuteUpdate(p => p.SetProperty(user => user.LastName,
        user => "Updated" + user.LastName)
             .SetProperty(user => user.FirstName,
                 user => "Updated" + user.FirstName));
با این خروجی نهایی:
UPDATE [u]
SET [u].[FirstName] = N'Updated' + [u].[FirstName],
[u].[LastName] = N'Updated' + [u].[LastName]
FROM [Users] AS [u]
WHERE [u].[Id] <= 800
متد ExecuteUpdate، تعداد رکوردهای به‌روز رسانی شده را نیز بازگشت می‌دهد.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید:  EF7BulkOperations.zip
مطالب
شروع به کار با EF Core 1.0 - قسمت 1 - برپایی تنظیمات اولیه
در ادامه‌ی سری «ارتقاء به ASP.NET Core 1.0» اگر بخواهیم مباحث اعتبارسنجی کاربران و ASP.NET Identity مخصوص آن‌را بررسی کنیم، نیاز است ابتدا مباحث Entity framework Core 1.0 را بررسی کنیم. به همین جهت در طی چند قسمت مباحث پایه‌ای کار با EF Core 1.0 را در ASP.NET Core 1.0، بررسی خواهیم کرد. بنابراین پیشنیاز ضروری این مباحث، مطالعه‌ی سری «ارتقاء به ASP.NET Core 1.0» است و در آن از مباحثی مانند چگونگی کار با فایل‌های کانفیگ جدید، تزریق وابستگی‌ها و سرویس‌ها، فعال سازی سرویس Logging، فعال سازی صفحات مخصوص توسعه دهنده‌ها و ... در ASP.NET Core 1.0 استفاده خواهد شد.


EF Core چیست؟

EF Core یک ORM یا object-relational mapper چندسکویی است که امکان کار با بانک‌های اطلاعاتی مختلف را از طریق اشیاء دات نتی میسر می‌کند. توسط آن قسمت عمده‌ی کدهای مستقیم کار با بانک‌های اطلاعاتی حذف شده و تبدیل به کدهای دات نتی می‌شوند. مزیت این لایه‌ی Abstraction اضافی (لایه‌ای بر روی کدهای مستقیم لایه ADO.NET زیرین)، امکان تعویض بانک اطلاعاتی مورد استفاده، تنها با تغییر کدهای آغازین برنامه‌است؛ بدون نیاز به تغییری در سایر قسمت‌های برنامه. همچنین کار با اشیاء دات نتی و LINQ، مزایایی مانند تحت نظر قرار گرفتن کدها توسط کامپایلر و برخورداری از ابزارهای Refactoring پیشرفته را میسر می‌کنند. به علاوه SQL خودکار تولیدی توسط آن نیز همواره پارامتری بوده و مشکلات حملات تزریق SQL در این حالت تقریبا به صفر می‌رسند (اگر مستقیما SQL نویسی نکنید و صرفا از LINQ استفاده کنید). مزیت دیگر همواره پارامتری بودن کوئری‌ها، رفتار بسیاری از بانک‌های اطلاعاتی با آن‌ها همانند رویه‌های ذخیره شده است که به عدم تولید Query plan‌های مجزایی به ازای هر کوئری رسیده منجر می‌شود که در نهایت سبب بالا رفتن سرعت اجرای کوئری‌ها و مصرف حافظه‌ی کمتری در سمت سرور بانک اطلاعاتی می‌گردد.


تفاوت EF Core با نگارش‌های دیگر Entity framework در چیست؟

سورس باز بودن
EF از نگارش‌های آخر آن بود که سورس باز شد؛ اما EF Core از زمان نگارش‌های پیش نمایش آن به صورت سورس باز در GitHub قابل دسترسی است.

چند سکویی بودن
EF Core برخلاف EF 6.x (آخرین نگارش مبتنی بر Full Framework آن)، نه تنها چندسکویی است و قابلیت اجرای بر روی Mac و لینوکس را نیز دارا است، به علاوه امکان استفاده‌ی از آن در انواع و اقسام برنامه‌های دات نتی مانند UWP یا Universal Windows Platform و Windows phone که پیشتر با EF 6.x میسر نبود، وجود دارد. لیست این نوع سکوها و برنامه‌های مختلف به شرح زیر است:
 • All .NET application (Console, ASP.NET 4, WinForms, WPF)
 • Mac and Linux applications (Mono)
 • UWP (Universal Windows Platform)
 • ASP.NET Core applications
 • Can use EF Core in Windows phone and Windows store app

افزایش تعداد بانک‌های اطلاعاتی پشتیبانی شده
در EF Full یا EF 6.x، هدف اصلی، تنها کار با بانک‌های اطلاعاتی رابطه‌‌ای بود و همچنین مایکروسافت صرفا نگارش‌های مختلف SQL Server را به صورت رسمی پشتیبانی می‌کرد و برای سایر بانک‌های اطلاعاتی دیگر باید از پروایدرهای ثالث استفاده کرد.
در EF Core علاوه بر افزایش تعداد پروایدرهای رسمی بانک‌های اطلاعاتی رابطه‌ای، پشتیبانی از بانک‌های اطلاعاتی NoSQL هم اضافه شده‌است؛ به همراه پروایدر ویژه‌‌ای به نام In Memory جهت انجام ساده‌تر Unit testing. کاری که با نگارش‌های پیشین EF به سادگی و از روز اول پشتیبانی نمی‌شد.

حذف و یا عدم پیاده سازی تعدادی از قابلیت‌های EF 6.x
اگر موارد فوق جزو مهم‌ترین مزایای کار با EF Core باشند، باید درنظر داشت که به علت حذف و یا تقلیل یافتن یک سری از ویژگی‌ها در NET Core.، مانند Reflection (جهت پشتیبانی از دات نت در سکوهای مختلف کاری و خصوصا پشتیبانی از حالتی که کامپایلر مخصوص برنامه‌های UWP نیاز دارد تمام نوع‌ها را همانند زبان‌های C و ++C، در زمان کامپایل بداند)، یک سری از قابلیت‌های EF 6.x مانند Lazy loading هنوز در EF Core پشتیبانی نمی‌شوند. لیست کامل و به روز شده‌ی این موارد را در اینجا می‌توانید مطالعه کنید.
بنابراین امکان انتقال برنامه‌های EF 6.x به EF Core 1.0 عموما وجود نداشته و نیاز به بازنویسی کامل دارند. هرچند بسیاری از مفاهیم آن با EF Code First یکی است.


برپایی تنظیمات اولیه‌ی EF Core 1.0 در یک برنامه‌ی ASP.NET Core 1.0

برای نصب تنظیمات اولیه‌ی EF Core 1.0 در یک برنامه‌ی ASP.NET Core 1.0، جهت کار با مشتقات SQL Server (و SQL LocalDB) نیاز است سه بسته‌ی ذیل را نصب کرد (از طریق منوی Tools -> NuGet Package Manager -> Package Manager Console):
PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer
PM> Install-Package Microsoft.EntityFrameworkCore.Tools -Pre
PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer.Design
البته در این قسمت صرفا از بسته‌ی اول که جهت اتصال به SQL Server است استفاده می‌کنیم. بسته‌های دیگر را در قسمت‌های بعد، برای به روز رسانی اسکیمای بانک اطلاعاتی (مباحث Migrations) و مباحث scaffolding استفاده خواهیم کرد.
پس از اجرای سه دستور فوق، تغییرات مداخل فایل project.json برنامه به صورت ذیل خواهند بود:
{
    "dependencies": {
       // same as before
        "Microsoft.EntityFrameworkCore.SqlServer": "1.0.0",
        "Microsoft.EntityFrameworkCore.Tools": "1.0.0-preview2-final",
        "Microsoft.EntityFrameworkCore.SqlServer.Design": "1.0.0"
    }
}
این مداخلی که توسط نیوگت اضافه شده‌اند، نیاز به اصلاح دارند؛ به این صورت:
{
    "dependencies": {
       // same as before
        "Microsoft.EntityFrameworkCore.SqlServer": "1.0.0",
        "Microsoft.EntityFrameworkCore.Tools": {
            "version": "1.0.0-preview2-final",
            "type": "build"
        },
        "Microsoft.EntityFrameworkCore.SqlServer.Design": {
            "version": "1.0.0",
            "type": "build"
        }
    },

    "tools": {
       // same as before
        "Microsoft.EntityFrameworkCore.Tools": {
            "version": "1.0.0-preview2-final",
            "imports": [
                "portable-net45+win8"
            ]
        }   
   }
}
نیاز است در قسمت dependencies مشخص کنیم که ابزارهای اضافه شده مخصوص build هستند و نه اجرای برنامه. همچنین قسمت tools را باید با Microsoft.EntityFrameworkCore.Tools مقدار دهی کرد تا بتوان از این ابزار در خط فرمان، جهت اجرای فرامین migrations استفاده کرد.
بنابراین از همین ابتدای کار، بدون مراجعه‌ی به Package Manager Console، چهار تغییر فوق را به فایل project.json اعمال کرده و آن‌را ذخیره کنید؛ تا کار به روز رسانی و نصب بسته‌ها، به صورت خودکار و همچنین صحیحی انجام شود.


فعال سازی صفحات مخصوص توسعه دهنده‌های EF Core 1.0

در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 5 - فعال سازی صفحات مخصوص توسعه دهنده‌ها» با تعدادی از اینگونه صفحات آشنا شدیم. برای EF Core نیز بسته‌ی مخصوصی به نام Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore وجود دارد که امکان فعال سازی صفحه‌ی نمایش خطاهای بانک اطلاعاتی را میسر می‌کند. بنابراین ابتدا به فایل project.json مراجعه کرده و این بسته را اضافه کنید:
{
    "dependencies": {
       // same as before
        "Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore": "1.0.0"
    }
}
سپس می‌توان متد جدید UseDatabaseErrorPage را در متد Configure کلاس آغازین برنامه، فراخوانی کرد:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
   if (env.IsDevelopment())
   {
      app.UseDatabaseErrorPage();
   }
با فعال سازی این صفحه، اگر در حین توسعه‌ی برنامه و اتصال به بانک اطلاعاتی، خطایی رخ دهد، بجای مشاهده‌ی یک صفحه‌ی خطای عمومی (اگر UseDeveloperExceptionPage را فعال کرده باشید)، اینبار ریز جزئیات بیشتری را به همراه توصیه‌هایی از طرف تیم EF مشاهده خواهید کرد.


تعریف اولین Context برنامه و مشخص سازی رشته‌ی اتصالی آن


در این تصویر، زیر ساخت نگاشت‌های EF Core را مشاهده می‌کنید. در سمت چپ، ظرفی را داریم به نام DB Context که در برگیرنده‌ی Db Setها است. در سمت راست که بیانگر ساختار کلی یک بانک اطلاعاتی است، معادل این‌ها را مشاهده می‌کنیم. هر Db Set به یک جدول بانک اطلاعاتی نگاشت خواهد شد و متشکل است از کلاسی به همراه یک سری خواص که این‌ها نیز به فیلدها و ستون‌های آن جدول در سمت بانک اطلاعاتی نگاشت می‌شوند.
بنابراین برای شروع کار، پوشه‌ای را به نام Entities به پروژه اضافه کرده و سپس کلاس ذیل را به آن اضافه می‌کنیم:
namespace Core1RtmEmptyTest.Entities
{
    public class Person
    {
        public int PersonId { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
}
کلاس Person بیانگر ساختار جدول اشخاص بانک اطلاعاتی است. برای اینکه این کلاس را تبدیل و نگاشت به یک جدول کنیم، نیاز است آن‌را به صورت یک DbSet در معرض دید EF Core قرار دهیم و اینکار در کلاسی که از DbContex مشتق می‌شود، صورت خواهد گرفت:
using Microsoft.EntityFrameworkCore;

namespace Core1RtmEmptyTest.Entities
{
    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
        {
        }

        public DbSet<Person> Persons { get; set; }
    }
}
بنابراین در ادامه کلاس جدید ApplicationDbContext را که از کلاس پایه DbContext مشتق می‌شود تعریف کرده و سپس کلاس Person را به صورت یک DbSet در معرض دید EF Core قرار می‌دهیم.
سازنده‌ی این کلاس نیز به نحو خاصی تعریف شده‌است. اگر به سورس‌های EF Core مراجعه کنیم، کلاس پایه‌ی DbContext دارای دو سازنده‌ی با و بدون پارامتر است:
protected DbContext()
   : this((DbContextOptions) new DbContextOptions<DbContext>())
{
}

public DbContext([NotNull] DbContextOptions options)
{
  // …
}
اگر از سازنده‌ی بدون پارامتر استفاده کنیم و برای مثال در کلاس ApplicationDbContext فوق، به طور کامل سازنده‌ی تعریف شده را حذف کنیم، باید به نحو ذیل تنظیمات بانک اطلاعاتی را مشخص کنیم:
using Microsoft.EntityFrameworkCore;

namespace Core1RtmEmptyTest.Entities
{
    public class ApplicationDbContext : DbContext
    {
        public DbSet<Person> Persons { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"... connection string ...");
        }
    }
}
در این حالت باید متد OnConfiguring را override و یا بازنویسی کنیم، تا بتوان از اول مشخص کرد که قرار است از پروایدر SQL Server استفاده کنیم و ثانیا رشته‌ی اتصالی به آن چیست.
اما چون در یک برنامه‌ی ASP.NET Core، کار ثبت سرویس مربوط به EF Core، در کلاس آغازین برنامه انجام می‌شود و در آنجا به سادگی می‌توان به خاصیت Configuration برنامه دسترسی یافت و توسط آن رشته‌ی اتصالی را دریافت کرد، مرسوم است از سازنده‌ی با پارامتر DbContext به نحوی که در ابتدا عنوان شد، استفاده شود.
بنابراین در ادامه، پس از مطالعه‌ی مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایل‌های config» به فایل appsettings.json مراجعه کرده و تنظیمات رشته‌ی اتصالی برنامه را به صورت ذیل در آن مشخص می‌کنیم:
{
    "ConnectionStrings": {
        "ApplicationDbContextConnection": "Data Source=(local);Initial Catalog=TestDbCore2016;Integrated Security = true"
    }
}
باید دقت داشت که نام این مداخل کاملا اختیاری هستند و در نهایت باید در کلاس آغازین برنامه به صورت صریحی مشخص شوند.
در اینجا به وهله‌ی پیش فرض SQL Server اشاره شده‌است؛ از حالت اعتبارسنجی ویندوزی SQL Server استفاده می‌شود و بانک اطلاعاتی جدیدی به نام TestDbCore2016 در آن مشخص گردیده‌است.

پس از تعریف رشته‌ی اتصالی، متد OnConfiguring را از کلاس ApplicationDbContext حذف کرده و از همان نگارش دارای سازنده‌ی با پارامتر آن استفاده می‌کنیم. برای اینکار به کلاس آغازین برنامه مراجعه کرده و توسط متد AddDbContext این Context را به سرویس‌های ASP.NET Core معرفی می‌کنیم:
    public class Startup
    {
        public IConfigurationRoot Configuration { set; get; }

        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                                .SetBasePath(env.ContentRootPath)
                                .AddJsonFile("appsettings.json", reloadOnChange: true, optional: false)
                                .AddJsonFile($"appsettings.{env}.json", optional: true);
            Configuration = builder.Build();
        }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<IConfigurationRoot>(provider => { return Configuration; });
            services.AddDbContext<ApplicationDbContext>(options =>
            {
                options.UseSqlServer(Configuration["ConnectionStrings:ApplicationDbContextConnection"]);
            });
در اینجا جهت یادآوری مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایل‌های config» نحوه‌ی وهله سازی خاصیت Configuration که در متد UseSqlServer مورد استفاده قرار گرفته‌است، نیز ذکر شده‌است.
بنابراین قسمت options.UseSqlServer را یا در اینجا مقدار دهی می‌کنید و یا از طریق بازنویسی متد OnConfiguring کلاس Context برنامه.


یک نکته: امکان تزریق IConfigurationRoot به کلاس Context برنامه وجود دارد

با توجه به اینکه Context برنامه را به صورت یک سرویس به ASP.NET Core معرفی کردیم، امکان تزریق وابستگی‌ها نیز در آن وجود دارد. یعنی بجای روش فوق، می‌توان IConfigurationRoot را به سازنده‌ی کلاس Context برنامه نیز تزریق کرد:
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;

namespace Core1RtmEmptyTest.Entities
{
    public class ApplicationDbContext : DbContext
    {
        private readonly IConfigurationRoot _configuration;

        public ApplicationDbContext(IConfigurationRoot configuration)
        {
            _configuration = configuration;
        }

        //public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
        //{
        //}

        public DbSet<Person> Persons { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(_configuration["ConnectionStrings:ApplicationDbContextConnection"]);
        }
    }
}
با توجه به اینکه IConfigurationRoot در کلاس ConfigureServices به صورت Singleton، به مجموعه‌ی سرویس‌های برنامه معرفی شده‌است، از آن در تمام کلاس‌های برنامه که تحت نظر سیستم تزریق وابستگی‌های توکار ASP.NET Core هستند، می‌توان استفاده کرد.
در این حالت متد ConfigureServices کلاس آغازین برنامه، چنین شکلی را پیدا می‌کند و ساده می‌شود:
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>();


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

همینقدر که ApplicationDbContext را به عنوان سرویسی در ConfigureServices تعریف کردیم، امکان تزریق آن در اجزای مختلف یک برنامه‌ی ASP.NET Core نیز وجود دارد:
using System.Linq;
using Core1RtmEmptyTest.Entities;
using Microsoft.AspNetCore.Mvc;

namespace Core1RtmEmptyTest.Controllers
{
    public class TestDBController : Controller
    {
        private readonly ApplicationDbContext _ctx;

        public TestDBController(ApplicationDbContext ctx)
        {
            _ctx = ctx;
        }

        public IActionResult Index()
        {
            var name = _ctx.Persons.First().FirstName;
            return Json(new { firstName = name });
        }
    }
}
در اینجا نحوه‌ی تزریق DB Context برنامه را به یک کنترلر مشاهده می‌کنید. البته هرچند تزریق یک کلاس مشخص به این شکل، تزریق وابستگی‌ها نام ندارد و هنوز این کنترلر دقیقا وابسته‌است به پیاده سازی خاص کلاس ApplicationDbContext، اما ... در کل امکان آن هست.

در این حالت پس از اجرای برنامه، خطای ذیل را مشاهده خواهیم کرد:


علت اینجا است که هنوز این بانک اطلاعاتی ایجاد نشده‌است و همچنین ساختار جداول را به آن منتقل نکرده‌ایم که این موارد را در قسمت‌های بعدی مرور خواهیم کرد.
مطالب
Minimal API's در دات نت 6 - قسمت سوم - ایجاد endpoints مقدماتی
در دو قسمت قبل، ساختار ابتدایی برنامه‌ی Minimal API's بلاگ دهی را ایجاد کردیم. در این قسمت می‌خواهیم بررسی کنیم، معادل‌های کنترلرهای MVC و اکشن متدهای آن‌ها در سیستم جدید Minimal API، به چه صورتی ایجاد می‌شوند.


ایجاد اولین endpoint از نوع Get مبتنی بر Minimal API

برای افزودن اولین endpoint برنامه، به فایل Program.cs برنامه مراجعه کرده و آن‌را به صورت زیر تکمیل می‌کنیم:
// ...

app.UseHttpsRedirection();

app.MapGet("/api/authors", async (MinimalBlogDbContext ctx) =>
{
    var authors = await ctx.Authors.ToListAsync();
    return authors;
});

app.Run();
app.MapGet، معادل یک اکشن متد کنترلرهای MVC را که از نوع HttpGet هستند، ارائه می‌دهد. در همینجا می‌توان آدرس دقیق این endpoint را به عنوان پارامتر اول، مشخص کرد که پس از فراخوانی آن در مرورگر، یک Delegate که هندلر نام دارد (پارامتر دوم این متد)، اجرا می‌شود تا Response ای را ارائه دهد.
همانطور که مشاهده می‌کنید می‌توان در اینجا، این Delegate را از نوع Lambda expressions تعریف کرد و با ذکر MinimalBlogDbContext به صورت یک پارامتر آن، کار تزریق وابستگی‌های خودکار آن نیز صورت می‌گیرد. شبیه به حالتی که می‌توان یک سرویس را به عنوان پارامتر یک اکشن متد، با ذکر ویژگی [FromServices] در کنترلرهای MVC معرفی کرد؛ البته در اینجا بدون نیاز به ذکر این ویژگی (هرچند هنوز هم قابل ذکر است). مزیت آن این است که هر endpoint، تنها سرویس‌های مورد نیاز خودش را دریافت می‌کند و نه یک لیست قابل توجه از تمام سرویس‌هایی که قرار است در قسمت‌های مختلف یک کنترلر استفاده شوند.
پس از آن می‌توان با Context ای که در اختیار داریم، عملیات مدنظر را پیاده سازی کرده و یک خروجی را ارائه دهیم. در اینجا دیگر نیازی به تعریف IActionResult‌ها و امثال آن نیست و همه چیز ساده شده‌است.


ایجاد اولین endpoint از نوع Post مبتنی بر Minimal API

app.MapPost، معادل یک اکشن متد کنترلرهای MVC را که از نوع HttpPost هستند، ارائه می‌دهد:
//...

app.UseHttpsRedirection();

//...

app.MapPost("/api/authors", async (MinimalBlogDbContext ctx, AuthorDto authorDto) =>
{
    var author = new Author();
    author.FirstName = authorDto.FirstName;
    author.LastName = authorDto.LastName;
    author.Bio = authorDto.Bio;
    author.DateOfBirth = authorDto.DateOfBirth;

    ctx.Authors.Add(author);
    await ctx.SaveChangesAsync();

    return author;
});

app.Run();

internal record AuthorDto(string FirstName, string LastName, DateTime DateOfBirth, string? Bio);
در ابتدا یک Dto را که حاوی اطلاعات نویسنده‌ی جدیدی است، معادل خواص مدل Author دومین برنامه، تعریف می‌کنیم. سپس می‌توان این Dto را نیز به صورت یک پارامتر جدید به Lambda Expression متد app.MapPost معرفی کرد تا کار نگاشت اطلاعات دریافتی به آن، به صورت خودکار انجام شود (حالت پیش‌فرض آن [FromBody] است که نیازی به ذکر آن نیست).
سعی شده‌است تا این مثال در ساده‌ترین شکل ممکن خودش ارائه شود. در ادامه کار نگاشت خواص Dto را به مدل دومین برنامه، توسط AutoMapper انجام خواهیم داد.
مابقی نکات متد app.MapPost نیز مانند متد app.MapGet است؛ برای مثال در اینجا نیز تعریف مسیر endpoint، توسط اولین پارامتر این متد صورت می‌گیرد و نحوه‌ی تزریق سرویس DbContext برنامه نیز یکی است.


آزمایش برنامه‌ی Minimal API's

برنامه‌ی Minimal API's تهیه شده، به همراه یک Swagger از پیش تنظیم شده نیز هست. به همین جهت برای کار با این API الزاما نیازی به استفاده‌ی از مثلا برنامه‌ی Postman یا راه حل‌های مشابه نیست. بنابراین فقط کافی است تا برنامه‌ی API را اجرا کرده و در رابط کاربری ظاهر شده در آدرس https://localhost:7085/swagger/index.html، بر روی دکمه‌ی Try it out هر کدام از endpointها کلیک کنیم. برای مثال اگر چنین کاری را در قسمت Post انجام دهیم، به تصویر زیر می‌رسیم:



در اینجا پس از ویرایش اطلاعات شیء JSON ای که برای ما تدارک دیده‌است، فقط کافی است بر روی دکمه‌ی execute ذیل آن کلیک کنیم تا اطلاعات این Dto را به app.MapPost متناظر فوق ارسال کند و برای نمونه خروجی بازگشتی از سرور را نیز در همینجا نمایش می‌دهد که در آن، Id رکورد نیز پس از ثبت در بانک اطلاعاتی، مشخص است:



شروع به Refactoring و خلوت کردن فایل Program.cs

اگر بخواهیم به همین نحو تمام endpoints و dtoها را داخل فایل Program.cs اضافه کنیم، پس از مدتی به یک فایل بسیار حجیم و غیرقابل نگهداری خواهیم رسید. بنابراین در مرحله‌ی اول، تنظیمات سرویس‌ها و میان افزارها را به خارج از آن منتقل می‌کنیم. برای این منظور پوشه‌ی جدید Extensions را به همراه دو کلاس زیر ایجاد می‌کنیم:
using Microsoft.EntityFrameworkCore;
using MinimalBlog.Dal;

namespace MinimalBlog.Api.Extensions;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddApplicationServices(this IServiceCollection services,
        WebApplicationBuilder builder)
    {
        if (builder == null)
        {
            throw new ArgumentNullException(nameof(builder));
        }

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

        var connectionString = builder.Configuration.GetConnectionString("Default");
        builder.Services.AddDbContext<MinimalBlogDbContext>(opt => opt.UseSqlServer(connectionString));

        return services;
    }
}
کار این متد الحاقی، خارج کردن تنظیمات سرویس‌های برنامه از کلاس Program است.

همچنین نیاز به متد الحاقی دیگری برای خارج کردن تنظیمات میان‌افزارها داریم:
namespace MinimalBlog.Api.Extensions;

public static class WebApplicationExtensions
{
    public static WebApplication ConfigureApplication(this WebApplication app)
    {
        if (app == null)
        {
            throw new ArgumentNullException(nameof(app));
        }

        if (app.Environment.IsDevelopment())
        {
            app.UseSwagger();
            app.UseSwaggerUI();
        }

        app.UseHttpsRedirection();

        return app;
    }
}
پس از این تغییرات، اکنون ابتدای کلاس Program برنامه‌ی Api به صورت زیر تغییر می‌کند و خلاصه می‌شود:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddApplicationServices(builder);

var app = builder.Build();
app.ConfigureApplication();

در قسمت بعد، endpoints را از این کلاس آغازین برنامه خارج می‌کنیم.
نظرات مطالب
بررسی تفصیلی رابطه Many-to-Many در EF Code first
سلام. من ساختاری شبیه به این دارم:
public class Person
{
        //...
        public virtual IList<Center> PreferedCenters { get; set; }
        public virtual IList<Center> ActiveCenters { get; set; }

        public Person()
        {
            PreferedCenters = new List<Center>();
            ActiveCenters = new List<Center>();
        }
}
و کلا Center هم به شکل زیره:
public class Center
{
    //...
    public virtual IList<Person> Persons { get; set; }

    public Center()
    {
        Persons = new List<Person>();
    }
}
مشکلی که دارم اینه که منطقا باید دوتا رابطه‌ی Many-to-many تشکیل بشه: ActiveCenters و PreferedCenters. ولی توی جدول خروجی EF، فقط ستونی به اسم Center_ID ساخته می‌شه و وقتی هم که می‌خوام به سیستم چیزی اضافه کنم اروری شبیه به این می‌گیرم: 
An unhandled exception of type 'System.InvalidOperationException' occurred in EntityFramework.dll

Additional information: Multiplicity constraint violated. The role 'Center_Persons_Source' of the relationship 'Yarigaran.DataLayer.Center_Persons' has multiplicity 1 or 0..1.
مطالب
آشنایی با KnownTypeAttribute در WCF
تشریح مسئله : KnownTypeAttribute چیست و چگونه از آن استفاده کنیم؟
پیش نیاز : آشنایی اولیه با مفاهیم WCF برای فهم بهتر مطالب
در ابتدا یک WCf Service Application ایجاد کنید  و مدل زیر را بسازید:
 [DataContract] 
    public abstract class Person
    {
        [DataMember]
        public int Code { get; set; }

        [DataMember]
        public string Name { get; set; }     
}
یک کلاس پایه برای Person ایجاد کردیم به صورت abstract که وهله سازی از آن میسر نباشد و 2 کلاس دیگر می‌سازیم که از کلاس بالا ارث ببرند:
کلاس #1
 [DataContract]  
    public class Student : Person
    {
        [DataMember]
        public int StudentId { get; set; }
    }
کلاس #2
[DataContract]
    public class Teacher : Person
    {
        public int TeacherId { get; set; }
    }
فرض کنید قصد داریم سرویسی ایجاد کنیم که لیست تمام اشخاص موجود در سیستم را در اختیار ما قرار دهد. (هم Student و هم Teacher). ابتدا Contract مربوطه را به صورت زیر تعریف می‌کنیم:
[ServiceContract]
    public interface IStudentService
    {
        [OperationContract]
        IEnumerable<Person> GetAll();
    }
همان طور که می‌بینید خروجی متد GetAll  از نوع Person است (نوع پایه کلاس Student , Teacher) . سرویس مربوطه بدین شکل خواهد شد.
 public class StudentService : IStudentService
    {
        public IEnumerable<Person> GetAll()
        {
            List<Person> listOfPerson = new List<Person>();

            listOfPerson.Add( new Student() { Code = 1, StudentId = 123, Name = "Masoud Pakdel" } );
            listOfPerson.Add( new Student() { Code = 1, StudentId = 123, Name = "Mostafa Asgari"} );
            listOfPerson.Add( new Student() { Code = 1, StudentId = 123, Name = "Saeed Alizadeh"} );          
listOfPerson.Add( new Teacher() { Code = 1, TeacherId = 321, Name = "Mahdi Rad"} ); listOfPerson.Add( new Teacher() { Code = 1, TeacherId = 321, Name = "Mohammad Heydari" } ); listOfPerson.Add( new Teacher() { Code = 1, TeacherId = 321, Name = "Saeed Khatami"} ); return listOfPerson; }
در این سرویس در متد GetAll لیستی از تمام اشخاص رو ایجاد می‌کنیم . 3 تا Student و 3 تا Teacher رو به این لیست اضافه میکنیم. برای نمایش اطلاعات در خروجی یک پروژه Console Application ایجاد کنید و سرویس بالا رو از روش AddServiceReference به پروژه اضافه کنید سپس در کلاس Program کد‌های زیر رو کپی کنید.
 class Program
    {
        static void Main( string[] args )
        {
            StudentService.StudentServiceClient client = new StudentService.StudentServiceClient();

            client.GetAll().ToList().ForEach( _record => 
            {
                Console.Write( "Name : {0}", _record.Name );
                Console.WriteLine( "Code : {0}", _record.Code );
            } );

            Console.ReadLine();
        }
    }
پروژه رو کامپایل کنید. تا اینجا هیچ گونه مشکلی مشاهده نشد و انتظار داریم که خروجی مورد نظر رو مشاهده کنیم. بعد از اجرای پروژه با خطای زیر متوقف می‌شویم:

مشکل از اینجا ناشی می‌شود که هنگام عمل سریالایز ، WCF Runtime با توجه به وهله سازی از کلاس Person می‌دونه که باید کلاس Student یا Teacher رو سریالایز کنه ولی در هنگام عمل دی سریالایز، WCF Runtime این موضوع رو درک نمی‌کنه به همین دلیل یک Communication Exception پرتاب می‌کنه. برای حل این مشکل و برای اینکه WCF Deserialize Engine  رو متوجه نوع وهله سازی کلاس‌های مشتق شده از کلاس پایه کنیم باید از KnownTypeAttribute استفاده کنیم. فقط کافیست که این Attribute رو بالای کلاس Person به ازای تمام کلاس‌های مشتق شده از اون قرار بدید.بدین صورت:

   [DataContract]
    [KnownType( typeof( Student ) )]
    [KnownType( typeof(Teacher) )]
    public abstract class Person
    {
        [DataMember]
        public int Code { get; set; }

        [DataMember]
        public string Name { get; set; }
    }
حالا پروژه سمت سرور رو دوباره کامپایل کنید و سرویس سمت کلاینت رو Update کنید. بعد پروژه رو دوباره اجرا کرده تا خروجی زیر رو مشاهده کنید.


با وجود KnownType دیگه WCF Deserialize Engine میدونه که باید از کدام DataContact برای عمل دی سریالاز نمونه ساخته شده از کلاس Person استفاده کنه. دانستن این مطلب هنگام پیاده سازی مفاهیم ارث بری در ORM ها زمانی که از WCF استفاده می‌کنیم ضروری است.