مطالب
Implementing second level caching in EF code first
هدف اصلی از انواع و اقسام مباحث caching اطلاعات، فراهم آوردن روش‌هایی جهت میسر ساختن دسترسی سریعتر به داده‌هایی است که به صورت متناوب در برنامه مورد استفاده قرار می‌گیرند، بجای مراجعه مستقیم به بانک اطلاعاتی و خواندن اطلاعات از دیسک سخت.

عموما در ORMها دو سطح کش می‌تواند وجود داشته باشد:
الف) سطح اول کش
که نمونه بارز آن در EF Code first استفاده از متد context.Entity.Find است. در بار اول فراخوانی این متد، مراجعه‌ای به بانک اطلاعاتی صورت گرفته تا بر اساس primary key ذکر شده در آرگومان آن، رکورد متناظری بازگشت داده شود. در بار دوم فراخوانی متد Find، دیگر مراجعه‌ای به بانک اطلاعاتی صورت نخواهد گرفت و اطلاعات از سطح اول کش (یا همان Context جاری) خوانده می‌شود.
بنابراین سطح اول کش در طول عمر یک تراکنش معنا پیدا می‌کند و به صورت خودکار توسط EF مدیریت می‌شود.

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


تلاش‌های صورت گرفته

تا کنون دو پیاده سازی نسبتا خوب از سطح دوم کش در EF صورت گرفته:

Entity Framework Code First Caching
Caching the results of LINQ queries

مورد اول برای ایده گرفتن خوب است. بحث اصلی پیاده سازی سطح دوم کش، یافتن کلیدی است که معادل کوئری LINQ در حال فراخوانی است. سطح دوم کش را به صورت یک Dictionary تصور کنید. هر آیتم آن تشکیل شده است از یک کلید و یک مقدار. از کلید برای یافتن مقدار متناظر استفاده می‌شود.
اکنون مشکل چیست؟ در یک برنامه ممکن است صدها کوئری لینک وجود داشته باشد. چطور باید به ازای هر کوئری LINQ یک کلید منحصربفرد تولید کرد؟
در مطلب «Entity Framework Code First Caching» از متد ToString استفاده شده است. اگر این متد، بر روی یک عبارت LINQ در EF Code first فراخوانی شود، معادل SQL آن نمایش داده می‌شود. بنابراین یک قدم به تولید کلید منحصربفرد متناظر با یک کوئری نزدیک شده‌ایم. اما ... مشکل اینجا است که متد ToString پارامترها را لحاظ نمی‌کند. بنابراین این روش اصلا قابل استفاده نیست. چون کاربر به ازای تمام پارامترهای ارسالی، همواره یک نتیجه را دریافت خواهد کرد.
در مقاله «Caching the results of LINQ queries» این مشکل برطرف شده است. با parse کامل expression tree یک عبارت LINQ کلید منحصربفرد معادل آن یافت می‌شود. سپس بر این اساس می‌توان نتیجه کوئری را به نحو صحیحی کش کرد. در این روش پارامترها هم لحاظ می‌شوند و مشکل مقاله قبلی را ندارد.
اما این مقاله دوم یک مشکل مهم را به همراه دارد: روشی را برای حذف آیتم‌ها از کش ارائه نمی‌دهد. فرض کنید مقالات سایت را در سطح دوم کش قرار داده‌اید. اکنون یک مقاله جدید در سایت ثبت شده است. اصطلاحا برای invalidating کش در این روش، راهکاری پیشنهاد نشده است.


پیاده سازی بهتری از سطح دوم کش در EF Code fist

می‌توان از همان روش یافتن کلید منحصربفرد معادل با یک کوئری LINQ، که در مقاله دوم فوق، یاد شد، کار را شروع کرد و سپس آن‌را به مرحله‌ای رساند که مباحث حذف کش نیز به صورت خودکار مدیریت شود. پیاده سازی آن را برای برنامه‌های وب در ذیل ملاحظه می‌کنید:

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Data.Objects;
using System.Diagnostics;
using System.Linq;
using System.Web;
using System.Web.Caching;

namespace EfSecondLevelCaching.Core
{
    public static class EfHttpRuntimeCacheProvider
    {
        #region Methods (6)

        // Public Methods (2) 

        public static IList<TEntity> ToCacheableList<TEntity>(
                            this IQueryable<TEntity> query,
                            int durationMinutes = 15,
                            CacheItemPriority priority = CacheItemPriority.Normal)
        {
            return query.Cacheable(x => x.ToList(), durationMinutes, priority);
        }

        /// <summary>
        /// Returns the result of the query; if possible from the cache, otherwise
        /// the query is materialized and the result cached before being returned.
        /// The cache entry has a one minute sliding expiration with normal priority.
        /// </summary>
        public static TResult Cacheable<TEntity, TResult>(
                            this IQueryable<TEntity> query,
                            Func<IQueryable<TEntity>, TResult> materializer,
                            int durationMinutes = 15,
                            CacheItemPriority priority = CacheItemPriority.Normal)
        {
            // Gets a cache key for a query.
            var queryCacheKey = query.GetCacheKey();

            // The name of the cache key used to clear the cache. All cached items depend on this key.
            var rootCacheKey = typeof(TEntity).FullName;

            // Try to get the query result from the cache.
            printAllCachedKeys();
            var result = HttpRuntime.Cache.Get(queryCacheKey);
            if (result != null)
            {
                debugWriteLine("Fetching object '{0}__{1}' from the cache.", rootCacheKey, queryCacheKey);
                return (TResult)result;
            }

            // Materialize the query.
            result = materializer(query);

            // Adding new data.
            debugWriteLine("Adding new data: queryKey={0}, dependencyKey={1}", queryCacheKey, rootCacheKey);
            storeRootCacheKey(rootCacheKey);
            HttpRuntime.Cache.Insert(
                    key: queryCacheKey,
                    value: result,
                    dependencies: new CacheDependency(null, new[] { rootCacheKey }),
                    absoluteExpiration: DateTime.Now.AddMinutes(durationMinutes),
                    slidingExpiration: Cache.NoSlidingExpiration,
                    priority: priority,
                    onRemoveCallback: null);

            return (TResult)result;
        }

        /// <summary>
        /// Call this method in `public override int SaveChanges()` of your DbContext class 
        /// to Invalidate Second Level Cache automatically.
        /// </summary>        
        public static void InvalidateSecondLevelCache(this DbContext ctx)
        {
            var changedEntityNames = ctx.ChangeTracker
                                      .Entries()
                                      .Where(x => x.State == EntityState.Added ||
                                                  x.State == EntityState.Modified ||
                                                  x.State == EntityState.Deleted)
                                      .Select(x => ObjectContext.GetObjectType(x.Entity.GetType()).FullName)
                                      .Distinct()
                                      .ToList();

            if (!changedEntityNames.Any()) return;

            printAllCachedKeys();
            foreach (var item in changedEntityNames)
            {
                item.removeEntityCache();
            }
            printAllCachedKeys();
        }
        // Private Methods (4) 

        private static void debugWriteLine(string format, params object[] args)
        {
            if (!Debugger.IsAttached) return;
            Debug.WriteLine(format, args);
        }

        private static void printAllCachedKeys()
        {
            if (!Debugger.IsAttached) return;
            debugWriteLine("Available cached keys list:");
            int count = 0;
            var enumerator = HttpRuntime.Cache.GetEnumerator();
            while (enumerator.MoveNext())
            {
                if (enumerator.Key.ToString().StartsWith("__")) continue; // such as __System.Web.WebPages.Deployment
                debugWriteLine("queryKey: {0}", enumerator.Key.ToString());
                count++;
            }
            debugWriteLine("count: {0}", count);
        }

        private static void removeEntityCache(this string rootCacheKey)
        {
            if (string.IsNullOrWhiteSpace(rootCacheKey)) return;
            debugWriteLine("Removing items with dependencyKey={0}", rootCacheKey);
            // Removes all cached items depend on this key.
            HttpRuntime.Cache.Remove(rootCacheKey);
        }

        private static void storeRootCacheKey(string rootCacheKey)
        {
            // The cacheKeys of a cacheDependency that are not already in cache ARE NOT inserted into the cache 
            // on the Insert of the item in which the dependency is used.
            if (HttpRuntime.Cache.Get(rootCacheKey) != null)
                return;

            HttpRuntime.Cache.Add(
                rootCacheKey,
                rootCacheKey,
                null,
                Cache.NoAbsoluteExpiration,
                Cache.NoSlidingExpiration,
                CacheItemPriority.Default,
                null);
        }

        #endregion Methods
    }
}

توضیحات کدهای فوق

در اینجا یک متدالحاقی به نام Cacheable توسعه داده شده است که می‌تواند در انتهای کوئری‌های LINQ شما قرار گیرد. مثلا:

var data = context.Products.AsQueryable().Cacheable(x => x.FirstOrDefault());

کاری که در این متد انجام می‌شود به این شرح است:
الف) ابتدا کلید منحصربفرد معادل کوئری LINQ فراخوانی شده محاسبه می‌شود.
ب) بر اساس نام کامل نوع Entity در حال استفاده، کلید دیگری به نام rootCacheKey تولید می‌گردد.
شاید بپرسید اهمیت این کلید چیست؟
فرض کنید در حال حاضر 1000 آیتم در کش وجود دارند. چه روشی را برای حذف آیتم‌های مرتبط با کش Entity1 پیشنهاد می‌دهید؟ احتمالا خواهید گفت تمام کش را بررسی کرده و آیتم‌ها را یکی یکی حذف می‌کنیم.
این روش بسیار کند است (و جواب هم نمی‌دهد؛ چون کلیدی که در اینجا تولید شده، هش MD5 معادل کوئری است و نمی‌توان آن‌را به موجودیتی خاص ربط داد) و ... نکته جالبی در متد HttpRuntime.Cache.Insert برای مدیریت آن پیش بینی شده است: استفاده از CacheDependency.
توسط CacheDependency می‌توان گروهی از آیتم‌های هم‌خانواده را تشکیل داد. سپس برای حذف کل این گروه کافی است کلید اصلی CacheDependency را حذف کرد. به این ترتیب به صورت خودکار کل کش مرتبط خالی می‌شود.
ج) مراحل بعدی آن هم یک سری اعمال متداول هستند. ابتدا توسط HttpRuntime.Cache.Get بررسی می‌شود که آیا بر اساس کلید متناظر با کوئری جاری، اطلاعاتی در کش وجود دارد یا خیر. اگر بله، نتیجه از کش خوانده می‌شود. اگر خیر، کوئری اصطلاحا materialized می‌شود تا بر روی بانک اطلاعاتی اجرا شده و نتیجه بازگشت داده شود. سپس این نتیجه را در کش قرار می‌دهیم.

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

using System.Data.Entity;
using EfSecondLevelCaching.Core;
using EfSecondLevelCaching.Test.Models;

namespace EfSecondLevelCaching.Test.DataLayer
{
    public class ProductContext : DbContext
    {
        public DbSet<Product> Products { get; set; }

        public override int SaveChanges()
        {
            this.InvalidateSecondLevelCache();
            return base.SaveChanges();
        }        
    }
}

در اینجا با تحریف متد SaveChanges، می‌توان درست در زمان اعمال تغییرات به بانک اطلاعاتی، قسمتی از کش را غیرمعتبر کرد.


نحوه استفاده از سطح دوم کش توسعه داده شده

مثالی از کاربرد متدهای الحاقی توسعه داده شده را در ذیل مشاهده می‌کنید:

using System.Data.Entity;
using System.Linq;
using EfSecondLevelCaching.Core;
using EfSecondLevelCaching.Test.DataLayer;
using EfSecondLevelCaching.Test.Models;
using System;

namespace EfSecondLevelCaching
{
    public static class TestUsages
    {
        public static void RunQueries()
        {
            using (ProductContext context = new ProductContext())
            {
                var isActive = true;
                var name = "Product1";

                // reading from db
                var list1 = context.Products
                                   .OrderBy(one => one.ProductNumber)
                                   .Where(x => x.IsActive == isActive && x.ProductName == name)
                                   .ToCacheableList();

                // reading from cache
                var list2 = context.Products
                                   .OrderBy(one => one.ProductNumber)
                                   .Where(x => x.IsActive == isActive && x.ProductName == name)
                                   .ToCacheableList();

                // reading from cache
                var list3 = context.Products
                                   .OrderBy(one => one.ProductNumber)
                                   .Where(x => x.IsActive == isActive && x.ProductName == name)
                                   .ToCacheableList();

                // reading from db
                var list4 = context.Products
                                   .OrderBy(one => one.ProductNumber)
                                   .Where(x => x.IsActive == isActive && x.ProductName == "Product2")
                                   .ToCacheableList();
            }

            // removes products cache
            using (ProductContext context = new ProductContext())
            {
                var p = new Product()
                {
                    IsActive = false,
                    ProductName = "P4",
                    ProductNumber = "004"
                };
                context.Products.Add(p);
                context.SaveChanges();
            }

            using (ProductContext context = new ProductContext())
            {
                var data = context.Products.AsQueryable().Cacheable(x => x.FirstOrDefault());
                var data2 = context.Products.AsQueryable().Cacheable(x => x.FirstOrDefault());
                context.SaveChanges();
            }
        }
    }
}

در این حالت اگر برنامه را اجرا کنیم به یک چنین خروجی در پنجره Debug ویژوال استودیو خواهیم رسید:

Adding new data: queryKey=72AF5DA1BA9B91E24DCCF83E88AD1C5F, dependencyKey=EfSecondLevelCaching.Test.Models.Product

Available cached keys list:
queryKey: EfSecondLevelCaching.Test.Models.Product
queryKey: 72AF5DA1BA9B91E24DCCF83E88AD1C5F
count: 2

Fetching object 'EfSecondLevelCaching.Test.Models.Product__72AF5DA1BA9B91E24DCCF83E88AD1C5F' from the cache.

Available cached keys list:
queryKey: EfSecondLevelCaching.Test.Models.Product
queryKey: 72AF5DA1BA9B91E24DCCF83E88AD1C5F
count: 2

Fetching object 'EfSecondLevelCaching.Test.Models.Product__72AF5DA1BA9B91E24DCCF83E88AD1C5F' from the cache.

Available cached keys list:
queryKey: EfSecondLevelCaching.Test.Models.Product
queryKey: 72AF5DA1BA9B91E24DCCF83E88AD1C5F
count: 2

Adding new data: queryKey=11A2C33F9AD7821A0A31003BFF1DF886, dependencyKey=EfSecondLevelCaching.Test.Models.Product

Available cached keys list:
queryKey: 72AF5DA1BA9B91E24DCCF83E88AD1C5F
queryKey: 11A2C33F9AD7821A0A31003BFF1DF886
queryKey: EfSecondLevelCaching.Test.Models.Product
count: 3

Removing items with dependencyKey=EfSecondLevelCaching.Test.Models.Product
Available cached keys list:
count: 0
Available cached keys list:
count: 0

Adding new data: queryKey=02E6FE403B461E45C5508684156C1D10, dependencyKey=EfSecondLevelCaching.Test.Models.Product

Available cached keys list:
queryKey: 02E6FE403B461E45C5508684156C1D10
queryKey: EfSecondLevelCaching.Test.Models.Product
count: 2


Fetching object 'EfSecondLevelCaching.Test.Models.Product__02E6FE403B461E45C5508684156C1D10' from the cache.

توضیحات:
در زمان تولید list1 چون اطلاعاتی در کش سطح دوم وجود ندارد، پیغام Adding new data قابل مشاهده است. اطلاعات از بانک اطلاعاتی دریافت شده و سپس در کش قرار داده می‌شود.
حین فراخوانی list2 که دقیقا همان کوئری list1 را یکبار دیگر فراخوانی می‌کند، به عبارت Fetching object خواهیم رسید که بر دریافت اطلاعات از کش سطح دوم بجای مراجعه به بانک اطلاعاتی دلالت دارد.
در list4 چون پارامترهای کوئری تغییر کرده‌اند، بنابراین دیگر کلید منحصربفرد معادل آن با list1 و lis2 یکی نیست و اینبار پیغام Adding new data مشاهده می‌شود؛ چون برای دریافت اطلاعات آن نیاز است که به بانک اطلاعاتی مراجعه شود.
در ادامه یک context دیگر باز شده و در آن رکوردی به بانک اطلاعاتی اضافه می‌شود. به همین دلیل اینبار پیام Removing items with dependencyKey قابل مشاهده است. به عبارتی متد InvalidateSecondLevelCache وارد عمل شده است و بر اساس تغییری که صورت گرفته، کش را غیرمعتبر کرده است.
سپس در context بعدی تعریف شده، دوبار متد FirstOrDefault فراخوانی شده است. اولین مورد Adding new data است و دومین فراخوانی به Fetching object ختم شده است (دریافت اطلاعات از کش).

کدهای کامل این پروژه را از اینجا می‌توانید دریافت کنید:
  EfSecondLevelCaching.zip
مطالب
ایجاد چارت سازمانی تحت وب #1
برای ایجاد چارت سازمانی همواره از راههای دیگر استفاده میکردیم مثلا از تصویر و فایل PDF ، فلش و یا ...
این مورد همیشه باعث اذیت طراحان وب و برنامه نویسان تحت وب بود. با ظهور برخی امکانات در HTML5 که میتوانید توضیحات آن را در مطلب Canvas Reference - قسمت اول سایت جاری مطالعه نمائید ، هم اکنون با استفاده از این امکانات و کتابخانه مربوط به ساخت نمودار سازمانی میتوانید مانند شکل ذیل نمودار در وب ایجاد نمائید.

نمودار سازمانی

برای اینکار ابتدا یک صفحه index.html ایجاد نموده و در قسمت body آن یک المنت از نوع canvas ایجاد نمائید:

<canvas id="canvas1" width="800" height="800" ></canvas>
سپس فایلهای مورد نیاز را در قسمت head آن ارجاع نمائید:
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" >
<!--[if ie]><script type="text/javascript" src="excanvas.js"></script><![endif]-->
<script type="text/javascript" src="orgchart.js"></script>
حال در انتهای متن فایل html این کد را قرار دهید :
var o = new orgChart();

o.addNode(1, '', '', 'Root node');
o.addNode(2, 1, 'u', 'u-node 1');
o.addNode(3, 1, 'u', 'u-node 2');
o.addNode(4, 1, 'u', 'u-node 3');
o.addNode(5, 1, 'l', 'l-node 1');
o.addNode(6, 1, 'l', 'l-node 2');
o.addNode(7, 1, 'r', 'r-node 1');
o.addNode(8, 1, 'r', 'r-node 2');
o.addNode(9, 1, 'r', 'r-node 3');

o.addNode('', '', '', 'Root 2');
o.addNode('', 'Root 2', 'r', 'using');
o.addNode('', 'Root 2', 'r', 'text as\nid');

o.drawChart('canvas1');
انواع نود در نمودار :
u -> نودهایی که در یک سطح افقی (کنار هم ) و در سطح زیرین یک پدر قرار میگیرند.
l -> نودهایی که در یک سطح عمودی ( زیر هم ) و در سمت چپ نود پدر قرار میگیرند.
r -> نوهایی که در یک سطح عمودی ( زیر هم ) و در سمت راست نود پدر قرار میگیرند.

پارامترهای نودها :
  1. شناسه نود است و میتواند عدد یا رشته باشد . در صورتی که خالی بماند متن نود بعنوان شناسه استفاده میشود.
  2. شناسه نود پدر است و در صورتی که خالی ( یا ناشناس ) باشد بعنوان نود اصلی ( پدر ) نمایش داده میشود.
  3. نوع اتصال نودهای r , l , u . برای نود پدر نادیده گرفته میشود.
  4. متن نود . استفاده از "n\"  جلمه را شکسته و به خط جدید منتقل میکند.
  5. نود با نشانه مشخص ( اگر 1 باشد ، حاشیه با ضخامت کشیده میشود. ( اختیاری )
  6. آدرس یک نود - اختصاص آدرس url  جهت اضافه کردن آن به یک نود ( اختیاری )
  7. رنگ خط حاشیه ( اختیاری )
  8. رنگ پس زمینه ( اختیاری )
  9. رنگ متن و فونت ( اختیاری )

پارامترهای تابع ()drowChart :

  1. شناسه المنت canvas
  2. تراز کردن نمودار با استفاده از حرف 'c' یا کلمه 'center' . در صورت حذف ، نمودار در سمت چپ صفحه نمایش داده خواهد شد. تراز چپ ( اختیاری )
  3. تناسب اندازه canvas .  اگر true باشد canvas به اندازه نمودار چارت ، تغییر اندازه میدهد . (اختیاری)
مطمئن باشید که فایل index.html , excanvas.js  ,orgchart.js  در یک فولدر قرار گرفته باشند .
در مطلب بعدی نمایش با رنگهای مختلف ارائه خواهد شد.

مورد نیازهای مطلب جاری :
  1. excanvas.js
  2. orgchart.js
  3. index.html
مطالب
یافتن Contextهای Dispose نشده در Entity framework
این دو متد را در نظر بگیرید:
        private static void disposedContext()
        {
            using (var context = new MyContext())
            {
                Debug.WriteLine("Posts count: " + context.BlogPosts.Count());
            }
        }

        private static void nonDisposedContext()
        {
            var context = new MyContext();
            Debug.WriteLine("Posts count: " + context.BlogPosts.Count());
        }
در اولی با استفاده از using، شیء context به صورت خودکار dispose خواهد شد؛ اما در دومی از using استفاده نشده‌است.

سؤال: در یک برنامه‌ی بزرگ چطور می‌توان لیست Contextهای Dispose نشده را یافت؟

در EF 6 با تعریف یک IDbConnectionInterceptor سفارشی می‌توان به متدهای باز، بسته و dispose شدن یک Connection دسترسی یافت. اگر Context ایی dispose نشده باشد، اتصال آن نیز dispose نخواهد شد.
using System.Data;
using System.Data.Common;
using System.Data.Entity.Infrastructure.Interception;

namespace EFNonDisposedContext.Core
{
    public class DatabaseInterceptor : IDbConnectionInterceptor
    {
        public void Closed(DbConnection connection, DbConnectionInterceptionContext interceptionContext)
        {
            Connections.AddOrUpdate(connection, ConnectionStatus.Closed);
        }

        public void Disposed(DbConnection connection, DbConnectionInterceptionContext interceptionContext)
        {
            Connections.AddOrUpdate(connection, ConnectionStatus.Disposed);
        }

        public void Opened(DbConnection connection, DbConnectionInterceptionContext interceptionContext)
        {
            Connections.AddOrUpdate(connection, ConnectionStatus.Opened);
        }
      
        // the rest of the IDbConnectionInterceptor methods ...

    }
}
همانطور که ملاحظه می‌کنید، با پیاده سازی IDbConnectionInterceptor، به سه متد Closed، Opened و Disposed یک DbConnection می‌توان دسترسی یافت.

مشکل مهم! در زمان فراخوانی متد Disposed، دقیقا کدام DbConnection باز شده، رها شده‌است؟
پاسخ به این سؤال را در مطلب «ایجاد خواص الحاقی» می‌توانید مطالعه کنید. با استفاده از یک ConditionalWeakTable به هر کدام از اشیاء DbConnection یک Id را انتساب خواهیم داد و پس از آن به سادگی می‌توان وضعیت این Id را ردگیری کرد.
برای این منظور، لیستی از ConnectionInfo را تشکیل خواهیم داد:
    public enum ConnectionStatus
    {
        None,
        Opened,
        Closed,
        Disposed
    }

    public class ConnectionInfo
    {
        public string ConnectionId { set; get; }
        public string StackTrace { set; get; }
        public ConnectionStatus Status { set; get; }

        public override string ToString()
        {
            return string.Format("{0}:{1} [{2}]",ConnectionId, Status, StackTrace);
        }
    }
در اینجا ConnectionId را به کمک ConditionalWeakTable محاسبه می‌کنیم.
StackTrace توسط نکته‌ی مطلب «کدام سلسله متدها، متد جاری را فراخوانی کرده‌اند؟ » تهیه می‌شود.
Status نیز وضعیت جاری اتصال است که بر اساس متدهای فراخوانی شده در پیاده سازی IDbConnectionInterceptor مشخص می‌گردد.

در پایان کار برنامه فقط باید یک گزارش تهیه کنیم از لیست ConnectionInfoهایی که Status آن‌ها مساوی Disposed نیست. این موارد با توجه به مشخص بودن Stack trace هر کدام، دقیقا محل متدی را که در آن context مورد استفاده dispose نشده‌است، مشخص می‌کنند.


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید
EFNonDisposedContext.zip
 
مطالب
نحوه نمایش تمام آیکون‌های تعریف شده در یک قلم در WPF
سال نو مبارک! به امید روزهایی شاد، سلامت و پر برکت.

پیرو مطلب قلم‌هایی حاوی آیکون که خصوصا در برنامه‌های مترو بیشتر مرسوم شده‌اند، شاید بد نباشد کار برنامه Character Map ویندوز را با WPF شبیه سازی کنیم.
ابتدا Model و ViewModel  این برنامه را درنظر بگیرید:
namespace CrMap.Models
{
    public class Symbol
    {
        public char Character { set; get; }
        public string CharacterCode { set; get; }
    }
}

using System;
using System.Collections.Generic;
using System.Windows.Media;
using CrMap.Models;

namespace CrMap.ViewModels
{
    public class CrMapViewModel
    {
        public IList<Symbol> Symbols { set; get; }
        public int GridRows { set; get; }
        public int GridCols { set; get; }

        public CrMapViewModel()
        {
            fillDataSource();
        }

        private void fillDataSource()
        {
            Symbols = new List<Symbol>();
            GridCols = 15;

            var fontFamily = new FontFamily(new Uri("pack://application:,,,/"), "/Fonts/#whhglyphs");

            GlyphTypeface glyph = null;
            foreach (var typeface in fontFamily.GetTypefaces())
            {
                if (typeface.TryGetGlyphTypeface(out glyph) && (glyph != null))
                    break;
            }

            if (glyph == null)
                throw new InvalidOperationException("Couldn't find a GlyphTypeface.");

            GridRows = (glyph.CharacterToGlyphMap.Count / GridCols) + 1;

            foreach (var item in glyph.CharacterToGlyphMap)
            {
                var index = item.Key;
                Symbols.Add(new Symbol
                {
                    Character = Convert.ToChar(index),
                    CharacterCode = string.Format("&#x{0:X}", index)
                });
            }
        }
    }
}
توضیحات:

یک سری قابلیت جالب در WPF برای استخراج اطلاعات قلم‌ها وجود دارند که در فضای نام System.Windows.Media اسمبلی PresentationCore.dll قرار گرفته‌اند. برای نمونه پس از وهله سازی FontFamily بر اساس یک قلم مدفون شده در برنامه، امکان استخراج تعداد گلیف‌های موجود در این قلم وجود دارد که نحوه انجام آن‌را در متد fillDataSource ملاحظه می‌کنید.
این اطلاعات استخراج شده و لیست Symbols برنامه را تشکیل می‌دهند. در نهایت برای نمایش این اطلاعات، از ترکیب ItemsControl و UniformGrid استفاده خواهیم کرد:
<Window x:Class="CrMap.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="clr-namespace:CrMap.ViewModels"
        Title="MainWindow" WindowStartupLocation="CenterScreen" WindowState="Maximized"
        Height="350" Width="525">
    <Window.Resources>
        <vm:CrMapViewModel x:Key="vmCrMapViewModel" />
    </Window.Resources>
    <ScrollViewer VerticalScrollBarVisibility="Visible">
        <ItemsControl
                      DataContext="{StaticResource vmCrMapViewModel}"  
                      ItemsSource="{Binding Symbols}"
                      Name="MainItemsControl"
                      VerticalAlignment="Top"     
                      HorizontalAlignment="Center"
                      Grid.Column="0" Grid.Row="1" Grid.ColumnSpan="4">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <UniformGrid
                        HorizontalAlignment="Center"
                        VerticalAlignment="Center"
                        Columns="{Binding GridCols}" 
                        Rows="{Binding GridRows}">
                    </UniformGrid>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <ContentControl>
                        <Border BorderBrush="SlateGray"
                            HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
                            BorderThickness="1" CornerRadius="3" Margin="3">
                            <StackPanel Margin="3" Orientation="Vertical">
                                <TextBlock                                         
                                    HorizontalAlignment="Center" 
                                    VerticalAlignment="Center" 
                                    FontFamily="Fonts/#whhglyphs"
                                    Foreground="DarkRed"
                                    FontSize="17"
                                    Text="{Binding Character}" />
                                <TextBlock                                         
                                    HorizontalAlignment="Center" 
                                    VerticalAlignment="Center" 
                                    Text="{Binding CharacterCode}" />
                            </StackPanel>
                        </Border>
                    </ContentControl>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </ScrollViewer>
</Window>


دریافت مثال این مطلب
CrMap.zip
 
نظرات مطالب
استفاده از Kendo UI TreeView به همراه یک منبع داده راه دور
در اینجا بحث شده. با این راه حل:
$(document).ready(function () { 
      var tv=$("#treeview").kendoTreeView({
        dragAndDrop: true,
        checkboxes: {checkChildren:true}, 
      }).data("kendoTreeView"); 
      
      for(var i=0; i < tv.dataSource._total; i++){
        tv.dataSource.data()[i].children.read();  
      } 
      tv.expand(".k-item"); 
    });
مطالب دوره‌ها
استفاده از AOP Interceptors برای حذف کدهای تکراری INotifyPropertyChanged در WPF
هرکسی که با WPF کار کرده باشد با دردی به نام اینترفیس INotifyPropertyChanged و پیاده سازی‌های تکراری مرتبط با آن آشنا است:
public class MyClass : INotifyPropertyChanged
{
    private string _myValue;
    public event PropertyChangedEventHandler PropertyChanged;
    public string MyValue
    {
        get
        {
            return _myValue;
        }
        set
        {
            _myValue = value;
            RaisePropertyChanged("MyValue");
        }
    }
    protected void RaisePropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}
چندین راه‌حل هم برای ساده سازی و یا بهبود آن وجود دارد از Strongly typed کردن آن تا روش‌های اخیر دات نت 4 و نیم در مورد استفاده از ویژگی‌های متدهای فراخوان. اما ... با استفاده از AOP Interceptors می‌توان در وهله سازی‌ها و فراخوانی‌ها دخالت کرد و کدهای مورد نظر را در مکان‌های مناسبی تزریق نمود. بنابراین در مطلب جاری قصد داریم ارائه متفاوتی را از پیاده سازی خودکار INotifyPropertyChanged ارائه دهیم. به عبارتی چقدر خوب می‌شد فقط می‌نوشتیم :
public class MyDreamClass : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    public string MyValue { get; set; }
}
و ... همه چیز مثل سابق کار می‌کرد. برای رسیدن به این هدف، باید فراخوانی‌های set خواص را تحت نظر قرار داد (یا همان Interception در اینجا). ابتدا باید اجازه دهیم تا set صورت گیرد، پس از آن کدهای معروف RaisePropertyChanged را به صورت خودکار فراخوانی کنیم.


پیشنیازها

ابتدا یک برنامه جدید WPF را آغاز کنید. تنظیمات آن‌را از حالت Client profile به Full تغییر دهید.
سپس همانند قسمت قبل، ارجاعات لازم را به StructureMap و Castle.Core نیز اضافه نمائید:
 PM> Install-Package structuremap
PM> Install-Package Castle.Core


ساختار برنامه

برنامه ما از یک اینترفیس و کلاس سرویس تشکیل شده است:
namespace AOP01.Services
{
    public interface ITestService
    {
        int GetCount();
    }
}

namespace AOP01.Services
{
    public class TestService: ITestService
    {     
        public int GetCount()
        {
            return 10; //این فقط یک مثال است برای بررسی تزریق وابستگی‌ها
        }
    }
}
همچنین دارای یک ViewModel به شکل زیر می‌باشد:
using AOP01.Services;
using AOP01.Core;

namespace AOP01.ViewModels
{
    public class TestViewModel  : BaseViewModel
    {
        private readonly ITestService _testService;
        //تزریق وابستگی‌ها در سازنده کلاس
        public TestViewModel(ITestService testService)
        {
            _testService = testService;
        }

        // Note: it's a virtual property.
        public virtual string Text { get; set; }
    }
}
سه نکته در این ViewModel حائز اهمیت هستند:
الف) استفاده از کلاس پایه BaseViewModel برای کاهش کدهای تکراری مرتبط با INotifyPropertyChanged که به صورت زیر تعریف شده است:
using System.ComponentModel;

namespace AOP01.Core
{
    public abstract class BaseViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public void RaisePropertyChanged(string propertyName)
        {
            var handler = PropertyChanged;

            if (handler != null)
                handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}
ب) کلاس سرویس، در حالت تزریق وابستگی‌ها در سازنده کلاس در اینجا مورد استفاده قرار گرفته است. وهله سازی خودکار آن توسط کلاس‌های پروکسی و DI صورت خواهند گرفت.
ج) خاصیتی که در اینجا تعریف شده از نوع virtual است؛ بدون پیاده سازی مفصل قسمت set آن و فراخوانی مستقیم RaisePropertyChanged کلاس پایه به صورت متداول. علت virtual تعریف کردن آن به امکان دخل و تصرف در نواحی get و set این خاصیت توسط Interceptor ایی که در ادامه تعریف خواهیم کرد بر می‌گردد.


پیاده سازی NotifyPropertyInterceptor

using System;
using Castle.DynamicProxy;

namespace AOP01.Core
{
    public class NotifyPropertyInterceptor : IInterceptor
    {
        public void Intercept(IInvocation invocation)
        {
            // متد ست، ابتدا فراخوانی می‌شود و سپس کار اطلاع رسانی را انجام خواهیم داد
            invocation.Proceed();

            if (invocation.Method.Name.StartsWith("set_"))
            {
                var propertyName = invocation.Method.Name.Substring(4);
                raisePropertyChangedEvent(invocation, propertyName, invocation.TargetType);
            }
        }

        void raisePropertyChangedEvent(IInvocation invocation, string propertyName, Type type)
        {
            var methodInfo = type.GetMethod("RaisePropertyChanged");
            if (methodInfo == null)
            {
                if (type.BaseType != null)
                    raisePropertyChangedEvent(invocation, propertyName, type.BaseType);
            }
            else
            {
                methodInfo.Invoke(invocation.InvocationTarget, new object[] { propertyName });
            }
        }
    }
}
با اینترفیس IInterceptor در قسمت قبل آشنا شدیم.
در اینجا ابتدا اجازه خواهیم داد تا کار set به صورت معمول انجام شود. دو حالت get و set ممکن است رخ دهند. بنابراین در ادامه بررسی خواهیم کرد که اگر حالت set بود، آنگاه متد RaisePropertyChanged کلاس پایه BaseViewModel را یافته و به صورت پویا با propertyName صحیحی فراخوانی می‌کنیم.
به این ترتیب دیگر نیازی نخواهد بود تا به ازای تمام خواص مورد نیاز، کار فراخوانی دستی RaisePropertyChanged صورت گیرد.


اتصال Interceptor به سیستم

خوب! تا اینجای کار صرفا تعاریف اولیه تدارک دیده شده‌اند. در ادامه نیاز است تا DI و DynamicProxy را از وجود آن‌ها مطلع کنیم.
برای این منظور فایل App.xaml.cs را گشوده و در نقطه آغاز برنامه تنظیمات ذیل را اعمال نمائید:
using System.Linq;
using System.Windows;
using AOP01.Core;
using AOP01.Services;
using Castle.DynamicProxy;
using StructureMap;

namespace AOP01
{
    public partial class App
    {
        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);

            ObjectFactory.Initialize(x =>
            {
                x.For<ITestService>().Use<TestService>();

                var dynamicProxy = new ProxyGenerator();
                x.For<BaseViewModel>().EnrichAllWith(vm =>
                {
                    var constructorArgs = vm.GetType()
                            .GetConstructors()
                            .FirstOrDefault()
                            .GetParameters()
                            .Select(p => ObjectFactory.GetInstance(p.ParameterType))
                            .ToArray();

                    return dynamicProxy.CreateClassProxy(
                                classToProxy: vm.GetType(),
                                constructorArguments: constructorArgs,
                                interceptors: new[] { new NotifyPropertyInterceptor() });
                });
            });
        }
    }
}
مطابق این تنظیمات، هرجایی که نیاز به نوعی از ITestService بود، از کلاس TestService استفاده خواهد شد.
همچنین در ادامه به DI مورد استفاده اعلام می‌کنیم که ViewModelهای ما دارای کلاس پایه BaseViewModel هستند. بنابراین هر زمانی که این نوع موارد وهله سازی شدند، آن‌ها را یافته و با پروکسی حاوی NotifyPropertyInterceptor مزین کن.
مثالی که در اینجا انتخاب شده، تقریبا مشکل‌ترین حالت ممکن است؛ چون به همراه تزریق خودکار وابستگی‌ها در سازنده کلاس ViewModel نیز می‌باشد. اگر ViewModelهای شما سازنده‌ای به این شکل ندارند، قسمت تشکیل constructorArgs را حذف کنید.


استفاده از ViewModel مزین شده با پروکسی در یک View

<Window x:Class="AOP01.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <TextBox Text="{Binding Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
    </Grid>
</Window>
اگر فرض کنیم که پنجره اصلی برنامه مصرف کننده ViewModel فوق است، در code behind آن خواهیم داشت:
using AOP01.ViewModels;
using StructureMap;

namespace AOP01
{
    public partial class MainWindow
    {
        public MainWindow()
        {
            InitializeComponent();

            //علاوه بر تشکیل پروکسی
            //کار وهله سازی و تزریق وابستگی‌ها در سازنده را هم به صورت خودکار انجام می‌دهد
            var vm = ObjectFactory.GetInstance<TestViewModel>(); 
            this.DataContext = vm;
        }
    }
}
به این ترتیب یک ViewModel محصور شده توسط DynamicProxy مزین با NotifyPropertyInterceptor به DataContext  ارسال می‌گردد.

اکنون اگر برنامه را اجرا کنیم، مشاهده خواهیم کرد که با وارد کردن مقداری در TextBox برنامه، NotifyPropertyInterceptor مورد استفاده قرار می‌گیرد:



دریافت مثال کامل این قسمت
AOP01.zip
مطالب دوره‌ها
استفاده از AOP Interceptors برای حذف کدهای تکراری کش کردن اطلاعات در لایه سرویس برنامه
اکثر برنامه‌های ما دارای قابلیت‌هایی هستند که با موضوعاتی مانند امنیت، کش کردن اطلاعات، مدیریت استثناها، ثبت وقایع و غیره گره خورده‌اند. به هر یک از این موضوعات یک Aspect یا cross-cutting concern نیز گفته می‌شود.
در این قسمت قصد داریم اطلاعات بازگشتی از لایه سرویس برنامه را کش کنیم؛ اما نمی‌خواهیم مدام کدهای مرتبط با کش کردن اطلاعات را در مکان‌های مختلف لایه سرویس پراکنده کنیم. می‌خواهیم یک ویژگی یا Attribute سفارشی را تهیه کرده (مثلا به نام CacheMethod) و به متد یا متدهایی خاص اعمال کنیم. سپس برنامه، در زمان اجرا، بر اساس این ویژگی‌ها، خروجی‌های متدهای تزئین شده با ویژگی CacheMethod را کش کند.
در اینجا نیز از ترکیب StructureMap و DynamicProxy پروژه Castle، برای رسیدن به این مقصود استفاده خواهیم کرد. به کمک StructureMap می‌توان در زمان وهله سازی کلاس‌ها، آن‌ها را به کمک متدی به نام EnrichWith توسط یک محصور کننده دلخواه، مزین یا غنی سازی کرد. این مزین کننده را جهت دخالت در فراخوانی‌های متدها، یک DynamicProxy درنظر می‌گیریم. با پیاده سازی اینترفیس IInterceptor کتابخانه DynamicProxy مورد استفاده و تحت کنترل قرار دادن نحوه و زمان فراخوانی متدهای لایه سرویس، یکی از کارهایی را که می‌توان انجام داد، کش کردن نتایج است که در ادامه به جزئیات آن خواهیم پرداخت.


پیشنیازها

ابتدا یک برنامه جدید کنسول را آغاز کنید. تنظیمات آن‌را از حالت Client profile به Full تغییر دهید.
سپس همانند قسمت‌های قبل، ارجاعات لازم را به StructureMap و Castle.Core نیز اضافه نمائید:
 PM> Install-Package structuremap
PM> Install-Package Castle.Core
همچنین ارجاعی را به اسمبلی استاندارد System.Web.dll نیز اضافه نمائید.
از این جهت که از HttpRuntime.Cache قصد داریم استفاده کنیم. HttpRuntime.Cache در برنامه‌های کنسول نیز کار می‌کند. در این حالت از حافظه سیستم استفاده خواهد کرد و در پروژه‌های وب از کش IIS بهره می‌برد.


ویژگی CacheMethod مورد استفاده

using System;

namespace AOP02.Core
{
    [AttributeUsage(AttributeTargets.Method)]
    public class CacheMethodAttribute : Attribute
    {
        public CacheMethodAttribute()
        {
            // مقدار پیش فرض
            SecondsToCache = 10;
        }

        public double SecondsToCache { get; set; }
    }
}
همانطور که عنوان شد، قصد داریم متدهای مورد نظر را توسط یک ویژگی سفارشی، مزین سازیم تا تنها این موارد توسط AOP Interceptor مورد استفاده پردازش شوند.
در ویژگی CacheMethod، خاصیت SecondsToCache بیانگر مدت زمان کش شدن نتیجه متد خواهد بود.


ساختار لایه سرویس برنامه

using System;
using System.Threading;
using AOP02.Core;

namespace AOP02.Services
{
    public interface IMyService
    {
        string GetLongRunningResult(string input);
    }

    public class MyService : IMyService
    {
        [CacheMethod(SecondsToCache = 60)]
        public string GetLongRunningResult(string input)
        {
            Thread.Sleep(5000); // simulate a long running process
            return string.Format("Result of '{0}' returned at {1}", input, DateTime.Now);
        }
    }
}
اینترفیس IMyService و پیاده سازی نمونه آن‌را در اینجا مشاهده می‌کنید. از این لایه در برنامه استفاده شده و قصد داریم نتیجه بازگشت داده شده توسط متدی زمانبر را در اینجا توسط AOP Interceptors کش کنیم.


تدارک یک CacheInterceptor

using System;
using System.Web;
using Castle.DynamicProxy;

namespace AOP02.Core
{
    public class CacheInterceptor : IInterceptor
    {
        private static object lockObject = new object();

        public void Intercept(IInvocation invocation)
        {
            cacheMethod(invocation);
        }

        private static void cacheMethod(IInvocation invocation)
        {
            var cacheMethodAttribute = getCacheMethodAttribute(invocation);
            if (cacheMethodAttribute == null)
            {
                // متد جاری توسط ویژگی کش شدن مزین نشده است
                // بنابراین آن‌را اجرا کرده و کار را خاتمه می‌دهیم
                invocation.Proceed();
                return;
            }

            // دراینجا مدت زمان کش شدن متد از ویژگی کش دریافت می‌شود
            var cacheDuration = ((CacheMethodAttribute)cacheMethodAttribute).SecondsToCache;

            // برای ذخیره سازی اطلاعات در کش نیاز است یک کلید منحصربفرد را
            //  بر اساس نام متد و پارامترهای ارسالی به آن تهیه کنیم
            var cacheKey = getCacheKey(invocation);

            var cache = HttpRuntime.Cache;
            var cachedResult = cache.Get(cacheKey);


            if (cachedResult != null)
            {
                // اگر نتیجه بر اساس کلید تشکیل شده در کش موجود بود
                // همان را بازگشت می‌دهیم
                invocation.ReturnValue = cachedResult;
            }
            else
            {
                lock (lockObject)
                {
                    // در غیر اینصورت ابتدا متد را اجرا کرده
                    invocation.Proceed();
                    if (invocation.ReturnValue == null)
                        return;

                    // سپس نتیجه آن‌را کش می‌کنیم
                    cache.Insert(key: cacheKey,
                                 value: invocation.ReturnValue,
                                 dependencies: null,
                                 absoluteExpiration: DateTime.Now.AddSeconds(cacheDuration),
                                 slidingExpiration: TimeSpan.Zero);
                }
            }
        }

        private static Attribute getCacheMethodAttribute(IInvocation invocation)
        {
            var methodInfo = invocation.MethodInvocationTarget;
            if (methodInfo == null)
            {
                methodInfo = invocation.Method;
            }
            return Attribute.GetCustomAttribute(methodInfo, typeof(CacheMethodAttribute), true);
        }

        private static string getCacheKey(IInvocation invocation)
        {
            var cacheKey = invocation.Method.Name;

            foreach (var argument in invocation.Arguments)
            {
                cacheKey += ":" + argument;
            }

            // todo: بهتر است هش این کلید طولانی بازگشت داده شود
            // کار کردن با هش سریعتر خواهد بود
            return cacheKey;
        }
    }
}
کدهای CacheInterceptor مورد استفاده را در بالا مشاهده می‌کنید.
توضیحات ریز قسمت‌های مختلف آن به صورت کامنت، جهت درک بهتر عملیات، ذکر شده‌اند.


اتصال Interceptor به سیستم

خوب! تا اینجای کار صرفا تعاریف اولیه تدارک دیده شده‌اند. در ادامه نیاز است تا DI و DynamicProxy را از وجود آن‌ها مطلع کنیم.
using System;
using AOP02.Core;
using AOP02.Services;
using Castle.DynamicProxy;
using StructureMap;

namespace AOP02
{
    class Program
    {
        static void Main(string[] args)
        {
            ObjectFactory.Initialize(x =>
            {
                var dynamicProxy = new ProxyGenerator();
                x.For<IMyService>()
                 .EnrichAllWith(myTypeInterface =>
                        dynamicProxy.CreateInterfaceProxyWithTarget(myTypeInterface, new CacheInterceptor()))
                 .Use<MyService>();
            });

            var myService = ObjectFactory.GetInstance<IMyService>();
            Console.WriteLine(myService.GetLongRunningResult("Test"));
            Console.WriteLine(myService.GetLongRunningResult("Test"));
        }
    }
}
در قسمت تنظیمات اولیه DI مورد استفاده، هر زمان که شیءایی از نوع IMyService درخواست شود، کلاس MyService وهله سازی شده و سپس توسط CacheInterceptor محصور می‌گردد. اکنون ادامه برنامه با این شیء محصور شده کار می‌کند.
حال اگر برنامه را اجرا کنید یک چنین خروجی قابل مشاهده خواهد بود:
 Result of 'Test' returned at 2013/04/09 07:19:43
Result of 'Test' returned at 2013/04/09 07:19:43
همانطور که ملاحظه می‌کنید هر دو فراخوانی یک زمان را بازگشت داده‌اند که بیانگر کش شدن اطلاعات اولی و خوانده شدن اطلاعات فراخوانی دوم از کش می‌باشد (با توجه به یکی بودن پارامترهای هر دو فراخوانی).

از این پیاده سازی می‌شود به عنوان کش سطح دوم ORMها نیز استفاده کرد (صرفنظر از نوع ORM در حال استفاده).

دریافت مثال کامل این قسمت
AOP02.zip
مطالب
استفاده از خواص راهبری در Entity framework بجای Join نویسی
یکی از مزایای مهم استفاده از Entity framework، خواص راهبری (navigation properties) آن هستند که امکان تهیه کوئری‌های بین جداول را به سادگی و به نحوی منطقی فراهم می‌کنند.
برای مثال دو جدول شهر‌ها و افراد را درنظر بگیرید. مقصود از تعریف جدول شهر‌ها در اینجا، مشخص سازی محل تولد افراد است:
    public class Person
    {
        public int Id { get; set; }
        public string Name { get; set; }

        [ForeignKey("BornInCityId")]
        public virtual City BornInCity { get; set; }
        public int BornInCityId { get; set; }
    }

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

        public virtual ICollection<Person> People { get; set; }
    }
در ادامه این کلاس‌ها را در معرض دید EF Code first قرار داده:
    public class MyContext : DbContext
    {
        public DbSet<City> Cities { get; set; }
        public DbSet<Person> People { get; set; }
    }


و همچنین تعدادی رکورد آغازین را نیز به جداول مرتبط اضافه می‌کنیم:
    public class Configuration : DbMigrationsConfiguration<MyContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = true;
            AutomaticMigrationDataLossAllowed = true;
        }

        protected override void Seed(MyContext context)
        {
            var city1 = new City { Name = "city-1" };
            var city2 = new City { Name = "city-2" };
            context.Cities.Add(city1);
            context.Cities.Add(city2);

            var person1 = new Person { Name = "user-1", BornInCity = city1 };
            var person2 = new Person { Name = "user-2", BornInCity = city1 };
            context.People.Add(person1);
            context.People.Add(person2);

            base.Seed(context);
        }
    }
در این حالت برای نمایش لیست نام افراد به همراه محل تولد آن‌ها، بنابر روال سابق SQL نویسی، نوشتن کوئری LINQ زیر بسیار متداول است:
    public static class Test
    {
        public static void RunTests()
        {
            Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, Configuration>());

            using (var context = new MyContext())
            {
                var peopleAndCitiesList = from person in context.People
                           join city in context.Cities
                           on person.BornInCityId equals city.Id
                           select new
                           {
                              PersonName = person.Name,
                              CityName = city.Name
                           };

                foreach (var item in peopleAndCitiesList)
                {
                    Console.WriteLine("{0}:{1}", item.PersonName, item.CityName);
                }
            }
        }
    }
که حاصل آن اجرای کوئری ذیل بر روی بانک اطلاعاتی خواهد بود:
SELECT 
          [Extent1].[BornInCityId] AS [BornInCityId], 
          [Extent1].[Name] AS [Name], 
          [Extent2].[Name] AS [Name1]
FROM  [dbo].[People] AS [Extent1]
INNER JOIN [dbo].[Cities] AS [Extent2] ON [Extent1].[BornInCityId] = [Extent2].[Id]
این نوع کوئری‌های join دار را به نحو ساده‌تری نیز می‌توان در EF با استفاده از خواص راهبری و بدون join نویسی مستقیم تهیه کرد:
var peopleAndCitiesList = context.People
                                  .Select(person => new
                                                         {
                                                             PersonName = person.Name,
                                                             CityName = person.BornInCity.Name
                                                         });
که دقیقا همان خروجی SQL یاد شده را تولید می‌کند.

مثال دوم:
می‌خواهیم لیست شهرها را بر اساس تعداد کاربر متناظر به صورت نزولی مرتب کنیم:
var citiesList = context.Cities.OrderByDescending(x => x.People.Count());
foreach (var item in citiesList)
{
    Console.WriteLine("{0}", item.Name);
}
همانطور که مشاهده می‌کنید از خواص راهبری در قسمت order by هم می‌شود استفاده کرد. خروجی SQL کوئری فوق به صورت زیر است:
SELECT 
[Project1].[Id] AS [Id], 
[Project1].[Name] AS [Name]
FROM ( SELECT 
        [Extent1].[Id] AS [Id], 
        [Extent1].[Name] AS [Name], 
        (SELECT 
                COUNT(1) AS [A1]
                FROM [dbo].[People] AS [Extent2]
                WHERE [Extent1].[Id] = [Extent2].[BornInCityId]) AS [C1]
        FROM [dbo].[Cities] AS [Extent1]
)  AS [Project1]
ORDER BY [Project1].[C1] DESC

مثال سوم:
در ادامه قصد داریم لیست شهرها را به همراه تعداد نفرات متناظر با آن‌ها نمایش دهیم:
 var peopleAndCitiesList = context.Cities
                                     .Select(city => new
                                                 {
                                                     InUseCount = city.People.Count(),
                                                     CityName = city.Name
                                                 });

foreach (var item in peopleAndCitiesList)
{
     Console.WriteLine("{0}:{1}", item.CityName, item.InUseCount);
}
در اینجا از خاصیت راهبری People برای شمارش تعداد اعضای متناظر با هر شهر استفاده شده است.
خروجی SQL کوئری فوق به نحو ذیل است:
SELECT 
[Extent1].[Id] AS [Id], 
(SELECT 
        COUNT(1) AS [A1]
        FROM [dbo].[People] AS [Extent2]
        WHERE [Extent1].[Id] = [Extent2].[BornInCityId]) AS [C1], 
[Extent1].[Name] AS [Name]
FROM [dbo].[Cities] AS [Extent1]
نظرات مطالب
واژه‌های کلیدی جدید and، or و not در C# 9.0
معادل‌های string.IsNullOrEmpty در C# 9.0 جهت اطلاع و آشنایی با Syntax جدید؛ اگر جائی آن‌ها را دیدید!
using System;

namespace CS9Features
{
    public static class ModernizingACodebase
    {
        public static void PropertyPatternToReplaceIsNullorEmpty1()
        {
            string hello = null;

            // Old approach
            if (!string.IsNullOrEmpty(hello))
            {
                Console.WriteLine($"{hello} has {hello.Length} letters.");
            }

            // New approach, with a property pattern
            if (hello is { Length: > 0 })
            {
                Console.WriteLine($"{hello} has {hello.Length} letters.");
            }
        }

        public static void PropertyPatternToReplaceIsNullorEmpty2()
        {
            // For arrays
            var greetings = new string[2];
            greetings[0] = "Hello world";
            greetings = null;

            // Old approach
            if (greetings != null && !string.IsNullOrEmpty(greetings[0]))
            {
                Console.WriteLine($"{greetings[0]} has {greetings[0].Length} letters.");
            }

            // New approach
            if (greetings?[0] is { Length: > 0 } hi)
            {
                Console.WriteLine($"{hi} has {hi.Length} letters.");
            }
        }
    }
}
برای مطالعه‌ی بیشتر