Дополнительные разделы о производительности

Создание пулов DbContext

Легкий объект, как правило: создание и удаление объекта не включает операцию базы данных, и большинство приложений могут выполнять это без заметного эффекта на производительность. Однако каждый экземпляр контекста настраивает различные внутренние службы и объекты, необходимые для выполнения своих обязанностей, и затраты на непрерывное выполнение этого могут быть значительными в сценариях высокой производительности. В таких случаях EF Core может использовать пул для экземпляров контекста: при удалении контекста EF Core сбрасывает его состояние и сохраняет его во внутреннем пуле. При следующем запросе вместо создания нового экземпляра возвращается экземпляр из пула. Пул контекстов позволяет платить затраты на настройку контекста только один раз при запуске программы, а не непрерывно.

Обратите внимание, что пул контекстов является ортогональным для пула подключений к базе данных, который управляется на более низком уровне в драйвере базы данных.

Типичный шаблон в приложении ASP.NET Core, используя EF Core, включает регистрацию пользовательского DbContext типа в контейнер внедрения зависимостей через . Затем экземпляры этого типа получаются с помощью параметров конструктора в контроллерах или Razor Pages.

Чтобы включить пул контекстов, просто замените AddDbContext на AddDbContextPool.

builder.Services.AddDbContextPool<WeatherForecastContext>(
    o => o.UseSqlServer(builder.Configuration.GetConnectionString("WeatherForecastContext")));

Параметр poolSizeAddDbContextPool задает максимальное количество экземпляров, сохраненных пулом (по умолчанию — 1024). После превышения poolSize новые экземпляры контекста не сохраняются в кэше, и EF возвращается к поведению без использования пула, создавая экземпляры по мере необходимости.

Тесты производительности

Ниже приведены результаты сравнительных тестов для получения одной строки из базы данных SQL Server, работающей локально на той же машине, с пулом контекстов и без него. Как всегда, результаты будут меняться с количеством строк, задержкой на сервере базы данных и другими факторами. Важно отметить, что это тесты производительности пула с одним потоком, в то время как реальный сценарий с конкурирующими потоками может иметь другие результаты; оцените производительность на вашей платформе перед принятием решений. Исходный код доступен здесь, вы можете использовать его в качестве основы для собственных измерений.

Способ NumBlogs Среднее Ошибка StdDev 0-го поколения Поколение 1 Поколение 2 Распределено
Без ContextPooling 1 701.6 мы 26.62 нас 78.48 мы 11.7188 - - 50,38 КБ
WithContextPooling 1 350.1 мы 6.80 нас 14.64 мы 0.9766 - - 4.63 КБ

Управление состоянием в пулах контекстов

Пул контекстов работает путем повторного использования одного и того же экземпляра контекста в запросах; это означает, что он фактически зарегистрирован в качестве Singleton, и один и тот же экземпляр повторно используется в нескольких запросах (или областей внедрения зависимостей (DI)). Это означает, что при работе с контекстом необходимо учитывать любое состояние, которое может изменяться между запросами. Крайне важно, что контекст OnConfiguring вызывается только один раз - когда создается контекст экземпляра, - и поэтому не может быть использован для установки состояния, которое должно изменяться (например, идентификатор арендатора).

Типичный сценарий с состоянием контекста будет мультитенантным приложением ASP.NET Core, где экземпляр контекста имеет идентификатор клиента, который учитывается запросами (дополнительные сведения см. в разделе "Глобальные фильтры запросов"). Поскольку идентификатор арендатора должен изменяться с каждым веб-запросом, необходимо предпринять дополнительные шаги, чтобы обеспечить его работу с пулом контекстов.

Предположим, что приложение регистрирует службу с областью действия ITenant , которая упаковывает идентификатор клиента и любую другую информацию, связанную с клиентом:

// Below is a minimal tenant resolution strategy, which registers a scoped ITenant service in DI.
// In this sample, we simply accept the tenant ID as a request query, which means that a client can impersonate any
// tenant. In a real application, the tenant ID would be set based on secure authentication data.
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ITenant>(sp =>
{
    var tenantIdString = sp.GetRequiredService<IHttpContextAccessor>().HttpContext.Request.Query["TenantId"];

    return tenantIdString != StringValues.Empty && int.TryParse(tenantIdString, out var tenantId)
        ? new Tenant(tenantId)
        : null;
});

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

После того как у нас есть наша служба с заданной сферой, зарегистрируйте фабрику пуловых контекстов в качестве службы Singleton, как обычно.

builder.Services.AddPooledDbContextFactory<WeatherForecastContext>(
    o => o.UseSqlServer(builder.Configuration.GetConnectionString("WeatherForecastContext")));

Затем напишите настраиваемую фабрику контекста, которая извлекает контекст из пула, управляемого зарегистрированной фабрикой Singleton, и внедряет идентификатор арендатора в экземпляры контекста, которые она передает:

public class WeatherForecastScopedFactory : IDbContextFactory<WeatherForecastContext>
{
    private const int DefaultTenantId = -1;

    private readonly IDbContextFactory<WeatherForecastContext> _pooledFactory;
    private readonly int _tenantId;

    public WeatherForecastScopedFactory(
        IDbContextFactory<WeatherForecastContext> pooledFactory,
        ITenant tenant)
    {
        _pooledFactory = pooledFactory;
        _tenantId = tenant?.TenantId ?? DefaultTenantId;
    }

    public WeatherForecastContext CreateDbContext()
    {
        var context = _pooledFactory.CreateDbContext();
        context.TenantId = _tenantId;
        return context;
    }
}

После создания настраиваемой фабрики контекстов зарегистрируйте его в качестве службы с областью действия:

builder.Services.AddScoped<WeatherForecastScopedFactory>();

Наконец, организуйте внедрение контекста из нашей фабрики с областью действия.

builder.Services.AddScoped(
    sp => sp.GetRequiredService<WeatherForecastScopedFactory>().CreateDbContext());

На этом этапе ваши контроллеры автоматически получают экземпляр контекста с правильным идентификатором арендатора, и при этом ничего не требуется знать об этом.

Полный исходный код для этого примера доступен здесь.

Примечание.

Хотя EF Core отвечает за сброс внутреннего состояния для DbContext и связанных служб, обычно он не сбрасывает состояние в базовом драйвере базы данных, который выходит за рамки EF. Например, если вы вручную открываете и используете DbConnection или иным образом управляете состоянием ADO.NET, вам следует восстановить это состояние перед возвратом экземпляра контекста в пул, например, закрыв соединение. Если этого не сделать, это может привести к утечке состояния через несвязанные запросы.

Рекомендации по пулу подключений

В большинстве баз данных для выполнения операций базы данных требуется длительное подключение, поэтому такие подключения могут быть дорогостоящими для открытия и закрытия. EF не реализует сам пул подключений, но использует базовый драйвер базы данных (например, драйвер ADO.NET) для управления подключениями к базе данных. Пул подключений — это клиентский механизм, который повторно использует существующие подключения к базе данных, чтобы сократить затраты на открытие и закрытие подключений многократно. Этот механизм обычно согласован между базами данных, поддерживаемыми EF, такими как База данных SQL Azure, PostgreSQL и другие. Хотя факторы, относящиеся к базе данных или среде, например ограничения ресурсов или конфигурации служб, могут повлиять на эффективность пула. Пул подключений обычно включен по умолчанию, и любая конфигурация пула должна выполняться на низкоуровневом уровне драйвера, как описано этим драйвером; Например, при использовании ADO.NET параметры, такие как минимальный или максимальный размер пула, обычно настраиваются с помощью строки подключения.

Пул подключений полностью ортогонален пулу EF DbContext, который описан выше: в то время как низкоуровневый драйвер базы данных использует пул подключений (чтобы избежать затрат на открытие и закрытие подключений), EF может использовать пулирование экземпляров контекста (чтобы избежать затрат на выделение памяти и инициализацию контекста). Независимо от того, спулирован ли экземпляр контекста или нет, EF обычно открывает подключения непосредственно перед каждой операцией (например, запросом) и закрывает их сразу же после, что приводит к тому, что они возвращаются в пул. Это делается для того, чтобы избежать удерживания подключений вне пула дольше, чем это необходимо.

Скомпилированные запросы

Когда EF получает дерево запросов LINQ для выполнения, он должен сначала скомпилировать это дерево, например создать SQL из него. Так как эта задача является тяжелым процессом, EF кэширует запросы по фигуре дерева запросов, поэтому запросы с той же структурой повторно используют внутренние кэшированные выходные данные компиляции. Это кэширование гарантирует, что выполнение одного запроса LINQ несколько раз очень быстро, даже если значения параметров отличаются.

Однако EF по-прежнему должен выполнять определенные задачи, прежде чем он сможет использовать внутренний кэш запросов. Например, дерево выражений запроса должно быть рекурсивно по сравнению с деревьями выражений кэшированных запросов, чтобы найти правильный кэшированный запрос. Затраты на эту начальную обработку незначительны в большинстве приложений EF, особенно при сравнении с другими затратами, связанными с выполнением запросов (сетевые операции ввода-вывода, фактические операции обработки запросов и операций ввода-вывода на диске в базе данных...). Однако в некоторых сценариях высокой производительности ее может потребоваться устранить.

EF поддерживает скомпилированные запросы, которые позволяют явно компилировать запрос LINQ в делегат .NET. После получения этого делегата его можно вызвать непосредственно для выполнения запроса, не предоставляя дерево выражений LINQ. Этот метод обходится без обращения к кэшу и предоставляет оптимальный способ выполнения запроса в EF Core. Ниже приведены некоторые результаты тестирования, сравнивающие скомпилированные и нескомпилированные производительность запросов; тестируйте на платформе перед принятием каких-либо решений. Исходный код доступен здесь, вы можете использовать его в качестве основы для собственных измерений.

Способ NumBlogs Среднее Ошибка StdDev 0-го поколения Распределено
WithCompiledQuery 1 564.2 нас 6.75 нас 5.99 $ 1.9531 9 КБ
Без CompiledQuery 1 671.6 мы 12.72 нас 16.54 мы 2.9297 13 КБ
WithCompiledQuery 10 645.3 мы 10.00 нас 9.35 нас 2.9297 13 КБ
Без CompiledQuery 10 709.8 нас 25.20 нас 73.10 мы 3.9063 18 КБ

Чтобы использовать скомпилированные запросы, сначала скомпилируйте запрос EF.CompileAsyncQuery следующим образом (используйте EF.CompileQuery для синхронных запросов):

private static readonly Func<BloggingContext, int, IAsyncEnumerable<Blog>> _compiledQuery
    = EF.CompileAsyncQuery(
        (BloggingContext context, int length) => context.Blogs.Where(b => b.Url.StartsWith("http://") && b.Url.Length == length));

В этом примере кода мы предоставляем EF лямбда-функцию, принимающую экземпляр DbContext, и произвольный параметр, который передается в запрос. Теперь этот делегат можно вызвать каждый раз, когда вы хотите выполнить запрос:

await foreach (var blog in _compiledQuery(context, 8))
{
    // Do something with the results
}

Обратите внимание, что делегат является потокобезопасным и может вызываться одновременно в различных экземплярах контекста.

Ограничения

  • Скомпилированные запросы могут использоваться только для одной модели EF Core. Различные экземпляры контекста одного типа иногда можно настроить для использования различных моделей; Выполнение скомпилированных запросов в этом сценарии не поддерживается.
  • При использовании параметров в скомпилированных запросах используйте простые скалярные параметры. Более сложные выражения параметров, такие как доступ к свойствам или методам экземпляров, не поддерживаются.

Кэширование запросов и параметризация

Когда EF получает дерево запросов LINQ для выполнения, он должен сначала скомпилировать это дерево, например создать SQL из него. Так как эта задача является тяжелым процессом, EF кэширует запросы по фигуре дерева запросов, поэтому запросы с той же структурой повторно используют внутренние кэшированные выходные данные компиляции. Это кэширование гарантирует, что выполнение одного запроса LINQ несколько раз очень быстро, даже если значения параметров отличаются.

Рассмотрим следующие два запроса:

var post1 = await context.Posts.FirstOrDefaultAsync(p => p.Title == "post1");
var post2 = await context.Posts.FirstOrDefaultAsync(p => p.Title == "post2");

Поскольку деревья выражений содержат разные константы, каждый из них отличается, и каждый из этих запросов будет скомпилирован отдельно с помощью EF Core. Кроме того, каждый запрос создает немного другую команду SQL:

SELECT TOP(1) [b].[Id], [b].[Name]
FROM [Posts] AS [b]
WHERE [b].[Name] = N'post1'

SELECT TOP(1) [b].[Id], [b].[Name]
FROM [Posts] AS [b]
WHERE [b].[Name] = N'post2'

Поскольку SQL отличается, вашему серверу баз данных, скорее всего, потребуется создать план запроса для обоих запросов, а не использовать тот же план повторно.

Небольшое изменение запросов может значительно изменить ситуацию:

var postTitle = "post1";
var post1 = await context.Posts.FirstOrDefaultAsync(p => p.Title == postTitle);
postTitle = "post2";
var post2 = await context.Posts.FirstOrDefaultAsync(p => p.Title == postTitle);

Так как имя блога теперь параметризовано, оба запроса имеют одну и ту же форму дерева, и EF необходимо скомпилировать только один раз. Созданный SQL также параметризован, что позволяет базе данных повторно использовать тот же план запроса:

SELECT TOP(1) [b].[Id], [b].[Name]
FROM [Posts] AS [b]
WHERE [b].[Name] = @__postTitle_0

Обратите внимание, что не требуется параметризировать каждый запрос: это идеально хорошо подходит для того, чтобы иметь некоторые запросы с константами, и, действительно, базы данных (и EF) иногда могут выполнять определенную оптимизацию вокруг констант, которые не могут быть возможными, когда запрос параметризован. См. раздел о динамически созданных запросах , например, где правильная параметризация имеет решающее значение.

Примечание.

Метрики EF Core сообщают о скорости попадания кэша запросов. В обычном приложении эта метрика достигает 100 % вскоре после запуска программы, после выполнения большинства запросов по крайней мере один раз. Если эта метрика остается стабильной на уровне ниже 100 %, это может указывать на то, что ваше приложение нарушает работу кэша запросов — рекомендуется провести его анализ.

Подсказка

EF Core используется IMemoryCache для внутреннего кэширования скомпилированных запросов и моделей. При необходимости можно настроить ограничение размера кэша. Дополнительные сведения см. в разделе "Интеграция кэша памяти ".

Примечание.

Как база данных управляет планами запросов кэша, зависит от базы данных. Например, SQL Server неявно поддерживает кэш плана запросов LRU, в то время как PostgreSQL не поддерживает (но подготовленные инструкции могут создать очень похожий конечный эффект). Дополнительные сведения см. в документации по базе данных.

Динамически созданные запросы

В некоторых ситуациях необходимо динамически создавать запросы LINQ, а не указывать их прямо в исходном коде. Это может произойти, например, на веб-сайте, который получает произвольные сведения о запросе от клиента, с открытыми операторами запросов (сортировка, фильтрация, разбиение по страницам...). В принципе, если это правильно, динамически созданные запросы могут быть столь же эффективными, как обычные (хотя невозможно использовать скомпилированную оптимизацию запросов с динамическими запросами). Однако на практике они часто являются источником проблем с производительностью, так как легко случайно создавать деревья выражений с фигурами, которые отличаются каждый раз.

В следующем примере используются три метода для создания лямбда-выражения запроса Where :

  1. API выражений с константой: динамическое построение выражения с помощью API выражений с помощью постоянного узла. Это частая ошибка при динамическом построении деревьев выражений, что приводит к перекомпиляции запроса при каждом вызове с другим константным значением (это также, как правило, приводит к загрязнению кэша планов на сервере базы данных).
  2. API выражений с параметром: улучшенная версия, которая заменяет константу параметром. Это гарантирует, что запрос компилируется только один раз, независимо от предоставленного значения, и создается тот же (параметризованный) SQL.
  3. Простой вариант с параметром: версия, которая не использует API выражений и, для сравнения, создаёт то же дерево, что и метод выше, но гораздо проще. Во многих случаях можно динамически создавать дерево выражений, не прибегая к API для выражений, которую легко сделать неправильно.

Мы добавим оператор к запросу, только если заданный Where параметр не имеет значения NULL. Обратите внимание, что это не хороший вариант использования для динамического создания запроса, но мы используем его для простоты:

[Benchmark]
public async Task<int> ExpressionApiWithConstant()
{
    var url = "blog" + Interlocked.Increment(ref _blogNumber);
    using var context = new BloggingContext();

    IQueryable<Blog> query = context.Blogs;

    if (_addWhereClause)
    {
        var blogParam = Expression.Parameter(typeof(Blog), "b");
        var whereLambda = Expression.Lambda<Func<Blog, bool>>(
            Expression.Equal(
                Expression.MakeMemberAccess(
                    blogParam,
                    typeof(Blog).GetMember(nameof(Blog.Url)).Single()),
                Expression.Constant(url)),
            blogParam);

        query = query.Where(whereLambda);
    }

    return await query.CountAsync();
}

Тестирование этих двух методов дает следующие результаты:

Способ Среднее Ошибка StdDev Нулевое поколение Поколение 1 Распределено
ExpressionApiWithConstant 1665.8 нас 56.99 мы 163.5 нас 15.6250 - 109.92 КБ
ExpressionApiWithParameter 757.1 мы 35.14 мы 103.6 нас 12,6953 0.9766 54.95 КБ
SimpleWithParameter 760.3 мы 37.99 мы 112.0 нас 12,6953 - 55.03 КБ

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

Примечание.

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

Скомпилированные модели

Скомпилированные модели могут улучшить время запуска EF Core для приложений с большими моделями. Большая модель обычно означает сотни тысяч типов сущностей и связей. Время запуска — это период, за который выполняется первая операция с DbContext, когда этот тип DbContext используется в приложении впервые. Обратите внимание, что только создание экземпляра DbContext не приводит к инициализации модели EF. Стандартные первые операции, которые приводят к инициализации модели, включают вызов DbContext.Add или выполнение первого запроса.

Скомпилированные модели создаются с помощью программы командной строки dotnet ef. Прежде чем продолжить, убедитесь, что у вас установлена последняя версия программы.

Для создания скомпилированной модели используется новая команда dbcontext optimize. Например:

dotnet ef dbcontext optimize

Параметры --output-dir и --namespace можно использовать для указания каталога и пространства имен, в которых будет создаваться скомпилированная модель. Например:

PS C:\dotnet\efdocs\samples\core\Miscellaneous\CompiledModels> dotnet ef dbcontext optimize --output-dir MyCompiledModels --namespace MyCompiledModels
Build started...
Build succeeded.
Successfully generated a compiled model, to use it call 'options.UseModel(MyCompiledModels.BlogsContextModel.Instance)'. Run this command again when the model is modified.
PS C:\dotnet\efdocs\samples\core\Miscellaneous\CompiledModels>
  • Дополнительные сведения см. по адресу dotnet ef dbcontext optimize.
  • Если вы более комфортно работаете в Visual Studio, вы также можете использовать Optimize-DbContext

Выходные данные выполнения этой команды содержат фрагмент кода для копирования и вставки в DbContext конфигурацию, чтобы EF Core использовала скомпилированную модель. Например:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseModel(MyCompiledModels.BlogsContextModel.Instance)
        .UseSqlite(@"Data Source=test.db");

Начальная загрузка скомпилированной модели

Обычно нет необходимости проверять созданный код начальной загрузки. Однако иногда может быть полезно настроить модель или ее загрузку. Код начальной загрузки выглядит примерно так:

[DbContext(typeof(BlogsContext))]
partial class BlogsContextModel : RuntimeModel
{
    private static BlogsContextModel _instance;
    public static IModel Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = new BlogsContextModel();
                _instance.Initialize();
                _instance.Customize();
            }

            return _instance;
        }
    }

    partial void Initialize();

    partial void Customize();
}

Это разделяемый класс с разделяемыми методами, которые можно реализовать для настройки модели по мере необходимости.

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

public static class RuntimeModelCache
{
    private static readonly ConcurrentDictionary<string, IModel> _runtimeModels
        = new();

    public static IModel GetOrCreateModel(string connectionString)
        => _runtimeModels.GetOrAdd(
            connectionString, cs =>
            {
                if (cs.Contains("X"))
                {
                    return BlogsContextModel1.Instance;
                }

                if (cs.Contains("Y"))
                {
                    return BlogsContextModel2.Instance;
                }

                throw new InvalidOperationException("No appropriate compiled model found.");
            });
}

Ограничения

У скомпилированных моделей есть некоторые ограничения:

В связи с этими ограничениями следует использовать только скомпилированные модели, если запуск EF Core выполняется слишком медленно. Компиляция небольших моделей, как правило, не стоит того.

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

Обработка ошибок компиляции из-за неоднозначных ссылок на тип

При компиляции моделей с типами одинаковыми именами, но существующих в разных пространствах имен, генерируемый код может вызывать ошибки компиляции из-за неоднозначных ссылок на тип. Чтобы устранить эту проблему, можно настроить создание кода для использования полных имен типов, переопределив CSharpHelper.ShouldUseFullName, чтобы возвращалось true. Дополнительные сведения о переопределении служб времени разработки см. в ICSharpHelper "Время разработки".

Сокращение временных накладных расходов

Как и в любом случае, EF Core увеличивает накладные расходы во время выполнения по сравнению с программированием непосредственно с использованием API более низкого уровня баз данных. Эта нагрузка на среду выполнения вряд ли влияет на большинство реальных приложений значительным образом; Другие разделы в этом руководстве по производительности, такие как эффективность запросов, использование индексов и минимизация циклов, являются гораздо более важными. Кроме того, даже для высокооптимизированных приложений задержка сети и операций ввода-вывода базы данных обычно доминируют в любой момент времени, потраченного внутри EF Core. Однако для высокопроизводительных приложений с низкой задержкой, где каждый бит perf важен, можно использовать следующие рекомендации, чтобы сократить затраты EF Core до минимума:

  • Включите пул DbContext, поскольку наши тесты показывают, что эта функция может иметь решающее влияние на высокопроизводительные приложения с низкой задержкой.
    • Убедитесь, что maxPoolSize соответствует вашему сценарию использования; если оно слишком низко, DbContext экземпляры будут постоянно создаваться и удаляться, ухудшая производительность. Установка слишком высокого значения может привести к ненужному расходу памяти, так как неиспользуемые экземпляры DbContext сохраняются в пуле.
    • Для дополнительного крошечного увеличения perf рекомендуется использовать PooledDbContextFactory вместо прямого внедрения экземпляров контекста DI. Управление пулом DbContext влечет за собой небольшую нагрузку.
  • Используйте предварительно скомпилированные запросы для горячих запросов.
    • Чем сложнее запрос LINQ - тем больше операторов, которые он содержит, и чем больше результирующее дерево выражений, тем больше результатов можно ожидать от использования скомпилированных запросов.
  • Рассмотрите возможность отключения проверок потокобезопасности, установив для EnableThreadSafetyChecks значение false в конфигурации контекста.
    • Использование одного DbContext экземпляра одновременно из разных потоков не поддерживается. EF Core имеет функцию безопасности, которая обнаруживает эту ошибку программирования во многих случаях (но не все), и немедленно вызывает информативное исключение. Однако эта функция безопасности добавляет некоторые временные накладные расходы.
    • ПРЕДУПРЕЖДЕНИЕ. Отключайте только проверки безопасности потоков после тщательного тестирования, что приложение не содержит таких ошибок параллелизма.

Интеграция кэша памяти

EF Core используется IMemoryCache для внутренних операций кэширования, таких как компиляция запросов и сборка моделей. По умолчанию EF Core настраивает собственный IMemoryCache с ограничением размера 10240. Для справки скомпилированный запрос имеет размер кэша 10, а встроенная модель имеет размер кэша 100.

Если необходимо изменить ограничение размера кэша по умолчанию, используйте UseMemoryCache для предоставления пользовательского IMemoryCache экземпляра:

var memoryCache = new MemoryCache(new MemoryCacheOptions { SizeLimit = 20480 });
services.AddSingleton<IDisposable>(memoryCache);

services.AddDbContext<ApplicationDbContext>(options =>
{
    options.UseMemoryCache(memoryCache);
    options.UseSqlServer(connectionString);
});

Экземпляр MemoryCache регистрируется как IDisposable с тем чтобы он был удален одновременно с удалением поставщика услуг, без замены общесистемного экземпляра IMemoryCache.

Кроме того, если вы регистрируете пользовательский IMemoryCache через AddMemoryCache в DI, вы можете определить его от поставщика услуг. Обратите внимание, что этот кэш используется между EF Core и любыми другими службами, которые используются IMemoryCache, поэтому ограничение размера должно учитываться для всех потребителей. SizeLimit Если задано, все записи кэша каждого потребителя должны указывать размер; в противном случае IMemoryCache при добавлении записей возникает следующее:

services.AddMemoryCache(options => options.SizeLimit = 20480);

services.AddDbContext<ApplicationDbContext>((serviceProvider, options) =>
{
    options.UseMemoryCache(serviceProvider.GetRequiredService<IMemoryCache>());
    options.UseSqlServer(connectionString);
});