Включение EF Core по-прежнему откладывает загрузку?

#asp.net-core-webapi #asp.net-core-2.2 #ef-core-2.2

#asp.net-core-webapi #asp.net-core-2.2 #ef-core-2.2

Вопрос:

В Asp.Net Проект Core 2.2 с EF core (все последние версии, сегодня были запущены все обновления NuGet) У меня есть эта операция:

 return Ok(_db.GlobalRoles
             .Include(gr => gr.GlobalRoleFeatures)
                 .ThenInclude(grf => grf.Feature)
             .Include(gr => gr.GlobalRoleCompanyGroupRoles)
                 .ThenInclude(grcgr => grcgr.CompanyGroupRole)
                     .ThenInclude(cgr => cgr.CompanyGroupRoleFeatures)
                         .ThenInclude(cgrf => cgrf.Feature)
             .ToList());
  

По большей части детали не важны, достаточно сказать, что это дерево объектов, которое я хочу загрузить с нетерпением. Когда я профилирую базу данных, это в конечном итоге приводит к 4 запросам. Сначала я счел это неожиданным, но отмахнулся от этого, поскольку, возможно, именно так EF оптимизировал получение этих результатов. Ничего страшного. И результирующие данные верны.

Но когда я оборачиваю это в IMemoryCache :

 return Ok(_cache.GetOrCreate(nameof(GlobalRole), entry =>
{
    entry.SlidingExpiration = TimeSpan.FromMinutes(_appSettings.DataCacherExpiryMinutes);
    return _db.GlobalRoles
              .Include(gr => gr.GlobalRoleFeatures)
                  .ThenInclude(grf => grf.Feature)
              .Include(gr => gr.GlobalRoleCompanyGroupRoles)
                  .ThenInclude(grcgr => grcgr.CompanyGroupRole)
                      .ThenInclude(cgr => cgr.CompanyGroupRoleFeatures)
                          .ThenInclude(cgrf => cgrf.Feature)
              .ToList();
}));
  

Хотя первая выборка этих данных работает должным образом, последующие выборки из кэша приводят к исключению:

Newtonsoft.Json.Исключение JsonSerializationException: ошибка при получении значения из ‘GlobalRoleCompanyGroupRoles’ в ‘Castle.Прокси.GlobalRoleProxy’. —> Система.Исключение InvalidOperationException: ошибка, сгенерированная для предупреждения «Microsoft.EntityFrameworkCore.Инфраструктура.LazyLoadOnDisposedContextWarning: была предпринята попытка отложенной загрузки навигационного свойства ‘GlobalRoleCompanyGroupRoles’ для объекта типа ‘GlobalRoleProxy’ после удаления связанного DbContext.’. Это исключение можно подавить или зарегистрировать, передав идентификатор события ‘CoreEventId.LazyLoadOnDisposedContextWarning’ методу ‘ConfigureWarnings’ в ‘DbContext.При настройке’ или ‘AddDbContext’.

Похоже, что при сериализации объекта нет загруженных списков содержащихся объектов. (Или, возможно, они есть, но он все еще пытается загрузить их снова? Или каким-то образом запрашивает контекст?) Естественно, экземпляр контекста уже давно удален, просто полностью материализованный список должен быть кэширован.

При отладке список верхнего уровня действительно возвращается из кэша. Но после проверки GlobalRoleFeatures и GlobalRoleCompanyGroupRoles свойств любого объекта в нем возникает то же самое исключение, что и выше.

Примечание: Такое же поведение используется .ToListAsync() в запросе и async на всем протяжении .GetOrCreateAsync() и через действие контроллера.

Я что-то упускаю из виду? Есть ли способ перенести полностью материализованный список, больше не зависящий от контекста БД, в кэш памяти?

Ответ №1:

Проблема в использовании IMemoryCache . На самом деле вы не сериализуете элементы в свой кэш. Объекты кэшируются непосредственно в памяти, что означает, что связи, которые они имеют с такими вещами, как a, DbContext сохраняются, даже если DbContext этого не происходит.

В частности, способ работы с отложенной загрузкой заключается в том, что EF фактически создает динамический прокси-сервер вашего класса сущностей и переопределяет (отсюда и необходимость в virtual ключевом слове) свойство reference или collection с помощью customer getter, который проверяет кэш объектов EF на наличие элементов, и, если они не могут быть найдены, выполняет запрос для их получения. Поскольку вы кэшируете непосредственно в памяти, вы кэшируете эти экземпляры прокси-класса, в которых все еще используется эта логика.

Это плохая идея использовать IMemoryCache независимо. Вместо этого вы всегда должны использовать IDistributedCache . Есть MemoryDistributedCache поставщик (который на самом деле используется по умолчанию), если вы все еще хотите кэшировать в памяти, но использование IDistributedCache выполняет за вас две вещи:

  1. Это более универсально, чем IMemoryCache , поэтому позже вы можете использовать замену в любом поставщике кэша (Redis, SQL Server и т.д.) Без изменения кода вашего приложения.

  2. Конкретно к вашей проблеме здесь, это заставит вас фактически сериализовать значение кэша, даже если вы используете поставщика кэша памяти, что означает, что у вас не возникнет такой же неочевидной проблемы.

Это означает, что требуется немного больше работы. Вам нужно будет использовать что-то вроде JsonConvert для сериализации и десериализации в / из кэша, но вы можете добавить расширения к IDistributedCache , чтобы позаботиться об этом за вас.

Комментарии:

1. Интересно. У меня сложилось (по-видимому, ошибочное) впечатление, что .ToList() все материализуется до того, как оно будет помещено в кэш. Ручная сериализация в IDistributedCache не должна быть проблемой, и гибкость действительно кажется полезной в будущем для этого проекта. Я попробую, спасибо!

2. .ToList() выполняется ли запрос. У вас есть полный объектный граф со всеми включенными объектами. Проблема в том, что связанные элементы на самом деле не находятся в классе. Как я уже сказал, есть пользовательский механизм получения, который извлекает их из кэша объектов или выполняет запрос для их получения. Ваши включения служат для заполнения этого объектного кэша, поэтому ему не нужно выполнять больше запросов. Однако после того, как эти объекты были извлечены из кэша памяти, он все еще пытается найти связанные элементы в кэше объектов контекста, но контекст, конечно, исчез. В этом проблема.