مطالب
افزودن یک DataType جدید برای نگه‌داری تاریخ خورشیدی - 1

ثبت و نگه‌داری تاریخ خورشیدی در SQL Server از دیرباز یکی از نگرانی‌های برنامه‌نویسان و طراحان پایگاه داده‌ها بوده است. در این نوشتار، راه‌کار تعریف یک DataType در SQL Server 2012 به روش CLR آموزش داده خواهد شد.

در ویژوال استودیو یک پروژه‌ی جدید از نوع SQL Server Database Project به شکل زیر ایجاد کنید: 

نام پروژه را به یاد تقویم خیام، prgJalaliDate می‌گذارم. در Solution Explorer روی نام پروژه راست‌کلیک کرده، سپس روی Add New Item کلیک کنید. در پنجره‌ی بازشده مطابق شکل SQL CLR C# User Defined Type را برگزینید؛ سپس نام JalaliDateType را برای آن انتخاب کنید.
 

 متن موجود در صفحه‌ی بازشده را کاملاً حذف کرده و با کد زیر جای‌گزین کنید.

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

using System;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;

[Serializable()]
[SqlUserDefinedType(Format.Native)]
public struct JalaliDate : INullable
{
    private Int16 m_Year;
    private byte m_Month;
    private byte m_Day;
    private byte m_Hour;
    private byte m_Minute;
    private byte m_Second;
    private bool is_Null;


    public Int16 Year
    {
        get
        {
            return (this.m_Year);
        }
        set
        {
            m_Year = value;
        }
    }

    public byte Month
    {
        get
        {
            return (this.m_Month);
        }
        set
        {
            m_Month = value;
        }
    }

    public byte Day
    {
        get
        {
            return (this.m_Day);
        }
        set
        {
            m_Day = value;
        }
    }

    public byte Hour
    {
        get
        {
            return (this.m_Hour);
        }
        set
        {
            m_Hour = value;
        }
    }

    public byte Minute
    {
        get
        {
            return (this.m_Minute);
        }
        set
        {
            m_Minute = value;
        }
    }

    public byte Second
    {
        get
        {
            return (this.m_Second);
        }
        set
        {
            m_Second = value;
        }
    }

    public bool IsNull
    {
        get
        {
            return is_Null;
        }
    }

    public static JalaliDate Null
    {
        get
        {
            JalaliDate jl = new JalaliDate();
            jl.is_Null = true;
            return (jl);
        }
    }


    public override string ToString()
    {
        if (this.IsNull)
        {
            return "NULL";
        }
        else
        {
            return this.m_Year.ToString("D4") + "/" + this.m_Month.ToString("D2") + "/" + this.m_Day.ToString("D2") + " " + this.Hour.ToString("D2") + ":" + this.Minute.ToString("D2") + ":" + this.Second.ToString("D2");
        }
    }


    public static JalaliDate Parse(SqlString s)
    {
        if (s.IsNull)
        {
            return Null;
        }

        System.Globalization.PersianCalendar pers = new System.Globalization.PersianCalendar();
        string str = Convert.ToString(s);
        string[] JDate = str.Split(' ')[0].Split('/');

        JalaliDate jl = new JalaliDate();

        jl.Year = Convert.ToInt16(JDate[0]);
        byte MonthsInYear = (byte)pers.GetMonthsInYear(jl.Year);
        jl.Month = (byte.Parse(JDate[1]) <= MonthsInYear ? (byte.Parse(JDate[1]) > 0 ? byte.Parse(JDate[1]) : (byte)1) : MonthsInYear);
        byte DaysInMonth = (byte)pers.GetDaysInMonth(jl.Year, jl.Month); ;
        jl.Day = (byte.Parse(JDate[2]) <= DaysInMonth ? (byte.Parse(JDate[2]) > 0 ? byte.Parse(JDate[2]) : (byte)1) : DaysInMonth);
        if (str.Split(' ').Length > 1)
        {
            string[] JTime = str.Split(' ')[1].Split(':');
            jl.Hour = (JTime.Length >= 1 ? (byte.Parse(JTime[0]) < 23 && byte.Parse(JTime[0]) >= (byte)0 ? byte.Parse(JTime[0]) : (byte)0) : (byte)0);
            jl.Minute = (JTime.Length >= 2 ? (byte.Parse(JTime[1]) < 59 && byte.Parse(JTime[1]) >= (byte)0 ? byte.Parse(JTime[1]) : (byte)0) : (byte)0);
            jl.Second = (JTime.Length >= 3 ? (byte.Parse(JTime[2]) < 59 && byte.Parse(JTime[2]) >= (byte)0 ? byte.Parse(JTime[2]) : (byte)0) : (byte)0);
        }
        else { jl.Hour = 0; jl.Minute = 0; jl.Second = 0; }

        return (jl);
    }

    public SqlString GetDate()
    {
        return this.m_Year.ToString("D4") + "/" + this.m_Month.ToString("D2") + "/" + this.m_Day.ToString("D2");
    }

    public SqlString GetTime()
    {
        return this.Hour.ToString("D2") + ":" + this.Minute.ToString("D2") + ":" + this.Second.ToString("D2");
    }

    public SqlDateTime ToGregorianTime()
    {
        System.Globalization.PersianCalendar pers = new System.Globalization.PersianCalendar();
        return SqlDateTime.Parse(pers.ToDateTime(this.Year, this.Month, this.Day, this.Hour, this.Minute, this.Second, 0).ToString());
    }

    public SqlString JalaliDateAdd(SqlString interval, int increment)
    {
        System.Globalization.PersianCalendar pers = new System.Globalization.PersianCalendar();
        DateTime dt = pers.ToDateTime(this.Year, this.Month, this.Day, this.Hour, this.Minute, this.Second, 0);
        string CInterval = interval.ToString();
        bool isConvert = true;
        switch (CInterval)
        {
            case "Year":
                dt = pers.AddYears(dt, increment);
                break;
            case "Month":
                dt = pers.AddMonths(dt, increment);
                break;
            case "Day":
                dt = pers.AddDays(dt, increment);
                break;
            case "Hour":
                dt = pers.AddHours(dt, increment);
                break;
            case "Minute":
                dt = pers.AddMinutes(dt, increment);
                break;
            case "Second":
                dt = pers.AddSeconds(dt, increment);
                break;
            default:
                isConvert = false;
                break;
        }

        if (isConvert == true)
        {
            this.Year = (Int16)pers.GetYear(dt);
            this.Month = (byte)pers.GetMonth(dt);
            this.Day = (byte)pers.GetDayOfMonth(dt);
            this.Hour = (byte)pers.GetHour(dt);
            this.Minute = (byte)pers.GetMinute(dt);
            this.Second = (byte)pers.GetSecond(dt);
        }


        return this.m_Year.ToString("D4") + "/" + this.m_Month.ToString("D2") + "/" + this.m_Day.ToString("D2") + " " + this.Hour.ToString("D2") + ":" + this.Minute.ToString("D2") + ":" + this.Second.ToString("D2");
    }
}

از منوهای بالا روی منوی Bulild و سپس گزینه‌ی Publish prgJalaliDate کلیک کتید:

در پنجره‌ی بازشده روی دکمه‌ی Edit کلیک کنید سپس تنظیمات مربوط به اتصال به پایگاه داده را انجام دهید.

روی دکمه‌ی OK کلیک کنید و سپس در پنجره‌ی اولیه، روی دکمه‌ی Publish کلیک کتید:

به همین سادگی، DataType مربوطه در SQL Server 2012 ساخته می‌شود. خبر خوش این‌که شما می‌توانید با راست‌کلیک روی نام پروژه و انتخاب گزینه‌ی Properties در قسمت Project Setting تنظیمات مربوط به نگارش SQL Server را انجام دهید. (از نگارش 2005 به بعد در VS 2012 پشتیبانی می‌شود.)


اکنون زمان آن رسیده است که DataType ایجادشده را در SQL Server 2012 بیازماییم. SQL Server را باز کنید و دستور زیر را در آن اجرا کتید.

USE Northwind

GO

CREATE TABLE dbo.TestTable
(
Id int NOT NULL IDENTITY (1, 1),
TestDate dbo.JalaliDate NULL
)  ON [PRIMARY]
GO
همین‌طور که مشاهده می‌کنید؛ امکان به‌کارگیری DataType تعریف‌شده وجود دارد. 
اکنون چند رکورد درون این جدول درج می‌کنیم:
Insert into TestTable (TestDate) Values ('1392/02/09'),('1392/02/09 22:40'),('1392/12/30 22:40')
پس از اجرای این دستور خطای زیر در پایین صفحه‌ی SQL Server نمایان می‌شود:

این خطا به این خاطر است که CLR را در SQL Server  فعال نکرده ایم. جهت فعال‌کردن CLR دستور زیر را اجرا کنید:
sp_configure 'clr enabled', 1
Reconfigure
بار دیگر دستور درج را اجرا می‌کنیم:
Insert into TestTable (TestDate) Values ('1392/02/09'),('1392/02/09 22:40'),('1392/12/30 22:40')
ملاحظه می‌کنید که داده‌ها در جدول مربوطه ذخیره شده است. در رکورد نخست چون ساعت، دقیقه و ثانیه تعریف نشده است؛ به طور هوشمند صفر درج شده است. در رکورد دوم، ساعت و دقیقه مقدار دارد ولی ثانیه صفر ثبت شده است. و در رکورد سوم چون سال 1392 کبیسه نیست؛ به صورت هوشمند آخرین روز ماه به جای روز ثبت شده است. هرچند می‌توان با دست‌کاری در توابع سی‌شارپ، این قوانین را عوض کرد.

اکنون زمان آن رسیده است که توسط یک پرس‌وجو، همه‌ی توابعی که در سی‌شارپ برای این نوع داده نوشتیم، بیازماییم. پرس‌وجوی زیر را اجرا کنید:
Select TestDate.ToString() as JalaliDateTime,
          TestDate.GetDate() as JalaliDate, TestDate.GetTime() as JalaliTime,
          TestDate.ToGregorianTime() as GregorianTime,
          TestDate.JalaliDateAdd('Day',1) JalaliTomorrow,
          TestDate.Month as JalaliMonth from TestTable
خروجی این پرس‌وجو به شکل زیر خواهد بود:

البته درباره‌ی ستون پنجم و ششم شما می‌توانید روی همه‌ی اجزای تاریخ افزایش و کاهش داشته باشید و هم‌چنین می‌توانید با تابع مربوطه هر کدام از اجزای زمان را جداگانه به دست بیاورید که در این مثال عدد ماه نشان داده شده است.

نیازی به گفتن نیست که می‌توانید به سادگی از توابع مربوط به DateTime در SQL Server بهره ببرید. برای مثال برای به دست آوردن فاصله‌ی میان دو روز از پرس‌وجوی زیر استفاده کنید:
Declare @a JalaliDate  = '1392/02/07 00:00:00'
Declare @b JalaliDate = '1392/02/05 00:00:00'

SELECT DATEDIFF("DAY",@b.ToGregorianTime(),@a.ToGregorianTime()) AS DiffDate

شاد و پیروز باشید.
مطالب
توسعه اپلیکیشن های ASP.NET با Windows Azure Active Directory
ابزار ASP.NET برای Windows Azure Active Directory فعال کردن احراز هویت در وب اپلیکیشن هایی که روی  Windows Azure Web Sites  میزبانی شده اند را ساده‌تر می‌کند. می‌توانید از Windows Azure Authentication برای احراز هویت کاربران Office 365 استفاده کنید، حساب‌های کاربری را از On-Premise Active Directory خود همگام سازی (Sync) کنید و یا از یک دامنه سفارشی Windows Azure Active Directory بهره ببرید. فعال سازی Windows Azure Authentication، اپلیکیشن شما را طوری پیکربندی می‌کند تا تمامی کاربران را با استفاده از یک  Windows Azure Active Directory tenant  احراز هویت کند.

این مقاله ساختن یک اپلیکیشن ASP.NET را که بر اساس  organizational accounts  پیکربندی شده و بر روی  Windows Azure Active Directory  میزبانی می‌شود، مرور می‌کند.

پیش نیاز ها

  1. Visual Studio Express 2013 RC for Web یا Visual Studio 2013 RC 
  2. یک حساب کاربری در Windows Azure. می‌توانید یک حساب رایگان بسازید.


یک مدیر کلی به حساب کاربری Active Directory خود اضافه کنید

  1. وارد Windows Azure Portal شوید.
  2. یک حساب کاربری (Windows Azure Active Directory (AD انتخاب یا ایجاد کنید. اگر قبلا حساب کاربری ساخته اید از همان استفاده کنید در غیر اینصورت یک حساب جدید ایجاد کنید. مشترکین Windows Azure یک AD پیش فرض با نام Default Directory خواهند داشت.
  3. در حساب کاربری AD خود یک کاربر جدید در نقش Global Administrator بسازید. اکانت AD خود را انتخاب کنید و Add User را کلیک کنید. برای اطلاعات کامل‌تر به Managing Windows Azure AD from the Windows Azure Portal 1– Sign Up with an Organizational Account مراجعه کنید.

یک نام کاربری انتخاب کرده و به مرحله بعد بروید.

نام کاربری را وارد کنید و نقش Global Administrator را به آن اختصاص دهید. مدیران کلی به یک آدرس ایمیل متناوب هم نیاز دارند. به مرحله بعد بروید.

بر روی Create کلیک کنید و کلمه‌ی عبور موقتی را کپی کنید. پس از اولین ورود باید کلمه عبور را تغییر دهید.


یک اپلیکیشن ASP.NET بسازید

در ویژوال استودیو یک پروژه جدید ASP.NET Web Forms یا MVC بسازید و روی Change Authentication  کلیک کنید. 

گزینه Organizational Accounts را انتخاب کنید. نام دامنه خود را وارد کنید و سپس گزینه Single Sign On, Read directory data را انتخاب کنید. به مرحله بعد بروید.

نکته: در قسمت More Options می توانید قلمرو اپلیکیشن (Application ID URI) را تنظیم کنید. تنظیمات پیش فرض برای اکثر کاربران مناسب است اما در صورت لزوم می‌توانید آنها را ویرایش کنید، مثلا از طریق Windows Azure Portal دامنه‌های سفارشی خودتان را تنظیم کنید.

اگر گزینه Overwrite را انتخاب کنید اپلیکیشن جدیدی در Windows Azure برای شما ساخته خواهد شد. در غیر اینصورت فریم ورک سعی می‌کند اپلیکیشنی با شناسه یکسان پیدا کند (در پست متدهای احراز هویت در VS 2013 به تنظیمات این قسمت پرداخته شده).

اطلاعات مدیر کلی دامنه در Active Directory خود را وارد کنید (Credentials) و پروژه را با کلیک کردن بر روی Create Project بسازید.

با کلیدهای ترکیبی Ctrl + F5، اپلیکیشن را اجرا کنید. مرورگر شما باید یک اخطار SSL Certificate به شما بدهد. این بدین دلیل است که مدرک استفاده شده توسط IIS Express مورد اعتماد (trusted) نیست. این اخطار را بپذیرید و اجازه اجرا را به آن بدهید. پس از آنکه اپلیکیشن خود را روی Windows Azure منتشر کردید، این پیغام دیگر تولید نمی‌شود؛ چرا که Certificate‌های استفاده شده trusted هستند.

با حساب کاربری سازمانی (organizational account) که ایجاد کرده‌اید، وارد شوید.

همانطور که مشاهده می‌کنید هم اکنون به سایت وارد شده اید.



توزیع اپلیکیشن روی Windows Azure

در Windows Azure Portal یک وب سایت را به‌همراه یک دیتابیس، ایجاد کنید. در پانل سمت چپ صفحه روی Websites کلیک کنید و بعد New را انتخاب کنید. سپس گزینه Custom Create را برگزینید.

اپلیکیشن را روی Windows Azure منتشر کنید. روی پروژه کلیک راست کرده و Publish را انتخاب کنید. در مرحله تنظیمات (Settings) مشاهده می‌کنید که احراز هویت حساب‌های سازمانی (organizational accounts) فعال است. همچنین سطح دسترسی به خواندن تنظیم شده است. در قسمت Database رشته اتصال دیتابیس را تنظیم کنید.

حال به وب سایت Windows Azure خود بروید و توسط حساب کاربری ایجاد شده وارد سایت شوید. در این مرحله دیگر نباید خطای امنیتی SSL را دریافت کنید.


خواندن اطلاعات پروفایل کاربران توسط Graph API

قالب پروژه ویژوال استودیو برای organizational accounts یک متد و نما بنام UserProfile به پروژه اضافه کرده است.
[Authorize]
public async Task<ActionResult> UserProfile()
{
    string tenantId = ClaimsPrincipal.Current.FindFirst(TenantSchema).Value;

    // Get a token for calling the Windows Azure Active Directory Graph
    AuthenticationContext authContext = new AuthenticationContext(String.Format(CultureInfo.InvariantCulture, LoginUrl, tenantId));
    ClientCredential credential = new ClientCredential(AppPrincipalId, AppKey);
    AuthenticationResult assertionCredential = authContext.AcquireToken(GraphUrl, credential);
    string authHeader = assertionCredential.CreateAuthorizationHeader();
    string requestUrl = String.Format(
        CultureInfo.InvariantCulture,
        GraphUserUrl,
        HttpUtility.UrlEncode(tenantId),
        HttpUtility.UrlEncode(User.Identity.Name));

    HttpClient client = new HttpClient();
    HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUrl);
    request.Headers.TryAddWithoutValidation("Authorization", authHeader);
    HttpResponseMessage response = await client.SendAsync(request);
    string responseString = await response.Content.ReadAsStringAsync();
    UserProfile profile = JsonConvert.DeserializeObject<UserProfile>(responseString);

    return View(profile);
}

کلیک کردن لینک UserProfile  اطلاعات پروفایل کاربر جاری را نمایش می‌دهد.



اطلاعات بیشتر

مطالب
به روز رسانی ساده‌تر اجزاء ارتباطات در EF Code first به کمک GraphDiff
دو نوع حالت کلی کارکردن با EF وجود دارند: متصل و منقطع.
در حالت متصل مانند برنامه‌های متداول دسکتاپ، Context مورد استفاده در طول عمر صفحه‌ی جاری زنده نگه داشته می‌شود. در این حالت اگر شیءایی اضافه شود، حذف شود یا تغییر کند، توسط EF ردیابی شده و تنها با فراخوانی متد SaveChanges، تمام این تغییرات به صورت یکجا به بانک اطلاعاتی اعمال می‌شوند.
در حالت غیرمتصل مانند برنامه‌های وب، طول عمر Context در حد طول عمر یک درخواست است. پس از آن از بین خواهد رفت و دیگر فرصت ردیابی تغییرات سمت کاربر را نخواهد یافت. در این حالت به روز رسانی کلیه تغییرات انجام شده در خواص و همچنین ارتباطات اشیاء موجود، کاری مشکل و زمانبر خواهد بود.
برای حل این مشکل، کتابخانه‌ای به نام GraphDiff طراحی شده‌است که صرفا با فراخوانی متد UpdateGraph آن، به صورت خودکار، محاسبات تغییرات صورت گرفته در اشیاء منقطع و اعمال آن‌ها به بانک اطلاعاتی صورت خواهد گرفت. البته ذکر متد SaveChanges پس از آن نباید فراموش شود.


اصطلاحات بکار رفته در GraphDiff

برای کار با GraphDiff نیاز است با یک سری اصطلاح آشنا بود:

Aggregate root
گرافی است از اشیاء به هم وابسته که مرجع تغییرات داده‌ها به شمار می‌رود. برای مثال یک سفارش و آیتم‌های آن‌را درنظر بگیرید. بارگذاری آیتم‌های سفارش، بدون سفارش معنایی ندارند. بنابراین در اینجا سفارش aggregate root است.

AssociatedCollection/AssociatedEntity
حالت‌های Associated به GraphDiff اعلام می‌کنند که اینگونه خواص راهبری تعریف شده، در حین به روز رسانی aggregate root نباید به روز رسانی شوند. در این حالت تنها ارجاعات به روز رسانی خواهند شد.
اگر خاصیت راهبری از نوع ICollection است، حالت AssociatedCollection و اگر صرفا یک شیء ساده است، از AssociatedEntity استفاده خواهد شد.

OwnedCollection/OwnedEntity
حالت‌های Owned به GraphDiff اعلام می‌کنند که جزئیات و همچنین ارجاعات اینگونه خواص راهبری تعریف شده، در حین به روز رسانی aggregate root باید به روز رسانی شوند.


دریافت و نصب GraphDiff

برای نصب خودکار کتابخانه‌ی GraphDiff می‌توان از دستور نیوگت ذیل استفاده کرد:
 PM> Install-Package RefactorThis.GraphDiff


بررسی GraphDiff در طی یک مثال

مدل‌های برنامه آزمایشی، از سه کلاس ذیل که روابط many-to-many و one-to-many با یکدیگر دارند، تشکیل شده‌است:
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;

namespace GraphDiffTests.Models
{
    public class BlogPost
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }

        public virtual ICollection<Tag> Tags { set; get; } // many-to-many

        [ForeignKey("UserId")]
        public virtual User User { get; set; }
        public int UserId { get; set; }

        public BlogPost()
        {
            Tags = new List<Tag>();
        }
    }

    public class Tag
    {
        public int Id { set; get; }

        [StringLength(maximumLength: 450), Required]
        public string Name { set; get; }

        public virtual ICollection<BlogPost> BlogPosts { set; get; } // many-to-many

        public Tag()
        {
            BlogPosts = new List<BlogPost>();
        }
    }

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

        public virtual ICollection<BlogPost> BlogPosts { get; set; } // one-to-many
    }
}
- یک مطلب می‌تواند چندین برچسب داشته باشد و هر برچسب می‌تواند به چندین مطلب انتساب داده شود.
- هر کاربر می‌تواند چندین مطلب ارسال کند.

در این حالت، Context برنامه چنین شکلی را خواهد یافت:
using System;
using System.Data.Entity;
using GraphDiffTests.Models;

namespace GraphDiffTests.Config
{
    public class MyContext : DbContext
    {
        public DbSet<User> Users { get; set; }
        public DbSet<BlogPost> BlogPosts { get; set; }
        public DbSet<Tag> Tags { get; set; }


        public MyContext()
            : base("Connection1")
        {
            this.Database.Log = sql => Console.Write(sql);
        }
    }
}
به همراه تنظیمات به روز رسانی ساختار بانک اطلاعاتی به صورت خودکار:
using System.Data.Entity.Migrations;
using System.Linq;
using GraphDiffTests.Models;

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

        protected override void Seed(MyContext context)
        {
            if(context.Users.Any())
                return;

            var user1 = new User {Name = "User 1"};
            context.Users.Add(user1);

            var tag1 = new Tag { Name = "Tag1" };
            context.Tags.Add(tag1);

            var post1 = new BlogPost { Title = "Title...1", Content = "Content...1", User = user1};
            context.BlogPosts.Add(post1);

            post1.Tags.Add(tag1);

            base.Seed(context);
        }
    }
}
در متد Seed آن یک سری اطلاعات ابتدایی ثبت شده‌اند؛ یک کاربر، یک برچسب و یک مطلب.




در این تصاویر به Id هر کدام از رکوردها دقت کنید. از آن‌ها در ادامه استفاده خواهیم کرد.
در اینجا نمونه‌ای از نحوه‌ی استفاده از GraphDiff را جهت به روز رسانی یک Aggregate root ملاحظه می‌کنید:
            using (var context = new MyContext())
            {
                var user1 = new User { Id = 1, Name = "User 1_1_1" };
                var post1 = new BlogPost { Id = 1, Title = "Title...1_1", Content = "Body...1_1",
                    User = user1, UserId = user1.Id };
                var tags = new List<Tag>
                {
                    new Tag {Id = 1, Name = "Tag1_1"},
                    new Tag {Id=12, Name = "Tag2_1"},
                    new Tag {Name = "Tag3"},
                    new Tag {Name = "Tag4"},
                };
                tags.ForEach(tag => post1.Tags.Add(tag));

                context.UpdateGraph(post1, map => map
                    .OwnedEntity(p => p.User)
                    .OwnedCollection(p => p.Tags)
                    );

                context.SaveChanges();
            }
پارامتر اول UpdateGraph، گرافی از اشیاء است که قرار است به روز رسانی شوند.
پارامتر دوم آن، همان مباحث Owned و Associated بحث شده در ابتدای مطلب را مشخص می‌کنند. در اینجا چون می‌خواهیم هم برچسب‌ها و هم اطلاعات کاربر مطلب اول به روز شوند، نوع رابطه را Owned تعریف کرده‌ایم.
در حین کار با متد UpdateGraph، ذکر Idهای اشیاء منقطع از Context بسیار مهم هستند. اگر دستورات فوق را اجرا کنیم به خروجی ذیل خواهیم رسید:




- همانطور که مشخص است، چون id کاربر ذکر شده و همچنین این Id در post1 نیز درج گردیده است، صرفا نام او ویرایش گردیده است. اگر یکی از موارد ذکر شده رعایت نشوند، ابتدا کاربر جدیدی ثبت شده و سپس رابطه‌ی مطلب و کاربر به روز رسانی خواهد شد (userId آن به userId آخرین کاربر ثبت شده تنظیم می‌شود).
- در حین ثبت برچسب‌ها، چون Id=1 از پیش در بانک اطلاعاتی موجود بوده، تنها نام آن ویرایش شده‌است. در سایر موارد، برچسب‌های تعریف شده صرفا اضافه شده‌اند (چون Id مشخصی ندارند یا Id=12 در بانک اطلاعاتی وجود خارجی ندارد).
- چون Id مطلب مشخص شده‌است، فیلدهای عنوان و محتوای آن نیز به صورت خودکار ویرایش شده‌اند.

و ... تمام این کارها صرفا با فراخوانی متدهای UpdateGraph و سپس SaveChanges رخ داده‌است.


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید:
GraphDiffTests.zip
نظرات مطالب
طراحی افزونه پذیر با ASP.NET MVC 4.x/5.x - قسمت سوم
من در قسمت کلاس اهراز هویت به کمی مشکل برخوردم .
کلاس زیر رو دارم که البته در پلاگینی جدا نوشته شده  :
    public class CmsUser : IdentityUser
    {
        public string DisplayName { get; set; }

    }
هر پست باید دارای یک نویسنده باشد که می‌خوام از CmsUser  استفاده کنم .
این وابستگی و ارتباط باید کجا نوشته شود ؟ 
 public class Post
    {

        private IList<string> _tags = new List<string>();
        public int id { get; set; }
        public string name { get; set; }
        public string slug { get; set; }
        public string description { get; set; }
        public DateTime? publishTime { get; set; }
        public string content { get; set; }
        public IList<string> tags
        {

            get { return _tags; }
            set { _tags = value; }
        }
        public string CombineTags
        {
            get { return string.Join(",", _tags); }
            set { _tags = value.Split(',').Select(x => x.Trim()).ToList(); }
        }

        public string AuthorID { get; set; }

      //  [ForeignKey("AuthorID")]
      //  public CmsUser Author { get; set; }
    }
هرجا می‌نویسم برای کلید‌های خارجیش اخطار میده  و این که زیاد نمی‌خوام پلاگین پست از وجود CmsUser  با خبر باشه
مطالب
کوئری نویسی در EF Core - قسمت اول - تشکیل بانک اطلاعاتی و مقدار دهی اولیه‌ی آن
عموم کسانیکه برای بار اول با LINQ آشنا می‌شوند، مشکل ترجمه‌ی کوئری‌های قبلی SQL خود را به آن دارند. به همین جهت پس از چند سعی و خطا ترجیح می‌دهند تا از ORMها استفاده نکنند؛ چون در کوئری نویسی با آن‌ها مشکل دارند. در این سری، تمام مثال‌های سایت PostgreSQL Exercises با EF Core و LINQ to Entities آن پیاده سازی خواهند شد تا بتواند به عنوان راهنمایی برای تازه‌کاران مورد استفاده قرار گیرد.


بررسی ساختار بانک اطلاعاتی تمرین‌های سایت PostgreSQL Exercises

بانک اطلاعاتی مثال‌های سایت PostgreSQL Exercises از سه جدول با مشخصات زیر تشکیل می‌شود:

جدول کاربران
 CREATE TABLE cd.members
    (
       memid integer NOT NULL, 
       surname character varying(200) NOT NULL, 
       firstname character varying(200) NOT NULL, 
       address character varying(300) NOT NULL, 
       zipcode integer NOT NULL, 
       telephone character varying(20) NOT NULL, 
       recommendedby integer,
       joindate timestamp not null,
       CONSTRAINT members_pk PRIMARY KEY (memid),
       CONSTRAINT fk_members_recommendedby FOREIGN KEY (recommendedby)
            REFERENCES cd.members(memid) ON DELETE SET NULL
    );
هر کاربر در اینجا به همراه یک ID و آدرس است. همچنین به همراه اطلاعات کاربری که او را توصیه کرده‌است (یک جدول خود ارجاع دهنده‌است).


جدول امکانات قابل ارائه‌ی به کاربران
   CREATE TABLE cd.facilities
    (
       facid integer NOT NULL, 
       name character varying(100) NOT NULL, 
       membercost numeric NOT NULL, 
       guestcost numeric NOT NULL, 
       initialoutlay numeric NOT NULL, 
       monthlymaintenance numeric NOT NULL, 
       CONSTRAINT facilities_pk PRIMARY KEY (facid)
    );
در این جدول، امکاناتی مانند «زمین تنیس» و امثال آن ثبت می‌شوند؛ به همراه اطلاعاتی مانند هزینه‌ی اجاره‌ی آن توسط کاربران و یا مهمان‌ها که این دو هزینه، با هم متفاوت هستند. همچنین اطلاعاتی مانند هزینه‌ی راه‌اندازی اولیه‌ی آن‌ها، به همراه هزینه‌ی نگهداری ماهیانه‌ی هر کدام از امکانات نیز ثبت می‌شوند؛ تا در آینده بتوان یک سری محاسبات مالی را نیز در مورد امکانات مهیای مجموعه انجام داد تا مشخص شود که آیا برای مثال داشتن مجموعه‌ای خاص، مقرون به صرفه هست یا خیر.


جدول سوابق استفاده‌ی کاربران از امکانات مجموعه
CREATE TABLE cd.bookings
    (
       bookid integer NOT NULL, 
       facid integer NOT NULL, 
       memid integer NOT NULL, 
       starttime timestamp NOT NULL,
       slots integer NOT NULL,
       CONSTRAINT bookings_pk PRIMARY KEY (bookid),
       CONSTRAINT fk_bookings_facid FOREIGN KEY (facid) REFERENCES cd.facilities(facid),
       CONSTRAINT fk_bookings_memid FOREIGN KEY (memid) REFERENCES cd.members(memid)
    );
در این جدول با ثبت ID کاربر و امکاناتی را که درخواست داده، سوابق رزرو آن‌ها نگهداری می‌شوند.
هر رزرو کردن مکان و امکاناتی در این مجموعه، «نیم ساعته» است. بنابراین Slots در اینجا به معنای تعداد نیم ساعت‌های رزرو کردن یک مکان خاص است؛ که به آن «half hour slots» نیز گفته می‌شود و زمان شروع این رزرو نیز ثبت می‌شود.


تبدیل ساختار بانک اطلاعاتی سایت PostgreSQL Exercises به EF Core Code First


در این دیاگرام، دیتابیس متشکل از سه جدول یاد شده را ملاحظه می‌کنید. برای تبدیل آن‌ها به موجودیت‌های EF Core، می‌توان به صورت زیر عمل کرد:

موجودیت کاربران

namespace EFCorePgExercises.Entities
{
    public class Member
    {
        public int MemId { set; get; }

        public string Surname { set; get; }

        public string FirstName { set; get; }

        public string Address { set; get; }

        public int ZipCode { set; get; }

        public string Telephone { set; get; }

        public virtual ICollection<Member> Children { get; set; }
        public virtual Member Recommender { set; get; }
        public int? RecommendedBy { set; get; }

        public DateTime JoinDate { set; get; }

        public virtual ICollection<Booking> Bookings { set; get; }
    }
}
خواص این کلاس دقیقا بر اساس فیلدهای جدول کاربران مثال‌های سایت تهیه شده‌است. تنها تفاوت آن، داشتن خواص راهبری (navigation properties) مانند Children، Member و Bookings است که نوع روابط این موجودیت را با سایر موجودیت‌ها مشخص می‌کنند:
- خاصیت‌های Children و Recommender برای تعریف رابطه‌ی «خود ارجاعی» اضافه شده‌اند. در اینجا هر کاربر می‌تواند توسط کاربر دیگری توصیه شده باشد.
- خاصیت Bookings برای بیان رابطه‌ی یک به چند با موجودیت Booking، تعریف شده‌است؛ هر یک کاربر می‌تواند به هر تعدادی رزرو امکانات داشته باشد.


موجودیت Facility

namespace EFCorePgExercises.Entities
{
    public class Facility
    {
        public int FacId { set; get; }

        public string Name { set; get; }

        public decimal MemberCost { set; get; }

        public decimal GuestCost { set; get; }

        public decimal InitialOutlay { set; get; }

        public decimal MonthlyMaintenance { set; get; }

        public virtual ICollection<Booking> Bookings { set; get; }
    }
}
- در این جدول، خواص از نوع پولی، توسط نوع decimal معرفی شده‌اند. برای این موارد هیچگاه از double و یا float استفاده نکنید؛ اطلاعات بیشتر.
- خاصیت راهبری Bookings، بیانگر رابطه‌ی یک به چند هرکدام از امکانات مجموعه با تعداد بار و سوابق رزرو شدن آن‌ها است.


موجودیت Booking

namespace EFCorePgExercises.Entities
{
    public class Booking
    {
        public int BookId { set; get; }

        public int FacId { set; get; }
        public virtual Facility Facility { set; get; }

        public int MemId { set; get; }
        public virtual Member Member { set; get; }

        public DateTime StartTime { set; get; }

        public int Slots { set; get; }
    }
}
در جدول ثبت وقایع این مجموعه، اطلاعات کاربر و اطلاعات امکانات درخواستی توسط او ثبت می‌شوند. به همین جهت دو خاصیت راهبری Facility و Member نیز به ازای هر کدام از این Idها تعریف شده‌اند. وجود آن‌ها، جوین نویسی را در آینده بسیار ساده خواهند کرد.


تنظیمات هر کدام از موجودیت‌ها و روابط بین آن‌ها در EF Core Code First

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

تنظیمات موجودیت کاربران

namespace EFCorePgExercises.Entities
{
    public class MemberConfiguration : IEntityTypeConfiguration<Member>
    {
        public void Configure(EntityTypeBuilder<Member> builder)
        {
            builder.HasKey(member => member.MemId);
            builder.Property(member => member.MemId).IsRequired().UseIdentityColumn(seed: 0, increment: 1);

            builder.Property(member => member.Surname).HasMaxLength(200).IsRequired();
            builder.Property(member => member.FirstName).HasMaxLength(200).IsRequired();
            builder.Property(member => member.Address).HasMaxLength(300).IsRequired();
            builder.Property(member => member.ZipCode).IsRequired();
            builder.Property(member => member.Telephone).HasMaxLength(20).IsRequired();

            builder.HasIndex(member => member.RecommendedBy);
            builder.HasOne(member => member.Recommender)
                    .WithMany(member => member.Children)
                    .HasForeignKey(member => member.RecommendedBy);

            builder.Property(member => member.JoinDate).IsRequired();

            builder.HasIndex(member => member.JoinDate).HasName("IX_JoinDate");
            builder.HasIndex(member => member.RecommendedBy).HasName("IX_RecommendedBy");
        }
    }
}
- در اینجا بر اساس تعاریفی که در ابتدای بحث مشاهده کردید، برای مثال طول هر کدام از فیلدهای رشته‌ای متناظر تعریف شده‌اند.
- سپس نحوه‌ی تعریف رابطه‌ی خود راجاعی این موجودیت را مشاهده می‌کنید.
- دو ایندکس هم در اینجا تعریف شده‌اند که جزو اطلاعات موجود در فایل SQL این سری از مثال‌ها هستند.

نکته‌ی مهم: در اینجا یک UseIdentityColumn(seed: 0, increment: 1) را نیز مشاهده می‌کنید که شاید برای شما تازگی داشته باشد. فیلد ID تمام جداول این مجموعه برخلاف معمول که از 1 شروع می‌شود، از صفر شروع می‌شود و ID مساوی صفر را برای کاربران مهمان درنظر گرفته‌است. روش تعریف چنین تنظیم خاصی را توسط متد UseIdentityColumn و دو پارامتر آن در اینجا مشاهده می‌کنید. این ID مساوی صفر، نکات خاصی را هم در حین ثبت اطلاعات اولیه‌ی هر جدول، به همراه دارد که در ادامه بررسی خواهد شد.


تنظیمات موجودیت امکانات مجموعه

namespace EFCorePgExercises.Entities
{
    public class FacilityConfiguration : IEntityTypeConfiguration<Facility>
    {
        public void Configure(EntityTypeBuilder<Facility> builder)
        {
            builder.HasKey(facility => facility.FacId);
            builder.Property(facility => facility.FacId).IsRequired().UseIdentityColumn(seed: 0, increment: 1);

            builder.Property(facility => facility.Name).HasMaxLength(100).IsRequired();

            builder.Property(facility => facility.MemberCost).IsRequired().HasColumnType("decimal(18, 6)");

            builder.Property(facility => facility.GuestCost).IsRequired().HasColumnType("decimal(18, 6)");

            builder.Property(facility => facility.InitialOutlay).IsRequired().HasColumnType("decimal(18, 6)");

            builder.Property(facility => facility.MonthlyMaintenance).IsRequired().HasColumnType("decimal(18, 6)");
        }
    }
}
تنها نکته‌ی مهم این تنظیمات، ذکر دقت نوع decimal است؛ بدون تنظیم آن، EF Core در حین اجرای Migrations، اخطاری را صادر می‌کند.


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

namespace EFCorePgExercises.Entities
{
    public class BookingConfiguration : IEntityTypeConfiguration<Booking>
    {
        public void Configure(EntityTypeBuilder<Booking> builder)
        {
            builder.HasKey(booking => booking.BookId);
            builder.Property(booking => booking.BookId).IsRequired().UseIdentityColumn(seed: 0, increment: 1);

            builder.Property(booking => booking.FacId).IsRequired();
            builder.HasOne(booking => booking.Facility)
                    .WithMany(facility => facility.Bookings)
                    .HasForeignKey(booking => booking.FacId);

            builder.Property(booking => booking.MemId).IsRequired();
            builder.HasOne(booking => booking.Member)
                    .WithMany(member => member.Bookings)
                    .HasForeignKey(booking => booking.MemId);

            builder.Property(booking => booking.StartTime).IsRequired();

            builder.Property(booking => booking.Slots).IsRequired();

            builder.HasIndex(booking => new { booking.MemId, booking.FacId }).HasName("IX_memid_facid");
            builder.HasIndex(booking => new { booking.FacId, booking.StartTime }).HasName("IX_facid_starttime");
            builder.HasIndex(booking => new { booking.MemId, booking.StartTime }).HasName("IX_memid_starttime");
            builder.HasIndex(booking => booking.StartTime).HasName("IX_starttime");
        }
    }
}
روابط یک به چند بین امکانات و رزروها و کاربران و رزروها، در تنظیمات فوق بیان شده‌اند و ذکر آن‌ها در یک سمت رابطه کافی است.


ایجاد Context و معرفی موجودیت‌ها و تنظیمات آن‌ها

در ادامه توسط ApplicationDbContext که از DbContext ارث‌بری می‌کند، سه موجودیت تعریف شده را در معرض دید EF Core قرار می‌دهیم:
namespace EFCorePgExercises.DataLayer
{
    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext(DbContextOptions options)
            : base(options)
        {
        }

        public DbSet<Member> Members { get; set; }

        public DbSet<Booking> Bookings { get; set; }

        public DbSet<Facility> Facilities { get; set; }

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

            modelBuilder.ApplyConfigurationsFromAssembly(typeof(MemberConfiguration).Assembly);
        }
    }
}
همچنین تمام تنظیماتی را که تعریف کردیم، توسط یک سطر ApplyConfigurationsFromAssembly می‌توان از اسمبلی دربرگیرنده‌ی آن‌ها خواند و به Context اضافه کرد.


اجرای Migrations جهت تشکیل ساختار بانک اطلاعاتی

اکنون که موجودیت‌ها، روابط بین آن‌ها و Context برنامه مشخص شدند، می‌توان با اجرای دستوارت زیر، سبب تولید کدهای Migration شد که با اجرای آن‌ها، بانک اطلاعاتی متناظری به صورت خودکار تولید می‌شود:
dotnet tool install --global dotnet-ef --version 3.1.6
dotnet tool update --global dotnet-ef --version 3.1.6
dotnet build
dotnet ef migrations add Init --context ApplicationDbContext
در نگارش EF Core 3x، نیاز است ابزار dotnet-ef را به صورت جداگانه‌ای دریافت و یا به روز رسانی کرد (دو دستور اول) و سپس دستور dotnet ef را اجرا نمود.


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

سایت PostgreSQL Exercises به همراه فایل SQL ایجاد جداول و مقدار دهی اولیه‌ی آن‌ها نیز هست. شاید عنوان کنید که چرا این اطلاعات به صورت متدهای HasData، به تنظیمات موجودیت‌ها اضافه نشدند؟ علت آن به همان ID مساوی صفر بر می‌گردد! در حین استفاده‌ی از متد HasData نمی‌توانید ID ای داشته باشید که مقدار آن با مقدار پیش‌فرض آن نوع، یکی باشد. برای مثال مقدار پیش فرض int، مساوی صفر است. به همین جهت حتی با تنظیم UseIdentityColumn(seed: 0, increment: 1)، اجازه‌ی ثبت Id مساوی صفر را نمی‌دهد؛ چون نمی‌تواند تشخیص دهد که این مقدار، یک مقدار صریح است یا خیر (^). بنابراین مجبور هستیم تا آن‌ها را به صورت معمولی ثبت کنیم:
context.Facilities.Add(new Facility { Name = "Tennis Court 1", MemberCost = 5, GuestCost = 25, InitialOutlay = 10000, MonthlyMaintenance = 200 });
// مابقی موارد
context.SaveChanges();
در این حالت، اول رکورد ثبت شده، Id مساوی صفر را خواهد داشت و مابقی هم یکی یکی افزایش می‌یابند.
این روش برای ثبت اطلاعات Facilities و Booking کار می‌کند؛ اما ... چون Idهای کاربران پشت سر هم نیست و بین آن‌ها فاصله وجود دارد، دیگر نمی‌توان از روش فوق استفاده کرد و نیاز است بتوان مقدار Id را به صورت صریحی تعیین کرد که این مورد نکات جالبی را به همراه دارد:
- در حین کار با SQL Server نیاز است دستور SET IDENTITY_INSERT Members ON را در ابتدای کار، فراخوانی کرد تا بتوان مقدار فیلد ID خود افزایش دهنده را به صورت دستی مقدار دهی کرد.
- در هر زمان، فقط یک جدول و فقط یک سشن (یک اتصال) را می‌توان توسط IDENTITY_INSERT در حالت ثبت و مقدار دهی ID آن قرار داد.
- EF Core، به ازای هر batch اطلاعاتی که ثبت می‌کند، یکبار اتصال را باز و بسته می‌کند. این مورد سبب می‌شود که فراخوانی ExecuteSqlCommand با دستور یاد شده، تاثیری نداشته باشد. برای رفع این مشکل باید یک تراکنش را باز کرد، تا اتصال به بانک اطلاعاتی، در طول آن باز باقی بماند.

در اینجا برای ثبت کاربر با ID مساوی صفر، باز هم می‌توان به صورت معمولی عمل کرد:
context.Members.Add(new Member { ... });
context.SaveChanges(); // For id = 0 = Int's CLR Default Value!
چون اولین رکورد است، ID آن مساوی صفر خواهد شد. برای مابقی از روش ویژه‌ی زیر استفاده می‌کنیم:
using (var transaction = context.Database.BeginTransaction())
{
    try
    {
        context.Database.ExecuteSqlRaw("SET IDENTITY_INSERT Members ON");

        context.Members.Add(new Member { ... });
        // مابقی موارد

        context.SaveChanges();

        transaction.Commit();
    }
    catch
    {
        transaction.Rollback();
        throw;
    }
    finally
    {
        context.Database.ExecuteSqlRaw("SET IDENTITY_INSERT Members OFF");
    }
}
ابتدا یک تراکنش را بر روی context ایجاد می‌کنیم تا اتصال باز شده، در طول آن ثابت باقی بماند. اکنون اجرای دستور SET IDENTITY_INSERT، مؤثر واقع می‌شود. سپس تمام رکوردها را با ذکر ID صریح آن‌ها به context اضافه کرد، آن‌ها را ذخیره نموده و تراکنش را Commit می‌کنیم. در پایان کار هم باید دستور خاموش کردن SET IDENTITY_INSERT صادر شود.


کدهای کامل موجودیت‌های این قسمت به همراه تنظیمات آن‌ها
کدهای کامل تنظیم Context و همچنین مقدار دهی اولیه‌ی بانک اطلاعاتی
مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت دوم - ایجاد ساختار اولیه‌ی مثال این سری
در این قسمت قصد داریم ساختار مقدماتی مثال این سری را که لیستی از تصاویر آپلود شده‌ی توسط کاربران مختلف را نمایش می‌دهد، بدون افزودن مباحث امنیتی و سطوح دسترسی کاربران وارد شده‌ی به سیستم، تشکیل دهیم. در قسمت‌های بعدی، به تدریج آن‌را با قابلیت‌های مختلف IdentityServer 4x یکپارچه خواهیم کرد. در اینجا فرض بر این است که حداقل SDK نگارش 2.1.401 را پیشتر نصب کرده‌اید.


بررسی ساختار WebAPI مقدماتی مثال این سری


این پروژه‌ی مقدماتی که هنوز قسمت‌های اعتبارسنجی به آن اضافه نشده‌اند، از دو قسمت WebApi و MvcClient تشکیل می‌شود.
کار قسمت WebApi، ارائه‌ی یک Restful-API برای کار با گالری تصاویر است. برای اجرای آن وارد پوشه‌ی src\WebApi\ImageGallery.WebApi.WebApp شده و ابتدا فایل restore.bat و سپس dotnet_run.bat را اجرا کنید.
در این حالت برنامه بر روی پورت 7001 در دسترس خواهد بود:


این پورت نیز در فایل Properties\launchSettings.json تنظیم شده‌است تا با پورت کلاینت MVC تهیه شده، تداخل نکند.
کار این سرویس، ارائه‌ی ImagesController است که توسط آن می‌توان لیستی از تصاویر موجود، اطلاعات یک تصویر و همچنین کار ثبت، ویرایش و حذف تصاویر را انجام داد.
در این Solution، رکوردهای تصاویری که در بانک اطلاعاتی ذخیره می‌شوند، یک چنین ساختاری را دارند:
using System;
using System.ComponentModel.DataAnnotations;

namespace ImageGallery.WebApi.DomainClasses
{
    public class Image
    {
        [Key]
        public Guid Id { get; set; }

        [Required]
        [MaxLength(150)]
        public string Title { get; set; }

        [Required]
        [MaxLength(200)]
        public string FileName { get; set; }

        [Required]
        [MaxLength(50)]
        public string OwnerId { get; set; }
    }
}
همانطور که ملاحظه می‌کنید در اینجا OwnerId نیز در نظر گرفته شده‌است تا بتوان پس از اعمال مباحث اعتبارسنجی، تصاویر را از کاربری خاص دریافت و همچنین صرفا تصاویر متعلق به او را به آن کاربر خاص نمایش داد.
در این قسمت توسط کلاس ImageConfiguration کار مقدار دهی اولیه‌ی بانک اطلاعاتی به کمک متد HasData مربوط به EF Core 2.1 صورت گرفته‌است و به این ترتیب می‌توان برنامه را برای نمایش مقدماتی جاری، بدون داشتن سیستم اعتبارسنجی و مفاهیم کاربران، نمایش داد.
این قسمت از Solution، به نحو زیر تشکیل شده‌است:
- ImageGallery.WebApi.DataLayer
در اینجا کار تشکیل DbContext برنامه و همچنین مقدار دهی اولیه‌ی بانک اطلاعاتی و تنظیمات Migrations قرار گرفته‌اند.
- ImageGallery.WebApi.DomainClasses
در این پروژه کلاس‌های موجودیت‌های متناظر با جداول بانک اطلاعاتی قرار می‌گیرند.
- ImageGallery.WebApi.Mappings
این پروژه کار تهیه نگاشت‌های AutoMapper برنامه را انجام می‌دهد؛ نگاشت‌هایی بین Models و DomainClasses که در ImagesController از آن‌ها استفاده می‌شود.
- ImageGallery.WebApi.Models
در این پروژه همان DTO's پروژه قرار گرفته‌اند. جهت رعایت مسایل امنیتی نباید کلاس موجودیت Image فوق را مستقیما در معرض دید API عمومی قرار داد. به همین جهت تعدادی Model در اینجا تعریف شده‌اند که هم برای ثبت، ویرایش و حذف اطلاعات بکار می‌روند و هم جهت گزارشگیری از رکوردهای موجود جدول تصاویر.
- ImageGallery.WebApi.Services
در این پروژه کار با DbContext انجام شده و توسط آن اطلاعات تصاویر به بانک اطلاعاتی اضافه شده و یا واکشی می‌شوند.
- ImageGallery.WebApi.WebApp
این پروژه، همان پروژه‌ی اصلی است که سایر قسمت‌های یاد شده را در کنار هم قرار داده و به صورت یک Restful-API ارائه می‌دهد.



بررسی ساختار MvcClient مقدماتی مثال این سری

پس از اجرای WebAPI و آماده بودن آن جهت استفاده، اکنون یک کلاینت ASP.NET Core MVC را جهت کار با امکانات ImagesController آن، تدارک دیده‌ایم.
برای اجرای آن وارد پوشه‌ی src\MvcClient\ImageGallery.MvcClient.WebApp شده و ابتدا فایل restore.bat و سپس dotnet_run.bat را اجرا کنید.
در این حالت برنامه بر روی پورت 5001 در دسترس خواهد بود:


این پورت نیز در فایل Properties\launchSettings.json تنظیم شده‌است.


در اینجا نمایی از اجرای این برنامه را مشاهده می‌کنید که لیستی از تصاویر را توسط GalleryController، از سرویس ImagesController مربوط به WebAPI، دریافت کرده و سپس نمایش می‌دهد. در این لیست تصاویر تمام کاربران با هم نمایش داده شده‌اند و هنوز امکان فیلتر کردن آن‌ها بر اساس کاربران وارد شده‌ی به سیستم را نداریم که در قسمت‌های بعدی آن‌ها را تکمیل خواهیم کرد.

این قسمت از Solution به نحو زیر تشکیل شده‌است:
- ImageGallery.MvcClient.Services
در اینجا یک Typed HTTP Client مخصوص NET Core 2.1. را تهیه کرده‌ایم. این سرویس جهت دسترسی به آدرس https://localhost:7001 که WebAPI برنامه در آن قرار دارد، تشکیل شده‌است. روش ثبت مخصوص آن‌را نیز در فایل آغازین پروژه‌ی MvcClient.WebApp توسط متد services.AddHttpClient ملاحظه می‌کنید.
- ImageGallery.MvcClient.ViewModels
مدل‌های متناظر با ساختار Viewهای Razor برنامه‌ی وب، در اینجا قرار می‌گیرند.
- ImageGallery.MvcClient.WebApp
این پروژه، همان پروژه‌ی اصلی است که سایر قسمت‌های یاد شده را در کنار هم قرار داده و به صورت یک برنامه‌ی MVC قابل مرور در مرورگر، ارائه می‌دهد.

کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
برای اجرای آن ابتدا باید پروژه‌ی WebApi.WebApp را اجرا کنید و سپس پروژه‌ی MvcClient.WebApp.
مطالب
حذف محدودیت‌های فایل‌های PDF توسط iTextSharp
پیشنیاز
«رمزنگاری فایل‌های PDF با استفاده از کلید عمومی توسط iTextSharp»

در مطلب فوق در مورد رمزنگاری اطلاعات فایل‌های PDF به کمک iTextSharp بحث شد. در مطلب جاری به نحوه رفع این محدودیت‌ها خواهیم پرداخت.

الف) رمزگشایی با استفاده از کلمه عبور
using System.IO;
using iTextSharp.text.pdf;

namespace PdfDecryptor.Core
{
    public class PasswordDecryptor
    {
        public string ReadPassword { set; get; }
        public string PdfPath { set; get; }
        public string OutputPdf { set; get; }

        public void DecryptPdf()
        {
            PdfReader.unethicalreading = true;

            PdfReader reader;
            if (string.IsNullOrWhiteSpace(ReadPassword))
                reader = new PdfReader(PdfPath);
            else
                reader = new PdfReader(PdfPath, System.Text.Encoding.UTF8.GetBytes(ReadPassword));

            using (var stamper = new PdfStamper(reader, new FileStream(OutputPdf, FileMode.Create)))
            {
                stamper.Close();
            }
        }
    }
}
کلاس فوق دوکاربرد را می‌تواند به همراه داشته باشد:
- اگر PDF ایی صرفا دارای محدودیت چاپ بوده و این قابلیت ویژه آن غیرفعال شده است، فقط کافی است مسیر فایل PDF موجود (PdfPath) و مسیر فایل جدیدی که قرار است تولید شود (OutputPdf) ذکر گردد. خروجی فایلی خواهد بود که هیچگونه محدودیتی ندارد. این مساله هم صرفا توسط PdfReader.unethicalreading میسر شده است. به عبارتی ذکر و تنظیم edit password در فایل‌های PDF فاقد امنیت است. همین اندازه که PdfReader می‌تواند فایلی را بخواند، امکان تهیه یک کپی بدون محدودیت از آن توسط PdfStamper وجود خواهد داشت.
در مورد ReadPassword در پیشنیاز ذکر شده، توضیحات کافی به همراه تصویر وجود دارد؛ حالت خاصی که کاربران برای مشاهده محتویات فایل نیاز خواهند داشت تا کلمه‌ی عبور مرتبط را وارد نمایند. در اینجا ذکر ReadPassword الزامی  است. خروجی نهایی کلاس فوق رفع کامل این محدودیت است.


ب) رمزگشایی توسط کلید عمومی
using System.IO;
using iTextSharp.text.pdf;

namespace PdfDecryptor.Core
{
    public class Decryptor
    {
        public string PfxPath { set; get; }
        public string PfxPassword { set; get; }
        public string InputPdf { set; get; }
        public string OutputPdf { set; get; }

        public void DecryptPdf()
        {
            var certs = new PfxReader().ReadCertificate(PfxPath, PfxPassword);
            var reader = new PdfReader(InputPdf, certs.X509Certificates[0], certs.PrivateKey);
            using (var stamper = new PdfStamper(reader, new FileStream(OutputPdf, FileMode.Create)))
            {
                stamper.Close();
            }
        }
    }
}
در اینجا کدهای کامل رمزگشایی فایل PDF ایی که توسط فایل‌های مخصوص PFX رمزنگاری شده است را مشاهده می‌کنید. کلاس PfxReader آن در پیشنیاز بحث موجود است.
در این حالت مسیر فایل PFX به همراه کلمه عبور آن (PfxPassword) باید مشخص شود. خروجی فایلی است بدون محدودیت خاصی.


پ.ن.
این مثال را به صورت یک فایل اجرایی از اینجا می‌توانید دریافت کنید.
مطالب
تقسیم جدول در Entity Framework Code First
سناریو هایی هستند که در آن ها، تعداد ستون‌های یک جدول، بیش از اندازه زیاد می‌شوند و یا آن جدول حاوی فیلدهایی هست که منابع زیادی مصرف می‌کنند، به مانند فیلدهای متنی طولانی یا عکس. معمولا متوجه می‌شویم که در اکثر مواقع، به هنگام واکشی اطلاعات آن جدول، احتیاجی به داده‌های آن فیلد‌ها نداریم و با واکشی بی مورد آن ها، سربار اضافه ای به سیستم تحمیل می‌کنیم، چرا که این داده‌ها ، منابع حافظه ای ما را به هدر می‌دهند.
برای مثال، جدول Post مدل بلاگ را در نظر بگیرید که در آن دو فیلد Body و Image تعریف شده اند.فیلد Body از نوع nvarchar max و فیلد Image از نوع varbinary max است و بدیهی است که این دو داده، به هنگام واکشی حافظه‌ی زیادی مصرف می‌کنند.موارد بسیاری وجود دارند که ما به اطلاعات این دو فیلد احتیاجی نداریم از جمله: نمایش پست‌های پر بازدید، پسته هایی که اخیرا ارسال شده اند و اصولا ما فقط به چند فیلد جدول Post احتیاج داریم  و نه همه‌ی آن ها.
namespace SplittingTableSample.DomainClasses
{
    public class Post
    {
        public virtual int Id { get; set; }
        public virtual string Title { get; set; }
        public virtual DateTime CreatedDate { get; set; }
        public virtual string  Body { get; set; }
        public virtual byte[] Image { get; set; }
    }
}

دلیل اینکه در مدل فوق، تمامی خواص به صورت virtual تعریف شده اند، فعال سازی پروکسی‌های ردیابی تغییر است. اگر دستور زیر را برای واکشی اطلاعات post با id=1 انجام دهیم: 

            using (var context = new MyDbContext())
            {
                var post = context.Posts.Find(1);
            }
خروجی زیر را در SQL Server Profiler مشاهده خواهید کرد:

exec sp_executesql N'SELECT TOP (2) 
[Extent1].[Id] AS [Id], 
[Extent1].[Title] AS [Title], 
[Extent1].[CreatedDate] AS [CreatedDate], 
[Extent1].[Body] AS [Body], 
[Extent1].[Image] AS [Image]
FROM [dbo].[Posts] AS [Extent1]
WHERE [Extent1].[Id] = @p0',N'@p0 int',@p0=1

همان طور که مشاهده می‌کنید، با اجرای دستور فوق تمامی فیلد‌های جدول Posts که id آن‌ها برابر 1 بود واکشی شدند، ولی من تنها به فیلدهای Id و Title آن احتیاج داشتم. خب شاید بگویید که من به سادگی با projection، این مشکل را حل می‌کنم و تنها از فیلد هایی که به آن‌ها احتیاج دارم، کوئری می‌گیرم. همه‌ی این‌ها درست، اما projection هم مشکلات خود را دارد،به صورت پیش فرض، نوع بدون نام بر می‌گرداند و اگر بخواهیم این گونه نباشد، باید مقادیر آن را به یک کلاس(مثلا viewmodel) نگاشت کنیم و کلی مشکل دیگر.
راه حل دیگری که برای حل این مشکل ارائه می‌شود و برای نرمال سازی جداول نیز کاربرد دارد این است که، جدول Posts را به دو جدول مجزا که با یکدیگر رابطه‌ی یک به یک دارند تقسیم کنیم، فیلد‌های پر مصرف را در یک جدول و فیلدهای حجیم و کم مصرف را در جدول دیگری تعریف کنیم و سپس یک رابطه‌ی یک به یک بین آن دو برقرار می‌کنیم.
به  طور مثال این کار را بر روی جدول Posts ، به شکل زیر انجام شده است:
 
namespace SplittingTableSample.DomainClasses
{
    public class Post
    {
        public virtual int Id { get; set; }
        public virtual string Title { get; set; }
        public virtual DateTime CreatedDate { get; set; }
        public virtual PostMetaData PostMetaData { get; set; }
    }
}
namespace SplittingTableSample.DomainClasses
{
    public class PostMetaData
    {
        public virtual int PostId { get; set; }
        public virtual string Body { get; set; }
        public virtual byte[] Image { get; set; }
        public virtual Post Post { get; set; }
    }
}
همان طور که می‌بینید، خواص حجیم به جدول دیگری به نام PostMetaData منتقل شده و با تعریف خواص راهبری ارجاعی در هر دو کلاس،رابطه‌ی یک به یک بین آن‌ها برقرار شده است.جز الزامات تعریف روابط یک به یک این است که، با استفاده از Fluent API یا Data Annotations ، طرف‌های Depenedent و Principal، صریحا به EF معرفی شوند.

namespace SplittingTableSample.DomainClasses
{
    public class PostMetaDataConfig : EntityTypeConfiguration<PostMetaData>
    {
        public PostMetaDataConfig()
        {
            HasKey(x => x.PostId);
            HasRequired(x => x.Post).WithRequiredDependent(x => x.PostMetaData);
        }
    }
}
اولین نکته ای که باید به آن توجه شود، این است که در کلاس PostMetaData، قوانین پیش فرض EF برای تعیین کلید اصلی نقض شده است و به همین دلیل، صراحتا با استفاده از متد HasKey ، کلید اصلی به EF معرفی شده است. نکته‌ی مهم دیگری که به آن باید توجه شود این است که هر دو سر رابطه به صورت Required تعریف شده است. دلیل این موضوع هم با توجه به مطلبی که قرار است گفته شود،کمی جلوتر خواهید فهمید. حال اگر تعاریف DbSet‌ها را نیز اصلاح کنیم و دستور زیر را اجرا کنیم:

var post = context.Posts.Find(1);
خروجی sql زیر را مشاهده خواهید کرد:

exec sp_executesql N'SELECT TOP (2) 
[Extent1].[Id] AS [Id], 
[Extent1].[Title] AS [Title], 
[Extent1].[CreatedDate] AS [CreatedDate]
FROM [dbo].[Posts] AS [Extent1]
WHERE [Extent1].[Id] = @p0',N'@p0 int',@p0=1
خیلی خوب! دیگر خبری از  فیلدهای اضافی Body و Image نیست. دلیل اینکه در اینجا join  بین دو جدول مشاهده نمی‌شود، قابلیت lazy loading است، که با virtual تعریف کردن خواص راهبری حاصل شده است. پس lazy loading در اینجا واقعا مفید است.
اما راه حل ذکر شده نیز کاملا بدون ایراد نیست. مشکل اساسی آن تعدد تعداد جداول آن است. آیا جدول Post ، واقعا احتیاج به چنین سطح نرمال سازی و تبدیل آن به دو جدول مجزا را داشت؟ مطمئنا خیر. آیا واقعا راه حلی وجود دارد که ما در سمت کد‌های خود با دو موجودیت مجزا کار کنیم، در صورتی که در دیتابیس این دو موجودیت، ساختار یک جدول را تشکیل دهند. در اینجا روشی مطرح می‌شود به نام تقسیم جدول (Table Splitting).
برای انجام این کار فقط چند تنظیم ساده لازم است:
1) فیلد‌های موجودیت مورد نظر را به موجودیت‌های کوچکتر، نگاشت می‌کنیم.
2) بین موجودیت‌های کوچک تر، رابطه‌ی یک به یک که هر دو سر رابطه Required هستند، رابطه برقرار می‌کنم.
3) با استفاده از Fluent API یا DataAnnotations، تمامی موجودیت‌ها را به یک نام در دیتابیس نگاشت می‌کنیم.
برای مثال، تنظیمات Fluent برای کلاس Post و PostMetaData که رابطه‌ی بین آن‌ها یک به یک است را مشاهده می‌کنید:
 
namespace SplittingTableSample.DomainClasses
{
    public class PostConfig : EntityTypeConfiguration<Post>
    {
        public PostConfig()
        {
            ToTable("Posts");
        }
    }
}
namespace SplittingTableSample.DomainClasses
{
    public class PostMetaDataConfig : EntityTypeConfiguration<PostMetaData>
    {
        public PostMetaDataConfig()
        {
            ToTable("Posts");
            HasKey(x => x.PostId);
            HasRequired(x => x.Post).WithRequiredDependent(x => x.PostMetaData);
        }
    }
}
نکته مهم این است که در هر دو کلاس(حتی کلاس Post) باید با استفاده از متد ToTable، کلاس‌ها را به یک نام در دیتابیس نگاشت کنیم. در نتیجه با استفاده از متد ToTable در هر دو موجودیت، آنها در دیتابیس به جدولی به نام  Posts نگاشت خواهند شد. تصویر زیر پس از اجرای برنامه، بیان گر این موضوع خواهد بود.

اگر دستورات زیر را اجرا کنید:


var post = context.Posts.Find(1);
Console.WriteLine(post.PostMetaData.Body);
خروجی زیر را در  SQL Server Profiler مشاهده خواهید کرد:
برای متد Find خروجی زیر:
 
exec sp_executesql N'SELECT TOP (2) 
[Extent1].[Id] AS [Id], 
[Extent1].[Title] AS [Title], 
[Extent1].[CreatedDate] AS [CreatedDate]
FROM [dbo].[Posts] AS [Extent1]
WHERE [Extent1].[Id] = @p0',N'@p0 int',@p0=1
و برای post.PostMetaData.Body دستور sql زیر را مشاهده می‌کنید:

exec sp_executesql N'SELECT 
[Extent1].[Id] AS [Id], 
[Extent1].[Body] AS [Body], 
[Extent1].[Image] AS [Image]
FROM [dbo].[Posts] AS [Extent1]
WHERE [Extent1].[Id] = @EntityKeyValue1',N'@EntityKeyValue1 int',@EntityKeyValue1=1
دلیل این که در اینجا ،دو دستور sql به دیتابیس ارسال شده است، فعال بودن ویژگی lazy loading ،به دلیل virtual  تعریف کردن خواص راهبری موجودیت‌ها است.
حال اگر بخواهیم با یک رفت و آمد به دیتابیس کلیه اطلاعات را واکشی کنیم، می‌توانیم از Eager Loading استفاده کنیم:
 
var post = context.Posts.Include(x => x.PostMetaData).SingleOrDefault(x => x.Id == 1);
که خروجی sql آن نیز به شکل زیر است:

SELECT 
[Limit1].[Id] AS [Id], 
[Limit1].[Title] AS [Title], 
[Limit1].[CreatedDate] AS [CreatedDate], 
[Extent2].[Id] AS [Id1], 
[Extent2].[Body] AS [Body], 
[Extent2].[Image] AS [Image]
FROM   (SELECT TOP (2) [Extent1].[Id] AS [Id], [Extent1].[Title] AS [Title], [Extent1].[CreatedDate] AS [CreatedDate]
FROM [dbo].[Posts] AS [Extent1]
WHERE 1 = [Extent1].[Id] ) AS [Limit1]
LEFT OUTER JOIN [dbo].[Posts] AS [Extent2] ON [Limit1].[Id] = [Extent2].[Id]
در نتیجه با کمک این تکنیک توانستیم، با چند موجودیت، در قالب یک جدول رفتار کنیم و از مزیت‌های آن همچون lazy loading، نیز بهره مند شویم.

دریافت کد‌های این بخش: SplittingTable-Sample.rar 
مطالب
بررسی خطای cycles or multiple cascade paths و یا cyclical reference در EF Code first
ابتدا مثال کامل این قسمت را با شرح زیر درنظر بگیرید؛ در اینجا هر کاربر، یک کارتابل می‌تواند داشته باشد (رابطه یک به صفر یا یک) و تعدادی سند منتسب به او (رابطه یک به چند).  همچنین روابط بین کارتابل و اسناد نیز چند به چند است:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Data.Entity.ModelConfiguration;

namespace EF_General.Models.Ex18
{
    public class UserProfile
    {
        public int UserProfileId { set; get; }
        public string UserName { set; get; }

        [ForeignKey("CartableId")]
        public virtual Cartable Cartable { set; get; } // one-to-zero-or-one
        public int? CartableId { set; get; }

        public virtual ICollection<Doc> Docs { set; get; } // one-to-many
    }

    public class Doc
    {
        public int DocId { set; get; }
        public string Title { set; get; }
        public string Body { set; get; }

        [ForeignKey("UserProfileId")]
        public virtual UserProfile UserProfile { set; get; }
        public int UserProfileId { set; get; }

        public virtual ICollection<Cartable> Cartables { set; get; } // many-to-many
    }

    public class Cartable
    {
        public int CartableId { set; get; }

        [ForeignKey("UserProfileId")]
        public virtual UserProfile UserProfile { set; get; }
        public int UserProfileId { set; get; }

        public virtual ICollection<Doc> Docs { set; get; } // many-to-many
    }

    public class UserProfileMap : EntityTypeConfiguration<UserProfile>
    {
        public UserProfileMap()
        {
            this.HasOptional(x => x.Cartable)
                .WithRequired(x => x.UserProfile)
                .WillCascadeOnDelete();
        }
    }

    public class MyContext : DbContext
    {
        public DbSet<UserProfile> UserProfiles { get; set; }
        public DbSet<Doc> Docs { get; set; }
        public DbSet<Cartable> Cartables { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Configurations.Add(new UserProfileMap());
            base.OnModelCreating(modelBuilder);
        }
    }

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

    public static class Test
    {
        public static void RunTests()
        {
            Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, Configuration>());
            using (var context = new MyContext())
            {
                var user = context.UserProfiles.Find(1);
                if (user != null)
                    Console.WriteLine(user.UserName);
            }
        }
    }
}
اگر این مثال را اجرا کنیم، به خطای ذیل برخواهیم خورد:
Introducing FOREIGN KEY constraint 'FK_DocCartables_Cartables_Cartable_CartableId' 
on table 'DocCartables' may cause cycles or multiple cascade paths. Specify 
ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
Could not create constraint. See previous errors.
علت اینجا است که EF به صورت پیش فرض ویژگی cascade delete را برای حالات many-to-many و یا کلیدهای خارجی غیرنال پذیر اعمال می‌کند.
این دو مورد در کلاس‌های Doc و Cartable با هم وجود دارند که در نهایت سبب بروز circular cascade delete (حذف آبشاری حلقوی) می‌شوند و بیشتر مشکل SQL Server است تا EF؛ از این لحاظ که SQL Server در این حالت نمی‌تواند در مورد نحوه حذف خودکار رکوردهای وابسته درست تصمیم‌گیری و عمل کند. برای رفع این مشکل تنها کافی است کلید خارجی تعریف شده در دو کلاس Doc و کارتابل را nullable تعریف کرد تا cascade delete اضافی پیش فرض را لغو کند:
public int? UserProfileId { set; get; }
راه دیگر، استفاده از تنظیمات Fluent و تنظیم WillCascadeOnDelete به false است که به صورت پیش فرض در حالات ذکر شده (روابط چند به چند و یا کلید خارجی غیرنال پذیر)، true است.

شبیه به همین خطا نیز زمانی رخ خواهد داد که در یک کلاس حداقل دو کلید خارجی تعریف شده باشند:
The referential relationship will result in a cyclical reference that is not allowed. [ Constraint name =  ]
در اینجا نیز با نال پذیر تعریف کردن این کلیدهای خارجی، خطای cyclical reference برطرف خواهد شد.
مطالب
سری بررسی 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);

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