Время существования, настройка и инициализация DbContext

В этой статье описаны основные шаблоны для инициализации и настройки экземпляра DbContext.

Время существования DbContext

Время существования DbContext начинается, когда создается экземпляр, и заканчивается, когда экземпляр удаляется. Экземпляр DbContext предназначен для выполнения однойединицы работы. Это означает, что время существования экземпляра DbContext обычно невелико.

Совет

Цитируя Мартина Фаулера (по ссылке выше): "С помощью единицы работы отслеживаются все операции во время бизнес-транзакции, которые могут повлиять на базу данных. Когда вы закончите работу, такая единица определяет, что нужно сделать, чтобы изменить базу данных в результате ваших действий".

Обычная единица работы при использовании Entity Framework Core (EF Core) включает следующее:

Важно!

  • Очень важно также удалить DbContext после использования. Это гарантирует, что все неуправляемые ресурсы будут освобождены и что регистрация всех событий или других перехватчиков будет отменена. Так можно предотвратить утечки памяти в тех случаях, если на экземпляр сохраняются ссылки.
  • DbContext не является потокобезопасным. Не передавайте контексты в другие потоки. Выполняйте оператор await для всех вызовов синхронизации, прежде чем продолжать использование экземпляра контекста.
  • Исключение InvalidOperationException, генероируемое кодом EF Core, может перевести контекст в невосстанавливаемое состояние. Такие исключения указывают на ошибку в программе и не допускают восстановление.

DbContext во внедрении зависимостей для ASP.NET Core

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

Приложения ASP.NET Core настраиваются с использованием внедрения зависимостей. Вы можете добавить EF Core в такую конфигурацию с помощью AddDbContext в методе ConfigureServices файла Startup.cs. Например:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddDbContext<ApplicationDbContext>(
        options => options.UseSqlServer("name=ConnectionStrings:DefaultConnection"));
}

В этом примере регистрируется подкласс DbContext с именем ApplicationDbContext в качестве службы с заданной областью в поставщике службы приложений ASP.NET Core (т. е. в контейнере внедрения зависимостей). Контекст при этом настраивается для использования поставщика базы данных SQL Server и считывания строки подключения из конфигурации ASP.NET Core. Обычно не имеет значения, где в ConfigureServices выполняется вызов к AddDbContext.

Класс ApplicationDbContext должен предоставить доступ к общедоступному конструктору с помощью параметра DbContextOptions<ApplicationDbContext>. Именно так передается конфигурация контекста из AddDbContext в DbContext. Например:

public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }
}

Затем ApplicationDbContext можно использовать в контроллерах ASP.NET Core или других службах посредством внедрения конструктора. Например:

public class MyController
{
    private readonly ApplicationDbContext _context;

    public MyController(ApplicationDbContext context)
    {
        _context = context;
    }
}

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

Дополнительные сведения о параметрах конфигурации см. далее в этой статье. Кроме того, дополнительные сведения о конфигурации и внедрении зависимостей можно найти в статьях Запуск приложения в ASP.NET Core и Внедрение зависимостей в ASP.NET Core.

Простая инициализация DbContext с помощью оператора new

Экземпляры DbContext можно создать средствами .NET, например с помощью оператора new в C#. Настройку можно выполнить путем переопределения метода OnConfiguring или передачи параметров в конструктор. Например:

public class ApplicationDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test");
    }
}

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

public class ApplicationDbContext : DbContext
{
    private readonly string _connectionString;

    public ApplicationDbContext(string connectionString)
    {
        _connectionString = connectionString;
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(_connectionString);
    }
}

Кроме того, вы можете использовать DbContextOptionsBuilder для создания объекта DbContextOptions, который затем передается в конструктор DbContext. Это позволяет явно создать DbContext, настроенный для внедрения зависимостей. Например, при использовании ApplicationDbContext, определенного для указанных выше веб-приложений ASP.NET Core:

public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }
}

Вы можете создать DbContextOptions, а конструктор может быть вызван явно:

var contextOptions = new DbContextOptionsBuilder<ApplicationDbContext>()
    .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test")
    .Options;

using var context = new ApplicationDbContext(contextOptions);

Использование фабрики DbContext (например, для Blazor)

Некоторые типы приложений (например, ASP.NET Core Blazor) используют внедрение зависимостей, но не создают область службы, соответствующую нужному времени существования DbContext. Даже если такое соответствие отсутствует, приложению, возможно, потребуется выполнять несколько единиц работы в рамках такой области, например в одном HTTP-запросе.

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

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContextFactory<ApplicationDbContext>(
        options =>
            options.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test"));
}

Класс ApplicationDbContext должен предоставить доступ к общедоступному конструктору с помощью параметра DbContextOptions<ApplicationDbContext>. Такой же шаблон используется в традиционных приложениях ASP.NET Core (см. раздел выше).

public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }
}

Затем фабрику DbContextFactory можно использовать в других службах посредством внедрения конструктора. Например:

private readonly IDbContextFactory<ApplicationDbContext> _contextFactory;

public MyController(IDbContextFactory<ApplicationDbContext> contextFactory)
{
    _contextFactory = contextFactory;
}

Внедренную фабрику затем можно использовать для создания экземпляров DbContext в коде службы. Например:

public void DoSomething()
{
    using (var context = _contextFactory.CreateDbContext())
    {
        // ...
    }
}

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

Дополнительные сведения об использовании EF Core с Blazor см. в статье ASP.NET Core Blazor Server с Entity Framework Core.

DbContextOptions

Начальная точка для всех конфигураций DbContext — это DbContextOptionsBuilder. Есть три способа получения этого построителя:

  • В AddDbContext и связанных методах.
  • В OnConfiguring
  • При явном создании с помощью оператора new.

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

Настройка поставщика базы данных

Каждый экземпляр DbContext необходимо настроить для использования только одного поставщика баз данных. (Разные экземпляры подтипа DbContext можно использовать с разными поставщиками баз данных, но у одного экземпляра должен быть только один поставщик.) Поставщик баз данных настраивается через особый вызов Use*. Например, следующий код позволяет использовать поставщик базы данных SQL Server:

public class ApplicationDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test");
    }
}

Приведенные здесь методы Use* являются методами расширения, которые реализуются поставщиком. Это означает, что пакет NuGet поставщика базы данных нужно установить до использования метода расширения.

Совет

Поставщики базы данных EF Core активно используют методы расширения. Если компилятор указывает, что метод не найден, убедитесь, что пакет NuGet поставщика установлен и что ваш код включает using Microsoft.EntityFrameworkCore;.

В представленной ниже таблице приведены примеры для популярных поставщиков баз данных.

Система базы данных Пример конфигурации Пакет NuGet
SQL Server или Azure SQL .UseSqlServer(connectionString) Microsoft.EntityFrameworkCore.SqlServer
Azure Cosmos DB .UseCosmos(connectionString, databaseName) Microsoft.EntityFrameworkCore.Cosmos
SQLite .UseSqlite(connectionString) Microsoft.EntityFrameworkCore.Sqlite
Выполняющаяся в памяти база данных EF Core .UseInMemoryDatabase(databaseName) Microsoft.EntityFrameworkCore.InMemory
PostgreSQL* .UseNpgsql(connectionString) Npgsql.EntityFrameworkCore.PostgreSQL
MySQL или MariaDB* .UseMySql(connectionString) Pomelo.EntityFrameworkCore.MySql
Oracle* .UseOracle(connectionString) Oracle.EntityFrameworkCore

* Корпорация Майкрософт не предоставляет эти поставщики баз данных. Дополнительные сведения можно найти в статье Поставщики баз данных.

Предупреждение

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

Дополнительные сведения об использовании строк подключения с EF Core см. в статье Строки подключения.

Необязательная настройка, зависящая от поставщика базы данных, выполняется в дополнительном построителе, предоставляемом поставщиком. Например, вы можете использовать EnableRetryOnFailure для настройки повторных попыток для обеспечения устойчивости при подключении к Azure SQL:

public class ApplicationDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .UseSqlServer(
                @"Server=(localdb)\mssqllocaldb;Database=Test",
                providerOptions => { providerOptions.EnableRetryOnFailure(); });
    }
}

Совет

Этот же поставщик базы данных используется для SQL Server и Azure SQL. Но мы рекомендуем обеспечить устойчивость подключения к Azure SQL.

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

Другая конфигурация DbContext

Другую конфигурацию DbContext можно связать до или после (это не играет роли) вызоваUse*. Например, чтобы включить ведение журнала для конфиденциальных данных:

public class ApplicationDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .EnableSensitiveDataLogging()
            .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test");
    }
}

В следующей таблице приведены примеры популярных методов, вызываемых для DbContextOptionsBuilder.

Метод DbContextOptionsBuilder Что он делает Подробнее
UseQueryTrackingBehavior Задает поведение отслеживания по умолчанию для запросов. Поведение отслеживания запросов
LogTo Простой способ получения журналов EF Core Ведение журналов, регистрация событий и диагностика
UseLoggerFactory Регистрирует фабрику Microsoft.Extensions.Logging. Ведение журналов, регистрация событий и диагностика
EnableSensitiveDataLogging Отвечает за включение данных приложения в исключениях и ведение журналов. Ведение журналов, регистрация событий и диагностика
EnableDetailedErrors Подробные ошибки запросов (в ущерб производительности). Ведение журналов, регистрация событий и диагностика
ConfigureWarnings Позволяет игнорировать или генерировать предупреждения и другие события. Ведение журналов, регистрация событий и диагностика
AddInterceptors Регистрирует перехватчики EF Core. Ведение журналов, регистрация событий и диагностика
UseLazyLoadingProxies Позволяет использовать динамические прокси-серверы для отложенной загрузки. Отложенная загрузка
UseChangeTrackingProxies Позволяет использовать динамические прокси-серверы для отслеживания изменений. Ожидается в ближайшее время...

Примечание.

UseLazyLoadingProxies и UseChangeTrackingProxies — это методы расширения из пакета NuGet Microsoft.EntityFrameworkCore.Proxies. Такой тип вызова ".UseSomething()" рекомендуется для настройки и (или) использования расширений EF Core в других пакетах.

DbContextOptions и DbContextOptions<TContext>

Большинство подклассов DbContext, которые принимают DbContextOptions, должны использовать универсальный вариант DbContextOptions<TContext>. Например:

public sealed class SealedApplicationDbContext : DbContext
{
    public SealedApplicationDbContext(DbContextOptions<SealedApplicationDbContext> contextOptions)
        : base(contextOptions)
    {
    }
}

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

Совет

Запечатывать DbContext не обязательно, но рекомендуется для классов, от которых не будут осуществляться наследование.

Но если сам подтип DbContext предполагает наследование, он должен предоставить доступ к защищенному конструктору, принимающему неуниверсальный DbContextOptions. Например:

public abstract class ApplicationDbContextBase : DbContext
{
    protected ApplicationDbContextBase(DbContextOptions contextOptions)
        : base(contextOptions)
    {
    }
}

Это позволяет нескольким конкретным подклассам вызвать такой базовый конструктор с помощью разных универсальных экземпляров DbContextOptions<TContext>. Например:

public sealed class ApplicationDbContext1 : ApplicationDbContextBase
{
    public ApplicationDbContext1(DbContextOptions<ApplicationDbContext1> contextOptions)
        : base(contextOptions)
    {
    }
}

public sealed class ApplicationDbContext2 : ApplicationDbContextBase
{
    public ApplicationDbContext2(DbContextOptions<ApplicationDbContext2> contextOptions)
        : base(contextOptions)
    {
    }
}

Обратите внимание, что этот шаблон аналогичен наследованию непосредственно от DbContext. То есть конструктор DbContext принимает неуниверсальные DbContextOptions по этой причине.

Подкласс DbContext, который предусматривает создание экземпляра и от которого будет осуществляться наследование, должен предоставить доступ к обеим формам конструктора. Например:

public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> contextOptions)
        : base(contextOptions)
    {
    }

    protected ApplicationDbContext(DbContextOptions contextOptions)
        : base(contextOptions)
    {
    }
}

Настройка DbContext во время разработки

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

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

Предотвращение проблем с потоками DbContext

Entity Framework Core не поддерживает выполнение нескольких параллельных операций в одном экземпляре DbContext, включая параллельное выполнение асинхронных запросов и любое явное использование экземпляра из нескольких потоков одновременно. Поэтому обязательно сразу же применяйте await к асинхронным вызовам или используйте отдельные экземпляры DbContext, выполняемые параллельно.

Когда EF Core обнаруживает попытку одновременного использования экземпляра DbContext, отобразится InvalidOperationException с примерно таким сообщением:

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

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

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

Ошибки, связанные с асинхронными операциями

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

Обязательно сразу же применяйте await к асинхронным методам EF Core.

Неявное совместное использование экземпляров DbContext путем внедрения зависимостей

Метод расширения AddDbContext по умолчанию регистрирует типы DbContext с заданной областью времени существования.

Это обеспечивает защиту от проблем с одновременным доступом в большинстве приложений ASP.NET Core, так как есть только один поток, в котором каждый запрос клиента выполняется в определенный момент времени, и каждый такой запрос получает отдельную область внедрения зависимостей (и, следовательно, отдельный экземпляр DbContext). В модели размещения Blazor Server используется один логический запрос для обслуживания пользовательского канала Blazor. Поэтому при использовании области внедрения по умолчанию для каждого пользовательского канала доступен только один экземпляр DbContext с заданной областью.

Если код явно выполняет несколько потоков в параллельном режиме, нужно исключить возможность одновременного доступа к экземплярам DbContext.

При использовании внедрения зависимостей это можно реализовать путем регистрации контекста как ограниченного областью, создания области (с помощью IServiceScopeFactory) для каждого потока или регистрации экземпляра DbContext как временного (с помощью перегрузки AddDbContext, при которой принимается параметр ServiceLifetime).

Дополнительные материалы