مطالب
بررسی خطای Circular References در ASP.NET MVC Json Serialization
خیلی وقت‌ها لازم است تا نتیجه کوئری حاصله را بصورت Json به ویوی مورد نظر ارسال نمایید. برای اینکار کافیست مانند زیر عمل کنیم
[HttpGet]
public JsonResult Get(int id)
{
    return Json(repository.Find(id), JsonRequestBehavior.AllowGet);
}
اما اگر کوئری پیچیده و یا یک مدل سلسله مراتبی داشته باشید که با خودش کلید خارجی داشته باشد، هنگام تبدیل نتایج به خروجی Json، با خطای Circular References مواجه می‌شوید.
A circular reference was detected while serializing an object of type ‘System.Data.Entity.DynamicProxies.ItemCategory_A79…’
علت این مشکل این است که Json Serialization پش فرض ASP.NET MVC فقط یک سطح پایین‌تر را لود می‌کند و در مدل‌های که خاصیتی از نوع خودشان داشته باشند خطای Circular References را فرا می‌خواند. کلاس نمونه در زیر آوره شده است.
    public class Item
    {
        public int Id { get; set; }
        [ForeignKey]
        public int ItemId { get; set; }
        public string Name { get; set; }
        public ICollection<Item> Items { get; set; }
    }

راه حل:
چندین راه حل برای رفع این خطا وجود دارد؛ یکی استفاده از  Automapper و راه حل دیگر استفاده از کتابخانه‌های‌های قوی‌تر کار بار Json مثل Json.net است. اما راه حل ساده‌تر تبدیل خروجی کوئری به یک شی بی نام و سپس تبدیل به Json می‌باشد
[HttpGet]
public JsonResult List()
{           
    var data = repository.AllIncluding(itemcategory => itemcategory.Items);
    var collection = data.Select(x => new
    {
        id = x.Id,
        name = x.Name,
        items = x.Items.Select(item => new
        {
            id=item.Id,
            name = item.Name
        })
    });
    return Json(collection, JsonRequestBehavior.AllowGet);
}
همین طور که در مثال بالا مشاهده می‌نمایید ابتدا همه رکورد‌ها در متغییر data ریخته شده و سپس با یک کوئری دیگر که در آن دوباره از پروپرتی items که از نوع کلاس item می‌باشد شی بی نامی ایجاد نموده ایم. با این کار براحتی این خطا رفع می‌گردد. 
مطالب
کار با کلیدهای اصلی و خارجی در EF Code first
در حین کار با ارتباطات بین اشیاء و جداول، دانستن یک سری از نکات می‌توانند در کم کردن تعداد رفت و برگشت‌های به سرور مؤثر واقع شده و نهایتا سبب بالا رفتن سرعت برنامه شوند. از این دست می‌توان به یک سری نکات ریز همراه با primary-keys و foreign-keys اشاره کرد که در ادامه به آن‌ها پرداخته خواهد شد.
در ابتدا کلا‌س‌های مدل و Context برنامه را به شکل زیر درنظر بگیرید:
using System;
using System.Data.Entity;
using System.Data.Entity.Migrations;

namespace TestKeys
{
    public class Bill
    {
        public int Id { get; set; }
        public decimal Amount { set; get; }
        public virtual Account Account { get; set; }
    }

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

    public class MyContext : DbContext
    {
        public DbSet<Bill> Bills { get; set; }
        public DbSet<Account> Accounts { get; set; }
    }

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

        protected override void Seed(MyContext context)
        {
            var a1 = new Account { Name = "a1" };
            var a2 = new Account { Name = "a2" };

            var bill1 = new Bill { Amount = 100, Account = a1 };
            var bill2 = new Bill { Amount = 200, Account = a2 };

            context.Bills.Add(bill1);
            context.Bills.Add(bill2);
            base.Seed(context);
        }
    }

    public static class Test
    {
        public static void Start()
        {
            Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, Configuration>());
            using (var ctx = new MyContext())
            {
                var bill1 = ctx.Bills.Find(1);
                Console.WriteLine(bill1.Amount);
            }
        }
    }
}

در اینجا کلاس صورتحساب و حساب مرتبط به آن تعریف شده‌اند. سپس به کمک DbContext این دو کلاس در معرض دید EF Code first قرار گرفته‌اند و در کلاس Configuration نحوه آغاز بانک اطلاعاتی به همراه تعدادی رکورد اولیه مشخص شده است.


نحوه صحیح مقدار دهی کلید خارجی در EF Code first

تا اینجا یک روال متداول را مشاهده کردیم. اکنون سؤال این است که اگر بخواهیم اولین رکورد صورتحساب ثبت شده توسط متد Seed را ویرایش کرده و مثلا حساب دوم را به آن انتساب دهیم، بهینه‌ترین روش چیست؟ بهینه‌ترین در اینجا منظور روشی است که کمترین تعداد رفت و برگشت به بانک اطلاعاتی را داشته باشد. همچنین فرض کنید در صفحه ویرایش، اطلاعات حساب‌ها در یک Drop down list شامل نام و id آ‌ن‌ها نیز وجود دارد.

روش اول:
using (var ctx = new MyContext())
{
     var bill1 = ctx.Bills.Find(1);
     var a2 = new Account { Id = 2, Name = "a2" };
     bill1.Account = a2;
     ctx.SaveChanges();
}
این روش مخصوص تازه واردهای EF Code first است و آنطور که مدنظر آن‌ها است کار نمی‌کند.
به کمک متد Find اولین رکورد یافت شده و سپس بر اساس اطلاعات drop down در دسترس، یک شیء جدید حساب را ایجاد و سپس تغییرات لازم را اعمال می‌کنیم. در نهایت اطلاعات را هم ذخیره خواهیم کرد.
این روش به ظاهر کار می‌کنه اما حاصل آن ذخیره رکورد حساب سومی با id=3 در بانک اطلاعاتی است و سپس انتساب آن به اولین صورتحساب ثبت شده.
نتیجه: Id را دستی مقدار دهی نکنید؛ تاثیری ندارد. زیرا اطلاعات شیء جدید حساب، در سیستم tracking مرتبط با Context جاری وجود ندارد. بنابراین EF آن‌را به عنوان یک شیء کاملا جدید درنظر خواهد گرفت، صرفنظر از اینکه Id را به چه مقداری تنظیم کرده‌اید.

روش دوم:
using (var ctx = new MyContext())
{
    var bill1 = ctx.Bills.Find(1);
    var a2 = ctx.Accounts.Find(2);
    bill1.Account = a2;
    ctx.SaveChanges();
}
اینبار بر اساس Id دریافت شده از Drop down list، شیء حساب دوم را یافته و به صورتحساب اول انتساب می‌دهیم. این روش درست کار می‌کند؛ اما ... بهینه نیست. فرض کنید شیء جاری دارای 5 کلید خارجی است. آیا باید به ازای هر کلید خارجی یکبار از بانک اطلاعاتی کوئری گرفت؟
مگر نه این است که اطلاعات نهایی ذخیره شده در بانک اطلاعاتی متناظر با حساب صورتحساب جاری، فقط یک عدد بیشتر نیست. بنابراین آیا نمی‌شود ما تنها همین عدد متناظر را بجای دریافت کل شیء به صورتحساب نسبت دهیم؟
پاسخ: بله. می‌شود! ادامه آن در روش سوم.

روش سوم:
در اینجا بهترین کار و یکی از best practices طراحی مدل‌های EF این است که طراحی کلاس صورتحساب را به نحو زیر تغییر دهیم:
public class Bill
{
        public int Id { get; set; }
        public decimal Amount { set; get; }

        [ForeignKey("AccountId")]
        public virtual Account Account { get; set; }
        public int AccountId { set; get; }
}
به این ترتیب هم navigation property که سبب تعریف رابطه بین دو شیء و همچنین lazy loading اطلاعات آن می‌شود پابرجا خواهد بود و هم توسط خاصیت جدید AccountId که توسط ویژگی ForeignKey معرفی شده است، ویرایش اطلاعات آن دقیقا همانند کار با یک بانک اطلاعاتی واقعی خواهد شد.
اینبار به کمک خاصیت متناظر با کلید خارجی جدول، مقدار دهی و ویرایش کلید‌های خارجی یک شیء به سادگی زیر خواهد بود؛ خصوصا بدون نیاز به رفت و برگشت اضافی به بانک اطلاعاتی جهت دریافت اطلاعات متناظر با اشیاء تعریف شده به صورت navigation property :

using (var ctx = new MyContext())
{
    var bill1 = ctx.Bills.Find(1);
    bill1.AccountId = 2;
    ctx.SaveChanges();
}


وارد کردن یک شیء به سیستم Tracking

در قسمت قبل عنوان شد که Id را دستی مقدار دهی نکنید، چون تاثیری ندارد. سؤال: آیا می‌شود این شیء ویژه تعریف شده را به سیستم Tracking وارد کرد؟
پاسخ: بلی. به نحو زیر:
using (var ctx = new MyContext())
{
     var a2 = new Account { Id = 2, Name = "a2_a2" };
     ctx.Entry(a2).State = System.Data.EntityState.Modified;
     ctx.SaveChanges();
}
در اینجا شیء حساب دوم را به صورت دستی و بدون واکشی از بانک اطلاعاتی ایجاد کرده‌ایم. بنابراین از دیدگاه Context جاری هیچ ارتباطی به بانک اطلاعاتی نداشته و یک شیء جدید درنظر گرفته می‌شود (صرفنظر از Id آن). اما می‌توان این وضعیت را تغییر داد. فقط کافی است State آن‌را به نحوی که ملاحظه می‌کنید به Modified تغییر دهیم. اکنون اگر اطلاعات این شیء را ذخیره کنیم، دقیقا حساب با id=2 در بانک اطلاعاتی ویرایش خواهد شد و نه اینکه حساب جدیدی ثبت گردد.

 
نظرات مطالب
شروع به کار با EF Core 1.0 - قسمت 10 - استفاده از امکانات بومی بانک‌های اطلاعاتی
یک نکته‌ی تکمیلی: در EF-Core 8x، برای کار با کوئری‌های دستی، دیگر نیازی به تعریف DbQuery و نگاشت‌های آن نیست

تا پیش از EF-Core 8x، جهت نگاشت خروجی کوئری‌های دستی به مدل‌های سفارشی، ابتدا می‌بایستی این خروجی دقیقا معادل یکی از موجودیت‌های تعریف شده می‌بود. سپس DbQuery معرفی شد که شرح آن در بالا آمده و این محدودیت «دقیقا معادل بودن با یکی از موجودیت‌ها» را لغو کرد و ... اکنون در EF-Core 8x، این محدودیت‌ها و تنظیمات مرتبط، به‌طور کامل برطرف شده‌اند. برای مثال همین مثال نگاشت View سفارشی فوق و کوئری گرفتن از آن، در EF 8x، فقط نیاز به یک سطر زیر را دارد که توسط متد SqlQuery انجام می‌شود:
var postCounts = await context.Database.SqlQuery<BlogPostsCount>(@$"SELECT * FROM View_BlogPostCounts").ToListAsync();
و دیگر نیازی به تعریف آن به صورت DbQuery و سپس تعریف نگاشتی برای آن نیست.
خروجی SqlQuery، از نوع IQueryable است. یعنی می‌توان بر روی آن، توابع Linq، مانند Where را هم در صورت نیاز اعمال کرد:
var postCounts = await context.Database
                              .SqlQuery<BlogPostsCount>(@$"SELECT * FROM View_BlogPostCounts")
                              .Where(x => x.PostCount > 1)
                              .ToListAsync();
به این ترتیب کار کردن با کوئری‌های دستی، Viewها و حتی رویه‌های ذخیره شده‌ای که خروجی را بر می‌گردانند، به سادگی فراخوانی متد SqlQuery، مانند مثال‌های فوق شده‌است و نیازی به تنظیمات اضافه‌تری ندارد (و ... حتی نیازی به Dapper هم ندارد!).

چند نکته:
  • مدلی که در اینجا تعریف می‌شود، باید ساده بوده و چندسطحی و یا به همراه روابطی نباشد.
  • نگاشت‌ها، بر اساس نام ستون‌های بازگشت داده شده، انجام می‌شوند و حتی بکارگیری mapping attributes هم مجاز هستند.
  • مدل‌ها، بدون کلید اصلی هستند.
  • متد SqlQuery، برای بار اول در EF 7x اضافه شد که توسط آن، تنها امکان داشتن خروجی‌های scalar (یا غیر موجودیتی)، مانند اعداد و رشته‌ها وجود داشت (<SqlQuery<int).

مشاهده یک مثال کامل رسمی در این مورد که به همراه تعریف یک View، یک Function و یک رویه‌ی ذخیره شده و فراخوانی آن‌ها توسط متد SqlQuery است.

مطالب
طراحی پایگاه داده چند زبانه

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

ستون اضافی :  این ساده‌ترین راه است و به ازای هر ستونی که نیاز به ترجمه داشته باشد، ستون اضافی در نظر می‌گیریم. 
CREATE TABLE app_product (
     Id Int IDENTITY NOT NULL,
     Description_en Text,
     Description_pl Text,
     PRIMARY KEY (Id)
);

مزایا :

  • سادگی
  • کوئری‌های آسان (بدون نیاز به join )

معایب :

  • اضافه کردن زبان جدید نیاز به تغییر جداولی که چند زبانه هستند دارد
  • اگر وارد کردن داده برای همه زبان‌ها الزامی نباشد (بعضی جاها فقط زبان پیش فرض الزامی است) ممکن است داده‌های زیاد و یا فیلد‌های خالی در دیتابیس ایجاد شود
  • نگهداری آن مشکل است  

یک جدول ترجمه :
این روش تمیزترین راه از دیدگاه ساختار دیتابیس به نظر می‌رسد. شما همه متن هایی را که نیاز به ترجمه دارد، در یک جدول ذخیره می‌کنید. 
CREATE TABLE ref_language (
         Code Char(2)NOT NULL,
         Name Varchar(20) NOT NULL,
         PRIMARY KEY (Code)
);
 
CREATE TABLE app_translation (
        Id Int IDENTITY NOT NULL,
        PRIMARY KEY (Id)
);
 
CREATE TABLE app_translation_entry (
       TranslationId Int NOT NULL,
       LanguageCode Char(2) NOT NULL,
       Text Text NOT NULL,
       FOREIGN KEY (TranslationId) REFERENCES app_translation(Id),
       FOREIGN KEY (LanguageCode) REFERENCES ref_language(Code)
);
 
CREATE TABLE app_product (
      Id Int IDENTITY NOT NULL,
      Description Int NOT NULL,
      PRIMARY KEY (Id),
      FOREIGN KEY (Description) REFERENCES app_translation(Id)
);

مزایا :

  • اضافه کردن زبان جدید به تغییر طرح دیتابیس نیاز ندارد
  • به نظر تمیز است و رویکرد رابطه‌ای دارد
  • همه ترجمه‌ها در یک مکان است (بعضی‌ها می‌گویند این جز معایب است چون امکان خوانایی و نگهداری کمتر است)

معایب :

  • کوئری‌های پیچیده (به join ‌های چندگانه نیاز دارد تا شرح کالا را به درستی نمایش دهد) 
  • پیچیدگی زیاد 
جدول ترجمه اضافی به ازای هر جدول چند زبانه : برای هر جدولی که نیاز به ترجمه دارد یک جدول اضافی ساخته می‌شود. جدول اصلی داده‌های غیر مرتبط به زبان و جدول دوم همه اطلاعات ترجمه شده را ذخیره می‌کند. 
CREATE TABLE ref_language (
      Code Char(2)NOT NULL,
      Name Varchar(20) NOT NULL,
      PRIMARY KEY (Code)
);
 
CREATE TABLE app_product (
      Id Int IDENTITY NOT NULL,
      PRIMARY KEY (Id)
);
 
CREATE TABLE app_product_translation (
      ProductId Int NOT NULL,
      LanguageCode Char(2) NOT NULL,
      Description Text NOT NULL,
      FOREIGN KEY (ProductId) REFERENCES app_product(Id),
      FOREIGN KEY (LanguageCode) REFERENCES ref_language(Code)
);

مزایا :

  • اضافه کردن زبان جدید به تغییر طرح دیتابیس نیاز ندارد
  • کوئری‌های نسبتا ساده است

معایب :

  • ممکن است تعداد جداول دو برابر شود 

سه مثال نشان داده شده در بالا به ما ایده می‌دهند که چگونه روش‌های مختلف ممکن است استفاده شوند. البته اینها همه گزینه‌های ممکن نیستند، فقط محبوبترین روش‌ها هستند و شما می‌توانید آنها را ویرایش کنید؛ به عنوان مثال با تعریف View ‌های اضافی که join ‌های پیچیده شما را در کدها، ذخیره می‌کنند. راه حلی  که شما انتخاب می‌کنید به نیازمندی‌های پروژه وابسته است. اگر شما به سادگی نیاز دارید و مطمئن هستید تعداد زبان‌های پیشتیبانی کم و ثابت است، می‌توانید راه حل اول را انتخاب کنید. اگر به انعطاف پذیری بیشتری نیاز دارید، می‌توانید join ‌های ساده در کوئری‌های چند زبانه را انتخاب کنید. راه حل سوم انتخاب مناسبی است. 

منبع :

http://fczaja.blogspot.com/2010/08/multilanguage-database-design.html 

نظرات مطالب
پشتیبانی توکار از انجام کارهای پس‌زمینه در ASP.NET Core 2x
یک نکته‌ی تکمیلی: الزامات زنده نگه داشتن کارهای پس‌زمینه در IIS به صورت یک اسکریپت پاورشل
    ## IIS WebAdmin Module
    Import-Module WebAdministration

    $AppPoolInstance = Get-Item IIS:\AppPools\$AppPool

    Write-Output "Set Site PreLoadEnabled to true"
    Set-ItemProperty IIS:\Sites\$Site -name applicationDefaults.preloadEnabled -value True

    Write-Output "Set Recycling.periodicRestart.time  = 0"
    $AppPoolInstance.Recycling.periodicRestart.time = [TimeSpan]::Parse("0");
    $AppPoolInstance | Set-Item

    Write-Output "Set App Pool start up mode to AlwaysRunning"
    $AppPoolInstance.startMode = "alwaysrunning"

    Write-Output "Disable App Pool Idle Timeout"
    $AppPoolInstance.processModel.idleTimeout = [TimeSpan]::FromMinutes(0)
    $AppPoolInstance | Set-Item

    if ($appPoolStatus -ne "Started") {
        Write-Output "Starting App Pool"
        Start-WebAppPool $AppPool    
    } else {
        Write-Output "Restarting App Pool"
        Restart-WebAppPool $AppPool
    }
مطالب
سفارشی سازی ASP.NET Core Identity - قسمت اول - موجودیت‌های پایه و DbContext برنامه
با به پایان رسیدن مرحله‌ی توسعه‌ی ASP.NET Identity 2.x مخصوص نگارش‌های ASP.NETایی که از Full .NET Framework استفاده می‌کنند، نگارش جدید آن صرفا بر پایه‌ی ASP.NET Core تهیه شده‌است و در طی یک سری، نحوه‌ی سفارشی سازی تقریبا تمام اجزای آن‌را بررسی خواهیم کرد. جهت سهولت پیگیری این سری، پروژه‌ی کامل سفارشی سازی شده‌ی ASP.NET Core Identity را از مخزن کد DNT Identity می‌توانید دریافت کنید.


پیشنیازهای اجرای پروژه‌ی DNT Identity

 - ابتدا نیاز است حداقل ASP.NET Core Identity 1.1 را نصب کرده باشید.
 - همچنین بانک اطلاعاتی پایه‌ی آن که به صورت خودکار در اولین بار اجرای برنامه تشکیل می‌شود، مبتنی بر LocalDB است. بنابراین اگر قصد تغییری را در تنظیمات Context آن ندارید، بهتر است LocalDB را نیز بر روی سیستم نصب کنید. هرچند با تغییر تنظیم ActiveDatabase به SqlServer در فایل appsettings.json، برنامه به صورت خودکار از نگارش کامل SqlServer استفاده خواهد کرد. رشته‌ی اتصالی آن نیز در مدخل ConnectionStrings فایل appsettings.json ذکر شده‌است و قابل تغییر است. برای شروع به کار، نیازی به اجرای مراحل Migrations را نیز ندارید و همینقدر که برنامه را اجرا کنید، بانک اطلاعاتی آن نیز تشکیل خواهد شد.
 - کاربر پیش فرض Admin سیستم و کلمه‌ی عبور آن از مدخل AdminUserSeed فایل appsettings.json خوانده می‌شوند.
 - تنظیمات ایمیل پیش فرض برنامه به استفاده‌ی از PickupFolder در مدخل Smtp فایل appsettings.json تنظیم شده‌است. بنابراین تمام ایمیل‌های برنامه را جهت آزمایش محلی می‌توانید در مسیر PickupFolder آن یا همان c:\smtppickup مشاهده کنید. محتوای این ایمیل‌ها را نیز توسط مرورگر (drag&drop بر روی یک tab جدید) و یا برنامه‌ی Outlook می‌توان مشاهده کرد.


سفارشی سازی کلید اصلی موجودیت‌های ASP.NET Core Identity

ASP.NET Core Identity به همراه دو سری موجودیت است. یک سری ساده‌ی آن که از یک string به عنوان نوع کلید اصلی استفاده می‌کنند و سری دوم، حالت جنریک که در آن می‌توان نوع کلید اصلی را به صورت صریحی قید کرد و تغییر داد. در اینجا نیز قصد داریم از حالت جنریک استفاده کرده و نوع کلید اصلی جداول را تغییر دهیم. تمام این موجودیت‌های تغییر یافته را در پوشه‌ی src\ASPNETCoreIdentitySample.Entities\Identity نیز می‌توانید مشاهده کنید و شامل موارد ذیل هستند:

جدول نقش‌های سیستم
    public class Role : IdentityRole<int, UserRole, RoleClaim>, IAuditableEntity
    {
        public Role()
        {
        }

        public Role(string name)
            : this()
        {
            Name = name;
        }

        public Role(string name, string description)
            : this(name)
        {
            Description = description;
        }

        public string Description { get; set; }
    }
کار با ارث بری از نگارش جنریک کلاس IdentityRole شروع می‌شود. این کلاس پایه، حاوی تعاریف اصلی فیلدهای جدول نقش‌های سیستم است که اولین آرگومان جنریک آن، نوع کلید اصلی جدول مرتبط را نیز مشخص می‌کند و در اینجا به int تنظیم شده‌است. همچنین یک اینترفیس جدید IAuditableEntity را نیز در انتهای این تعریف‌ها مشاهده می‌کنید. در مورد این اینترفیس و Shadow properties متناظر با آن، در ادامه‌ی بحث با سفارشی سازی DbContext برنامه بیشتر توضیح داده خواهد شد.


در اولین بار اجرای برنامه، نقش Admin در این جدول ثبت خواهد شد.

جدول کاربران منتسب به نقش‌ها
    public class UserRole : IdentityUserRole<int>, IAuditableEntity
    {
        public virtual User User { get; set; }

        public virtual Role Role { get; set; }
    }
کلاس پایه‌ی جدول کاربران منتسب به نقش‌ها، کلاس جنریک IdentityUserRole است که در اینجا با تغییر آرگومان جنریک آن به int، نوع فیلدهای UserId و RoleId آن به int تنظیم می‌شوند. در کلاس سفارشی سازی شده‌ی فوق، دو خاصیت اضافه‌تر User و Role نیز را مشاهده می‌کنید. مزیت تعریف آن‌ها، دسترسی ساده‌تر به اطلاعات کاربران و نقش‌ها توسط EF Core است.


در اولین بار اجرای برنامه، کاربر شماره 1 یا همان Admin به نقش شماره 1 یا همان Admin، انتساب داده می‌شود.


جدول جدید  IdentityRoleClaim سیستم
public class RoleClaim : IdentityRoleClaim<int>, IAuditableEntity
{
   public virtual Role Role { get; set; }
}
در ASP.NET Core Identity، جدول جدیدی به نام RoleClaim نیز اضافه شده‌است. در این سری از آن برای پیاده سازی سطوح دسترسی پویای به صفحات استفاده خواهیم کرد. ابتدا یک سری نقش ثابت در جدول Roles ثبت خواهند شد. سپس تعدادی کاربر به هر نقش نسبت داده می‌شوند. اکنون می‌توان به هر نقش نیز تعدادی Claim را انتساب داد. برای مثال یک Claim سفارشی که شامل ID سفارشی area:controller:action باشد. به این ترتیب و با بررسی سفارشی آن می‌توان سطوح دسترسی پویا را نیز پیاده سازی کرد و مزیت آن این است که تمام این Claims به صورت خودکار به کوکی شخص نیز اضافه شده و دسترسی به اطلاعات آن بسیار سریع است و نیازی به مراجعه‌ی به بانک اطلاعاتی را ندارد.

جدول UserClaim سیستم
public class UserClaim : IdentityUserClaim<int>, IAuditableEntity
{
   public virtual User User { get; set; }
}
می‌توان به هر کاربر یک سری Claim مخصوص را نیز انتساب داد. برای مثال مسیر عکس ذخیره شده‌ی او در سرور، چه موردی است و این اطلاعات به صورت خودکار به کوکی او نیز توسط ASP.NET Core Identity اضافه می‌شوند. البته ما در این سری روش دیگری را برای سفارشی سازی Claims عمومی کاربران بکار خواهیم گرفت (با سفارشی سازی کلاس ApplicationClaimsPrincipalFactory آن).

جداول توکن و لاگین‌های کاربران
public class UserToken : IdentityUserToken<int>, IAuditableEntity
{
   public virtual User User { get; set; }
}

public class UserLogin : IdentityUserLogin<int>, IAuditableEntity
{
   public virtual User User { get; set; }
}
دراینجا نیز نحوه‌ی سفارشی سازی و تغییر جداول لاگین‌های کاربران و توکن‌های مرتبط با آن‌ها را مشاهده می‌کنید. این جداول بیشتر جهت دسترسی به حالت‌هایی مانند لاگین با حساب کاربری جی‌میل مورد استفاده قرار می‌گیرند و کاربرد پیش فرضی ندارند (اگر از تامین کننده‌های لاگین خارجی نمی‌خواهید استفاده کنید).

جدول کاربران سیستم
    public class User : IdentityUser<int, UserClaim, UserRole, UserLogin>, IAuditableEntity
    {
        public User()
        {
            UserUsedPasswords = new HashSet<UserUsedPassword>();
            UserTokens = new HashSet<UserToken>();
        }

        [StringLength(450)]
        public string FirstName { get; set; }

        [StringLength(450)]
        public string LastName { get; set; }

        [NotMapped]
        public string DisplayName
        {
            get
            {
                var displayName = $"{FirstName} {LastName}";
                return string.IsNullOrWhiteSpace(displayName) ? UserName : displayName;
            }
        }

        [StringLength(450)]
        public string PhotoFileName { get; set; }

        public DateTimeOffset? BirthDate { get; set; }

        public DateTimeOffset? CreatedDateTime { get; set; }

        public DateTimeOffset? LastVisitDateTime { get; set; }

        public bool IsEmailPublic { get; set; }

        public string Location { set; get; }

        public bool IsActive { get; set; } = true;

        public virtual ICollection<UserUsedPassword> UserUsedPasswords { get; set; }

        public virtual ICollection<UserToken> UserTokens { get; set; }
    }

    public class UserUsedPassword : IAuditableEntity
    {
        public int Id { get; set; }

        public string HashedPassword { get; set; }

        public virtual User User { get; set; }
        public int UserId { get; set; }
    }
در اینجا علاوه بر نحوه‌ی تغییر نوع کلید اصلی جدول کاربران سیستم، نحوه‌ی افزودن خواص اضافه‌تری مانند نام، تاریخ تولد، مکان، تصویر و غیره را نیز مشاهده می‌کنید. به علاوه جدولی نیز جهت ثبت سابقه‌ی کلمات عبور هش شده‌ی کاربران نیز تدارک دیده شده‌است تا کاربران نتوانند از 5 کلمه‌ی عبور اخیر خود (تنظیم NotAllowedPreviouslyUsedPasswords در فایل appsettings.json) استفاده کنند.
فیلد IsActive نیز از این جهت اضافه شده‌است تا بجای حذف فیزیکی یک کاربر، بتوان اکانت او را غیرفعال کرد.


تعریف Shadow properties ثبت تغییرات رکوردها

در #C ارث‌بری چندگانه‌ی کلاس‌ها ممنوع است؛ مگر اینکه از اینترفیس‌ها استفاده شود. برای مثال IdentityUser یک کلاس است و در اینجا دیگر نمی‌توان کلاس دومی را به نام BaseEntity جهت اعمال خواص اضافه‌تری اعمال کرد. به همین جهت است که اعمال اینترفیس خالی IAuditableEntity را در اینجا مشاهده می‌کنید. این اینترفیس کار علامت‌گذاری کلاس‌هایی را انجام می‌دهد که قصد داریم به آن‌ها به صورت خودکار، خواصی مانند تاریخ ثبت رکورد، تاریخ ویرایش آن و غیره را اعمال کنیم.
در Context برنامه، به اطلاعات src\ASPNETCoreIdentitySample.Entities\AuditableEntity مراجعه شده و متد AddAuditableShadowProperties بر روی تمام کلاس‌هایی از نوع IAuditableEntity اعمال می‌شود. این متد خواص مدنظر ما را مانند ModifiedDateTime به صورت Shadow properties به موجودیت‌های علامت‌گذاری شده اضافه می‌کند.
همچنین متد SetAuditableEntityPropertyValues، کار مقدار دهی خودکار این خواص را انجام خواهد داد. بنابراین دیگر نیازی نیست در برنامه برای مثال IP شخص ثبت کننده یا ویرایش کننده را به صورت دستی مقدار دهی کرد. هم تعریف و هم مقدار دهی آن توسط Change tracker سیستم به صورت خودکار انجام خواهند شد.


تاثیر افزودن Shadow properties را بر روی کلاس نقش‌های سیستم، در تصویر فوق ملاحظه می‌کنید. خواصی که به صورت معمول در کلاس‌های برنامه ظاهر نمی‌شوند و صرفا هدف بازبینی سیستم را برآورده می‌کنند و مدیریت آن‌ها نیز در اینجا کاملا خودکار است.


سفارشی سازی DbContext برنامه

نحوه‌ی سفارشی سازی DbContext برنامه را در پوشه‌ی src\ASPNETCoreIdentitySample.DataLayer\Context و src\ASPNETCoreIdentitySample.DataLayer\Mappings ملاحظه می‌کنید. پوشه‌ی Context حاوی کلاس ApplicationDbContextBase است که تمام سفارشی سازی‌های لازم بر روی آن انجام شده‌است؛ شامل:
 - تغییر نوع کلید اصلی موجودیت‌ها به همراه معرفی موجودیت‌های تغییر یافته:
 public abstract class ApplicationDbContextBase :
  IdentityDbContext<User, Role, int, UserClaim, UserRole, UserLogin, RoleClaim, UserToken>,
  IUnitOfWork
ما در ابتدای بحث، برای مثال کلاس Role را سفارشی سازی کردیم. اما برنامه از وجود آن بی‌اطلاع است. با ارث بری از IdentityDbContext و ذکر این کلاس‌های سفارشی به همراه نوع int کلید اصلی مورد استفاده، کار معرفی موجودیت‌های سفارشی سازی شده انجام می‌شود.

 - اعمال متد BeforeSaveTriggers به تمام نگارش‌های مختلف SaveChanges
protected void BeforeSaveTriggers()
{
  ValidateEntities();
  SetShadowProperties();
  this.ApplyCorrectYeKe();
}
در اینجا پیش از ذخیره‌ی اطلاعات، ابتدا موجودیت‌ها اعتبارسنجی می‌شوند. سپس مقادیر Shadow properties تنظیم شده و دست آخر، ی و ک فارسی نیز به اطلاعات ثبت شده، جهت یک دست سازی اطلاعات سیستم، اعمال می‌شوند.

- انتخاب نوع بانک اطلاعاتی مورد استفاده در متد OnConfiguring
در اینجا است که خاصیت ActiveDatabase تنظیم شده‌ی در فایل appsettings.json خوانده شده و اعمال می‌شوند. تعریف متد GetDbConnectionString را در کلاس SiteSettingsExtesnsions مشاهده می‌کنید. کار آن استفاده‌ی از بانک اطلاعاتی درون حافظه‌ای، جهت انجام آزمون‌های واحد و یا استفاده‌ی از LocalDb و یا نگارش کامل SQL Server می‌باشد. اگر علاقمند بودید تا بانک اطلاعاتی دیگری (مثلا SQLite) را نیز اضافه کنید، ابتدا enum ایی به نام ActiveDatabase را تغییر داده و سپس متد GetDbConnectionString و متد OnConfiguring را جهت درج اطلاعات این بانک اطلاعاتی جدید، اصلاح کنید.

پس از تعریف این DbContext پایه‌ی سفارشی سازی شده، کلاس جدید ApplicationDbContext را مشاهده می‌کنید. این کلاس ‍Context ایی است که در برنامه از آن استفاده می‌شود و از کلاس پایه ApplicationDbContextBase مشتق شده‌است:
 public class ApplicationDbContext : ApplicationDbContextBase
تعاریف موجودیت‌های جدید خود را به این کلاس اضافه کنید.
تنظیمات mapping آن‌ها نیز به متد OnModelCreating این کلاس اضافه خواهند شد. فقط نحوه‌ی استفاده‌ی از آن را به‌خاطر داشته باشید:
        protected override void OnModelCreating(ModelBuilder builder)
        {
            // it should be placed here, otherwise it will rewrite the following settings!
            base.OnModelCreating(builder);

            // Adds all of the ASP.NET Core Identity related mappings at once.
            builder.AddCustomIdentityMappings(SiteSettings.Value);

            // Custom application mappings


            // This should be placed here, at the end.
            builder.AddAuditableShadowProperties();
        }
ابتدا باید base.OnModelCreating را ذکر کنید. در غیراینصورت تمام سفارشی سازی‌های شما بازنویسی می‌شوند.
سپس متد AddCustomIdentityMappings ذکر شده‌است. این متد اطلاعات src\ASPNETCoreIdentitySample.DataLayer\Mappings را به صورت خودکار و یکجا اضافه می‌کند که در آن برای مثال نام جداول پیش فرض Identity سفارشی سازی شده‌اند.


در آخر باید AddAuditableShadowProperties فراخوانی شود تا خواص سایه‌ای که پیشتر در مورد آن‌ها بحث شد، به سیستم به صورت خودکار اضافه شوند.
تمام نگاشت‌های سفارشی شما باید در این میان و در قسمت «Custom application mappings» درج شوند.

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


کدهای کامل این سری را در مخزن کد DNT Identity می‌توانید ملاحظه کنید.
مطالب
آشنایی با CLR: قسمت بیست و چهارم
آغاز فصل چهارم: بنیان و اساس نوع‌ها (Type Fundamentals)
در این فصل قرار است به بررسی اساس نوع داده‌ها بپردازیم؛ اینکه رفتارهایی که باید از هر نوع Type انتظار داشته باشیم، چه چیزهایی هستند. معانی فضای نام، اسمبلی‌ها، امن بودن نوع‌ها (Safety) و تبدیلات (Casts) را بهتر درک کنیم.
اولین نکته‌ای که باید بدانید این است که هر نوع داده‌ای که شما در دات نت دارید و تعریف می‌کنید، از والدی به نام System.Object مشتق می‌شود. برای مثال وقتی شما کلاسی را تعریف می‌کنید، چه ضمنی و چه صریح، کلاس شما از System.Object ارث بری خواهد کرد.
یعنی کد:
class DotnetTips
{
....
}
با کد زیر:
class DotnetTips:System.Object
{
...
}
برابر است.
برای همین، همه‌ی کلاس‌ها و انواع داده‌ها، شامل یک سری متدها و خصوصیات مشترک هستند. در لیست زیر تعدادی از متدهایی را که دسترسی public و Protected دارند، در جداول جداگانه‌ای بررسی می‌کنیم:
Public Methods
Equals
اگر هر دو شیء مقادیر یکسانی داشته باشند، True باز می‌گرداند. در فصل پنجم با این مورد بیشتر آشنا می‌شویم.
GetHashCode
 بر اساس مقادیر این شیء، یک کد هش شده می‌سازد. اگر شیء قرار است مقدار هش آن در جداول هش، مثل Dictionary یا HashSet به عنوان کلید به کار رود بهتر است این متد رونویسی شود. متاسفانه این متد در شیء Object تعریف شده است و از آنجا که بیشتر نوع‌ها هیچ وقت به عنوان یک کلید استفاده نمی‌شوند؛ بهتر بود در این رابطه به یک اینترفیس منتقل می‌شد. در فصل 5 بیشتر در این مورد بحث خواهد شد (مطالب مرتبط در اینباره +  + + ).
ToString
پیاده سازی پیش‌فرض آن اینست که این متد با اجرای کد
this.GetType().FullName
نام نوع را برمی‌گرداند. ولی برای بعضی نوع‌ها چون Boolean,int32 این متد، رونویسی شده و مقدار مربوطه را به طور رشته‌ای باز می‌گرداند. در بعضی موارد هم این متد برای استفاده‌هایی چون دیباگ برنامه هم رونویسی می‌شود. توجه داشته باشید که که نسبت به CultureInfo ترد مربوطه، واکنش نشان می‌دهد که در فصل 14 آن را بررسی می‌کنیم.
GetType
 یک نمونه از شیء مورد نظر را برمی‌گرداند که با استفاده از ریفلکشن می‌توان به اطلاعات متادیتای آن شیء دسترسی پیدا کرد. در مورد ریفلکشن در فصل 23 صحبت خواهد شد.


Protected Methods

MemberwiseClone
 این متد از شیء جاری یک Shallow copy (+ )گرفته و فیلدهای غیر ایستای آن را همانند شیء جاری پر می‌کند. اگر فیلدهای غیر ایستا از نوع Value باشند که کپی بیت به بیت صورت خواهد گرفت. ولی اگر فیلدی از نوع Reference باشد، از همان نوع Ref می‌باشند؛ ولی خود شیء اصلی، یک شیء جدید به حساب می‌آید و به شیء سازنده‌اش ارتباطی ندارد.
 Finalize یک متد Virtual است و زمانی صدا زده می‌شود که GC قصد نابودسازی آن شیء را داشته باشد. این متد برای زمانی نیاز است که شما قصد دارید قبل از نابودسازی، کاری را انجام دهید. در فصل 21 در این مورد بیشتر صحبت می‌کنیم.


همانطور که می‌دانید، همه‌ی اشیا نیاز دارند تا با استفاده از کلمه‌ی new، در حافظه تعریف شوند:

DotnetTips dotnettips=new DotnetTips("i am constructor");

اتفاقاتی که با استفاده از عملگر new رخ می‌دهد به شرح زیر است:

  1. اولین کاری که CLR انجام می‌دهد، محاسبه‌ی مقدار حافظه یا تعداد بایت‌های فیلدهای شیء مربوطه است و همچنین اشیایی که از آن‌ها مشتق شده است؛ مثل System.Object که همیشه وجود دارد. هر شیء‌ایی که بر روی حافظه‌ی Heap قرار می‌گیرد، به یک سری اعضای اضافه هم نیاز دارد که Type Object Pointer و Sync Block Index نامیده می‌شوند و CLR از آن‌ها برای مدیریت حافظه استفاده می‌کند. تعداد بایت‌های این اعضای اضافه هم با تعداد بایت‌های شیء مدنظر جمع می‌شوند.
  2. طبق تعداد بایت‌های به دست آمده، یک مقدار از حافظه‌ی Heap دریافت می‌شود.
  3. دو عضو اضافه که در بند شماره یک به آن اشاره کردیم، آماده سازی می‌شوند.
  4. سازنده‌ی شیء صدا زده می‌شود. سازنده بر اساس نوع ورودی شما انتخاب می‌شود و در صورت ارجاع به سازنده‌های والد، ابتدا سازنده‌های والد صدا زده می‌شوند و بعد از اینکه همه‌ی سازنده‌ها صدا زده شدند، سازنده‌ی System.Object صدا زده می‌شود که هیچ کدی ندارد؛ ولی به هر حال عمل Return آن انجام می‌شود.

بعد از انجام همه‌ی موارد بالا، یک اشاره گر به متغیر شما (در اینجا dotnettips) برگشت داده می‌شود.

مطالب
بازنویسی ساده‌تر پیش فرض‌های EF Code first در نگارش 6 آن
فرض کنید مطابق اصول نامگذاری که تعیین کرده‌اید، تمام جداول بانک اطلاعاتی شما باید با پیشوند tbl شروع شوند. برای انجام اینکار در نگارش‌های قبلی EF Code first می‌بایستی از ویژگی Table جهت مزین کردن تمامی کلاس‌ها استفاده می‌شد و یا به ازای تک تک موجودیت‌ها، یک کلاس تنظیمات ویژه را افزود و سپس از متد ToTable برای تعیین نامی جدید، استفاده می‌شد. در EF 6 امکان بازنویسی ساده‌تر پیش فرض‌های تعیین نام جداول، طول فیلدها و غیره، پیش بینی شده‌اند که در ادامه تعدادی از آن‌ها را مرور خواهیم کرد.


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

اگر نیاز باشد تا به تمامی جداول تهیه شده، بر اساس نام کلاس‌های مدل‌های برنامه، یک پیشوند tbl اضافه شود، می‌توان با بازنویسی متد OnModelCreating کلاس Context برنامه شروع کرد:
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            // TableNameConvention
            modelBuilder.Types()
                        .Configure(entity => entity.ToTable("tbl" + entity.ClrType.Name));                        

            base.OnModelCreating(modelBuilder);
        }
سپس متد modelBuilder.Types، کلیه موجودیت‌های برنامه را در اختیار قرار داده و در ادامه می‌توان برای مثال از متد ToTable، برای تعیین نامی جدید به ازای کلیه کلاس‌های مدل‌های برنامه استفاده کرد.


تعیین نام دیگری برای کلید اصلی کلیه‌ی جداول برنامه

فرض کنید نیاز است کلیه PKها، با پیشوند نام جدول جاری در بانک اطلاعاتی تشکیل شوند. یعنی اگر نام PK مساوی Id است و نام جدول Menu، نام کلید اصلی نهایی تشکیل شده در بانک اطلاعاتی باید MenuId باشد و نه Id.
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            // PrimaryKeyNameConvention
            modelBuilder.Properties()
                        .Where(p => p.Name == "Id")
                        .Configure(p => p.IsKey().HasColumnName(p.ClrPropertyInfo.ReflectedType.Name + "Id"));

            base.OnModelCreating(modelBuilder);
        }
این مورد نیز با بازنویسی متد OnModelCreating کلاس Context و سپس استفاده از متد modelBuilder.Properties برای دسترسی به کلیه خواص در حال نگاشت، قابل انجام است. در اینجا کلیه خواصی که نام Id دارند، توسط متد IsKey تبدیل به PK شده و سپس به کمک متد HasColumnName، نام دلخواه جدیدی را خواهند یافت.


تعیین حداکثر طول کلیه فیلدهای رشته‌ای تمامی جداول بانک اطلاعاتی

اگر نیاز باشد تا پیش فرض MaxLength تمام خواص رشته‌ای را تغییر داد، می‌توان از پیاده سازی اینترفیس جدید IStoreModelConvention کمک گرفت:
    public class StringConventions : IStoreModelConvention<EdmProperty>
    {
        public void Apply(EdmProperty property, DbModel model)
        {
            if (property.PrimitiveType.PrimitiveTypeKind == PrimitiveTypeKind.String)
            {
                property.MaxLength = 450;
            }
        }
    }
در اینجا MaxLength کلیه خواص رشته‌ای در حال نگاشت به بانک اطلاعاتی، به 450 تنظیم می‌شود. سپس برای معرفی آن به برنامه خواهیم داشت:
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Conventions.Add<StringConventions>();
            base.OnModelCreating(modelBuilder);
        }
توسط متد modelBuilder.Conventions.Add، می‌توان قراردادهای جدید سفارشی را به برنامه افزود.


نظم بخشیدن به تعاریف قراردادهای پیش فرض

اگر علاقمند نیستید که کلاس Context برنامه را شلوغ کنید، می‌توان با ارث بری از کلاس پایه Convention، قراردادهای جدید را تعریف و سپس  توسط متد modelBuilder.Conventions.Add، کلاس نهایی تهیه شده را به برنامه معرفی کرد.
    public class MyConventions : Convention
    {
        public MyConventions()
        {
            // PrimaryKeyNameConvention
            this.Properties()
                .Where(p => p.Name == "Id")
                .Configure(p => p.IsKey().HasColumnName(p.ClrPropertyInfo.ReflectedType.Name + "Id"));

            // TableNameConvention
            this.Types()
                .Configure(entity => entity.ToTable("tbl" + entity.ClrType.Name));
        }
    }


مثال‌های بیشتر
اگر به مستندات EF 6 مراجعه کنید، مثال‌های بیشتری را در مورد بکارگیری اینترفیس IStoreModelConvention و یا بازنویسی قراردادهای موجود، خواهید یافت.
مطالب
سرعت واکشی اطلاعات در List و Dictionary
دسترسی به داده‌ها پیش شرط انجام همه‌ی منطق‌های اکثر نرم افزار‌های تجاری می‌باشد. داده‌های ممکن در حافظه ، پایگاه داده ، فایل‌های فیزیکی و هر منبع دیگری قرار گرفته باشند.
هنگامی که حجم داده‌ها کم باشد شاید روش دسترسی و الگوریتم مورد استفاده اهمیتی نداشته باشد اما با افزایش حجم داده‌ها روش‌های بهینه‌تر تاثیر مستقیم در کارایی برنامه دارند.
در این مثال سعی بر این است که در یک سناریوی خاص تفاوت بین Dictionary و List را بررسی کنیم :
فرض کنید 2 کلاس Student  و Grade موجود است که وظیفه‌ی نگهداری اطلاعات دانش آموز و نمره را بر عهده دارند.
    public class Grade
    {
        public Guid StudentId { get; set; }
        public string Value { get; set; }

        public static IEnumerable<Grade> GetData()
        {
            for (int i = 0; i < 10000; i++)
            {
                yield return new Grade
                                 {
                                     StudentId = GuidHelper.ListOfIds[i], Value = "Value " + i
                                 };
            }
        }
    }

    public class Student
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
        public string Grade { get; set; }

        public static IEnumerable<Student> GetStudents()
        {
            for (int i = 0; i < 10000; i++)
            {
                yield return new Student
                                 {
                                     Id = GuidHelper.ListOfIds[i],
                                     Name = "Name " + i
                                 };
            }
        }
    }
از کلاس GuidHelper برای تولید و نگهداری شناسه‌های یکتا برای دانش آموز کمک گرفته شده است :
    public class GuidHelper
    {
        public static List<Guid> ListOfIds=new List<Guid>();

        static GuidHelper()
        {
            for (int i = 0; i < 10000; i++)
            {
                ListOfIds.Add(Guid.NewGuid());
            }
        }
    }
سپس لیستی از دانش آموزان و نمرات را درون حافظه ایجاد کرده و با یک حلقه  نمره‌ی هر دانش آموز به Property مورد نظر مقدار داده می‌شود.

ابتدا از LINQ روی لیست برای پیدا کردن نمره‌ی مورد نظر استفاده کرده و در روش دوم برای پیدا کردن نمره‌ی هر دانش آموز از Dictionary  استفاده شده :
    internal class Program
    {
        private static void Main(string[] args)
        {
            var stopwatch = new Stopwatch();
            List<Grade> grades = Grade.GetData().ToList();
            List<Student> students = Student.GetStudents().ToList();

            stopwatch.Start();
            foreach (Student student in students)
            {
                student.Grade = grades.Single(x => x.StudentId == student.Id).Value;
            }
            stopwatch.Stop();
            Console.WriteLine("Using list {0}", stopwatch.Elapsed);
            stopwatch.Reset();
            students = Student.GetStudents().ToList();
            stopwatch.Start();
            Dictionary<Guid, string> dictionary = Grade.GetData().ToDictionary(x => x.StudentId, x => x.Value);

            foreach (Student student in students)
            {
                student.Grade = dictionary[student.Id];
            }
            stopwatch.Stop();
            Console.WriteLine("Using dictionary {0}", stopwatch.Elapsed);
            Console.ReadKey();
        }
    }
نتیجه‌ی مقایسه در سیستم من اینگونه می‌باشد :



همانگونه که مشاهده می‌شود در این سناریو خواندن نمره از روی Dictionary بر اساس 'کلید' بسیار سریع‌تر از انجام یک پرس و جوی LINQ روی لیست است.

زمانی که از LINQ on list
   student.Grade = grades.Single(x => x.StudentId == student.Id).Value;
برای پیدا کردن مقدار مورد نظر یک به یک روی اعضا لیست حرکت می‌کند تا به مقدار مورد نظر برسد در نتیجه پیچیدگی زمانی آن O n هست. پس هر چه میزان داده‌ها بیشتر باشد این روش کند‌تر می‌شود.

زمانی که از Dictonary
         student.Grade = dictionary[student.Id];
برای پیدا کردن مقدار استفاده می‌شود با اولین تلاش مقدار مورد نظر یافت می‌شود پس پیچیدگی زمانی آن O 1 می‌باشد.

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

تفاوت این 2 روش وقتی مشخص می‌شود که میزان داده‌ها زیاد باشد.

در همین رابطه (1 ، 2

DictionaryVsList.zip
مطالب
معرفی و استفاده از DDL Triggers در SQL Server

استفاده از DDL Trigger

امکان ایجاد Trigger برای عملیات (DDL(Data Definition Language از SQL Server 2005  فراهم گردید. عملیاتی مانند ایجاد یک جدول جدید در بانک اطلاعاتی، اضافه شدن یک Login جدید و یا ایجاد یک بانک اطلاعاتی جدید را به وسیله این نوع Trigger‌ها می‌توان کنترل نمود. در حقیقت DDL Trigger به شما اجازه می‌دهد که از  تاثیر تعدادی از دستورات DDL جلوگیری کنید. بدین ترتیب که تقریباً هر دستور DDL به طور خودکار، تراکنشی (Transactional) اجرا می‌شود . می‌توان با دستور ROLLBACK TRANSACTION اجرای دستور DDL را لغو نمود. توجه شود همه دستورات DDL به صورت تراکنشی اجرا نمی‌شوند، به عنوان مثال دستور  ALTER DATABASE ممکن است Database را تغییر دهد. در این صورت ساختار فایلی Database را تغییر می‌دهد، از آنجائی که سیستم عامل ویندوز به صورت تراکنشی عمل نمی‌کند بنابراین شما نمی‌توانید این عمل فایل سیستمی را لغو نمائید. به هر حال شما می‌توانید Trigger را با ALTER DATABASE فعال (fire) کنید برای عملیات Auditing، ولی نمی‌توان از انجام عمل  ALTER  DATABASE جلوگیری کرد.
برای نمونه می‌خواهیم از حذف و یا تغییر جداول یک بانک اطلاعاتی که به صورت عملیاتی در حال سرویس دهی است جلوگیری کنیم، برای اینکار از دستورهای زیر استفاده می‌کنیم:
create trigger Prevent_AlterDrop
on database
for drop_table, alter_table
as
  print 'table can not be dropped or altered'
  rollback transaction
از عبارت  ON  برای مشخص کردن محدوده Trigger در سطح SQL Instance (در این صورت ON All SERVER نوشته می‌شود) و یا در سطح Database (در این حالت ON DATABASE نوشته می‌شود)  استفاده می‌شود و از عبارت FOR  برای مشخص کردن رویداد یا گروه رویدادی که سبب فراخوانی  Trigger می‌شود، استفاده  خواهد شد. 

1- معرفی تابع ()EVENTDATA

  این تابع، یک تابع سیستمی مهم است که در DDL Trigger استفاده می‌شود. در حالیکه DDL Trigger در هر سطحی فعال (fire) شود تابع سیستمی ()EVENTDATA فراخوانی (raise) می‌شود. خروجی تابع در قالب XML است. می‌توان اطلاعات را از تابع EVENTDATA دریافت کرد و آنها را در یک جدول با فیلدی از جنس XML و یا با استفاده از XPath Query  ثبت کرد (Logging). عناصر کلیدی (Key Elements) تابع EVENTDATA  به شرح زیر است:
•  EventType: نوع رویدادی که باعث فراخوانی Trigger شده است.
•  PostTime: زمانی که رویداد رخ می‌دهد.
•  SPID :SPID کاربری که باعث ایجاد رویداد شده است.
•  ServerName:  نام SQL Instance  که  رویداد در آن رخ داده است.
•  LoginName: نام Login که عمل مربوط به وقوع رویداد را اجرا می‌کند.
•  UserName: نام User که عمل مربوط به وقوع رویداد را اجرا می‌کند.
•  DatabaseName: نام Database که رویداد در آن رخ می‌دهد.
•  ObjectType: نوع Object که اصلاح، حذف و یا ایجاد شده است.
•  ObjectName: نام Object که اصلاح، حذف و یا ایجاد شده است.
•  TSQLCommand: دستور T-SQL که اجرا شده و باعث اجرا شدن Trigger شده است. 

2- بررسی یک سناریو نمونه

  برای نمونه در دستورات زیر جدولی با نام ddl_log
  CREATE TABLE ddl_log 
  (
     EventType nvarchar(100), 
 PostTime datetime,
 SPID nvarchar(100),
 ServerName nvarchar(100),
 LoginName nvarchar(100),
 UserName  nvarchar(100),
 DatabaseName  nvarchar(100),
 ObjectName  nvarchar(100),
 ObjectType  nvarchar(100),
 DefaultSchema nvarchar(100),
 [SID]  nvarchar(100),
 TSQLCommand nvarchar(2000));
و یک Trigger با نام log برای رویدادهایی که در سطح Database رخ می‌دهد، ایجاد می‌کنیم.
CREATE TRIGGER [Log] ON DATABASE
 FOR DDL_DATABASE_LEVEL_EVENTS
 AS
 DECLARE @data XML
 SET @data = EVENTDATA()
 INSERT INTO ddl_log
 VALUES (
   @data.value('(/EVENT_INSTANCE/EventType)[1]',   'nvarchar(100)'),
   @data.value('(/EVENT_INSTANCE/PostTime)[1]',   'datetime'),
   @data.value('(/EVENT_INSTANCE/SPID)[1]',   'nvarchar(100)'),
   @data.value('(/EVENT_INSTANCE/ServerName)[1]',   'nvarchar(100)'),
   @data.value('(/EVENT_INSTANCE/LoginName)[1]',   'nvarchar(100)'),
   @data.value('(/EVENT_INSTANCE/UserName)[1]',   'nvarchar(100)'),
   @data.value('(/EVENT_INSTANCE/DatabaseName)[1]',   'nvarchar(100)'),
   @data.value('(/EVENT_INSTANCE/ObjectName)[1]',   'nvarchar(100)'),
   @data.value('(/EVENT_INSTANCE/ObjectType)[1]',   'nvarchar(100)'),
   @data.value('(/EVENT_INSTANCE/DefaultSchema)[1]',   'nvarchar(100)'),
   @data.value('(/EVENT_INSTANCE/SID)[1]',   'nvarchar(100)'),
   @data.value('(/EVENT_INSTANCE/TSQLCommand)[1]',   'nvarchar(2000)'));
نمونه ای از مقادیر ذخیره شده در جدول  ddl_log به شکل زیر خواهد بود:

3- ملاحظات

  در صورت فعال شدن  Trigger می‌توان برخی موارد مانند محدودیت زمانی، کاربر اجرا کننده و ... را  اضافه نمود. برای مثال در دستور زیر اجازه تغییرات در این زمان ( بین  7:00 A.M. تا .8:00 P.M ) امکان پذیر نیست و در صورت اقدام پیغام خطا دریافت می‌کنید و دستورات Create لغو خواهند شد و اگر خارج از زمان فوق دستورات DDL را اجرا کنید دستورات به طور موفقیت آمیز اجرا می‌شود و البته تغییرات نیز Log می‌شوند. 
 

  این Trigger تاثیرات کمی بر روی کارایی دارد به این دلیل که معمولاً رویداد‌های DDL به ندرت رخ می‌دهد. می‌توانید هنگامی که قصد دارید دستورات DDL را اجرا کنید موقتاً Trigger را با دستورات زیر غیر فعال نمائید:

پس از Overrdie کردن می‌توانید مجدداً Trigger را فعال کنید: 


4- معرفی DDL Event Groups: 

برای مشاهده جزئیات بیشتر می‌توانید به این لینک مراجعه کنید.

Server Level

DDL_SERVER_LEVEL_EVENTS

 DDL_LINKED_SERVER_EVENTS
 DDL_LINKED_SERVER_LOGIN_EVENTS
 DDL_REMOTE_SERVER_EVENTS
 DDL_EXTENDED_PROCEDURE_EVENTS
 DDL_MESSAGE_EVENTS
 DDL_ENDPOINT_EVENTS
 DDL_SERVER_SECURITY_EVENTS
 DDL_LOGIN_EVENTS
 DDL_GDR_SERVER_EVENTS
 DDL_AUTHORIZATION_SERVER_EVENT Database Level

Database Level

DDL_DATABASE_LEVEL_EVENTS  
DDL_TABLE_VIEW_EVENTS
DDL_TABLE-EVENTS
DDL_VIEW_EVENTS
DDL_INDEX_EVENTS
DDL_STATISTICS_EVENTS
DDL_DATABASE_SECURITY_EVENTS
DDL_CERTIFICATE_EVENTS
DDL_USER_EVENTS
DDL_ROLE_EVENTS
DDL_APPLICATION_ROLE_EVENTS
DDL_SCHEMA_EVENTS
DDL_GDR_DATABASE_EVENTS
DDL_AUTHORIZATION_DATABASE_EVENTS
DDL_FUNCTION_EVENTS
DDL_PROCEDUER_EVENTS
DDL_TRIGGER_EVENTS
DDL_PARTITION_EVENTS
DDL_PARTITION_FUNCTION_EVENTS
DDL_PARTITION_SCHEME_EVENTS
DDL_SSB_EVENTS
DDL_MESSAGE_TYPE_EVENTS
DDL_CONTRACT_EVENTS
DDL_QUEUE_EVENTS
DDL_SERVER_EVENTS
DDL_ROUTE_EVENTS
DDL_REMOTE_SERVICE_BINDING_EVENTS
DDL_XML_SCHEMA_COLEECTION_EVENTS
DDL_FULLTEXT_CATALOG_EVENTS
DDL_DEFAULT_EVENTS
DDL_EXTENDED_PROPERTY_EVENTS
DDL_PLAN_GUIDE_EVENTS
DDL_RULE_EVENTS
DDL_SYNONYM_EVENTS
DDL_EVENT_NOTIFICATION_EVENTS
DDL_ASSEMBLY_EVENTS
DDL_TYPE_EVENTS