مطالب
شروع به کار با EF Core 1.0 - قسمت 13 - بررسی سیستم ردیابی تغییرات
هر Context در EF Core، دارای خاصیتی است به نام ChangeTracker که وظیفه‌ی آن ردیابی تغییراتی است که نیاز است به بانک اطلاعاتی منعکس شوند. برای مثال زمانیکه توسط یک کوئری، شیءایی را باز می‌گردانید و سپس مقدار یکی از خواص آن‌را تغییر داده و متد SaveChanges را فراخوانی می‌کنید، این ChangeTracker است که به EF اعلام می‌کند، کوئری Update ایی را که قرار است تولید کنی، فقط نیاز است یک خاصیت را به روز رسانی کند؛ آن هم تنها با این مقدار تغییر یافته.

روش‌های مختلف اطلاع رسانی به سیستم ردیابی تغییرات

متد DbSet.Add کار اطلاع رسانی تبدیل وهله‌‌های ثبت شده را به کوئری‌های Insert رکوردهای جدید، انجام می‌دهد:
using (var db = new BloggingContext())
{
   var blog = new Blog { Url = "http://sample.com" };
   db.Blogs.Add(blog);
   db.SaveChanges();
}

سیستم ردیابی اطلاعات، اگر تغییراتی را در خواص اشیاء تحت نظر خود مشاهده کند، سبب تولید کوئری‌های Update می‌گردد. یک چنین اشیایی تحت نظر Context هستند:
الف) اشیایی که در طول عمر Context از دیتابیس کوئری گرفته شده‌اند.
ب) اشیایی که در طول عمر Context به آن اضافه شده‌اند (حالت قبل).
using (var db = new BloggingContext())
{
  var blog = db.Blogs.First();
  blog.Url = "http://sample.com/blog";
  db.SaveChanges();
}

و متد DbSet.Remove کار اطلاع رسانی تبدیل وهله‌های حذف شده را به کوئری‌های Delete معادل، انجام می‌دهد:
using (var db = new BloggingContext())
{
  var blog = db.Blogs.First();
  db.Blogs.Remove(blog);
  db.SaveChanges();
}
اگر شیء حذف شده پیشتر توسط متد DbSet.Add اضافه شده باشد، تنها این شیء از Context حذف می‌شود و کوئری در مورد آن تولید نخواهد شد.

به علاوه امکان ترکیب متدهای Add، Remove و همچنین به روز رسانی اشیاء در طی یک Context و با فراخوانی یک SaveChanges در انتهای کار نیز وجود دارد. از این جهت که یک Context، الگوی واحد کار را پیاده سازی می‌کند و بیانگر یک تراکنش است. در این حالت ترکیبی، یا کل تراکنش با موفقیت به پایان می‌رسد و یا در صورت بروز مشکلی، هیچکدام از تغییرات درخواستی، اعمال نخواهند شد.


عملیات ردیابی، بر روی هر نوع Projections صورت نمی‌گیرد

اگر توسط LINQ Projections، نتیجه‌ی نهایی کوئری را تغییر دادید، فقط در زمانی سیستم ردیابی بر روی آن فعال خواهد بود که projection نهایی حاوی اصل موجودیت مدنظر باشد. برای مثال در کوئری ذیل چون در Projection صورت گرفته‌ی در متد Select، هنوز در خاصیت Blog، به اصل موجودیت Blog اشاره می‌شود، نتیجه‌ی این کوئری نیز تحت نظر سیستم ردیابی خواهد بود:
using (var context = new BloggingContext())
{
   var blog = context.Blogs
      .Select(b =>
            new
            {
               Blog = b,
               Posts = b.Posts.Count()
            });
 }
اما در کوئری ذیل، خیر:
using (var context = new BloggingContext())
{
   var blog = context.Blogs
            .Select(b =>
                 new
                 {
                   Id = b.BlogId,
                   Url = b.Url
                 });
 }
در اینجا در Projection انجام شده، نتیجه‌ی نهایی، به هیچکدام از موجودیت‌های ممکن اشاره نمی‌کند. بنابراین نتیجه‌ی آن تحت نظر سیستم ردیابی قرار نمی‌گیرد.


لغو سیستم ردیابی تغییرات، در زمانیکه به آن نیازی نیست

سیستم ردیابی تغییرات بر اساس مفاهیم AOP و تولید پروکسی‌های آن کار می‌کند. این پروکسی‌ها، اشیایی شفاف هستند که اشیاء شما را احاطه می‌کنند و هر تغییری را که اعمال می‌کنید، ابتدا از این غشاء رد شده و در سیستم ردیابی EF ثبت می‌شوند. سپس به وهله‌ی اصلی شیء موجود اعمال خواهند شد.
بدیهی است تولید این پروکسی‌ها، دارای سربار است و اگر هدف شما صرفا کوئری گرفتن از اطلاعات، جهت نمایش آن‌ها است، نیازی به تولید خودکار این پروکسی‌ها را ندارید و این مساله سبب کاهش مصرف حافظه‌ی برنامه و بالا رفتن سرعت آن می‌شود.
در قسمت قبل عنوان شد که «یک چنین اشیایی تحت نظر Context هستند: الف) اشیایی که در طول عمر Context از دیتابیس کوئری گرفته شده‌اند.»
اگر می‌خواهید این حالت پیش فرض را لغو کنید، از متد AsNoTracking استفاده نمائید:
using (var context = new BloggingContext())
{
  var blogs = context.Blogs.AsNoTracking().ToList();
}
یک چنین کوئری‌هایی برای سناریوهای فقط خواندنی (گزارشگیری‌ها) مناسب هستند و بدیهی است هرگونه تغییری در لیست blogs حاصل، توسط context جاری ردیابی نشده و در نهایت به بانک اطلاعاتی (در صورت فراخوانی SaveChanges) اعمال نمی‌گردد.

اگر می‌خواهید متد AsNoTracking را به صورت خودکار به تمام کوئری‌های یک context خاص اعمال کنید، روش کار و تنظیم آن به صورت زیر است:
using (var context = new BloggingContext())
{
    context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;


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

دو حالت زیر را درنظر بگیرید که در اولی، blog از بانک اطلاعاتی واکشی شده‌است و post به صورت مستقیم وهله سازی شده‌است:
using (var context = new BloggingContext())
{
  var blog = context.Blogs.First();
  var post = new Post { Title = "Intro to EF Core" };

  blog.Posts.Add(post);
  context.SaveChanges();
}
و در دومی blog به صورت مستقیم وهله سازی گردیده‌است و post از بانک اطلاعاتی واکشی شده‌است:
using (var context = new BloggingContext())
{
  var blog = new Blog { Url = "http://blogs.msdn.com/visualstudio" };
  var post = context.Posts.First();

  blog.Posts.Add(post);
  context.SaveChanges();
}
در حالت اول، Post، ابتدا به بانک اطلاعاتی اضافه شده و سپس این مطلب جدید به لیست ارجاعات blog اضافه می‌شود (Post جدیدی اضافه شده و اولین Blog، جهت درج آن به روز رسانی می‌شود).
در حالت دوم، ابتدا blog در بانک اطلاعاتی ثبت می‌شود (چون برخلاف حالت اول، تحت نظر context نیست) و سپس این post (که تحت نظر context است) به مجموعه مطالب آن اضافه می‌شود (بلاگ جدیدی اضافه شده و ارجاع مطلب موجودی به آن اضافه می‌شود).


وارد کردن یک موجودیت به سیستم ردیابی اطلاعات

در مثال قبل مشاهده کردیم که اگر موجودیتی تحت نظر context نباشد (برای مثال توسط یک کوئری به context وارد نشده باشد)، در حین ذخیره سازی ارجاعات، با آن به صورت یک وهله‌ی جدید رفتار شده و حتما در بانک اطلاعاتی به صورت یک رکورد جدید ذخیره می‌شود؛ حتی اگر Id آن‌را دستی تنظیم کرده باشید که ندید گرفته خواهد شد.
اگر Id و سایر اطلاعات شیءایی را دارید، نیازی نیست تا حتما توسط یک کوئری ابتدا آن‌را از بانک اطلاعاتی دریافت و سپس به صورت خودکار وارد سیستم ردیابی کنید؛ متد Attach نیز یک چنین کاری را انجام می‌دهد:
 var blog = new Blog { Id = 2, Url = "https://www.dntips.ir" };
 context.Blog.Attach(blog);
 context.SaveChanges();
در اینجا هرچند شیء Blog از بانک اطلاعاتی واکشی نشده‌است، اما چون توسط متد Attach به DbSet اضافه شده‌است، اکنون جزئی از اشیاء تحت نظر به حساب می‌آید؛ اما با یک شرط. حالت اولیه‌ی این شیء به EntityState.Unchanged تنظیم شده‌است. یعنی زمانیکه SaveChanges فراخوانی می‌شود، عملیات خاصی صورت نخواهد گرفت و هیچ اطلاعاتی در بانک اطلاعاتی درج نمی‌گردد.
علاوه بر متد Attach، متد AttachRange نیز برای افزودن لیستی از موجودیت‌ها در حالت EntityState.Unchanged، پیش بینی شده‌است.

روش دیگر انجام اینکار به صورت ذیل است:
در اینجا ابتدا یک وهله‌ی جدید از Blog ایجاد شده‌است و سپس توسط متد Entry به Context وارد شده و همچنین حالت آن به صورت صریح، به تغییر یافته، مشخص گردیده‌است:
 var blog = new Blog { Id = 2, Url = "https://www.dntips.ir" };
 context.Entry(blog).State = EntityState.Modified ;
 context.SaveChanges();
و یا می‌توان این عملیات را به صورت زیر ساده کرد:
 var blog = new Blog { Id = 2, Url = "https://www.dntips.ir" };
 context.Update(blog);
 context.SaveChanges();
در اینجا متد جدید Update، همان کار Attach و سپس تنظیم حالت را به EntityState.Modified انجام می‌دهد.
به علاوه متد UpdateRange نیز برای افزودن لیستی از موجودیت‌ها در حالت EntityState.Modified، پیش بینی شده‌است.

یک نکته: متدهای Attach و Update، هم بر روی یک DbSet و هم بر روی Context، قابل اجرا هستند. اگر بر روی Context اجرا شدند، نوع موجودیت دریافتی به نوع DbSet متناظر به صورت خودکار نگاشت شده و استفاده می‌شود (context.Set<T>().Attach(entity)). یعنی در حقیقت بین این دو حالت تفاوتی نیست و امکان فراخوانی این متدها بر روی Context، صرفا جهت سهولت کار درنظر گرفته شده‌است.


تفاوت رفتار context.Entry در EF Core با EF 6.x

متد  context.Entry در EF 6.x هم وجود دارد. اما در EF core سبب تغییر وضعیت گراف متصل به یک شیء نمی‌شود و ضعیت روابط آن‌را به روز رسانی نمی‌کند (برخلاف EF 6.x). اگر در EF Core نیاز به یک چنین به روز رسانی گراف مانندی را داشتید، باید از متد جدید context.ChangeTracker.TrackGraph به نحو ذیل استفاده نمائید:
 context.ChangeTracker.TrackGraph(blog, e => e.Entry.State = EntityState.Added);


کوئری گرفتن از سیستم ردیابی اطلاعات

این سناریوها را درنظر بگیرید:
 - می‌خواهم سیستمی شبیه به تریگرهای اس کیوال سرور را با EF داشته باشم.
 - می‌خواهم اطلاعات تمام رکوردهای ثبت شده، حذف شده و به روز رسانی شده را لاگ کنم.
 - می‌خواهم پس از ثبت رکوردی در هر جای برنامه، شبیه به مباحث SQL Server Service Broker و SqlDependency بلافاصله مطلع شده و توسط SignalR اطلاع رسانی کنم.

و در حالت کلی می‌خواهم پیش و یا پس از ثبت اطلاعات، بتوانم به تغییرات صورت گرفته دسترسی داشته باشم و عملیاتی را بر روی آن‌ها انجام دهم. تمام این موارد و سناریوها را با کوئری گرفتن از سیستم ردیابی اطلاعات EF می‌توان پیاده سازی کرد.
برای نمونه در مطلب قبل و قسمت «طراحی یک کلاس پایه، بدون تنظیمات ارث بری روابط»، یک کلاس پایه را که مقادیر پیش فرض خود را از SQL Server دریافت می‌کند، طراحی کردیم. در اینجا می‌خواهیم با استفاده از سیستم ردیابی EF، طراحی این کلاس پایه را عمومی کرده و سازگار با تمام بانک‌های اطلاعاتی موجود کنیم.
جهت یادآوری، کلاس پایه موجودیت‌ها، یک چنین شکلی را داشته:
public class BaseEntity
{
   public int Id { set; get; }
   public DateTime? DateAdded { set; get; }
   public DateTime? DateUpdated { set; get; }
}
و پس از آن، هر موجودیت برنامه به این شکل خلاصه شده و نشانه گذاری می‌شود:
public class Person : BaseEntity
{
   public string FirstName { get; set; }
   public string LastName { get; set; }
}
اکنون به کلاس Context برنامه مراجعه کرده و متد SaveChanges آن‌را بازنویسی می‌کنیم:
    public class ApplicationDbContext : DbContext
    {
        // same as before 

        public override int SaveChanges()
        {
            this.ChangeTracker.DetectChanges();

            var modifiedEntries = this.ChangeTracker
                                      .Entries<BaseEntity>()
                                      .Where(x => x.State == EntityState.Modified);
            foreach (var modifiedEntry in modifiedEntries)
            {
                modifiedEntry.Entity.DateUpdated = DateTime.UtcNow;
            }
 
            var addedEntries = this.ChangeTracker
                                      .Entries<BaseEntity>()
                                      .Where(x => x.State == EntityState.Added);
            foreach (var addedEntry in addedEntries)
            {
                addedEntry.Entity.DateAdded = DateTime.UtcNow;
            }
 
            return base.SaveChanges();
        }
    }
این متد SaveChanges، نقطه‌ی مشترک تمام تغییرات برنامه است. به همین دلیل است که اینجا را می‌توان جهت اعمالی، پیش و پس از فراخوانی متد اصلی base.SaveChanges که کار نهایی درج تغییرات را به بانک اطلاعاتی انجام می‌دهد، مورد استفاده قرار داد.
در اینجا کار با کوئری گرفتن از خاصیت ChangeTracker شروع می‌شود. سپس باید مشخص کنیم چه نوع موجودیت‌هایی را مدنظر داریم. چون تمام موجودیت‌های ما از کلاس پایه‌ی BaseEntity مشتق می‌شوند، بنابراین کوئری گرفتن بر روی این نوع، به معنای دسترسی به تمام موجودیت‌های برنامه نیز هست. سپس در اینجا اگر حالتی EntityState.Modified بود، فقط مقدار خاصیت DateUpdated را به صورت خودکار مقدار دهی می‌کنیم و اگر حالتی EntityState.Added بود، تنها مقدار خاصیت DateAdded را به روز رسانی خواهیم کرد.
در یک چنین حالتی دیگر نیازی نیست تا مقادیر این خواص را در حین ثبت اطلاعات برنامه به صورت دستی مشخص کنیم.

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

نکته‌ای در مورد بهبود کیفیت کدهای متد SaveChanges: استفاده‌ی Change Tracker به این صورت با بازنویسی متد SaveChanges بسیار مرسوم است. اما پس از مدتی به متد SaveChanges ایی خواهید رسید که کنترل آن از دست خارج می‌شود. به همین جهت برای EF 6.x پروژه‌هایی مانند EFHooks طراحی شده‌اند تا کپسوله سازی بهتری را بتوان ارائه داد. انتقال کدهای آن به EF Core کار مشکلی نیست و اصل آن، بازنویسی HookedDbContext آن است که نحوه‌ی مدیریت شکیل‌تر کوئری گرفتن از ChangeTracker را بیان می‌کند.


خواص سایه‌ای یا Shadow properties

EF Core به همراه مفهوم کاملا جدیدی است به نام خواص سایه‌ای. این نوع خواص در سمت کدهای ما و در کلاس‌های موجودیت‌های برنامه وجود خارجی نداشته، اما در سمت جداول بانک اطلاعاتی وجود دارند و اکنون امکان کوئری گرفتن و کار کردن با آن‌ها در EF Core میسر شده‌است.
برای تعریف آن‌ها، بجای افزودن خاصیتی به کلاس‌های برنامه، کار از متد OnModelCreating به نحو ذیل شروع می‌شود:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
  modelBuilder.Entity<Blog>().Property<DateTime>("DateAdded");
در اینجا یک خاصیت جدید به نام DateAdded، از نوع DateTime که در کلاس Blog وجود خارجی ندارد، تعریف شده‌است. به این خاصیت، shadow property می‌گویند (سایه‌ای است از ستونی از جدول بلاگ).
سپس برای کار کردن و کوئری گرفتن از آن می‌توان از متد جدید EF.Property، به نحو ذیل استفاده کرد:
 var blogs = context.Blogs.OrderBy(b => EF.Property<DateTime>(b, "DateAdded"));
همچنین برای مقدار دهی آن تنها می‌توان توسط سیستم Change Tracker اقدام نمود:
 context.Entry(myBlog).Property("DateAdded").CurrentValue = DateTime.Now;
و یا در همان قطعه کد بازنویسی متد SaveChanges فوق، نحوه‌ی دسترسی به اینگونه خواص، به صورت زیر می‌باشد:
foreach (var addedEntry in addedEntries)
{
  addedEntry.Property("DateAdded").CurrentValue = DateTime.UtcNow;
}
مهم‌ترین دلیل وجودی این خواص، پیاده سازی روابطی مانند many-to-many، در نگارش‌های بعدی EF Core هستند. در حقیقت جدول واسطی که در اینجا به صورت خودکار تشکیل می‌شود، به همراه خواصی است که تاکنون امکان دسترسی به آن‌ها در کدهای EF وجود نداشت؛ اما Shadow properties این امر را میسر می‌کنند (فیلدهایی که در سمت بانک اطلاعاتی وجود دارند، اما در کدهای کلاس‌های ما، خیر).
مطالب
طراحی شیء گرا: OO Design Heuristics - قسمت سوم
پیشنهاد می‌کنم قسمت‌های قبل را مطالعه کنید تا با اصطلاحات استفاده شده در ادامه مقالات آشنا باشید. در مقالات آتی، مباحث کمی قابل بحث‌تر خواهند بود.

Class Coupling and Cohesion

تعدادی از قواعد شهودی هم، با Coupling و Cohesion به ترتیب مابین و درون کلاس‌ها، سروکار دارند. تلاش ما در راستای افزایش Cohesion درون کلاس‌ها و سست کردن و کاهش Coupling مابین کلاس‌ها می‌باشد. این قواعد شهودی همین اهداف را در پارادایم action-oriented، در ارتباط با توابع دارند. هدف از Tight Cohesion (انسجام و چسبندگی قوی) در توابع، انسجام بالا و ارتباط نزدیک مابین کدهای موجود در تابع، می‌باشد. هدفی که Loose Coupling (اتصال سست و ضعیف، وابستگی ضعیف) در بین توابع دنبال می‌کند، اشاره دارد به اینکه اگر تابعی قصد استفاده از تابع دیگری را داشته باشد، باید وارد شدن و خروج از آن، از یک نقطه صورت گیرد. این مباحث منجربه مطرح شدن قواعد شهودی از جمله: «یک تابع باید طوری سازماندهی شود که تنها یک دستور return  داشته باشد»، در پارادایم action-oriented می‌شود.
ما در پارادایم شیء گرا، اهداف خود از Loose Coupling و Tight Cohesion را در سطح کلاس مطرح می‌کنیم. 5 شکل اصلی Coupling مابین کلاس‌ها به شرح زیر می‌باشد:

  •  Ni Coupling
  •  Export Coupling
  •  Overt Coupling
  •  Covert Coupling
  •  Surreptitious Coupling
Nil Coupling

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

Export Coupling

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

Overt Coupling

این نوع اتصال زمانی رخ می‌دهد که یک کلاس از جزئیات پیاده سازی کلاس دیگر با داشتن اجازه دسترسی از جانب آن، استفاده کند. به عنوان مثال، مکانیزم کلاس‌های friend در زبان سی پلاس پلاس، که امکان این را می‌دهد کلاس X اجازه دوستی به کلاس Y را اعطا کند و در این صورت کلاس Y می‌تواند به جزئیات پیاده سازی خصوصی کلاس X دسترسی داشته باشد.

Cover Coupling

این نوع اتصال هم به مانند Overt می‌باشد؛ با این تفاوت که هیچ اجازه دسترسی به کلاس Y داده نشده است. اگر زبانی داشته باشیم که به کلاس Y اجازه دهد خود را به عنوان دوست کلاس X معرفی کند، در این صورت نوع اتصال بین دو کلاس X و Y از نوع Covert می‌باشد. به عنوان مثال واقعی، می‌توان به استفاده از Reflection در دات نت اشاره کرد.

Surreptitious Coupling (اتصال پنهان)

آخرین نوع اتصال که بدترین حالت هم محسوب می‌شود، مربوط است به زمانیکه کلاس X به هر طریقی که شده از جزئیات داخلی کلاس Y آگاه باشد و از اعضای عمومی داده‌ای (public data member) آن کلاس استفاده کند. منظور این است که با تغییر این داده‌های کلاس متوجه میشود که بر روی عملیات b از کلاس چه تأثیری می‌گذارد و با اتکاء به این دستاورد، جزئیات داخلی خود را پیاده سازی می‌کند و یک اتصال پنهان را با کلاس Y ایجاد کرده است. در این حالت یک وابستگی قوی به صورت پنهان مابین رفتاری از کلاس Y و پیاده سازی کلاس X ایجاد شده است.

قاعده شهودی 2.7
اتصال و پیوستگی مابین کلاس‌ها باید از نوع Nil یا Export باشد؛ به این معنی که یک کلاس فقط از واسط عمومی کلاس دیگر استفاده کند یا کاری با آن نداشته باشد. (Classes should only exhibit nil or export coupling with other classes, that is, a class should only use operations in the public interface of another class or have nothing to do with that class.)
بجز این دو نوع اتصال، بقیه شکل‌های اتصال به طریقی اجازه دسترسی به جزئیات پیاده سازی کلاس‌ها را اعطا می‌کنند. در نتیجه باعث ایجاد وابستگی مابین پیاده سازی دو کلاس می‌شوند. این وابستگی ما بین پیاده سازی‌ها به محض نیاز به تغییر پیاده سازی یکی از کلاس‌ها ، باعث به وجود آمدن مشکلات نگهداری خواهند شد.
Cohesion درون کلاس‌ها سعی بر این دارد که مطمئن شود تمام اجزای یک کلاس به شدت باهم مرتبط هستند. تعدادی از قواعد شهودی نیز در ادامه بر این خصوصیت دلالت دارند.

قاعده شهودی 2.8
یک کلاس باید یک و تنها یک Key Abstraction را تسخیر نماید. (A class should capture one and only one key abstraction)
یک Key Abstraction به عنوان یک Entity در Domain Model تعریف می‌شود و اغلب در غالب اسم در اسناد و مشخصات نیازمندی‌ها ظاهر می‌شوند. هر کدام از آنها باید فقط به یک کلاس نگاشت پیدا کنند. اگر این نگاشت به بیش از یک کلاس انجام گیرد، در نتیجه احتمالا طراح هر تابع را به عنوان یک کلاس تسخیر کرده است. اگر بیش از یک Key Abstraction به یک کلاس نگاشت پیدا کرده باشد، پس احتمالا طراح یک سیستم متمرکز را طراحی کرده است. این کلاس‌ها Vague Classes نامیده می‌شوند و باید آنها در دو کلاس یا بیشتر، تسخیر شوند.

قاعده شهودی 2.9
داده و رفتار مرتبط را در یک جا (کلاس) نگه دارید. (Keep related data and behavior in one place)
در واقع هدفی که این قاعده به دنبال آن می‌باشد این است که هر دو جزء تشکیل دهنده یک Key Abstraction ، یعنی همان داده و رفتار، باید توسط فقط یک کلاس تسخیر شوند. با نقض این قاعده، توسعه دهنده باید با قرار داد (Convention) خاصی برنامه نویسی کند. 
راه شناسایی
طراح باید کلاس‌هایی را که مرتبا داده‌های مورد نیاز خود را با متدهای get از سایر کلاس‌ها دریافت می‌کنند، شناسایی کند. زیرا این نوع کلاس‌ها این قاعده شهودی را نقض کرده‌اند.

مثال واقعی
استفاده از الگوی Domain Model ارائه شده توسط اقای Martin Fowler که دقیقا اشاره به این قاعده دارد.
یا برعکس آن ضد الگوی Anemic Domain Model که ناقض این قاعده می‌باشد. 
در قسمت اول اشاره کردیم این قواعد را به راحتی می‌توان در صورت نیاز نقض کرد. بعضی از مواقع نیاز به طراحی فیزیکی است که باعث تغییر در طراحی منطقی شده و چه بسا می‌تواند باعث نقض هر کدام از این قواعد شهودی نیز شود. اگر به بخش پروژه‌های سایت رجوع کنید این نقض کاملا مشهود (DomainClasses و ServiceLayer موجود در طراحی فیزیکی آنها) می‌باشد (بیشتر از Anemic Domain Model استفاده شده است)؛ ولی نمی‌توان گفت که این کار اشتباه است.

قاعده شهودی 2.10
اطلاعات نامرتبط به هم را در کلاس‌های جدا از هم قرار دهید. ((Spin off nonrelated information into another class (i.e., noncommunicating behavior) 

هدف از این قاعده این است که اگر کلاسی داریم که یکسری از متدهایش با بخشی از داده و یکسری دیگر با بخش دیگر داده‌ها کار میکنند، در واقع شما دو Key Abstraction را به یک کلاس نگاشت کرده اید (Vague Class) و باید آنها را به کلاس‌های جدا نگاشت کنید.

شکل 2.6 A class with noncommunicating behavior

مثال واقعی

به کلاس Dictionary در تصویر زیر توجه کنید.
برای تعداد کمی داده، بهترین پیاده سازی با استفاده از List و در مقابل برای تعداد داده زیاد بهترین پیاده سازی، استفاده از HashTable می‌باشد. هر یک از این پیاده سازی‌ها، به متدهایی برای add و find کلمات نیاز دارند. طراحی سمت چپ تصویر نشان از نقض این قاعده شهودی دارد.

شکل 2.7 (Noncommunicating behavior (real-world example

در طرح سمت چپ، استفاده کننده باید بداند که چقدر داده وارد کند. از طرفی نمایش جزئیات پیاده سازی در نام کلاس هم ایده خوبی نیست (طرح سمت چپ). بهترین راه حل که در مقالات آینده به آنها خواهیم رسید، بحث استفاده از ارث بری می‌باشد. به این ترتیب، با استفاده از  یک کلاس Dictionary که نمایش جزئیات داخلی خود را مخفی کرده و در شرایط لازم نمایش جزئیات داخلی خود را تغییر دهد. منظور این است که استفاده کننده درگیر جزئیات داخلی آن نشود و این جزئیات که کدام نوع PDict یا HDict استفاده خواهد شد، از دید او مخفی باشد.

مطالب
معرفی ASP.NET Identity

سیستم ASP.NET Membership بهمراه ASP.NET 2.0 در سال 2005 معرفی شد، و از آن زمان تا بحال تغییرات زیادی در چگونگی مدیریت احزار هویت و اختیارات کاربران توسط اپلیکیشن‌های وب بوجود آمده است. ASP.NET Identity نگاهی تازه است به آنچه که سیستم Membership هنگام تولید اپلیکیشن‌های مدرن برای وب، موبایل و تبلت باید باشد.

پیش زمینه: سیستم عضویت در ASP.NET


ASP.NET Membership

ASP.NET Membership طراحی شده بود تا نیازهای سیستم عضویت وب سایت‌ها را تامین کند، نیازهایی که در سال 2005 رایج بود و شامل مواردی مانند مدل احراز هویت فرم، و یک پایگاه داده SQL Server برای ذخیره اطلاعات کاربران و پروفایل هایشان می‌شد. امروزه گزینه‌های بسیار بیشتری برای ذخیره داده‌های وب اپلیکیشن‌ها وجود دارد، و اکثر توسعه دهندگان می‌خواهند از اطلاعات شبکه‌های اجتماعی نیز برای احراز هویت و تعیین سطوح دسترسی کاربرانشان استفاده کنند. محدودیت‌های طراحی سیستم ASP.NET Membership گذر از این تحول را دشوار می‌کند:
  • الگوی پایگاه داده آن برای SQL Server طراحی شده است، و قادر به تغییرش هم نیستید. می‌توانید اطلاعات پروفایل را اضافه کنید، اما تمام داده‌ها در یک جدول دیگر ذخیره می‌شوند، که دسترسی به آنها نیز مشکل‌تر است، تنها راه دسترسی Profile Provider API خواهد بود.
  • سیستم تامین کننده (Provider System) امکان تغییر منبع داده‌ها را به شما می‌دهد، مثلا می‌توانید از بانک‌های اطلاعاتی MySQL یا Oracle استفاده کنید. اما تمام سیستم بر اساس پیش فرض هایی طراحی شده است که تنها برای بانک‌های اطلاعاتی relational درست هستند. می‌توانید تامین کننده (Provider) ای بنویسید که داده‌های سیستم عضویت را در منبعی به غیر از دیتابیس‌های relational ذخیره می‌کند؛ مثلا Windows Azure Storage Tables. اما در این صورت باید مقادیر زیادی کد بنویسید. مقادیر زیادی هم  System.NotImplementedException باید بنویسید، برای متد هایی که به دیتابیس‌های NoSQL مربوط نیستند.
  • از آنجایی که سیستم ورود/خروج سایت بر اساس مدل Forms Authentication کار می‌کند، سیستم عضویت نمی‌تواند از OWIN استفاده کند. OWIN شامل کامپوننت هایی برای احراز هویت است که شامل سرویس‌های خارجی هم می‌شود (مانند Microsoft Accounts, Facebook, Google, Twitter). همچنین امکان ورود به سیستم توسط حساب‌های کاربری سازمانی (Organizational Accounts) نیز وجود دارد مانند Active Directory و Windows Azure Active Directory. این کتابخانه از OAuth 2.0، JWT و CORS نیز پشتیبانی می‌کند.

ASP.NET Simple Membership

ASP.NET simple membership به عنوان یک سیستم عضویت، برای فریم ورک Web Pages توسعه داده شد. این سیستم با WebMatrix و Visual Studio 2010 SP1 انتشار یافت. هدف از توسعه این سیستم، آسان کردن پروسه افزودن سیستم عضویت به یک اپلیکیشن Web Pages بود.
این سیستم پروسه کلی کار را آسان‌تر کرد، اما هنوز مشکلات ASP.NET Membership را نیز داشت. محدودیت هایی نیز وجود دارند:
  • ذخیره داده‌های سیستم عضویت در بانک‌های اطلاعاتی non-relational مشکل است.
  • نمی توانید از آن در کنار OWIN استفاده کنید.
  • با فراهم کننده‌های موجود ASP.NET Membership بخوبی کار نمی‌کند. توسعه پذیر هم نیست.

ASP.NET Universal Providers   

ASP.NET Universal Providers برای ذخیره سازی اطلاعات سیستم عضویت در Windows Azure SQL Database توسعه پیدا کردند. با SQL Server Compact هم بخوبی کار می‌کنند. این تامین کننده‌ها بر اساس Entity Framework Code First ساخته شده بودند و بدین معنا بود که داده‌های سیستم عضویت را می‌توان در هر منبع داده ای که توسط EF پشتیبانی می‌شود ذخیره کرد. با انتشار این تامین کننده‌ها الگوی دیتابیس سیستم عضویت نیز بسیار سبک‌تر و بهتر شد. اما این سیستم بر پایه زیر ساخت ASP.NET Membership نوشته شده است، بنابراین محدودیت‌های پیشین مانند محدودیت‌های SqlMembershipProvider هنوز وجود دارند. به بیان دیگر، این سیستم‌ها همچنان برای بانک‌های اطلاعاتی relational طراحی شده اند، پس سفارشی سازی اطلاعات کاربران و پروفایل‌ها هنوز مشکل است. در آخر آنکه این تامین کننده‌ها هنوز از مدل احراز هویت فرم استفاده می‌کنند.


ASP.NET Identity

همانطور که داستان سیستم عضویت ASP.NET طی سالیان تغییر و رشد کرده است، تیم ASP.NET نیز آموخته‌های زیادی از بازخورد‌های مشتریان شان بدست آورده اند.
این پیش فرض که کاربران شما توسط یک نام کاربری و کلمه عبور که در اپلیکیشن خودتان هم ثبت شده است به سایت وارد خواهند شد، دیگر معتبر نیست. دنیای وب اجتماعی شده است. کاربران از طریق وب سایت‌ها و شبکه‌های اجتماعی متعددی با یکدیگر در تماس هستند، خیلی از اوقت بصورت زنده! شبکه هایی مانند Facebook و Twitter.
همانطور که توسعه نرم افزار‌های تحت وب رشد کرده است، الگو‌ها و مدل‌های پیاده سازی نیز تغییر و رشد کرده اند. امکان Unit Testing روی کد اپلیکیشن‌ها، یکی از مهم‌ترین دلواپسی‌های توسعه دهندگان شده است. در سال 2008 تیم ASP.NET فریم ورک جدیدی را بر اساس الگوی (Model-View-Controller (MVC اضافه کردند. هدف آن کمک به توسعه دهندگان، برای تولید برنامه‌های ASP.NET با قابلیت Unit Testing بهتر بود. توسعه دهندگانی که می‌خواستند کد اپلیکیشن‌های خود را Unit Test کنند، همین امکان را برای سیستم عضویت نیز می‌خواستند.

با در نظر گرفتن تغییراتی که در توسعه اپلیکیشن‌های وب بوجود آمده ASP.NET Identity با اهداف زیر متولد شد:
  • یک سیستم هویت واحد (One ASP.NET Identity system)
    • سیستم ASP.NET Identity می‌تواند در تمام فریم ورک‌های مشتق از ASP.NET استفاده شود. مانند ASP.NET MVC, Web Forms, Web Pages, Web API و SignalR 
    • از این سیستم می‌توانید در تولید اپلیکیشن‌های وب، موبایل، استور (Store) و یا اپلیکیشن‌های ترکیبی استفاده کنید. 
  • سادگی تزریق داده‌های پروفایل درباره کاربران
    • روی الگوی دیتابیس برای اطلاعات کاربران و پروفایل‌ها کنترل کامل دارید. مثلا می‌توانید به سادگی یک فیلد، برای تاریخ تولد در نظر بگیرید که کاربران هنگام ثبت نام در سایت باید آن را وارد کنند.
  • کنترل ذخیره سازی/واکشی اطلاعات 
    • بصورت پیش فرض ASP.NET Identity تمام اطلاعات کاربران را در یک دیتابیس ذخیره می‌کند. تمام مکانیزم‌های دسترسی به داده‌ها توسط EF Code First کار می‌کنند.
    • از آنجا که روی الگوی دیتابیس، کنترل کامل دارید، تغییر نام جداول و یا نوع داده فیلد‌های کلیدی و غیره ساده است.
    • استفاده از مکانیزم‌های دیگر برای مدیریت داده‌های آن ساده است، مانند SharePoint, Windows Azure Storage Table و دیتابیس‌های NoSQL.
  • تست پذیری
    • ASP.NET Identity تست پذیری اپلیکیشن وب شما را بیشتر می‌کند. می‌توانید برای تمام قسمت هایی که از ASP.NET Identity استفاده می‌کنند تست بنویسید.
  • تامین کننده نقش (Role Provider)
    • تامین کننده ای وجود دارد که به شما امکان محدود کردن سطوح دسترسی بر اساس نقوش را می‌دهد. بسادگی می‌توانید نقش‌های جدید مانند "Admin" بسازید و بخش‌های مختلف اپلیکیشن خود را محدود کنید.
  • Claims Based
    • ASP.NET Identity از امکان احراز هویت بر اساس Claims نیز پشتیبانی می‌کند. در این مدل، هویت کاربر بر اساس دسته ای از اختیارات او شناسایی می‌شود. با استفاده از این روش توسعه دهندگان برای تعریف هویت کاربران، آزادی عمل بیشتری نسبت به مدل Roles دارند. مدل نقش‌ها تنها یک مقدار منطقی (bool) است؛ یا عضو یک نقش هستید یا خیر، در حالیکه با استفاده از روش Claims می‌توانید اطلاعات بسیار ریز و دقیقی از هویت کاربر در دست داشته باشید.
  • تامین کنندگان اجتماعی
    • به راحتی می‌توانید از تامین کنندگان دیگری مانند Microsoft, Facebook, Twitter, Google و غیره استفاده کنید و اطلاعات مربوط به کاربران را در اپلیکیشن خود ذخیره کنید.
  • Windows Azure Active Directory
    • برای اطلاعات بیشتر به این لینک مراجعه کنید.
  • یکپارچگی با OWIN
    • ASP.NET Identity بر اساس OWIN توسعه پیدا کرده است، بنابراین از هر میزبانی که از OWIN پشتیبانی می‌کند می‌توانید استفاده کنید. همچنین هیچ وابستگی ای به System.Web وجود ندارد. ASP.NET Identity یک فریم ورک کامل و مستقل برای OWIN است و می‌تواند در هر اپلیکیشنی که روی OWIN میزبانی شده استفاده شود.
    • ASP.NET Identity از OWIN برای ورود/خروج کاربران در سایت استفاده می‌کند. این بدین معنا است که بجای استفاده از Forms Authentication برای تولید یک کوکی، از OWIN CookieAuthentication استفاده می‌شود.
  • پکیج NuGet
    • ASP.NET Identity در قالب یک بسته NuGet توزیع می‌شود. این بسته در قالب پروژه‌های ASP.NET MVC, Web Forms و Web API که با Visual Studio 2013 منتشر شدند گنجانده شده است.
    • توزیع این فریم ورک در قالب یک بسته NuGet این امکان را به تیم ASP.NET می‌دهد تا امکانات جدیدی توسعه دهند، باگ‌ها را برطرف کنند و نتیجه را بصورت چابک به توسعه دهندگان عرضه کنند.

شروع کار با ASP.NET Identity

ASP.NET Identity در قالب پروژه‌های ASP.NET MVC, Web Forms, Web API و SPA که بهمراه Visual Studio 2013 منتشر شده اند استفاده می‌شود. در ادامه به اختصار خواهیم دید که چگونه ASP.NET Identity کار می‌کند.
  1. یک پروژه جدید ASP.NET MVC با تنظیمات Individual User Accounts بسازید.

  2. پروژه ایجاد شده شامل سه بسته می‌شود که مربوط به ASP.NET Identity هستند: 

  • Microsoft.AspNet.Identity.EntityFramework این بسته شامل پیاده سازی ASP.NET Identity با Entity Framework می‌شود، که تمام داده‌های مربوطه را در یک دیتابیس SQL Server ذخیره می‌کند.
  • Microsoft.AspNet.Identity.Core این بسته محتوی تمام interface‌‌های ASP.NET Identity است. با استفاده از این بسته می‌توانید پیاده سازی دیگری از ASP.NET Identity بسازید که منبع داده متفاوتی را هدف قرار می‌دهد. مثلا Windows Azure Storage Table و دیتابیس‌های NoSQL.
  • Microsoft.AspNet.Identity.OWIN این بسته امکان استفاده از احراز هویت OWIN را در اپلیکیشن‌های ASP.NET فراهم می‌کند. هنگام تولید کوکی‌ها از OWIN Cookie Authentication استفاده خواهد شد.
اپلیکیشن را اجرا کرده و روی لینک Register کلیک کنید تا یک حساب کاربری جدید ایجاد کنید.

هنگامیکه بر روی دکمه‌ی Register کلیک شود، کنترلر Account، اکشن متد Register را فراخوانی می‌کند تا حساب کاربری جدیدی با استفاده از ASP.NET Identity API ساخته شود.

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
    if (ModelState.IsValid)
    {
        var user = new ApplicationUser() { UserName = model.UserName };
        var result = await UserManager.CreateAsync(user, model.Password);
        if (result.Succeeded)
        {
            await SignInAsync(user, isPersistent: false);
            return RedirectToAction("Index", "Home");
        }
        else
        {
            AddErrors(result);
        }
    }

    // If we got this far, something failed, redisplay form
    return View(model);
}

اگر حساب کاربری با موفقیت ایجاد شود، کاربر توسط فراخوانی متد SignInAsync به سایت وارد می‌شود.

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
    if (ModelState.IsValid)
    {
        var user = new ApplicationUser() { UserName = model.UserName };
        var result = await UserManager.CreateAsync(user, model.Password);
        if (result.Succeeded)
        {
            await SignInAsync(user, isPersistent: false);
            return RedirectToAction("Index", "Home");
        }
        else
        {
            AddErrors(result);
        }
    }

    // If we got this far, something failed, redisplay form
    return View(model);
}
private async Task SignInAsync(ApplicationUser user, bool isPersistent)
{
    AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);

    var identity = await UserManager.CreateIdentityAsync(
       user, DefaultAuthenticationTypes.ApplicationCookie);

    AuthenticationManager.SignIn(
       new AuthenticationProperties() { 
          IsPersistent = isPersistent 
       }, identity);
}

از آنجا که ASP.NET Identity و OWIN Cookie Authentication هر دو Claims-based هستند، فریم ورک، انتظار آبجکتی از نوع ClaimsIdentity را خواهد داشت. این آبجکت تمامی اطلاعات لازم برای تشخیص هویت کاربر را در بر دارد. مثلا اینکه کاربر مورد نظر به چه نقش هایی تعلق دارد؟ و اطلاعاتی از این قبیل. در این مرحله می‌توانید Claim‌‌های بیشتری را به کاربر بیافزایید.

کلیک کردن روی لینک Log off در سایت، اکشن متد LogOff در کنترلر Account را اجرا می‌کند.

// POST: /Account/LogOff
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult LogOff()
{
    AuthenticationManager.SignOut();
    return RedirectToAction("Index", "Home");
}

همانطور که مشاهده می‌کنید برای ورود/خروج کاربران از AuthenticationManager استفاده می‌شود که متعلق به OWIN است. متد SignOut همتای متد FormsAuthentication.SignOut است.


کامپوننت‌های ASP.NET Identity

تصویر زیر اجزای تشکیل دهنده ASP.NET Identity را نمایش می‌دهد. بسته هایی که با رنگ سبز نشان داده شده اند سیستم کلی ASP.NET Identity را می‌سازند. مابقی بسته‌ها وابستگی هایی هستند که برای استفاده از ASP.NET Identity در اپلیکیشن‌های ASP.NET لازم اند.

دو پکیج دیگر نیز وجود دارند که به آنها اشاره نشد:

  • Microsoft.Security.Owin.Cookies این بسته امکان استفاده از مدل احراز هویت مبتنی بر کوکی (Cookie-based Authentication) را فراهم می‌کند. مدلی مانند سیستم ASP.NET Forms Authentication.
  • EntityFramework که نیازی به معرفی ندارد.


مهاجرت از Membership به ASP.NET Identity

تیم ASP.NET و مایکروسافت هنوز راهنمایی رسمی، برای این مقوله ارائه نکرده اند. گرچه پست‌های وبلاگ‌ها و منابع مختلفی وجود دارند که از جنبه‌های مختلفی به این مقوله پرداخته اند. امیدواریم تا در آینده نزدیک مایکروسافت راهنمایی‌های لازم را منتشر کند، ممکن است ابزار و افزونه هایی نیز توسعه پیدا کنند. اما در یک نگاه کلی می‌توان گفت مهاجرت بین این دو فریم ورک زیاد ساده نیست. تفاوت‌های فنی و ساختاری زیادی وجود دارند، مثلا الگوی دیتابیس‌ها برای ذخیره اطلاعات کاربران، مبتنی بودن بر فریم ورک OWIN و غیره. اگر قصد اجرای پروژه جدیدی را دارید پیشنهاد می‌کنم از فریم ورک جدید مایکروسافت ASP.NET Identity استفاده کنید.


قدم‌های بعدی

در این مقاله خواهید دید چگونه اطلاعات پروفایل را اضافه کنید و چطور از ASP.NET Identity برای احراز هویت کاربران توسط Facebook و Google استفاده کنید.
پروژه نمونه ASP.NET Identity می‌تواند مفید باشد. در این پروژه نحوه کارکردن با کاربران و نقش‌ها و همچنین نیازهای مدیریتی رایج نمایش داده شده. 
مطالب دوره‌ها
ایندکس‌ها در RavenDB
RavenDB یک Document database است و در این نوع بانک‌های اطلاعاتی، اسکیما و ساختار مشخصی وجود ندارد. شاید اینطور به نظر برسد، زمانیکه با دات نت کلاینت RavenDB کار می‌کنیم، یک سری کلاس مشخص دات نتی داشته و این‌ها ساختار اصلی کار را مشخص می‌کنند. اما در عمل RavenDB چیزی از این کلاس‌ها و خواص نمی‌داند و این کلاس‌های دات نتی صرفا کمکی هستند جهت سهولت اعمال Serialization و Deserialization اطلاعات. زمانیکه اطلاعاتی را در RavenDB ذخیره می‌کنیم، هیچ نوع قیدی در مورد ساختار نوع سندی که در حال ذخیره است، اعمال نمی‌شود.
خوب؛ اکنون این سؤال مطرح می‌شود که RavenDB چگونه اطلاعاتی را در این اسناد بدون اسکیما جستجو می‌کند؟ اینجا است که مفهوم و کاربرد ایندکس‌ها مطرح می‌شوند. ما در قسمت قبل که کوئری نویسی مقدماتی را بررسی کردیم، عملا ایندکس خاصی را به صورت دستی جهت انجام جستجو‌ها ایجاد نکردیم؛ از این جهت که خود RavenDB به کمک امکانات dynamic indexing آن، پیشتر اینکار را انجام داده است. برای نمونه به سطر ارسال کوئری به سرور، که در قسمت قبل ارائه شد، دقت کنید. در اینجا ارسال کوئری به indexes/dynamic کاملا مشخص است:
Request #   2: GET     - 3,818 ms - <system>   - 200 - /indexes/dynamic/Questions?&query=Title%3ARaven*&pageSize=128

Dynamic Indexes یا ایندکس‌های پویا

ایندکس‌های پویا زمانی ایجاد خواهند شد که ایندکس صریحی توسط برنامه نویس تعریف نگردد. برای مثال زمانیکه یک کوئری LINQ را صادر می‌کنیم، RavenDB بر این اساس و برای مثال فیلدهای قسمت Where آن، ایندکس پویایی را تولید خواهد کرد. ایجاد ایندکس‌ها در RavenDB از اصل عاقبت یک دست شدن پیروی می‌کنند. یعنی مدتی طول خواهد کشید تا کل اطلاعات بر اساس ایندکس جدیدی که در حال تهیه است، ایندکس شوند. بنابراین تولید ایندکس‌های پویا در زمان اولین بار اجرای کوئری، کوئری اول را اندکی کند جلوه خواهند داد؛ اما کوئری‌های بعدی که بر روی یک ایندکس آماده اجرا می‌شوند، بسیار سریع خواهند بود.


Static indexes یا ایندکس‌های ایستا

ایندکس‌های پویا به دلیل وقفه ابتدایی که برای تولید آن‌ها وجود خواهد داشت، شاید آنچنان مطلوب به نظر نرسند. اینجا است که مفهوم ایندکس‌های ایستا مطرح می‌شوند. در این حالت ما به RavenDB خواهیم گفت که چه چیزی را ایندکس کند. برای تولید ایندکس‌های ایستا، از مفاهیم Map/Reduce که در پیشنیازهای دوره جاری در مورد آن بحث شد، استفاده می‌گردد. خوشبختانه تهیه Map/Reduceها در RavenDB پیچیده نبوده و کل عملیات آن توسط کوئری‌های LINQ قابل پیاده سازی است.
تهیه ایندکس‌های پویا نیز در تردهای پس‌زمینه انجام می‌شوند. از آنجائیکه RavenDB برای اعمال Read، بهینه سازی شده است، با ارسال یک کوئری به آن، این بانک اطلاعاتی، کلیه اطلاعات آماده را در اختیار شما قرار خواهد داد؛ صرفنظر از اینکه کار تهیه ایندکس تمام شده است یا خیر.


چگونه یک ایندکس ایستا را ایجاد کنیم؟

اگر به کنسول مدیریتی سیلورلایت RavenDB مراجعه کنیم، حاصل کوئری‌های LINQ قسمت قبل را در برگه‌ی ایندکس‌های آن می‌توان مشاهده کرد:


در اینجا بر روی دکمه Edit کلیک نمائید، تا با نحوه تهیه این ایندکس پویا آشنا شویم:


این ایندکس، یک نام داشته به همراه قسمت Map از پروسه Map/Reduce که توسط یک کوئری LINQ تهیه شده است. کاری که در اینجا انجام شده، ایندکس کردن کلیه سؤالات، بر اساس خاصیت عنوان آن‌ها است.
اکنون اگر بخواهیم همین کار را با کدنویسی انجام دهیم، به صورت زیر می‌توان عمل کرد:
using System;
using System.Linq;
using Raven.Client.Document;
using RavenDBSample01.Models;
using Raven.Client;
using Raven.Client.Linq;
using Raven.Client.Indexes;

namespace RavenDBSample01
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var store = new DocumentStore
            {
                Url = "http://localhost:8080"
            }.Initialize())
            {
                store.DatabaseCommands.PutIndex(
                name: "Questions/ByTitle",
                indexDef: new IndexDefinitionBuilder<Question>
                {
                    Map = questions => questions.Select(question => new { Title = question.Title } )
                });
            }
        }
    }
}
کار با شیء DatabaseCommands یک DocumentStore شروع می‌شود. سپس توسط متد PutIndex آن می‌توان یک ایندکس جدید را تعریف کرد. این متد نیاز به نام ایندکس ایجاد شده و همچنین حداقل، متد Map آن‌را دارد. برای این منظور از شیء IndexDefinitionBuilder برای تعریف نحوه جمع آوری اطلاعات ایندکس کمک خواهیم گرفت. در اینجا خاصیت Map آن‌را باید توسط یک کوئری LINQ که فیلدهای مدنظر را بازگشت می‌دهد، مقدار دهی کنیم.
برنامه را اجرا کرده و سپس به کنسول مدیریتی تحت وب RavenDB، قسمت ایندکس‌های آن مراجعه کنید. در اینجا می‌توان ایندکس جدید ایجاد شده را مشاهده کرد:


هرچند همین اعمال را در کنسول مدیریتی نیز می‌توان انجام داد، اما مزیت آن در سمت کدها، دسترسی به intellisense و نوشتن کوئری‌های strongly typed است.

روش استفاده از store.DatabaseCommands.PutIndex اولین روش تولید Index در RavenDB با کدنویسی است. روش دوم، بر اساس ارث بری از کلاس AbstractIndexCreationTask شروع می‌شود و مناسب است برای حالتیکه نمی‌خواهید کدهای تولید ایندکس، با کدهای سایر قسمت‌های برنامه مخلوط شوند:
    public class QuestionsByTitle : AbstractIndexCreationTask<Question>
    {
        public QuestionsByTitle()
        {
            Map = questions => questions.Select(question => new { Title = question.Title });
        }
    }
در اینجا با ایجاد یک کلاس جدید و ارث بری از کلاس AbstractIndexCreationTask کار شروع می‌شود. سپس در سازنده این کلاس، خاصیت Map را مقدار دهی می‌کنیم. مقدار آن نیز یک کوئری LINQ است که کار Select فیلدهای شرکت دهنده در کار تهیه ایندکس را انجام می‌دهد.
اکنون برای معرفی آن به برنامه باید از متد IndexCreation.CreateIndexes استفاده کرد. این متد، نیاز به دریافت اسمبلی محل تعریف کلاس‌های تولید ایندکس را دارد. به این ترتیب تمام کلاس‌های مشتق شده از AbstractIndexCreationTask را یافته و ایندکس‌های متناظری را تولید می‌کند.
            using (var store = new DocumentStore
            {
                Url = "http://localhost:8080"
            }.Initialize())
            {
                IndexCreation.CreateIndexes(typeof(QuestionsByTitle).Assembly, store);
            }
این روش، قابلیت نگهداری و نظم بهتری دارد.


استفاده از ایندکس‌های ایستای ایجاد شده

تا اینجا موفق شدیم ایندکس‌های ایستای خود را با کد نویسی ایجاد کنیم. در ادامه قصد داریم از این ایندکس‌ها در کوئری‌های خود استفاده نمائیم.
            using (var store = new DocumentStore
            {
                Url = "http://localhost:8080"
            }.Initialize())
            {
                using (var session = store.OpenSession())
                {
                    var questions = session.Query<Question>(indexName: "QuestionsByTitle")
                                           .Where(x => x.Title.StartsWith("Raven")).Take(128);
                    foreach (var question in questions)
                    {
                        Console.WriteLine(question.Title);
                    }
                }
            }
استفاده از ایندکس تعریف شده نیز بسیار ساده می‌باشد. تنها کافی است نام آن‌را به متد Query ارسال نمائیم. اینبار اگر به خروجی کنسول سرور RavenDB دقت کنیم، از ایندکس indexes/QuestionsByTitle بجای ایندکس‌های پویا استفاده کرده است:
Request # 147: GET     -    58 ms - <system>   - 200 - /indexes/QuestionsByTitle?&query=Title%3ARaven*&pageSize=128
        Query: Title:Raven*
        Time: 7 ms
        Index: QuestionsByTitle
        Results: 2 returned out of 2 total.
روش مشخص سازی نام ایندکس با استفاده از رشته‌ها، با هر دو روش store.DatabaseCommands.PutIndex و استفاده از AbstractIndexCreationTask سازگار است. اما اگر ایندکس‌های خود را با ارث بری از AbstractIndexCreationTask ایجاد کرده‌ایم، می‌توان نام کلاس مشتق شده را به صورت یک آرگومان جنریک دوم به متد Query به شکل زیر ارسال کرد تا از مزایای تعریف strongly typed آن نیز بهره‌مند شویم:
                    var questions = session.Query<Question, QuestionsByTitle>()
                                           .Where(x => x.Title.StartsWith("Raven")).Take(128);

ایجاد ایندکس‌های پیشرفته با پیاده سازی Map/Reduce

حالتی را در نظر بگیرید که در آن قصد داریم تعداد عنوان‌های سؤالات مانند هم را بیابیم (یا تعداد مطالب گروه‌های مختلف یک وبلاگ را محاسبه کنیم). برای انجام اینکار با سرعت بسیار بالا، می‌توانیم از ایندکس‌هایی با قابلیت محاسباتی در RavenDB استفاده کنیم. کار با ارث بری از کلاس AbstractIndexCreationTask شروع می‌شود. آرگومان جنریک اول آن، نام کلاسی است که در تهیه ایندکس شرکت خواهد داشت و آرگومان دوم (و اختیاری) ذکر شده، نتیجه عملیات Reduce است:
    public class QuestionsCountByTitleReduceResult
    {
        public string Title { set; get; }
        public int Count { set; get; }
    }

    public class QuestionsCountByTitle : AbstractIndexCreationTask<Question, QuestionsCountByTitleReduceResult>
    {
        public QuestionsCountByTitle()
        {
            Map = questions => questions.Select(question =>
                                                    new
                                                    {
                                                        Title = question.Title,
                                                        Count = 1
                                                    });
            Reduce = results => results.GroupBy(x => x.Title)
                                       .Select(g =>
                                                   new
                                                   {
                                                       Title = g.Key,
                                                       Count = g.Sum(x => x.Count)
                                                   });
        }
    }
در اینجا یک ایندکس پیشرفته را تعریف کرده‌ایم که در آن در قسمت Map، کار ایندکس کردن تک تک عنوان‌ها انجام خواهد شد. به همین جهت مقدار Count در این حالت، عدد یک است. در قسمت Reduce، بر روی نتیجه قسمت Map کوئری LINQ دیگری نوشته شده و تعداد عنوان‌های همانند، با گروه بندی اطلاعات، شمارش گردیده است.
اکنون برای استفاده از این ایندکس، ابتدا توسط متد IndexCreation.CreateIndexes، کار معرفی آن به RavenDB صورت گرفته و سپس متد Query سشن باز شده، دو آرگومان جنریگ را خواهد پذیرفت. اولین آرگومان، همان نتیجه Map/Reduce است و دومین آرگومان نام کلاس ایندکس جدید تعریف شده می‌باشد:
            using (var store = new DocumentStore
            {
                Url = "http://localhost:8080"
            }.Initialize())
            {
                IndexCreation.CreateIndexes(typeof(QuestionsCountByTitle).Assembly, store);

                using (var session = store.OpenSession())
                {
                    var result = session.Query<QuestionsCountByTitleReduceResult, QuestionsCountByTitle>()
                                         .FirstOrDefault(x => x.Title == "Raven") ?? new QuestionsCountByTitleReduceResult();
                    Console.WriteLine(result.Count);
                }
            }
در کوئری فوق چون عملیات بر روی نتیجه نهایی باید صورت گیرد از FirstOrDefault استفاده شده است. این کوئری در حقیقت بر روی قسمت Reduce پیشتر محاسبه شده، اجرا می‌شود.
نظرات مطالب
EF Code First #7
سلام وحید جان ممنون از این همه لطف

من یک پروژه را با CodeFirst شروع کردم اما یه جایی اشتباه کردم فکر کنم اشتباهم توی یکی از Mapping‌ها باشه. اگه لطف کنید ببینید مشکل چیه.بدون استفاده از Mapping مشکلی نیست و دیتا بیس با روابطی که میخوام ایجاد میشه اما وقتی از Mapping استفاده میکنم با این خطا مواجه میشم:
{"Sequence contains more than one matching element"} 
چندتا کلاس‌ها به شکل زیر هست:
public class Driver
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string NationalCode { get; set; }
        public string CellPhone { get; set; }
        public string LicenseNumber { get; set; }
        public bool IsDriverAssistance { get; set; }

        [InverseProperty("Driver")]
        public virtual ICollection<Transference> Transferences { get; set; }
        [InverseProperty("DriverAssistance")]
        public virtual ICollection<Transference> TransferencesForAssistance { get; set; }
        [InverseProperty("Driver")]
        public virtual ICollection<Tanker> Tankers { get; set; }
        [InverseProperty("DriverAssistance")]
        public virtual ICollection<Tanker> TankersForAssistance { get; set; }

    }

 public class Transference
    {
        public string Id { get; set; }
        public DateTime Date { get; set; }
        public Int16 Lytrazh { get; set; }
        public bool IsEMS { get; set; }
        public DateTime LoadingDate { get; set; }
        public DateTime DeliveryDate { get; set; }
        [InverseProperty("Transferences")]
        public virtual Driver Driver { get; set; }
        [InverseProperty("TransferencesForAssistance")]
        public virtual Driver DriverAssistance { get; set; }
        public virtual TypeOfTanker TypesOfTanker { get; set; }
        public virtual Tanker Tanker { get; set; }
        public virtual Consumer Consumer { get; set; }

    }
فکر کنم مشکل از این کلاس زیر باشه:
 public class TransferenceConfig : EntityTypeConfiguration<Transference>
    {
        public TransferenceConfig()
        {
            // one-to-many
            this.HasRequired(x => x.Consumer)
                .WithMany(x => x.Transferences);

                // one-to-many
            this.HasRequired(x => x.TypesOfTanker)
                .WithMany(x => x.Transferences);

            // one-to-many
            this.HasRequired(x => x.Tanker)
                .WithMany(x => x.Transferences);

            // one-to-many
            this.HasRequired(x => x.Driver)
                .WithMany(x => x.Transferences);

            // one-to-many
            this.HasRequired(x => x.DriverAssistance)
                .WithMany(x => x.Transferences);

        }
    }
مطالب
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 2#
در ادامه مطلب پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 1# به تشریح مابقی کلاس‌های برنامه می‌پردازیم.

با توجه به تجزیه و تحلیل انجام شده تمامی اشیا از کلاس پایه به نام Shape ارث بری دارند حال به توضیح کد‌های این کلاس می‌پردازیم. (به دلیل اینکه توضیحات این کلاس در دو پست نوشته خواهد شد برای این کلاس‌ها از partial class استفاده شده است)
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Net;
 
namespace PWS.ObjectOrientedPaint.Models
{
    /// <summary>
    /// Shape (Base Class)
    /// </summary>
    public abstract partial class Shape
    {
        #region Fields (1)

        private Brush _backgroundBrush;

        #endregion Fields

        #region Properties (16)

        /// <summary>
        /// Gets or sets the brush.
        /// </summary>
        /// <value>
        /// The brush.
        /// </value>
        public Brush BackgroundBrush
        {
            get { return _backgroundBrush ?? (_backgroundBrush = new SolidBrush(BackgroundColor)); }
            private set
            {
                _backgroundBrush = value ?? new SolidBrush(BackgroundColor);
            }
        }

        /// <summary>
        /// Gets or sets the color of the background.
        /// </summary>
        /// <value>
        /// The color of the background.
        /// </value>
        public Color BackgroundColor { get; set; }

        /// <summary>
        /// Gets or sets the end point.
        /// </summary>
        /// <value>
        /// The end point.
        /// </value>
        public PointF EndPoint { get; set; }

        /// <summary>
        /// Gets or sets the color of the fore.
        /// </summary>
        /// <value>
        /// The color of the fore.
        /// </value>
        public Color ForeColor { get; set; }

        /// <summary>
        /// Gets or sets the height.
        /// </summary>
        /// <value>
        /// The height.
        /// </value>
        public float Height
        {
            get
            {
                return Math.Abs(StartPoint.Y - EndPoint.Y);
            }
            set
            {
                if (value > 0)
                    EndPoint = new PointF(EndPoint.X, StartPoint.Y + value);
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether this instance is fill.
        /// </summary>
        /// <value>
        ///   <c>true</c> if this instance is fill; otherwise, <c>false</c>.
        /// </value>
        public bool IsFill { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether this instance is selected.
        /// </summary>
        /// <value>
        /// <c>true</c> if this instance is selected; otherwise, <c>false</c>.
        /// </value>
        public bool IsSelected { get; set; }

        /// <summary>
        /// Gets or sets my pen.
        /// </summary>
        /// <value>
        /// My pen.
        /// </value>
        public Pen Pen
        {
            get
            {
                return new Pen(ForeColor, Thickness);
            }
        }

        /// <summary>
        /// Gets or sets the type of the shape.
        /// </summary>
        /// <value>
        /// The type of the shape.
        /// </value>
        public ShapeType ShapeType { get; protected set; }

        /// <summary>
        /// Gets the size.
        /// </summary>
        /// <value>
        /// The size.
        /// </value>
        public SizeF Size
        {
            get
            {
                return new SizeF(Width, Height);
            }
        }

        /// <summary>
        /// Gets or sets the start point.
        /// </summary>
        /// <value>
        /// The start point.
        /// </value>
        public PointF StartPoint { get; set; }

        /// <summary>
        /// Gets or sets the thickness.
        /// </summary>
        /// <value>
        /// The thickness.
        /// </value>
        public byte Thickness { get; set; }

        /// <summary>
        /// Gets or sets the width.
        /// </summary>
        /// <value>
        /// The width.
        /// </value>
        public float Width
        {
            get
            {
                return Math.Abs(StartPoint.X - EndPoint.X);
            }
            set
            {
                if (value > 0)
                    EndPoint = new PointF(StartPoint.X + value, EndPoint.Y);
            }
        }

        /// <summary>
        /// Gets or sets the X.
        /// </summary>
        /// <value>
        /// The X.
        /// </value>
        public float X
        {
            get
            {
                return StartPoint.X;
            }
            set
            {
                if (value > 0)
                    StartPoint = new PointF(value, StartPoint.Y);
            }
        }

        /// <summary>
        /// Gets or sets the Y.
        /// </summary>
        /// <value>
        /// The Y.
        /// </value>
        public float Y
        {
            get
            {
                return StartPoint.Y;
            }
            set
            {
                if (value > 0)
                    StartPoint = new PointF(StartPoint.X, value);
            }
        }

        /// <summary>
        /// Gets or sets the index of the Z.
        /// </summary>
        /// <value>
        /// The index of the Z.
        /// </value>
        public int Zindex { get; set; }

        #endregion Properties
        }
}


ابتدا به تشریح خصوصیات کلاس می‌پردازیم:
خصوصیات:
  • BackgroundColor: در صورتی که شی مورد نظر به صورت توپررسم شود، این خاصیت رنگ پس زمینه شی را مشخص می‌کند.
  • BackgroundBrush: خاصیتی است که با توجه به خاصیت BackgroundColor یک الگوی پر کردن  زمینه شی می‌سازد.
  • StartPoint: نقطه شروع شی را در خود نگهداری می‌کند.
  • EndPoint: نقطه انتهای شی را در خود نگهداری می‌کند. (قبلا گفته شد که هر شی را در صورتی که در یک مستطیل فرض کنیم یک نقطه شروع و یک نقطه پایان دارد)
  • ForeColor: رنگ قلم ترسیم شی مورد نظر را تعیین می‌کند.
  • Height: ارتفاع شی مورد نظر را تعیین می‌کند ( این خصوصیت اختلاف عمودی StartPoint.Y و EndPoint.Y را محاسبه می‌کند و در زمان مقدار دهی EndPoint جدیدی ایجاد می‌کند).
  • Width: عرض شی مورد نظر را تعیین می‌کند ( این خصوصیت اختلاف افقیStartPoint.X و EndPoint.X را محاسبه می‌کند و در زمان مقدار دهی EndPoint جدیدی ایجاد می‌کند).
  • IsFill: این خصوصیت تعیین کننده توپر و یا توخالی بودن شی است.
  • IsSelected: این خاصیت تعیین می‌کند که آیا شی انتخاب شده است یا خیر (در زمان انتخاب شی چهار مربع کوچک روی شی رسم می‌شود).
  • Pen: قلم خط ترسیم شی را مشخص می‌کند. (قلم با ضخامت دلخواه)
  • ShapeType: این خصوصیت نوع شی را مشخص می‌کند (این خاصیت بیشتر برای زمان پیش نمایش ترسیم شی در زمان اجراست البته به نظر خودم اضافه هست اما راه بهتری به ذهنم نرسید)
  • Size: با استفاده از خصوصیات Height و Width ایجاد شده و تعیین کننده Size شی است.
  • Thickness: ضخامت خط ترسیمی شی را مشخص می‌کند، این خاصیت در خصوصیت Pen استفاده شده است.
  • X: مقدار افقی نقطه شروع شی را تعیین می‌کند در واقع StartPoint.X را برمی‌گرداند (این خاصیت اضافی بوده و جهت راحتی کار استفاده شده می‌توان آن را ننوشت).
  • Y: مقدار عمودی نقطه شروع شی را تعیین می‌کند در واقع StartPoint.Y را برمی‌گرداند (این خاصیت اضافی بوده و جهت راحتی کار استفاده شده می‌توان آن را ننوشت).
  • Zindex: در زمان ترسیم اشیا ممکن است اشیا روی هم ترسیم شوند، در واقع Zindex تعیین کننده عمق شی روی بوم گرافیکی است.

در پست بعدی به توضیح متدهای این کلاس می‌پردازیم.
نظرات مطالب
ASP.NET MVC #6
فکر کنم منظورتون این فریم ورک بود lessframework.com
نظرات مطالب
EF Code First #7
به ازای هر FK یک Collection مجزا باید داشته باشید و همچنین از Fluent API هم باید استفاده کنید:
public class User
{
    public int UserId { get; set;} 
    public string Name { get; set; }

    public virtual ICollection<Comment> HomeCommentes { get; set; }
    public virtual ICollection<Comment> AwayCommentes { get; set; }
}

public class Comment
{
    public int CommentId { get; set; }

    public int HomeUserId { get; set; }
    public int GuestUserId { get; set; }

    public virtual User HomeUser { get; set; }
    public virtual User GuestUser { get; set; }
}


public class Context : DbContext
{
    ...

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Comment>()
                    .HasRequired(m => m.HomeUser)
                    .WithMany(t => t.HomeCommentes)
                    .HasForeignKey(m => m.HomeUserId)
                    .WillCascadeOnDelete(false);

        modelBuilder.Entity<Comment>()
                    .HasRequired(m => m.GuestUser)
                    .WithMany(t => t.AwayCommentes)
                    .HasForeignKey(m => m.GuestUserId)
                    .WillCascadeOnDelete(false);
    }
}