عنوان:

‫بازنویسی سطح دوم کش برای Entity framework 6


نویسنده: وحید نصیری
تاریخ: ۱۳۹۳/۱۱/۰۶ ۱۶:۵۰
آدرس: www.dntips.ir
چندی قبل مطلبی را در مورد پیاده سازی سطح دوم کش در EF در این سایت مطالعه کردید. اساس آن مقاله‌ای بود که نحوه‌ی کش کردن اطلاعات حاصل از LINQ to Objects را بیان کرده بود (^). این مقاله پایه‌ی بسیاری از سیستم‌های کش مشابه نیز شده‌است (^ و ^ و ...).
مشکل مهم این روش عدم سازگاری کامل آن با EF است. برای مثال در آن تفاوتی بین (Include(x=>x.Tags و (Include(x=>x.Users وجود ندارد. به همین جهت در این نوع موارد، قادر به تولید کلید منحصربفردی جهت کش کردن اطلاعات یک کوئری مشخص نیست. در اینجا یک کوئری LINQ، به معادل رشته‌ای آن تبدیل می‌شود و سپس Hash آن محاسبه می‌گردد. این هش، کلید ذخیره سازی اطلاعات حاصل از کوئری، در سیستم کش خواهد بود. زمانیکه دو کوئری Include دار متفاوت EF، هش‌های یکسانی را تولید کنند، عملا این سیستم کش، کارآیی خودش را از دست می‌دهد. برای رفع این مشکل پروژه‌ی دیگری به نام EF cache ارائه شده‌است. این پروژه بسیار عالی طراحی شده و می‌تواند جهت ایده دادن به تیم EF نیز بکار رود. اما در آن فرض بر این است که شما می‌خواهید کل سیستم را در یک کش قرار دهید. وارد مکانیزم DBCommand و DataReader می‌شود و در آن‌جا کار کش کردن تمام کوئری‌ها را انجام می‌دهد؛ مگر آنکه به آن اعلام کنید از کوئری‌های خاصی صرفنظر کند.
با توجه به این مشکلات، روش بهتری برای تولید هش یک کوئری LINQ to Entities بر اساس کوئری واقعی SQL تولید شده توسط EF، پیش از ارسال آن به بانک اطلاعاتی به صورت زیر وجود دارد:
        private static ObjectQuery TryGetObjectQuery<T>(IQueryable<T> source)
        {
            var dbQuery = source as DbQuery<T>;

            if (dbQuery != null)
            {
                const BindingFlags privateFieldFlags = 
                    BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public;

                var internalQuery =
                    source.GetType().GetProperty("InternalQuery", privateFieldFlags)
                        .GetValue(source);

                return
                    (ObjectQuery)internalQuery.GetType().GetProperty("ObjectQuery", privateFieldFlags)
                        .GetValue(internalQuery);
            }

            return null;
        }
این متد یک کوئری LINQ مخصوص EF را دریافت می‌کند و با کمک Reflection، اطلاعات درونی آن که شامل ObjectQuery اصلی است را استخراج می‌کند. سپس فراخوانی متد objectQuery.ToTraceString بر روی حاصل آن، سبب تولید SQL معادل کوئری LINQ اصلی می‌گردد. همچنین objectQuery امکان دسترسی به پارامترهای تنظیم شده‌ی کوئری را نیز میسر می‌کند. به این ترتیب می‌توان به معادل رشته‌ای منطقی‌تری از یک کوئری LINQ رسید که قابلیت تشخیص JOINها و متد Include نیز به صورت خودکار در آن لحاظ شده‌است.

این اطلاعات، پایه‌ی تهیه‌ی کتابخانه‌ی جدیدی به نام EFSecondLevelCache گردید. برای نصب آن کافی است دستور ذیل را در کنسول پاورشل نیوگت صادر کنید:
 PM> Install-Package EFSecondLevelCache
سپس برای کش کردن کوئری معمولی مانند:
 var products = context.Products.Include(x => x.Tags).FirstOrDefault();
می‌توان از متد جدید Cacheable آن به نحو ذیل استفاده کرد (این روش بسیار تمیزتر است از روش مقاله‌ی قبلی و امکان استفاده‌ی از انواع و اقسام متدهای EF را به صورت متداولی میسر می‌کند):
 var products = context.Products.Include(x => x.Tags).Cacheable().FirstOrDefault(); // Async methods are supported too.

پس از آن نیاز است کدهای کلاس Context خود را نیز به نحو ذیل ویرایش کنید (به روز رسانی شده‌ی آن در اینجا):
namespace EFSecondLevelCache.TestDataLayer.DataLayer
{
    public class SampleContext : DbContext
    {
        // public DbSet<Product> Products { get; set; }
 
        public SampleContext()
            : base("connectionString1")
        {
        }
 
        public override int SaveChanges()
        {
            return SaveAllChanges(invalidateCacheDependencies: true);
        }
 
        public int SaveAllChanges(bool invalidateCacheDependencies = true)
        {
            var changedEntityNames = getChangedEntityNames();
            var result = base.SaveChanges();
            if (invalidateCacheDependencies)
            {
               new EFCacheServiceProvider().InvalidateCacheDependencies(changedEntityNames);
            }
            return result;
        }
 
        private string[] getChangedEntityNames()
        {
            return this.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()
                .ToArray();
        }
    }
}
متد InvalidateCacheDependencies سبب می‌شود تا اگر تغییری در بانک اطلاعاتی رخ‌داد، به صورت خودکار کش‌های کوئری‌های مرتبط غیر معتبر شوند و برنامه اطلاعات قدیمی را از کش نخواند.


کدهای کامل این پروژه را از مخزن کد ذیل می‌توانید دریافت کنید:
EFSecondLevelCache



پ.ن.
این کتابخانه هم اکنون در سایت جاری در حال استفاده است.

نظرات

  • اس حیدری در ۱۳۹۳/۱۱/۰۷ ۹:۹

    برای داشتن دو یا چند Context و یا تغییر کانکشن Context می‌توان از این Cash استفاده کرد؟

    چرا که کلید بر اساس معادل اسکیول عبارت Linq ایجاد می‌شود

    • وحید نصیری در ۱۳۹۳/۱۱/۰۷ ۹:۵۵
      رشته اتصالی هم در حین تولید کلید درنظر گرفته شده‌است. همچنین در صورت نیاز یک عبارت دلخواه را که به آن در اینجا saltKey گفته می‌شود، می‌توانید به رشته‌ی نهایی که از آن کلید تولید می‌شود، اضافه کنید. برای اینکار پارامتر EFCachePolicy را مقدار دهی کنید.
  • ایمان دارابی در ۱۳۹۳/۱۱/۰۷ ۹:۴۹
    این هم کتابخانه خوبی هست. البته expire شدن کش را با استفاده از تگ هندل می‌کنه. خوبیش اینه بچ دیلیت و آپدید و امکانات دیگه هم داره.
    می شه از تگ به صورت اتوماتیک با روش شما ایجاد کرد و از کش همین کتابخانه استفاده کرد.
    • وحید نصیری در ۱۳۹۳/۱۱/۰۷ ۱۰:۵
      در انتهای سطر دوم مطلب، به این کتابخانه اشاره شده‌است. این مشکلات را دارد:
      - چون از روش LINQ to Objects برای تهیه معادل رشته‌ای کوئری درخواستی استفاده می‌کند (دقیقا این روش: ^) قادر نیست Includeها و جوین‌های EF را پردازش کند و در این حالت برای تمام جوین‌ها یک هش مساوی را در سیستم خواهید داشت.
      - چون قادر نیست cache dependencies را از کوئری به صورت خودکار استخراج کند، شما نیاز خواهید داشت تا پارامتر تگ‌های آن‌را به صورت دستی به ازای هر کوئری تنظیم کنید. این‌کار به صورت خودکار در پروژه‌ی جاری انجام می‌شود. cache dependencies به این معنا است که کوئری جاری به چه موجودیت‌هایی در سیستم وابستگی دارد. از آن برای به روز رسانی کش استفاده می‌شود. برای مثال اگر یک کوئری به سه موجودیت وابستگی دارد، با تغییر یکی از آن‌ها، باید کش غیرمعتبر شده و در درخواست بعدی مجددا ساخته شود.
  • عیدی مراد در ۱۳۹۳/۱۱/۰۷ ۱۰:۲۲
    ظاهرا در حالت Lazy Loading زمانی که آبجکتی از کش لود میشه، پراپرتی‌های Navigation استثنای زیر را صادر میکنن:
    The ObjectContext instance has been disposed and can no longer be used for operations that require a connection 
    تیکه کدی که این ارور رو بر میگردونه:
    var userInRoles = user.UserInRoles.Union(user.UsersSurrogate.Where(a => a.SurrogateFromDate != null && a.SurrogateToDate != null && a.SurrogateFromDate <= DateTime.Now && a.SurrogateToDate >= DateTime.Now).SelectMany(a => a.UserInRoles));
      result = userInRoles.Any(a => a.Role.FormRoles.Any(b => b.IsActive && (b.Select && b.Form.SelectPath != null && b.Form.SelectPath.ToLower().Split(',').Contains(roleName))));

    • وحید نصیری در ۱۳۹۳/۱۱/۰۷ ۱۰:۴۴
      Lazy loading با کش سازگاری ندارد؛ چون اتصال اشیاء موجود در کش از context قطع شده‌اند. در بار اول فراخوانی یک کوئری که قرار است کش شود، از context و دیتابیس استفاده می‌شود. اما در بارهای بعد دیگر به context و دیتابیس مراجعه نخواهد شد. تمام اطلاعات از کش سیستم بارگذاری می‌شوند و حتی یک کوئری اضافی نیز به بانک اطلاعاتی ارسال نخواهد شد. به همین جهت در این موارد باید از متد Include برای eager loading اشیایی که نیاز دارید استفاده کنید.
  • اس حیدری در ۱۳۹۳/۱۱/۰۷ ۱۱:۲۹

    همچنین اتوماتیک بودن Cash به ازای کلیه Query‌ها هم می‌تواند یک آپشن در نظر گرفته شود و در مواری که دسترسی به کوئری‌های داخلی نیست مفید واقع شود.

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

    • وحید نصیری در ۱۳۹۳/۱۱/۰۷ ۱۱:۳۷
      - کش سطح دوم نباید برای کش کردن اطلاعات خصوصی استفاده شود؛ یا کلا اطلاعاتی که نیاز به سطح دسترسی دارند. هدف آن کش کردن اطلاعات عمومی و پر مصرف است. اطلاعات خاص یک کاربر نباید کش شوند.
      - در تمام سیستم‌ها، برای مواردی که به کوئری‌های آن دسترسی ندارید تا متد Cacheable را به آن‌ها اضافه کنید، نتیجه‌ی کوئری‌ها را باید خودتان از طریق روش‌های متداول کش کنید (مانند کلاس CacheManager مطلب یاد شده).
  • امین کاشانی در ۱۳۹۳/۱۲/۰۸ ۱:۲۵
    من در کد زیر expiretime  را 60 ثانیه گذاشتم .ولی در هربار فراخوانی در بازه زمانی 60 ثانیه  از کش نمی‌خواند و دیتا از دیتابیس برمی گرداند. ایراد کار کجاست؟ ولی بدون نوشتن  پارامتر زمان  در متد cacheable کش درست عمل می‌کند.
     public async Task<IList<Bestankaran>> GetBestankaran()
            {
             EFCachePolicy expirationTime = new EFCachePolicy { AbsoluteExpiration = new DateTime().AddSeconds(60) };
    
                var result =
                    Task.Run(() =>
                        _bestankaran.Cacheable(expirationTime).ToListAsync());
    
                return await result;
               
            }

    • وحید نصیری در ۱۳۹۳/۱۲/۰۸ ۱:۳۲
      - زمانیکه از متد ToListAsync استفاده می‌کنید، نیازی به استفاده از Task.Run نیست. اطلاعات بیشتر
      - بجای new DateTime باید از DateTime.Now استفاده کنید.
  • سالار ربال در ۱۳۹۴/۰۵/۰۹ ۱۶:۴۷
    با تشکر.
    آیا این کتابخانه با کتابخانه EntityFramework.Extended سازگاری دارد؟
    چون قصد  دارم از این دو  کنار هم استفاده کنم. یه کاری شبیه به کار زیر
      public IList<string> GetUserPermissions(int[] roleIds, int userId)
            {
                var permissionsOfRoles = (from p in _permissions
                                          from r in p.ApplicationRoles
                                          where roleIds.Contains(r.Id)
                                          select p.Name).Cacheable().Future();
    
                var permissionsOfUser = (from p in _permissions
                                         from r in p.AssignedUsers
                                         where userId == r.Id
                                         select p.Name).Cacheable().Future().ToList();
                return permissionsOfUser.Union(permissionsOfRoles).ToList();
            }
    ولی با خطای 
    The source query must be of type ObjectQuery or DbQuery.
    Parameter name: source
    [ArgumentException: The source query must be of type ObjectQuery or DbQuery.
    Parameter name: source]
       EntityFramework.Extensions.FutureExtensions.Future(IQueryable`1 source) +249
    مواجه شدم. که مشخص است برای اعمال متد Future باید مبدا از نوع IQueryable باشد.آیا اعمال متد AsQueryable در روند کار کتابخانه EFSecondLevelCache مشکلی ایجاد نخواهد شد؟
    • وحید نصیری در ۱۳۹۴/۰۵/۰۹ ۱۷:۲
      ترکیب ()Cacheable().Future غیرضروری است. چون این کوئری‌های Cacheable برای بار دوم به بعد، از کش و از حافظه خوانده می‌شوند و کاری به اطلاعات Context ندارند. بار اول اتصال به دیتابیس آن هم فقط یکبار انجام می‌شود و سربار آنچنانی ندارد.
  • علی ملکیان در ۱۳۹۴/۱۰/۰۳ ۱۹:۴۸
    آقای نصیری من برای صفحه بندی اطلاعات به این صورت کش میکنم اطلاعات رو :

                var source = users
                    .Include(d => d.RolesGroup)
                    .Skip(skipRecords)
                    .Take(recordsPerPage)
                    .Cacheable()
                    .ToList();

    درست عمل میکنه و فقط بار اول به دیتابیس متصل میشه ولی زمانی که یک رکورد رو حذف میکنم و متد SaveAllChanges رو هم صدا میزنم changedEntityNames همیشه خالی برگشت داده میشه.

    من به این صورت رکوردی رو حذف میکنم :

     public async Task<IdentityResult> RemoveByIdAsync(int userId)
            {
                var user = FindById(userId);
                var identityResult = await DeleteAsync(user);
                return identityResult;
            }

    به نظرتون مشکل از کجاست ؟
  • ایمان محمدی در ۱۳۹۵/۰۱/۱۹ ۱۲:۲۵
    من از این روش برای منو استفاده کردم ولی وقتی متد Cacheable()  رو به کوئری اضافه می‌کنم فرزندان بصورت درختی تشکیل نمی‌شوند:
    var li = _menus
                  .Where(m => m.Disable == false && m.LanguageId == culture)
                   .OrderBy(m => m.Order)
                   //.Cacheable()
                   .ToList()
                   .Where(m => m.ParentId == null)
                   .ToList();
  • وحید نصیری در ۱۳۹۵/۰۶/۲۸ ۱۵:۲۲
    جهت اطلاع
    کتابخانه‌ی مطلب جاری برای EF Core نیز بازنویسی شد. اطلاعات بیشتر
  • مصطفی سفاری در ۱۳۹۵/۰۷/۰۷ ۱۳:۳۵
    آقای نصیری 
    من یه کلاس کش دارم و یک کلاس سرویس که برای کش کردن اطلاعات باید کوئری رو از داخل سرویس به کش پاس بدم و در کلاس کش از Cachable استفاده می‌کنم. همه چی خوب کار می‌کنه بجز Include جواب نمیده 
    //‍function in cache class
    public IQueryable<T> FindAll(bool doCache)
            {
                if (doCache)
                    return _repository.FindAll(doCache).Cacheable();
                else
                    return _repository.FindAll(doCache);
    
            }
    // دستور در کلاس سرویس
    _cache.FindAll(true).Include(s=>s.Tag)

    • وحید نصیری در ۱۳۹۵/۰۷/۰۷ ۱۳:۵۲
      Include باید قبل از Cacheable و حتی قبل از FindAll شما فراخوانی شود. باید پس از ارجاعی به dbSet فراخوانی شود، تا EF بتواند چرخش کاری صحیح آن‌را تشکیل دهد. برای بار دوم فراخوانی، پس از Cacheable، شما یک لیست ساده‌ی LINQ to Objects را خواهید داشت که از Context منقطع است و صرفا از کش خوانده شده‌است. بنابراین فراخوانی متد Include بر روی آن مفهومی ندارد.
      • مصطفی سفاری در ۱۳۹۵/۰۷/۰۷ ۱۵:۲۴
        ممنون از پاسخ کاملتون 
        با فهمیدن این نکته کلاس repository رو به این روش تغییر دادم و کلاس کش رو هم اصلاح کردم 
        //Function in Repository Class 
        public IQueryable<T> FindAll(bool doCache, params Expression<Func<T, object>>[] includes)
                {
                    return includes.Aggregate<Expression<Func<T, object>>, IQueryable<T>>(
                         _dbset, (current, experssion) => current.Include(experssion));
                }
        //function in cache class
        public IQueryable<T> FindAll(bool doCache, params Expression<Func<T, object>>[] includes)
                {
                    if (doCache)
                    {
                        var result = _repository.FindAll(doCache, includes).Cacheable();
                        return result;
                    }
                    else
                        return _repository.FindAll(doCache, includes);
                }
        //Call  in service class
        _cache.FindAll(true,s=>s.Tag)

        • وحید نصیری در ۱۳۹۵/۰۷/۰۷ ۱۷:۴۸
          من اساسا مخالف هر نوع generic repository هستم. شما الان یک کد «ساده‌» و «زیبای» EF را تبدیل به یک کد پیچیده‌ی غیرقابل درک کردید. این نه پیشرفتی است و نه بر اساس هیچکدام از الگوهای برنامه نویسی.
          در این زمینه یک سری مطلب در سایت هستند برای مطالعه:
          -Repository‌ها روی UnitOfWork ایده خوبی نیستند
          - پیاده سازی generic repository یک ضد الگو است 
          - استفاده از الگوی Repository در EF Code First کار اضافی است.
          - 5 نکته برای بهبود کیفیت کدهای برنامه‌ای که از یک ORM استفاده می‌کند  
          • مصطفی سفاری در ۱۳۹۵/۰۷/۰۸ ۸:۴۲
            ممنون؛ فقط، من این اعمال را برای این انجام دادم که درحقیقت کلاس کش و لاگ و Repository در یک جا باشد و سعی کردم DI را روی آنها رعایت کنم تا با هر تراکنش حتما لاگ و کش به صورت صحیح اجرا شود و کلاس Repository  در نهایت تمامی این اعمال را انجام دهد، چون اگر دست برنامه نویس باشد ممکن است یک جا لاگ بگذارد یک جا خیر. و چون پروژه بزرگ است در صورتی که دو سال آینده به این نتیجه رسیدیم که مثلا لاگ بدرد نمی‌خورد و می‌توان لاگ بهتری پیاده سازی کرد فقط کلاس لاگ را عوض کنیم، درمورد کش و Repository  هم همینطور. در مورد DbContext Life time هم با Autofac  آن را کنترل می‌کنم.
  • مصطفی سفاری در ۱۳۹۵/۰۷/۱۰ ۱۳:۵۲
    من تست کردم روی
     context.DataBase.SqlQuery<x>("Test").Cachable()
    جواب میده.
     فقط می‌خواستم بدونم دچار مشکل نمی‌شیم با save 
    مثلا اینجا یه پروسیجر رو فراخوانی کردیم که شامل رکوردهای محاسباتی customer هست و حال مقادیر جدید برای customer با دستور saveChange انجام می‌شود. آیا کش پروسیجر هم خالی می‌شود.
    • وحید نصیری در ۱۳۹۵/۰۷/۱۰ ۱۴:۵
      «آیا کش پروسیجر هم خالی می‌شود.»
      خیر. هرچیزی که جزئیات آن تحت نظر Context نباشد، this.ChangeTracker از وجود آن‌ها بی‌اطلاع است. همچنین هر کوئری که جزئیات آن تحت نظر Context نباشد، محاسبه‌ی وابستگی‌های آن ممکن نیست. در یک چنین حالتی فقط می‌توانید کل کش را وادار به روز رسانی کنید.
  • کمال ح در ۱۳۹۵/۰۹/۱۰ ۲۲:۹
    آیا راهی وجود داره که به وسیله کد نویسی بشه  تمام اطلاعات کش رو پاک کرد؟ 
    به این خاطر که بعضی وقت‌ها مدیر سامانه به صورت خود سر وارد سرور میشه و اطلاعات دیتابیس رو دستی تغییر میده. توی همچین مواقعی برنامه با مشکل روبرو میشه.
    آیا راه کاری برای نمایش صحیح اطلاعات وجود دارد؟
    • وحید نصیری در ۱۳۹۵/۰۹/۱۰ ۲۳:۱۵
      پروایدر پیش فرض این کش، همان HttpRuntime.Cache استاندارد است. بنابراین هر روشی که برای خالی کردن آن بکار بگیرید، اینجا هم تاثیرگذار است:
      new EFCacheServiceProvider().ClearAllCachedEntries();
  • روح اله زارعی در ۱۳۹۵/۰۹/۳۰ ۱۲:۱
    کش در سمت سرور انجام میشه یا کلاینت؟
    اگر بخواهیم مکانش رو تغییر بدیم باید چیکار بکنیم؟
  • روح اله زارعی در ۱۳۹۵/۰۹/۳۰ ۲۳:۵۸
    تمام مراحل کش سطح دوم رو انجام دادم . چندبار تست گرفتم کار می‌کرد یعنی مقادیر دیتابیس رو بصورت دستی تغییر میدادم ولی در صفحات سایت من همان داده‌های قبلی را نشان می‌داد. الان دیگه کار نمی‌کنه ( احتمال می‌دهم بعد از ویرایش یک رکورد در برنامه )
    • وحید نصیری در ۱۳۹۵/۱۰/۰۱ ۰:۵۸
      تمام نکات «چه زمان‌هایی یک برنامه‌ی ASP.NET ری استارت می‌شود؟ » و کلا ری استارت شدن برنامه، HttpRuntime.Cache را هم پاک می‌کنند. بنابراین لاگ کنید که رخداد Application_Start چندبار در برنامه‌ی شما اجرا می‌شود.
      • روح اله زارعی در ۱۳۹۵/۱۰/۰۳ ۱۷:۸
        Application_Start یکبار اجرا می‌گردد. ضمنا lazyloading و tracking را خاموش کردم. یک نمونه کد هم بصورت زیر می‌باشد.
        var id = User.GetClaimValue("IdHamayesh").ToInt();
        var db = new ApplicationDbContext();
        var f= db.Faq.Cacheable().Where(x=>x.IdHamayesh == id && x.IdLanguage == idLanguage).OrderBy(x=>x.Order);
        return PartialView(f);
        • وحید نصیری در ۱۳۹۵/۱۰/۰۳ ۱۸:۲۵
          var f = db.Faq.Where(x=>x.IdHamayesh == id && x.IdLanguage == idLanguage).OrderBy(x=>x.Order).Cacheable().ToList();
  • روح اله زارعی در ۱۳۹۵/۱۰/۰۸ ۸:۴۹
    چطور میتونیم کش رو برای ذخیره تنظیمات متغیر مربوط به وب اپلیکیشن فعال کنیم؟
    • وحید نصیری در ۱۳۹۵/۱۰/۰۸ ۹:۱۴
            public virtual void Load(IUnitOfWork unitOfWork)
              {
                  //3 get setting for this type name
                  var settings = unitOfWork.Settings.Where(w => w.Type == _name).Cacheable().ToList();
      • روح اله زارعی در ۱۳۹۵/۱۰/۰۸ ۹:۳۶
        این روش رو استفاده کرده بودم. مشکلی که وجود داره اینه که بعد تغییر مقدار متغیر و ذخیره در دیتابیس ، تغییرات در کش اعمال نمیشه یعنی تابع load همچنان مقدار قبلی متغیر که در کش موجود هست رو بر می‌گردونه.
        • وحید نصیری در ۱۳۹۵/۱۰/۰۸ ۹:۴۱
          جائیکه InvalidateCacheDependencies در متد SaveChanges فراخوانی شده، یک break point قرار دهید و بررسی کنید که آیا فراخوانی می‌شود یا خیر و همچنین پارامتر changedEntityNames آن چه مقداری را دارد. به علاوه از کلاس جدید ChangeTrackerExtenstions آن نیز برای تامین this.GetChangedEntityNames استفاده کنید.
          • روح اله زارعی در ۱۳۹۵/۱۰/۰۸ ۱۰:۴۸
            تابع InvalidateCacheDependencies  فراخوانی می‌شود و مقدار changedEntityNames برابر string[0] می‌باشد.  کلاس جدید  ChangeTrackerExtenstions   را اعمال کردم در زمان اجرا با خطای زیر مواجه شدم. 
             Trace.WriteLine(string.Format("Changed Entity Names: {0}", changedEntityNames.Aggregate((e1, e2) => string.Format("{0}, {1}", e1, e2))))
            • وحید نصیری در ۱۳۹۵/۱۰/۰۸ ۱۱:۱۰
              - این سطر فقط برای دیباگ هست. حذفش کنید.
              - اگر change tracking را خاموش کردید، نیاز هست ابتدای متد SaveAllChanges کار فراخوانی this.ChangeTracker.DetectChanges را دستی انجام دهید تا لیست موجودیت‌های تغییر کرده قابل گزارشگیری باشد.
  • روح اله زارعی در ۱۳۹۵/۱۱/۳۰ ۱۳:۵۰
    اگر کد زیر را در دو اکشن در دو کنترلر مختلف استفاده کنم آیا کش اطلاعات یکی است یا به ازای هر اکشن یک کش در حافظه ایجاد میشه؟
    var text= context.tbl1.Include(x => x.tbl2).Cacheable().FirstOrDefault();

    • وحید نصیری در ۱۳۹۵/۱۱/۳۰ ۱۴:۳
      یکی هست. این کش (HttpRuntime.Cache) سراسری است.
  • روح اله زارعی در ۱۳۹۵/۱۲/۰۲ ۱۷:۵۶
    زمانی که در کش کردن کوئری از دستور select  استفاده کنیم گاهی وقتا با خطای زیر مواجه میشیم

    • وحید نصیری در ۱۳۹۵/۱۲/۰۲ ۱۹:۵
      ربطی به select ندارد. کامپایلر برای Anonymous type استفاده شده یک کد را تولید می‌کند و برای Anonymous type کش شده یک کد پویای دیگر را و تبدیل این دو به هم میسر نیست (Unable to cast one anonymous type to another). در این حالت خاص بجای Anonymous types، یک نوع جدید (یک کلاس POCO جدید) را تعریف کنید.
  • ابوالفضل بهاری س در ۱۳۹۶/۰۲/۱۶ ۱۳:۱۰
    سلام؛ من از این روش در حالت عادی (بدون استفاده از Await / Async) استفاده میکنم به درستی Cash انجام می‌گیرد
    اما وقتی از Await / Async استفاده میکنم هربار اطلاعات از DB فراخوانی می‌شود مشکل کار من کجاست؟
            private readonly IDbSet<Hotel> _hotels;
                _hotels = _unitOfWork.Set<Hotel>();
             
    
       var autoCompletesViewModel = await _hotels.Include(row => row.Profile.City.State.Country).Where(row => row.IsApproved && row.Profile.IsDeleted == false && row.Profile.IsDeactivated == false).Cacheable().ProjectToListAsync<AutoCompleteHotelViewModel>(_mapper.ConfigurationProvider);

    • وحید نصیری در ۱۳۹۶/۰۲/۱۶ ۱۳:۲۲
      - با AutoMapper و متد ProjectToListAsync آن آزمایش نشده‌است و بعید هم می‌دانم که با آن کار کند چون ProjectTo آن تفسیر دیگری دارد و با متدهای متداول LINQ یکی نیست.
      - اما در کل مشکلی با متدهای Async اصلی EF ندارد و به اندازه‌ی کافی در این مورد آزمون‌های واحد تهیه شده‌است.
      • ابوالفضل بهاری س در ۱۳۹۶/۰۲/۱۶ ۱۳:۳۰
        این مورد رو هم جداگانه به همراه Automapper تست کردم بدون هیچ مشکلی Cash شد
                    var autoCompletesViewModelTest = _hotels.Include(row => row.Profile.City.State.Country).Where(row => row.IsApproved && row.Profile.IsDeleted == false && row.Profile.IsDeactivated == false).Cacheable().ProjectToList<AutoCompleteHotelViewModel>(_mapper.ConfigurationProvider);
        اما بعد به صورت زیر استفاده کردم Cash صورت نمی‌پذیرد.
                    var autoCompletesViewModel = await _hotels.Include(row => row.Profile.City.State.Country).Where(row => row.IsApproved && row.Profile.IsDeleted == false && row.Profile.IsDeactivated == false).Cacheable().ProjectToListAsync<AutoCompleteHotelViewModel>(_mapper.ConfigurationProvider);

        • وحید نصیری در ۱۳۹۶/۰۲/۱۶ ۱۳:۳۴
          - من قصد پشتیبانی AutoMapper را ندارم.
          - مشکلی با متدهای Async اصلی EF ندارد و به اندازه‌ی کافی در این مورد آزمون‌های واحد تهیه شده‌است.  
          - ضمنا این پروژه هیچ به روز رسانی دیگری را نخواهد داشت و نسخه‌ی جدید آن، نسخه‌ی EF Core آن است.
  • محمد رعیت پیشه در ۱۳۹۷/۰۴/۱۷ ۱۲:۴۷
    کوئری من به صورت زیر هست
    _uow.Set<Post>().Include(x => x.Comments).Cacheable().ToList()
    و در اغلب مواقع بدون خطا کار میکنه ولی در بعضی مواقع (که کم هم نیست) خطای NullReferenceException رو میده. این کد توسط api به صورت مداوم در حال صدا زده شدن هست و وقتی که این خطا رو میده توی try catch با دوباره اجرا کردنش خطایی رخ نمیده!
    System.NullReferenceException: Object reference not set to an instance of an object. at System.Data.Entity.Internal.Linq.InternalQuery`1.Include(String path) at System.Data.Entity.Infrastructure.DbQuery`1.Include(String path) at System.Data.Entity.QueryableExtensions.Include[T,TProperty](IQueryable`1 source, Expression`1 path)

  • علی نقوی در ۱۴۰۰/۰۵/۲۶ ۱۸:۴۵
    درود
    وقت بخیر
    من چندتا سوال داشتم
    من از CacheManager.Core , CacheManager.StackExchange.Redis استفاده کردم
    1- توی redis-cli دستور get keyName رو که میزنم عبارت nill رو میاره که یعنی خالیه ، چجوری میتونم تو cli مقدار کلید رو ببینم
    2- ظاهرا هز cli تا 16 تا دیتابیس رو قبول میکنه ، این 16 تا دیتابیس رو فقط تو یه پروژه میشه استفاده کرد، نمیشه با یک cli چندتا پروژه رو دیتابیس‌های مختلف پیاده کرد (مثلا db0 برای یک پروژه ، db1 برای یک پروژه و ... ) ؟
    3-من الان دوتا پروژه دارم که تو جفتش سطح دوم کش رو پیاده کردم و جفتش رو یه سرور هست،پروژه ای که اول run میشه مشکل نداره ولی پروژه بعدی که run میکنم خطای زیر رو میده، حتی رو یک port دیگه هم redis رو run کردم و تو پروژه تنظیم کردم ولی بازم همین خطا رو میده
    Only one usage of each socket address (protocol/network address/port) is normally permitted.  
    4-ظاهرا شما چندتا کتابخانه توسعه دادید برای سطح دوم کش، کدومش اخرین ورژنه
    من از https://github.com/VahidN/EFCoreSecondLevelCacheInterceptor استفاده کردم

    ببخشید سوالاتم زیاد شد
    ممنونم
    • وحید نصیری در ۱۴۰۰/۰۵/۲۶ ۲۰:۱۱
      - از برنامه‌هایی مانند «AnotherRedisDesktopManager » استفاده کنید.
      - «یکبار» که سرور Redis اجرا شد (نه چند بار)، روش کار با آن مانند SQL Server است. فقط در اینجا در رشته‌ی اتصالی که ساخته می‌شود، نام یا شماره دیتابیس را هم باید مشخص کنید؛ مانند WithDatabase(0). یا حتی می‌توانید با یک دیتابیس هم کار کنید، اما برای cache-keyها پیشوند تعیین کنید تا با هم تداخل نکنند: UseCacheKeyPrefix  (این روش توسط نویسنده‌ی اصلی Redis هم توصیه شده‌است)
      - بله. همانیکه که آرشیو نشده.
      + redis را باید با داکر اجرا کنید؛ اگر آخرین نگارش آن‌را می‌خواهید.
      + CacheManager.Core را با easy-caching جایگزین کنید چون دیگر توسط نویسنده‌ی آن نگهداری نمی‌شود.