مطالب دوره‌ها
بررسی مثال‌ها و جزئیات بیشتر تولید کدهای پویا توسط Reflection.Emit
نحوه معرفی متغیرهای محلی در Reflection.Emit

ابتدا مثال کامل ذیل را درنظر بگیرید:
using System;
using System.Reflection.Emit;

namespace FastReflectionTests
{
    class Program
    {
        static int Calculate(int a, int b, int c)
        {
            var result = a * b;
            return result - c;
        }

        static void Main(string[] args)
        {
            //روش متداول
            Console.WriteLine(Calculate(10, 2, 3));

            //تعریف امضای متد
            var myMethod = new DynamicMethod(
                                        name: "CalculateMethod",
                                        returnType: typeof(int),
                                        parameterTypes: new[] { typeof(int), typeof(int), typeof(int) },
                                        m: typeof(Program).Module);
            //تعریف بدنه متد
            var il = myMethod.GetILGenerator();

            il.Emit(opcode: OpCodes.Ldarg_0); // بارگذاری اولین آرگومان بر روی پشته ارزیابی 
            il.Emit(opcode: OpCodes.Ldarg_1); // بارگذاری دومین آرگومان بر روی پشته ارزیابی 
            il.Emit(opcode: OpCodes.Mul); // انجام عملیات ضرب
            il.Emit(opcode: OpCodes.Stloc_0); // ذخیره سازی نتیجه عملیات ضرب در یک متغیر محلی
            il.Emit(opcode: OpCodes.Ldloc_0); // متغیر محلی را بر روی پشته ارزیابی قرار می‌دهد تا در عملیات بعدی قابل استفاده باشد
            il.Emit(opcode: OpCodes.Ldarg_2); // آرگومان سوم را بر روی پشته ارزیابی قرار می‌دهد
            il.Emit(opcode: OpCodes.Sub); // انجام عملیات تفریق
            il.Emit(opcode: OpCodes.Ret); // بازگشت نتیجه

            //فراخوانی متد پویا
            var method = (Func<int, int, int, int>)myMethod.CreateDelegate(typeof(Func<int, int, int, int>));
            Console.WriteLine(method(10, 2, 3));

        }
    }
}
در این مثال سعی کرده‌ایم معادل متد Calculate را که در ابتدای برنامه ملاحظه می‌کنید، با کدهای IL تولید کنیم. روش کار مانند قسمت قبل است. ابتدا وهله‌ی جدیدی را از کلاس DynamicMethod جهت معرفی امضای متد پویای خود ایجاد می‌کنیم. در اینجا نوع خروجی را int و نوع سه پارامتر آن‌را به نحوی که مشخص شده است توسط آرایه‌ای از typeهای int معرفی خواهیم کرد. سپس محل قرارگیری کد تولیدی پویا مشخص می‌شود.
در ادامه توسط ILGenerator، آرگومان‌های دریافتی بارگذاری شده، در هم ضرب می‌شوند. سپس نتیجه در یک متغیر محلی ذخیره شده و سپس از آرگومان سوم کسر می‌گردد. در آخر هم این نتیجه بازگشت داده خواهد شد.
در اینجا روش سومی را برای کار با متدهای پویا مشاهده می‌کنید. بجای تعریف یک delegate به صورت صریح همانند قسمت قبل، از یک Func یا حتی Action نیز بنابر امضای متد مد نظر، می‌توان استفاده کرد. در اینجا از یک Func که سه پارامتر int را قبول کرده و خروجی int نیز دارد، استفاده شده است.
اگر برنامه را اجرا کنید ... کرش خواهد کرد! با استثنای ذیل:
 System.InvalidProgramException was unhandled
Message=Common Language Runtime detected an invalid program.
علت اینجا است که در حین کار با System.Reflection.Emit، نیاز است نوع متغیر محلی مورد استفاده را نیز مشخص نمائیم. اینکار را توسط فراخوانی متد DeclareLocal که باید پس از فراخوانی GetILGenerator، درج گردد، می‌توان انجام داد:
 il.DeclareLocal(typeof(int));
با این تغییر، برنامه بدون مشکل اجرا خواهد شد.


نحوه تعریف برچسب‌ها در Reflection.Emit

در ادامه قصد داریم یک مثال پیشرفته‌تر را بررسی کنیم.
        static int Calculate(int x)
        {
            int result = 0;
            for (int i = 0; i < 10; i++)
            {
                result += i * x;
            }
            return result;
        }
در اینجا می‌خواهیم کدهای معادل متد محاسباتی فوق را توسط امکانات System.Reflection.Emit و کدهای IL تولید کنیم.
using System;
using System.Reflection.Emit;

namespace FastReflectionTests
{
    class Program
    {
        static int Calculate(int x)
        {
            int result = 0;
            for (int i = 0; i < 10; i++)
            {
                result += i * x;
            }
            return result;
        }

        static void Main(string[] args)
        {
            //روش متداول
            Console.WriteLine(Calculate(10));

            //تعریف امضای متد
            var myMethod = new DynamicMethod(
                                        name: "CalculateMethod",
                                        returnType: typeof(int), // خروجی متد عدد صحیح است
                                        parameterTypes: new[] { typeof(int) }, // یک پارامتر عدد صحیح دارد
                                        m: typeof(Program).Module);
            //تعریف بدنه متد
            var il = myMethod.GetILGenerator();

            // از برچسب‌ها برای انتقال کنترل استفاده می‌شود
            // در اینجا به دو برچسب برای تعریف ابتدای حلقه
            // و همچنین برای پرش به جایی که متد خاتمه می‌یابد نیاز داریم
            var loopStart = il.DefineLabel();
            var methodEnd = il.DefineLabel();

            // variable 0; result = 0
            il.DeclareLocal(typeof(int)); //  برای تعریف متغیر محلی نتیجه عملیات
            il.Emit(OpCodes.Ldc_I4_0); // عدد ثابت صفر را بر روی پشته ارزیابی قرار می‌دهد
            il.Emit(OpCodes.Stloc_0); // و نهایتا این عدد ثابت به متغیر محلی انتساب داده خواهد شد

            // variable 1; i = 0
            il.DeclareLocal(typeof(int)); // در اینجا کار تعریف و مقدار دهی متغیر حلقه انجام می‌شود
            il.Emit(OpCodes.Ldc_I4_0); // عدد ثابت صفر را بر روی پشته ارزیابی قرار می‌دهد
            il.Emit(OpCodes.Stloc_1); // و نهایتا این عدد ثابت به متغیر حلقه در ایندکس یک انتساب داده خواهد شد

            // در اینجا کار تعریف بدنه حلقه شروع می‌شود
            il.MarkLabel(loopStart); // شروع حلقه را علامتگذاری می‌کنیم تا بعدا بتوانیم به این نقطه پرش نمائیم
            il.Emit(OpCodes.Ldloc_1); // در ادامه می‌خواهیم بررسی کنیم که آیا مقدار متغیر حلقه از عدد 10 کوچکتر است یا خیر
            il.Emit(OpCodes.Ldc_I4, 10); // عدد ثابت ده را بر روی پشته ارزیابی قرار می‌دهد
            // برای انجام بررسی‌های تساوی یا کوچکتر یا بزرگتر نیاز است ابتدا دو متغیر مدنظر بر روی پشته قرار گیرند
            il.Emit(OpCodes.Bge, methodEnd);  // اگر اینطور نیست و مقدار متغیر از 10 کمتر نیست، کنترل برنامه را به انتهای متد هدایت خواهیم کرد

            // i * x
            il.Emit(OpCodes.Ldloc_1); // مقدار متغیر حلقه را بر روی پشته قرار می‌دهد
            il.Emit(OpCodes.Ldarg_0); // مقدار اولین آرگومان متد را بر روی پشته قرار می‌دهد
            il.Emit(OpCodes.Mul); // انجام عملیات ضرب
            // نتیجه این عملیات اکنون بر روی پشته قرار گرفته است

            // result += 
            il.Emit(OpCodes.Ldloc_0); // متغیر نتیجه را بر روی پشته قرار می‌دهد
            il.Emit(OpCodes.Add); // اکنون عملیات جمع بر روی نتیجه ضرب قسمت قبل که بر روی پشته قرار دارد و همچنین متغیر نتیجه انجام می‌شود
            il.Emit(OpCodes.Stloc_0); // ذخیره سازی نتیجه در متغیر محلی

            // i++
            // در اینجا کار افزایش متغیر حلقه انجام می‌شود
            il.Emit(OpCodes.Ldloc_1); // مقدار متغیر حلقه بر روی پشته قرار می‌گیرد
            il.Emit(OpCodes.Ldc_I4_1); // عدد ثابت یک بر روی پشته قرار می‌گیرد
            il.Emit(OpCodes.Add); // سپس این دو عدد بارگذاری شده با هم جمع خواهند شد
            il.Emit(OpCodes.Stloc_1); // نتیجه در متغیر حلقه ذخیره خواهد شد

            // مرحله بعد شبیه سازی حلقه با پرش به ابتدای برچسب آن است
            il.Emit(OpCodes.Br, loopStart);

            //در اینجا انتهای متد علامتگذاری شده است
            il.MarkLabel(methodEnd);
            il.Emit(OpCodes.Ldloc_0); // مقدار نتیجه بر روی پشته قرار داده شده
            il.Emit(OpCodes.Ret); // و بازگشت داده می‌شود

            //فراخوانی متد پویا
            var method = (Func<int, int>)myMethod.CreateDelegate(typeof(Func<int, int>));
            Console.WriteLine(method(10));
        }
    }
}
کد کامل معادل را به همراه کامنت گذاری سطر به سطر آن، ملاحظه می‌کنید. در اینجا نکته‌های جدید، نحوه تعریف برچسب‌ها و انتقال کنترل برنامه به آن‌ها هستند؛ جهت شبیه سازی حلقه و همچنین خاتمه آن و انتقال کنترل به انتهای متد.


فراخوانی متدها توسط کدهای پویای Reflection.Emit

در ادامه کدهای کامل یک مثال متد پویا را که متد print را فراخوانی می‌کند، ملاحظه می‌کنید:
using System;
using System.Reflection.Emit;

namespace FastReflectionTests
{
    class Program
    {
        public static void print(int i)
        {
            Console.WriteLine("i: {0}", i);
        }

        static void Main(string[] args)
        {
            //روش متداول
            print(10);

            //تعریف امضای متد
            var myMethod = new DynamicMethod(
                                        name: "myMethod",
                                        returnType: typeof(void),
                                        parameterTypes: null, // پارامتری ندارد
                                        m: typeof(Program).Module);
            //تعریف بدنه متد
            var il = myMethod.GetILGenerator();
            il.Emit(OpCodes.Ldc_I4, 10); // عدد ثابت 10 را بر روی پشته قرار می‌دهد
            // اکنون این مقدار بر روی پشته است و از آن می‌توان برای فراخوانی متد پرینت استفاده کرد
            il.Emit(OpCodes.Call, typeof(Program).GetMethod("print"));
            il.Emit(OpCodes.Ret);


            //فراخوانی متد پویا
            var method = (Action)myMethod.CreateDelegate(typeof(Action));
            method();
        }
    }
}
در اینجا از OpCode مخصوص فراخوانی متدها به نام Call که در قسمت‌های قبل در مورد آن بحث شد، استفاده گردیده است. برای اینکه امضای دقیقی را در اختیار آن قرار دهیم، می‌توان از Reflection استفاده کرد که نمونه‌ای از آن‌را در اینجا ملاحظه می‌کنید.
به علاوه چون خروجی امضای متد ما از نوع void است، اینبار delegate تعریف شده را از نوع Action تعریف کرده‌ایم و نه از نوع Func.


فراخوانی متدهای پویای Reflection.Emit توسط سایر متدهای پویای Reflection.Emit

فراخوانی یک متد پویای مشخص از طریق متد‌های پویای دیگر نیز همانند مثال قبل است:
using System;
using System.Reflection.Emit;

namespace FastReflectionTests
{
    class Program
    {
        static void Main(string[] args)
        {
            //تعریف امضای متد
            var myMethod = new DynamicMethod(
                                        name: "mulMethod",
                                        returnType: typeof(int),
                                        parameterTypes: new[] { typeof(int) },
                                        m: typeof(Program).Module);
            //تعریف بدنه متد
            var il = myMethod.GetILGenerator();
            il.Emit(OpCodes.Ldarg_0); // اولین آرگومان متد را بر روی پشته قرار می‌دهد
            il.Emit(OpCodes.Ldc_I4, 42); // عدد ثابت 42 را بر روی پشته قرار می‌دهد
            il.Emit(OpCodes.Mul); // ضرب این دو در هم
            il.Emit(OpCodes.Ret); // بازگشت نتیجه

            //فراخوانی متد پویا
            var method = (Func<int, int>)myMethod.CreateDelegate(typeof(Func<int, int>));
            Console.WriteLine(method(10));

            // فراخوانی متد پویای فوق در یک متد پویای دیگر
            var callerMethod = new DynamicMethod(
                                        name: "callerMethod",
                                        returnType: typeof(int),
                                        parameterTypes: new[] { typeof(int), typeof(int) },
                                        m: typeof(Program).Module);
            //تعریف بدنه متد
            var callerMethodIL = callerMethod.GetILGenerator();
            callerMethodIL.Emit(OpCodes.Ldarg_0); // پارامتر اول متد را بر روی پشته قرار می‌دهد
            callerMethodIL.Emit(OpCodes.Ldarg_1); // پارامتر دوم متد را بر روی پشته قرار می‌دهد
            callerMethodIL.Emit(OpCodes.Mul); // ضرب این دو در هم
            //حاصل ضرب اکنون بر روی پشته است که در فراخوانی بعدی استفاده می‌شود
            callerMethodIL.Emit(OpCodes.Call, myMethod); // فراخوانی یک متد پویای دیگر
            callerMethodIL.Emit(OpCodes.Ret);

            //فراخوانی متد پویای جدید
            var method2 = (Func<int, int, int>)callerMethod.CreateDelegate(typeof(Func<int, int, int>));
            Console.WriteLine(method2(10, 2));
        }
    }
}
در مثال فوق ابتدا یک متد پویای ضرب را تعریف کرده‌ایم که عددی صحیح را دریافت و آن‌را در 42 ضرب می‌کند و نتیجه را بازگشت می‌دهد.
سپس متد پویای دومی تعریف شده است که دو عدد صحیح را دریافت و این دو را در هم ضرب کرده و سپس نتیجه را به عنوان پارامتر به متد پویای اول ارسال می‌کند.
هنگام فراخوانی OpCodes.Call، پارامتر دوم باید از نوع MethodInfo باشد. نوع یک DynamicMethod نیز همان MethodInfo است. بنابراین برای فراخوانی آن، کار خاصی نباید انجام شود و صرفا ذکر نام متغیر مرتبط با مد پویای مدنظر کفایت می‌کند.
مطالب
C# 6 - String Interpolation
تا پیش از C# 6 یکی از روش‌های توصیه شده‌ی جهت اتصال رشته‌ها به هم، استفاده از متدهایی مانند string.Format و StringBuilder.AppendFormat بود:
using System;
 
namespace CS6NewFeatures
{
    class Person
    {
        public string FirstName { set; get; }
        public string LastName { set; get; }
        public int Age { set; get; }
    }
 
    class Program
    {
        static void Main(string[] args)
        {
            var person = new Person { FirstName = "User 1", LastName = "Last Name 1", Age = 50 };
            var message = string.Format("Hello!  My name is {0} {1} and I am {2} years old.",
                                          person.FirstName, person.LastName, person.Age);
            Console.Write(message);
        }
    }
}
مشکل این روش، کاهش خوانایی آن با بالا رفتن تعداد پارامترهای متد Format است و همچنین گاهی از اوقات فراموش کردن مقدار دهی بعضی از آن‌ها و یا حتی ذکر ایندکس‌هایی غیر معتبر که در زمان اجرا، برنامه را با یک خطا متوقف می‌کنند.
در C# 6 جهت رفع این مشکلات، راه حلی به نام String interpolation ارائه شده‌است و اگر افزونه‌ی ReSharper یا یکی از افزونه‌های Roslyn را نصب کرده باشید، به سادگی امکان تبدیل کدهای قدیمی را به فرمت جدید آن خواهید یافت:


در این حالت کد قدیمی فوق، به کد ذیل تبدیل خواهد شد:
static void Main(string[] args)
{
    var person = new Person { FirstName = "User 1", LastName = "Last Name 1", Age = 50 };
    var message = $"Hello!  My name is {person.FirstName} {person.LastName} and I am {person.Age} years old.";
    Console.Write(message);
}
در اینجا ابتدا یک $ در ابتدای رشته قرار گرفته و سپس هر متغیر به داخل {} انتقال یافته‌است. همچنین دیگر نیازی هم به ذکر string.Format نیست.
عملیاتی که در اینجا توسط کامپایلر صورت خواهد گرفت، تبدیل این کدهای جدید مبتنی بر String interpolation به همان string.Format قدیمی در پشت صحنه‌است. بنابراین این قابلیت جدید C# 6 را به کدهای قدیمی خود نیز می‌توانید اعمال کنید. فقط کافی است VS 2015 را نصب کرده باشید و دیگر شماره‌ی دات نت فریم ورک مورد استفاده مهم نیست.


امکان انجام محاسبات با String interpolation

زمانیکه $ در ابتدای رشته قرار گرفت، عبارات داخل {}‌ها توسط کامپایلر محاسبه و جایگزین می‌شوند. بنابراین می‌توان چنین محاسباتی را نیز انجام داد:
 var message2 = $"{Environment.NewLine}Test {DateTime.Now}, {3*2}";
Console.Write(message2);
بدیهی اگر $ ابتدای رشته فراموش شود، اتفاق خاصی رخ نخواهد داد.


تغییر فرمت عبارات نمایش داده شده توسط String interpolation

همانطور که با string.Format می‌توان نمایش سه رقم جدا کننده‌ی هزارها را فعال کرد و یا تاریخی را به نحوی خاص نمایش داد، در اینجا نیز همان قابلیت‌ها برقرار هستند و باید پس از ذکر یک : عنوان شوند:
 var message3 = $"{Environment.NewLine}{1000000:n0} {DateTime.Now:dd-MM-yyyy}";
Console.Write(message3);
حالت کلی و استاندارد آن در متد string.Format به صورت {index[,alignment][:formatString]} است.


سفارشی سازی String interpolation
 
اگر متغیر رشته‌‌ای معرفی شده‌ی توسط $ را با یک var مشخص کنیم، نوع آن به صورت پیش فرض، از نوع string خواهد بود. برای نمونه در مثال‌های فوق، message و message2 از نوع string تعریف می‌شوند. اما این رشته‌های ویژه را می‌توان از نوع IFormattable و یا FormattableString نیز تعریف کرد.
در حقیقت رشته‌های آغاز شده‌ی با $ از نوع IFormattable هستند و اگر نوع متغیر آن‌ها ذکر نشود، به صورت خودکار به نوع FormattableString که اینترفیس IFormattable را پیاده سازی می‌کند، تبدیل می‌شوند. بنابراین پیاده سازی این اینترفیس، امکان سفارشی سازی خروجی string interpolation را میسر می‌کند. برای نمونه می‌خواهیم در مثال message2، نحوه‌ی نمایش تاریخ را سفارشی سازی کنیم.
class MyDateFormatProvider : IFormatProvider
{
    readonly MyDateFormatter _formatter = new MyDateFormatter();
 
    public object GetFormat(Type formatType)
    {
        return formatType == typeof(ICustomFormatter) ? _formatter : null;
    }
 
    class MyDateFormatter : ICustomFormatter
    {
        public string Format(string format, object arg, IFormatProvider formatProvider)
        {
            if (arg is DateTime)
                return ((DateTime)arg).ToString("MM/dd/yyyy");
            return arg.ToString();
        }
    }
}
در اینجا ابتدا کار با پیاده سازی اینترفیس IFormatProvider شروع می‌شود. متد GetFormat آن همیشه به همین شکل خواهد بود و هر زمانیکه نوع ارسالی به آن ICustomFormatter بود، یعنی یکی از اجزای {} دار در حال آنالیز است و خروجی مدنظر آن همیشه از نوع ICustomFormatter است که نمونه‌ای از پیاده سازی آن‌را جهت سفارشی سازی DateTime ملاحظه می‌کنید.
پس از پیاده سازی این سفارشی کننده‌ی تاریخ، نحوه‌ی استفاده‌ی از آن به صورت ذیل است:
static string formatMyDate(FormattableString formattable)
{
      return formattable.ToString(new MyDateFormatProvider());
}
ابتدا یک متد static را تعریف کنید که ورودی آن از نوع FormattableString باشد؛ از این جهت که رشته‌های شروع شده‌ی با $ نیز از همین نوع هستند. سپس سفارشی سازی پردازش {}‌ها در قسمت ToString آن انجام می‌شود و در اینجا می‌توان یک IFormatProvider جدید را معرفی کرد.
در ادامه برای اعمال این سفارشی سازی، فقط کافی است متد formatMyDate را به رشته‌ی مدنظر اعمال کنیم:
 var message2 = formatMyDate($"{Environment.NewLine}Test {DateTime.Now}, {3*2}");
Console.Write(message2);

و اگر تنها می‌خواهید فرهنگ جاری را عوض کنید، از روش ساده‌ی زیر استفاده نمائید:
public static string faIr(IFormattable formattable)
{
    return formattable.ToString(null, new CultureInfo("fa-Ir"));
}
در اینجا با اعمال متد faIr به عبارت شروع شده‌ی با $، فرهنگ ایران به رشته‌ی جاری اعمال خواهد شد.
نمونه‌ی کاربردی‌تر آن اعمال InvariantCulture به String interpolation است:
 static string invariant(FormattableString formattable)
{
    return formattable.ToString(CultureInfo.InvariantCulture);
}


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


غیرفعال سازی String interpolation

اگر می‌خواهید در رشته‌ای که با $ شروع شده، بجای محاسبه‌ی عبارتی، دقیقا خود آن‌را نمایش دهید (و { را escape کنید)، از {{}} استفاده کنید:
 var message0 = $"Hello! My name is {person.FirstName} {{person.FirstName}}";
در این مثال اولین {} محاسبه خواهد شد و دومی خیر.


پردازش عبارات شرطی توسط String interpolation

همانطور که عنوان شد، امکان ذکر یک عبارت کامل هم در بین {} وجود دارد (محاسبات، ذکر یک عبارت LINQ، ذکر یک متد و امثال آن). اما در این میان اگر یک عبارت شرطی مدنظر بود، باید بین () قرار گیرد:
 Console.Write($"{(person.Age>50 ? "old": "young")}");
علت اینجا است که کامپایلر سی‌شارپ، : بین {} را به format specifier تفسیر می‌کند. نمونه‌ی آن‌را پیشتر با مثال «تغییر فرمت عبارات نمایش داده شده» ملاحظه کردید. ذکر : در اینجا به معنای شروع مشخص سازی فرمتی است که قرار است به این حاصل اعمال شود. برای تغییر این رفتار پیش فرض، کافی است عبارت مدنظر را بین () ذکر کنیم تا تمام آن به صورت یک عبارت سی‌شارپ تفسیر شود.
نظرات مطالب
چگونگی گزارشگیری از Business Objects مانند List توسط StimulSoft
با کد زیر
    public class Book
    {
        public string Title { get; set; }
        public int Id { get; set; }
        public Author Author { get; set; }
        public Publisher Publisher { get; set; }

        public IList<Book> GetBooks()
        {
            var books = new List<Book>();

            for (int i = 0; i < 10; i++)
            {
                var book = new Book()
                {
                    Id = i,
                    Title = $"Title {i}",
                    Author = new Author()
                    {
                        FirstName = $"Author {i} First Name",
                        LastName = $"Author {i} Last Name",
                        Person = new Person()
                        {
                            Type = $"Type {i}",
                            Value = $"Value {i}"
                        }
                    },
                    Publisher = new Report.Publisher()
                    {
                        Name = $"Publiser {i}"
                    }
                };
                books.Add(book);
            }
            return books;
        }
    }

    public class Author
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public Person Person { get; set; }
    }

    public class Person
    {
        public string Type { get; set; }
        public string Value { get; set; }
    }

    public class Publisher
    {
        public string Name { get; set; }
    }
و فایل mrt زیر مشکلی دیده نشد.
نظرات مطالب
C# 6 - String Interpolation
یک نکته‌ی تکمیلی: اضافه شدن CompositeFormat به دات‌نت 8 برای کش کردن الگوهای رشته‌ها

زمانیکه از متد string.Format استفاده می‌کنیم، الگوی معرفی شده‌ی به آن، بارها و بارها در زمان اجرا Parse می‌شود که در برنامه‌های مبتنی بر رشته‌ها، حلقه‌ها و امثال آن، سبب افت کارآیی خواهد شد. برای رفع این مشکل، CompositeFormat به دات‌نت 8 اضافه شده‌است تا بتوان این Parse الگو را یکبار انجام داد و نتیجه را کش کرد.

یک مثال:
- عدم کش شدن الگوی تعریف شده، تا پیش از دات‌نت 8:
var text = string.Format("Format one value: {0}", 42);
- روش کش کردن الگوی تعریف شده، در دات‌نت 8:
private static readonly CompositeFormat StaticField = CompositeFormat.Parse("Format one value: {0}");

var text = string.Format(StaticField, 42);

اگر علاقمند هستید تا این نکته را به صورت یک خطا دریافت کنید و مجبور به تغییر آن‌ها شوید، یک سطر زیر را به فایل editorconfig. خود اضافه کنید:
dotnet_diagnostic.CA1863.severity = error
اشتراک‌ها
همکاری با DevComponents در زمینه تولید کامپوننت!

سال‌هاست که از کامپوننت‌های DevComponents در کارهای خودم استفاده می‌کنم. شاید شما هم از طرفداران این کامپوننت‌ها باشید چرا که در بین شرکت‌های موجود، ارائه کیفیت در عین حفظ کمترین تعداد dllها از ویژگی‌های بارز DevComponents محسوب می‌شود.
در حالیکه برای بررسی نخسه‌های آخر سری به وب‌سایت و بعد وبلاگشون زدم، متوجه شدم که DevComponents علاقمند به همکاری با برنامه‌نویسان و طراحان از سرتاسر دنیاست. متن آغازین این همکاری برام خیلی جالب بود:

"There are three kinds of people in this world: Those who make things happen; those who watch things happen; and those who just wonder what a heck happened? "

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

You can be located anywhere in the world as long as you have internet connection. You can work any hours you want as long as work gets done. You have freedom and control over your working hours. All we ask for is that things get done. You will also get responsibility of supporting whatever you develop since we believe that is the best way to understand what our customers need. 

همکاری با DevComponents در زمینه تولید کامپوننت!
اشتراک‌ها
معماری رویداد-محور مایکروسرویس ها در ASP.NET Core

In this guide, you will create two C# ASP.NET Core Microservices. Both microservices have their own bounded context and domain model. Each microservice has its own database and REST API. One microservice publishes integration events, that the other microservice consumes. 

معماری رویداد-محور مایکروسرویس ها در ASP.NET Core
اشتراک‌ها
خبرنامه هفتگی NET Stacks.

The .NET Stacks cuts through the noise and gives you all you need to be an informed .NET developer without having to rely on so many different sources. Every Monday morning, you'll receive one resource to keep you up to speed on all you missed. 

خبرنامه هفتگی NET Stacks.
نظرات مطالب
سفارشی سازی ASP.NET Core Identity - قسمت اول - موجودیت‌های پایه و DbContext برنامه
در Core 2 و identity 2 دیگر نمیشود از این حالت 
public class User : IdentityUser<int, UserClaim, UserRole, UserLogin>
ارث بری کرد و فقط نوع کلید قابل مشخص شدن هست. برای این حالت چگونه ارتباط‌ها و ... رو تعریف کنیم جهت بروزرسانی به Core 2 ؟ ایا رابطه‌ها باید به صراحت ذکر بشود؟
مطالب
آشنایی با NHibernate - قسمت اول

NHibernate کتابخانه‌ی تبدیل شده پروژه بسیار محبوب Hibernate جاوا به سی شارپ است و یکی از ORM های بسیار موفق، به شمار می‌رود. در طی تعدادی مقاله قصد آشنایی با این فریم ورک را داریم.

چرا نیاز است تا از یک ORM استفاده شود؟
تهیه قسمت و یا لایه دسترسی به داده‌ها در یک برنامه عموما تا 30 درصد زمان کل تهیه یک محصول را تشکیل می‌دهد. اما باید در نظر داشت که این پروسه‌ی تکراری هیچ کار خارق العاده‌ای نبوده و ارزش افزوده‌ی خاصی را به یک برنامه اضافه نمی‌کند. تقریبا تمام برنامه‌های تجاری نیاز به لایه دسترسی به داده‌ها را دارند. پس چرا ما باید به ازای هر پروژه، این کار تکراری و کسل کننده را بارها و بارها تکرار کنیم؟
هدف NHibernate ، کاستن این بار از روی شانه‌های یک برنامه نویس است. با کمک این کتابخانه، دیگر رویه ذخیره شده‌ای را نخواهید نوشت. دیگر هیچگاه با ADO.Net سر و کار نخواهید داشت. به این صورت می‌توان عمده وقت خود را صرف قسمت‌های اصلی و طراحی برنامه کرد تا کد نویسی یک لایه تکراری. همچنین عده‌ای از بزرگان اینگونه ابزارها اعتقاد دارند که برنامه نویس‌هایی که لایه دسترسی به داده‌ها را خود طراحی می‌کنند، مشغول کلاهبرداری از مشتری‌های خود هستند! (صرف زمان بیشتر برای تهیه یک محصول و همچنین وجود باگ‌های احتمالی در لایه دسترسی به داده‌های طراحی شده توسط یک برنامه نویس نه چندان حرفه‌ای)
برای مشاهده سایر مزایای استفاده از یک ORM لطفا به مقاله "5 دلیل برای استفاده از یک ابزار ORM" مراجعه نمائید.

در ادامه برای معرفی این کتابخانه یک سیستم ثبت سفارشات را با هم مرور خواهیم کرد.

بررسی مدل سیستم ثبت سفارشات

در این مدل ساده‌ی ما، مشتری‌ها (customers) امکان ثبت سفارشات (orders) را دارند. سفارشات توسط یک کارمند (employee) که مسؤول ثبت آن‌ها است به سیستم وارد می‌شود. هر سفارش می‌تواند شامل یک یا چند (one-to-many) آیتم (order items) باشد و هر آیتم معرف یک محصول (product) است که قرار است توسط یک مشتری (customer) خریداری شود. کلاس دیاگرام این مدل به صورت زیر می‌تواند باشد.


نگاشت مدل

زمانیکه مدل سیستم مشخص شد، اکنون نیاز است تا حالات (داده‌ها) آن‌را در مکانی ذخیره کنیم. عموما اینکار با کمک سیستم‌های مدیریت پایگاه‌های داده مانند SQL Server، Oracle، IBM DB2 ، MySql و امثال آن‌ها صورت می‌گیرد. زمانیکه از NHibernate استفاده کنید اهمیتی ندارد که برنامه شما قرار است با چه نوع دیتابیسی کار کند؛ زیرا این کتابخانه اکثر دیتابیس‌های شناخته شده موجود را پشتیبانی می‌کند و برنامه از این لحاظ مستقل از نوع دیتابیس عمل خواهد کرد و اگر نیاز بود روزی بجای اس کیوال سرور از مای اس کیوال استفاده شود، تنها کافی است تنظیمات ابتدایی NHibernate را تغییر دهید (بجای بازنویسی کل برنامه).
اگر برای ذخیره سازی داده‌ها و حالات سیستم از دیتابیس استفاده کنیم، نیاز است تا اشیاء مدل خود را به جداول دیتابیس نگاشت نمائیم. این نگاشت عموما یک به یک نیست (لزومی ندارد که حتما یک شیء به یک جدول نگاشت شود). در گذشته‌ی نچندان دور کتابخانه‌ی NHibernate ، این نگاشت عموما توسط فایل‌های XML ایی به نام hbm صورت می‌گرفت. این روش هنوز هم پشتیبانی شده و توسط بسیاری از برنامه نویس‌ها بکار گرفته می‌شود. روش دیگری که برای تعریف این نگاشت مرسوم است، مزین سازی اشیاء و خواص آن‌ها با یک سری از ویژگی‌ها می‌باشد که فریم ورک برتر این عملیات Castle Active Record نام دارد.
اخیرا کتابخانه‌ی دیگری برای انجام این نگاشت تهیه شده به نام Fluent NHibernate که بسیار مورد توجه علاقمندان به این فریم ورک واقع گردیده است. با کمک کتابخانه‌ی Fluent NHibernate عملیات نگاشت اشیاء به جداول، بجای استفاده از فایل‌های XML ، توسط کدهای برنامه صورت خواهند گرفت. این مورد مزایای بسیاری را همانند استفاده از یک زبان برنامه نویسی کامل برای تعریف نگاشت‌ها، بررسی خودکار نوع‌های داد‌ه‌ای و حتی امکان تعریف منطقی خاص برای قسمت نگاشت برنامه، به همراه خواهد داشت.

آماده سازی سیستم برای استفاده از NHibernate

در ادامه بجای دریافت پروژه سورس باز NHibernate از سایت سورس فورج، پروژه سورس باز Fluent NHibernate را از سایت گوگل کد دریافت خواهیم کرد که بر فراز کتابخانه‌ی NHibernate بنا شده است و آن‌را کاملا پوشش می‌دهد. سورس این کتابخانه را با checkout مسیر زیر توسط TortoiseSVN می‌توان دریافت کرد.





البته احتمالا برای دریافت آن از گوگل کد با توجه به تحریم موجود نیاز به پروکسی خواهد بود. برای تنظیم پروکسی در TortoiseSVN به قسمت تنظیمات آن مطابق تصویر ذیل مراجعه کنید:



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

پس از دریافت پروژه، باز کردن فایل solution آن در VS‌ و سپس build کل مجموعه، اگر به پوشه‌های آن مراجعه نمائید، فایل‌های زیر قابل مشاهده هستند:

Nhibernate.dll : اسمبلی فریم ورک NHibernate است.
NHibernate.Linq.dll : اسمبلی پروایدر LINQ to NHibernate می‌باشد.
FluentNHibernate.dll : اسمبلی فریم ورک Fluent NHibernate است.
Iesi.Collections.dll : یک سری مجموعه‌های ویژه مورد استفاده NHibernate را ارائه می‌دهد.
Log4net.dll : فریم ورک لاگ کردن اطلاعات NHibernate می‌باشد. (این فریم ورک نیز جهت عملیات logging بسیار معروف و محبوب است)
Castle.Core.dll : کتابخانه پایه Castle.DynamicProxy2.dll است.
Castle.DynamicProxy2.dll : جهت اعمال lazy loading در فریم ورک NHibernate بکار می‌رود.
System.Data.SQLite.dll : پروایدر دیتابیس SQLite است.
Nunit.framework.dll : نیز یکی از فریم ورک‌های بسیار محبوب آزمون واحد در دات نت فریم ورک است.

برای سادگی مراجعات بعدی، این فایل‌ها را یافته و در پوشه‌ای به نام lib کپی نمائید.

برپایی یک پروژه جدید

پس از دریافت Fluent NHibernate ، یک پروژه Class Library جدید را در VS.Net آغاز کنید (برای مثال به نام NHSample1 ). سپس یک پروژه دیگر را نیز از نوع Class Library به نام UnitTests به این solution ایجاد شده جهت انجام آزمون‌های واحد برنامه اضافه نمائید.
اکنون به پروژه NHSample1 ، ارجاع هایی را به فایل‌های FluentNHibernate.dll و سپس NHibernate.dll در که پوشه lib ایی که در قسمت قبل ساختیم، قرار دارند، اضافه نمائید.



در ادامه یک پوشه جدید به پروژه NHSample1 به نام Domain اضافه کنید. سپس به این پوشه، کلاس Customer را اضافه نمائید:

namespace NHSample1.Domain
{
public class Customer
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string AddressLine1 { get; set; }
public string AddressLine2 { get; set; }
public string PostalCode { get; set; }
public string City { get; set; }
public string CountryCode { get; set; }
}
}
اکنون نوبت تعریف نگاشت این شیء است. این کلاس باید از کلاس پایه ClassMap مشتق شود. سپس نگاشت‌ها در سازنده‌ی این کلاس باید تعریف گردند.

using FluentNHibernate.Mapping;

namespace NHSample1.Domain
{
class CustomerMapping : ClassMap<Customer>
{
}
}
همانطور که ملاحظه می‌کنید، نوع این کلاس Generic ، همان کلاسی است که قصد داریم نگاشت مرتبط با آن را تهیه نمائیم. در ادامه تعریف کامل این کلاس نگاشت را در نظر بگیرید:

using FluentNHibernate.Mapping;

namespace NHSample1.Domain
{
class CustomerMapping : ClassMap<Customer>
{
public CustomerMapping()
{
Not.LazyLoad();
Id(c => c.Id).GeneratedBy.HiLo("1000");
Map(c => c.FirstName).Not.Nullable().Length(50);
Map(c => c.LastName).Not.Nullable().Length(50);
Map(c => c.AddressLine1).Not.Nullable().Length(50);
Map(c => c.AddressLine2).Length(50);
Map(c => c.PostalCode).Not.Nullable().Length(10);
Map(c => c.City).Not.Nullable().Length(50);
Map(c => c.CountryCode).Not.Nullable().Length(2);
}
}
}
به صورت پیش فرض نگاشت‌های Fluent NHibernate از نوع lazy load هستند که در اینجا عکس آن در نظر گرفته شده است.
سپس وضعیت نگاشت تک تک خواص کلاس Customer را مشخص می‌کنیم. توسط Id(c => c.Id).GeneratedBy.HiLo به سیستم اعلام خواهیم کرد که فیلد Id از نوع identity است که از 1000 شروع خواهد شد. مابقی موارد هم بسیار واضح هستند. تمامی خواص کلاس Customer ذکر شده، نال را نمی‌پذیرند (منهای AddressLine2) و طول آن‌ها نیز مشخص گردیده است.
با کمک Fluent NHibernate ، بحث بررسی نوع‌های داده‌ای و همچنین یکی بودن موارد مطرح شده در نگاشت با کلاس اصلی Customer به سادگی توسط کامپایلر بررسی شده و خطاهای آتی کاهش خواهند یافت.

برای آشنایی بیشتر با lambda expressions می‌توان به مقاله زیر مراجعه کرد:
Step-by-step Introduction to Delegates and Lambda Expressions


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

مطالب
سری بررسی SQL Smell در EF Core - ایجاد روابط Polymorphic - بخش اول
سناریویی را در نظر بگیرید که برای هر کدام از مدلهای Article, Video, Event می‌خواهیم قابلیت کامنت‌گذاری جداگانه‌ای را داشته باشیم. چندین روش برای پیاده‌سازی این سناریو وجود دارد که در ادامه به آنها خواهیم پرداخت. 

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


public enum CommentType
{
    Article,
    Video,
    Event
}

public class Comment
{
    public int Id { get; set; }
    public string CommentText { get; set; }
    public string User { get; set; }
    public int? TypeId { get; set; }
    public CommentType CommentType { get; set; }
}

public class Article
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Slug { get; set; }
    public string Description { get; set; }
}

public class Video
{
    public int Id { get; set; }
    public string Url { get; set; }
    public string Description { get; set; }
}

public class Event
{
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTimeOffset? Start { get; set; }
    public DateTimeOffset? End { get; set; }
}

public class MyDbContext : DbContext
{
    public DbSet<Article> Articles { get; set; }
    public DbSet<Video> Videos { get; set; }
    public DbSet<Event> Events { get; set; }
    public DbSet<Comment> Comments { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlite("Data Source=polymorphic.db");
}

این روش در واقع به عنوان یک Anti Pattern و SQL Smell شناخته می‌شود؛ زیرا امکان کوئری گرفتن از دیتابیس را دشوار خواهد کرد. اکثر فریم‌ورک‌های غیر دات‌نتی به صورت توکار قابلیت پیاده‌سازی این نوع ارتباط را ارائه می‌دهند. اما در Entity Framework باید به صورت دستی تنظیمات انجام شوند و همچنین به دلیل نداشتن ارجاع مستقیم (کلید خارجی) درون جدول Comments با مشکل data integrity مواجه خواهیم شد. یکی دیگر از مشکلات آن امکان درج orphaned record است؛ زیرا هیچ Constraintی بر روی Polymorphic Key تعریف نشده‌است. در این روش مدیریت واکشی اطلاعات سخت خواهد بود و در حین کوئری گرفتن دیتا باید CommentType را نیز به همراه TypeId به صورت صریحی قید کنیم:
var articleComments = dbContext.Comments
                .Where(x => x.CommentType == CommentType.Article && x.TypeId.Value == 1);
foreach (var articleComment in articleComments)
{
    Console.WriteLine(articleComment.CommentText);
}

Join Table Per Relationship Type
 یک روش دیگر ایجاد Join Table به ازای هر ارتباط است:


public class Comment
{
    public int Id { get; set; }
    public string CommentText { get; set; }
    public string User { get; set; }
    
    public virtual ICollection<ArticleComment> ArticleComments { get; set; }
    public virtual ICollection<VideoComment> VideoComments { get; set; }
    public virtual ICollection<EventComment> EventComments { get; set; }
}

public class Article
{
    public Article()
    {
        ArticleComments = new HashSet<ArticleComment>();
    }
    
    public int Id { get; set; }
    public string Title { get; set; }
    public string Slug { get; set; }
    public string Description { get; set; }
    
    public virtual ICollection<ArticleComment> ArticleComments { get; set; }

}

public class Video
{
    public Video()
    {
        VideoComments = new HashSet<VideoComment>();
    }
    
    public int Id { get; set; }
    public string Url { get; set; }
    public string Description { get; set; }
    
    public virtual ICollection<VideoComment> VideoComments { get; set; }
}

public class Event
{
    public Event()
    {
        EventComments = new HashSet<EventComment>();
    }
    
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTimeOffset? Start { get; set; }
    public DateTimeOffset? End { get; set; }
    
    public virtual ICollection<EventComment> EventComments { get; set; }
}

public class MyDbContext : DbContext
{
    public DbSet<Article> Articles { get; set; }
    public DbSet<ArticleComment> ArticleComments { get; set; }
    public DbSet<Video> Videos { get; set; }
    public DbSet<VideoComment> VideoComments { get; set; }
    public DbSet<Event> Events { get; set; }
    public DbSet<EventComment> EventComments { get; set; }
    public DbSet<Comment> Comments { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlite("Data Source=polymorphic.db");

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<ArticleComment>(entity =>
        {
            entity.HasKey(e => new { e.CommentId, e.ArticleId })
                .HasName("PK_dbo.ArticleComments");

            entity.HasIndex(e => e.ArticleId)
                .HasName("IX_ArticleId");

            entity.HasIndex(e => e.CommentId)
                .HasName("IX_ArticleCommentId");

            entity.HasOne(d => d.Article)
                .WithMany(p => p.ArticleComments)
                .HasForeignKey(d => d.ArticleId)
                .HasConstraintName("FK_dbo.ArticleComments_dbo.Articles_ArticleId");

            entity.HasOne(d => d.Comment)
                .WithMany(p => p.ArticleComments)
                .HasForeignKey(d => d.CommentId)
                .HasConstraintName("FK_dbo.ArticleComments_dbo.Comments_CommentId");
        });
        
        modelBuilder.Entity<VideoComment>(entity =>
        {
            entity.HasKey(e => new { e.CommentId, e.VideoId })
                .HasName("PK_dbo.VideoComments");

            entity.HasIndex(e => e.VideoId)
                .HasName("IX_VideoId");

            entity.HasIndex(e => e.CommentId)
                .HasName("IX_VideoCommentId");

            entity.HasOne(d => d.Video)
                .WithMany(p => p.VideoComments)
                .HasForeignKey(d => d.VideoId)
                .HasConstraintName("FK_dbo.VideoComments_dbo.Videos_VideoId");

            entity.HasOne(d => d.Comment)
                .WithMany(p => p.VideoComments)
                .HasForeignKey(d => d.CommentId)
                .HasConstraintName("FK_dbo.VideoComments_dbo.Comments_CommentId");
        });
        
        modelBuilder.Entity<EventComment>(entity =>
        {
            entity.HasKey(e => new { e.CommentId, e.EventId })
                .HasName("PK_dbo.EventComments");

            entity.HasIndex(e => e.EventId)
                .HasName("IX_EventId");

            entity.HasIndex(e => e.CommentId)
                .HasName("IX_EventCommentId");

            entity.HasOne(d => d.Event)
                .WithMany(p => p.EventComments)
                .HasForeignKey(d => d.EventId)
                .HasConstraintName("FK_dbo.EventComments_dbo.Events_EventId");

            entity.HasOne(d => d.Comment)
                .WithMany(p => p.EventComments)
                .HasForeignKey(d => d.CommentId)
                .HasConstraintName("FK_dbo.EventComments_dbo.Comments_CommentId");
        });
    }
}


همانطور که مشاهده میکنید روش فوق نیاز به اضافه کردن مدلهای بیشتری دارد و همچنین تمام روابط چند به چند نیز نیاز است به صورت کامل تنظیم شوند. مزیت این روش داشتن Constraint برای تمامی کلیدهای خارجی است؛ بنابراین می‌توانیم از صحت دیتا مطمئن شویم:
var article = new Article
{
    Title = "Article A",
    Slug = "article_a",
    Description = "No Description"
};
var comment = new Comment
{
    CommentText = "It's great",
    User = "Sirwan"
};
dbContext.ArticleComments.Add(new ArticleComment
{
    Article = article,
    Comment = comment
});

dbContext.SaveChanges();

var articleOne = dbContext.Articles
    .Include(article => article.ArticleComments)
    .ThenInclude(comment => comment.Comment)
    .First(article => article.Id == 1);
var article1Comments = articleOne.ArticleComments.Select(x => x.Comment);
Console.WriteLine(article1Comments.Count());

Exclusive Belongs To  
یک روش دیگر، اضافه کردن ارجاعی به ازای هر کدام از مدلهای عنوان شده، درون موجودیت Comment می‌باشد که به صورت nullable خواهند بود. بنابراین اگر به عنوان مثال بخواهیم برای یک Article یک کامنت داشته باشیم، کلید رکورد ذخیره شده را به عنوان کلید خارجی در جدول Comments اضافه خواهیم کرد:


public class Comment
{
    public int Id { get; set; }
    public string CommentText { get; set; }
    public string User { get; set; }
    
    // Article
    public virtual Article Article { get; set; }
    public int? ArticleId { get; set; }
    
    // Video
    public virtual Video Video { get; set; }
    public int? VideoId { get; set; }
    
    // Event
    public virtual Event Event { get; set; }
    public int? EventId { get; set; }
}
public class Article
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Slug { get; set; }
    public string Description { get; set; }
    public virtual ICollection<Comment> Comments { get; set; }
}

public class Video
{
    public int Id { get; set; }
    public string Url { get; set; }
    public string Description { get; set; }
    public virtual ICollection<Comment> Comments { get; set; }
}

public class Event
{
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTimeOffset? Start { get; set; }
    public DateTimeOffset? End { get; set; }
    public virtual ICollection<Comment> Comments { get; set; }
}

public class MyDbContext : DbContext
{
    public DbSet<Article> Articles { get; set; }
    public DbSet<Video> Videos { get; set; }
    public DbSet<Event> Events { get; set; }
    public DbSet<Comment> Comments { get; set; }
    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlite("Data Source=polymorphic.db");
}


این روش از لحاظ منطقی و طراحی دیتابیس بدون اشکال است؛ زیرا مقدار نامعتبری را نمی‌توانیم برای کلیدهای خارجی درج کنیم. چون برای کلیدهای تعریف شده درون جدول Comment یکسری Constraint تعریف شده‌اند که صحت دیتای ورودی را بررسی خواهند کرد. حتی در صورت نیاز نیز می‌توانیم یک Constraint ترکیبی را جهت مطمئن شدن از خالی نبودن همزمان ستون‌های FK اضافه کنیم. البته SQLite Provider از HasCheckConstraint پشتیبانی نمی‌کند، ولی اگر به عنوان مثال از MySQL استفاده می‌کنید می‌توانید Constraint موردنظر را اینگونه اضافه کنید: 
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Comment>(entity =>
        entity.HasCheckConstraint("CHECK_FKs", 
            "(`ArticleId`  IS NOT NULL) AND (`VideoId`  IS NOT NULL) AND (`EventId`  IS NOT NULL)"));
}

با طراحی فوق می‌توانیم مطمئن شویم که orphaned record نخواهیم داشت. اما اگر تعداد مدل‌ها بیشتر شوند، باید به ازای هر مدل جدید، یک ارجاع به آن را به جدول Comment اضافه کنیم که در نهایت با تعداد زیادی کلیدهای خارجی مواجه خواهیم شد که در آن واحد فقط یکی از آنها مقدار دارند و بقیه NULL خواهند شد. در مقابل، مزیت این روش، امکان کوئری نویسی ساده‌ی آن است:
var articles = dbContext.Articles
                .Include(x => x.Comments).Where(x => x.Id == 1);
foreach (var article in articles)
{
    Console.WriteLine($"{article.Title} - Comments: {article.Comments.Count}");
}
var comment = dbContext.Comments.Include(x => x.Article)
    .FirstOrDefault(x => x.Id == 1);
Console.WriteLine(comment?.Article.Title);

کدهای مطلب جاری را می‌توانید از اینجا دریافت کنید (هر مثال بر روی برنچی جدا قرار دارد)