DbContext-Lebensdauer, -Konfiguration und -Initialisierung

Dieser Artikel zeigt grundlegende Muster für die Initialisierung und Konfiguration einer DbContext-Instanz.

Die DbContext-Lebensdauer

Die Lebensdauer eines DbContext beginnt mit dem Erstellen der Instanz und endet, wenn die Instanz verworfen wird. Eine DbContext-Instanz ist für die Verwendung in einer einzelnen Arbeitseinheit konzipiert. Dies bedeutet, dass die Lebensdauer einer DbContext-Instanz in der Regel sehr kurz ist.

Tipp

Um Martin Fowler aus dem Link oben zu zitieren: „Eine Arbeitseinheit verfolgt alle Aufgaben nach, die Sie während einer Geschäftstransaktion ausführen, die sich auf die Datenbank auswirken können. Wenn Sie fertig sind, findet sie alles heraus, was getan werden muss, um die Datenbank als Ergebnis Ihrer Arbeit zu verändern“.

Eine typische Arbeitseinheit bei Verwendung von Entity Framework Core (EF Core) umfasst Folgendes:

  • Erstellen einer DbContext-Instanz.
  • Nachverfolgen von Entitätsinstanzen durch den Kontext. Entitäten werden folgendermaßen nachverfolgt:
  • An den nachverfolgten Entitäten werden Änderungen vorgenommen, die für die Implementierung der Geschäftsregel erforderlich sind.
  • SaveChanges oder SaveChangesAsync wird aufgerufen. EF Core erkennt die vorgenommenen Änderungen und schreibt sie in die Datenbank.
  • Die DbContext Instanz wird verworfen.

Wichtig

  • Es ist sehr wichtig, DbContext nach der Verwendung zu verwerfen. Dadurch wird sichergestellt, dass alle nicht verwalteten Ressourcen freigegeben werden und dass die Registrierung für alle Ereignisse oder anderen Hooks aufgehoben wird, um Speicherverluste zu verhindern, falls die Instanz weiterhin referenziert wird.
  • DbContext ist nicht threadsicher. Geben Sie keine Kontexte zwischen Threads frei. Stellen Sie sicher, dass await für alle asynchronen Aufrufe verwendet wird, bevor Sie die Kontextinstanz weiterhin verwenden.
  • Eine InvalidOperationException, die von EF Core-Code ausgelöst wird, kann den Kontext in einen nicht wiederherstellbaren Zustand versetzen. Solche Ausnahmen weisen auf einen Programmfehler hin und sind nicht für Wiederherstellung konzipiert.

DbContext in Abhängigkeitsinjektion für ASP.NET Core

In vielen Webanwendungen entspricht jede HTTP-Anforderung einer einzelnen Arbeitseinheit. Dies bewirkt, dass das Anbinden der Kontextlebensdauer an die Anforderung für Webanwendungen eine gute Standardeinstellung ist.

ASP.NET Core-Anwendungen werden mithilfe von Abhängigkeitsinjektion konfiguriert. EF Core kann dieser Konfiguration mithilfe von AddDbContext in der ConfigureServices-Methode von Startup.cs hinzugefügt werden. Beispiel:

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

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

In diesem Beispiel wird eine DbContext-Unterklasse namens ApplicationDbContext als bereichsbezogener Dienst im ASP.NET Core-Anwendungsdienstanbieter registriert (auch als Container für Abhängigkeitsinjektion bezeichnet). Der Kontext ist so konfiguriert, dass der SQL Server-Datenbankanbieter verwendet und die Verbindungszeichenfolge aus der ASP.NET Core-Konfiguration gelesen wird. Es spielt in der Regel keine Rolle, wo in ConfigureServices der Aufruf von AddDbContext erfolgt.

Die ApplicationDbContext-Klasse muss einen öffentlichen Konstruktor mit einem Parameter DbContextOptions<ApplicationDbContext> bereitstellen. Auf diese Weise wird die Kontextkonfiguration aus AddDbContext an DbContext übergeben. Beispiel:

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

ApplicationDbContext kann dann mithilfe von Konstruktorinjektion in ASP.NET Core-Controllern oder anderen Diensten verwendet werden. Beispiel:

public class MyController
{
    private readonly ApplicationDbContext _context;

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

Das Endergebnis ist eine ApplicationDbContext-Instanz, die für jede Anforderung erstellt und an den Controller zum Ausführen einer Arbeitseinheit übergeben wird, bevor sie verworfen wird, wenn die Anforderung beendet wird.

Weitere Informationen zu den Konfigurationsoptionen finden Sie weiter unten in diesem Artikel. Weitere Informationen zur Konfiguration und zu Abhängigkeitsinjektion in ASP.NET Core finden Sie unter App-Start in ASP.NET Core und Abhängigkeitsinjektion in ASP.NET Core.

Einfache DbContext-Initialisierung mit „new“

DbContext-Instanzen können auf die normale .NET-Weise erstellt werden, z. B. mit new in C#. Die Konfiguration kann durch Überschreiben der OnConfiguring-Methode oder durch Übergeben von Optionen an den Konstruktor ausgeführt werden. Beispiel:

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

Mit diesem Muster ist es auch einfach, Konfiguration wie etwa eine Verbindungszeichenfolge über den DbContext-Konstruktor zu übergeben. Beispiel:

public class ApplicationDbContext : DbContext
{
    private readonly string _connectionString;

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

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

Alternativ kann DbContextOptionsBuilder verwendet werden, um ein DbContextOptions-Objekt zu erstellen, das dann an den DbContext-Konstruktor übergeben wird. Dadurch kann ein DbContext, der für Abhängigkeitsinjektion konfiguriert ist, ebenfalls explizit erstellt werden. Wenn Sie z. B. ApplicationDbContext verwenden, der für ASP.NET Core Web-Apps oben definiert ist:

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

DbContextOptions kann erstellt werden, und der Konstruktor kann explizit aufgerufen werden:

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

using var context = new ApplicationDbContext(contextOptions);

Verwenden einer DbContext-Factory (z. B. für Blazor)

Einige Anwendungstypen (z. B. ASP.NET Core Blazor) verwenden Abhängigkeitsinjektion, erstellen aber keinen Dienstbereich, der der gewünschten DbContext-Lebensdauer entspricht. Auch wenn eine solche Ausrichtung vorhanden ist, muss die Anwendung möglicherweise mehrere Arbeitseinheiten innerhalb dieses Bereichs ausführen. Beispielsweise mehrere Arbeitseinheiten innerhalb einer einzelnen HTTP-Anforderung.

In diesen Fällen kann AddDbContextFactory zum Registrieren einer Factory zum Erstellen von DbContext-Instanzen verwendet werden. Beispiel:

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

Die ApplicationDbContext-Klasse muss einen öffentlichen Konstruktor mit einem Parameter DbContextOptions<ApplicationDbContext> bereitstellen. Dabei handelt es sich um dasselbe Muster, das im Abschnitt „Traditionelles ASP.NET Core“ weiter oben verwendet wird.

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

Die DbContextFactory-Factory kann dann mithilfe von Konstruktorinjektion in anderen Diensten verwendet werden. Beispiel:

private readonly IDbContextFactory<ApplicationDbContext> _contextFactory;

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

Die injizierte Factory kann dann verwendet werden, um DbContext-Instanzen im Dienstcode zu erstellen. Beispiel:

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

Beachten Sie, dass die DbContext-Instanzen, die auf diese Weise erstellt werden, nicht vom Dienstanbieter der Anwendung verwaltet werden, weshalb diese von der Anwendung gelöscht werden müssen.

Weitere Informationen zur Verwendung von EF Core mit Blazor finden Sie unter ASP.NET Core Blazor Server mit Entity Framework Core.

DbContextOptions

Der Ausgangspunkt für die gesamte DbContext-Konfiguration ist DbContextOptionsBuilder. Es gibt drei Möglichkeiten, diesen Generator abzurufen:

  • In AddDbContext und verwandte Methoden
  • In OnConfiguring
  • Explizit generiert mit new

Beispiele für jede dieser Möglichkeiten werden in den vorangehenden Abschnitten aufgeführt. Dieselbe Konfiguration kann unabhängig davon angewendet werden, wie der Generator abgerufen wurde. Außerdem wird OnConfiguring immer unabhängig davon aufgerufen, wie der Kontext erstellt wird. Dies bedeutet, dass OnConfiguring selbst dann verwendet werden kann, um zusätzliche Konfigurationen auszuführen, wenn AddDbContext verwendet wird.

Konfigurieren des Datenbankanbieters

Jede DbContext-Instanz muss so konfiguriert werden, dass sie nur einen Datenbankanbieter verwendet. (Verschiedene Instanzen eines DbContext-Untertyps können mit unterschiedlichen Datenbankanbietern verwendet werden, eine einzelne Instanz darf jedoch nur einen Datenbankanbieter verwenden.) Ein Datenbankanbieter wird mithilfe eines bestimmten Use*-Aufrufs konfiguriert. Wenn beispielsweise der SQL Server-Datenbankanbieter verwendet werden soll:

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

Diese Use*-Methoden sind Erweiterungsmethoden, die vom Datenbankanbieter implementiert werden. Dies bedeutet, dass das NuGet-Paket des Datenbankanbieters installiert werden muss, bevor die Erweiterungsmethode verwendet werden kann.

Tipp

EF Core-Datenbankanbieter nutzen Erweiterungsmethoden ausgiebig. Wenn der Compiler angibt, dass eine Methode nicht gefunden werden kann, stellen Sie sicher, dass das NuGet-Paket des Anbieters installiert ist und dass Sie using Microsoft.EntityFrameworkCore; in Ihrem Code verwenden.

Die folgende Tabelle enthält Beispiele für gängige Datenbankanbieter.

Datenbanksystem Beispielkonfiguration NuGet-Paket
SQL Server oder Azure SQL .UseSqlServer(connectionString) Microsoft.EntityFrameworkCore.SqlServer
Azure Cosmos DB .UseCosmos(connectionString, databaseName) Microsoft.EntityFrameworkCore.Cosmos
SQLite .UseSqlite(connectionString) Microsoft.EntityFrameworkCore.Sqlite
EF Core-In-Memory-Datenbank .UseInMemoryDatabase(databaseName) Microsoft.EntityFrameworkCore.InMemory
PostgreSQL* .UseNpgsql(connectionString) Npgsql.EntityFrameworkCore.PostgreSQL
MySQL/MariaDB* .UseMySql(connectionString) Pomelo.EntityFrameworkCore.MySql
Oracle* .UseOracle(connectionString) Oracle.EntityFrameworkCore

*Diese Datenbankanbieter werden nicht von Microsoft ausgeliefert. Weitere Informationen zu Datenbankanbietern finden Sie unter Datenbankanbieter.

Warnung

Die In-Memory-Datenbank von EF Core ist nicht für die Verwendung in der Produktion vorgesehen. Außerdem ist sie möglicherweise auch nicht die beste Wahl für Tests. Weitere Informationen finden Sie unter Testen von Code, der EF Core verwendet.

Weitere Informationen zur Verwendung von Verbindungszeichenfolgen mit EF Core finden Sie unter Verbindungszeichenfolgen.

Eine für den Datenbankanbieter spezifische optionale Konfiguration wird in einem zusätzlichen anbieterspezifischen Generator ausgeführt. Verwenden Sie beispielsweise EnableRetryOnFailure, um Wiederholungsversuche für Verbindungsresilienz beim Herstellen einer Verbindung mit Azure SQL zu konfigurieren:

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

Tipp

Der gleiche Datenbankanbieter wird für SQL Server und Azure SQL verwendet. Es wird jedoch empfohlen, beim Herstellen einer Verbindung mit SQL Azure Verbindungsresilienz zu verwenden.

Weitere Informationen zur anbieterspezifischen Konfiguration finden Sie unter Datenbankanbieter.

Weitere DbContext-Konfiguration

Weitere DbContext-Konfiguration kann vor oder nach dem Aufruf von Use* verkettet werden (dies macht keinen Unterschied). So aktivieren Sie z. B. Protokollierung sensibler Daten:

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

Die folgende Tabelle enthält Beispiele für gängige Methoden, die für DbContextOptionsBuilder aufgerufen werden.

DbContextOptionsBuilder-Methode Funktion Weitere Informationen
UseQueryTrackingBehavior Legt das Standardverhalten der Nachverfolgung für Abfragen fest. Verhalten der Abfragenachverfolgung
LogTo Eine einfache Möglichkeit, EF Core-Protokolle zu erhalten Protokollierung, Ereignisse und Diagnose
UseLoggerFactory Registriert eine Microsoft.Extensions.Logging-Factory. Protokollierung, Ereignisse und Diagnose
EnableSensitiveDataLogging Schließt Anwendungsdaten in Ausnahmen und Protokollierung ein. Protokollierung, Ereignisse und Diagnose
EnableDetailedErrors Ausführlichere Abfragefehler (auf Kosten der Leistung). Protokollierung, Ereignisse und Diagnose
ConfigureWarnings Ignoriert Warnungen und andere Ereignisse oder löst diese aus. Protokollierung, Ereignisse und Diagnose
AddInterceptors Registriert EF Core-Interceptors. Protokollierung, Ereignisse und Diagnose
UseLazyLoadingProxies Verwendet dynamischer Proxys für verzögertes Laden. Verzögertes Laden
UseChangeTrackingProxies Verwendet dynamische Proxys für Änderungsnachverfolgung. Bald verfügbar...

Hinweis

UseLazyLoadingProxies und UseChangeTrackingProxies sind Erweiterungsmethoden aus dem NuGet-Paket Microsoft.EntityFrameworkCore.Proxies. Diese Art von „.UseSomething()“-Aufruf ist die empfohlene Methode zur Konfiguration und/oder Verwendung von EF Core-Erweiterungen, die in anderen Paketen enthalten sind.

DbContextOptions im Vergleich mit DbContextOptions<TContext>

Die meisten DbContext-Unterklassen, die DbContextOptions akzeptieren, sollten die generische DbContextOptions<TContext>-Variation verwenden. Beispiel:

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

Dadurch wird sichergestellt, dass die richtigen Optionen für den spezifischen DbContext-Untertyp aus der Abhängigkeitsinjektion aufgelöst werden, auch wenn mehrere DbContext-Untertypen registriert sind.

Tipp

Ihr DbContext muss nicht versiegelt sein, aber das Versiegeln ist eine bewährte Vorgehensweise für Klassen, die nicht für Vererbung entworfen wurden.

Wenn jedoch vom DbContext-Untertyp selbst geerbt werden soll, sollte ein geschützter Konstruktor bereitgestellt werden, der generische DbContextOptions annimmt. Beispiel:

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

Dies ermöglicht es mehreren konkreten Unterklassen, diesen Basiskonstruktor mithilfe ihrer verschiedenen generischen DbContextOptions<TContext>-Instanzen aufzurufen. Beispiel:

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

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

Beachten Sie, dass dies genau das gleiche Muster wie beim direkten Erben von DbContext ist. Das heißt, der DbContext-Konstruktor selbst akzeptiert aus diesem Grund nicht generische DbContextOptions.

Eine DbContext-Unterklasse, die instanziiert und von der geerbt werden soll, sollte beide Formen des Konstruktors verfügbar machen. Beispiel:

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

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

DbContext-Konfiguration zur Entwurfszeit

EF Core-Entwurfszeittools (z. B. für EF Core-Migrationen) müssen in der Lage sein, eine funktionierende Instanz eines DbContext-Typs zu ermitteln und zu erstellen, um Details zu den Entitätstypen der Anwendung und deren Zuordnung zu einem Datenbankschema zu erfassen. Dieser Prozess kann automatisch ausgeführt werden, solange das Tool die DbContext-Instanz problemlos so erstellen kann, dass sie ähnlich konfiguriert wird, wie sie zur Laufzeit konfiguriert würde.

Obwohl jedes Muster, das die erforderlichen Konfigurationsinformationen für die DbContext-Instanz bereitstellt, zur Laufzeit funktionieren kann, können Tools, die die Verwendung einer DbContext-Instanz zur Entwurfszeit erfordern, nur mit einer begrenzten Anzahl von Mustern funktionieren. Diese werden unter Kontexterstellung zur Entwurfszeit ausführlicher behandelt.

Vermeiden von DbContext-Threadingproblemen

Entity Framework Core unterstützt nicht die Ausführung mehrerer paralleler Vorgänge, die für dieselbe DbContext-Instanz ausgeführt werden. Dies schließt die parallele Ausführung von asynchronen Abfragen und jede explizite gleichzeitige Verwendung aus mehreren Threads ein. Verwenden Sie daher immer sofort asynchrone await-Aufrufe, oder verwenden Sie separate DbContext-Instanzen für Vorgänge, die parallel ausgeführt werden.

Wenn EF Core den Versuch erkennt, eine DbContext-Instanz parallel zu verwenden, wird eine InvalidOperationException mit einer Meldung wie der folgenden angezeigt:

Ein zweiter Vorgang wurde für diesen Kontext gestartet, bevor ein vorheriger Vorgang abgeschlossen wurde. Dies wird in der Regel durch verschiedene Threads verursacht, die dieselbe Instanz von DbContext verwenden, aber es ist nicht garantiert, dass Instanzmember threadsicher sind.

Wenn gleichzeitiger Zugriff nicht erkannt wird, kann dies zu nicht definiertem Verhalten, Anwendungsabstürzen und Datenbeschädigung führen.

Es gibt häufige Fehler, die versehentlich gleichzeitigen Zugriff auf dieselbe DbContext-Instanz verursachen können:

Fehler bei asynchronen Vorgängen

Asynchrone Methoden ermöglichen EF Core das Initiieren von Vorgängen, die auf nicht blockierende Weise auf die Datenbank zugreifen. Wenn ein Aufrufer jedoch nicht auf den Abschluss einer dieser Methoden wartet und andere Vorgänge für DbContext ausführt, kann der Status von DbContext beschädigt sein (und ist es sehr wahrscheinlich auch).

Warten Sie immer sofort auf asynchrone EF Core-Methoden.

Implizites Freigeben von DbContext-Instanzen über Abhängigkeitsinjektion

Die Erweiterungsmethode AddDbContext registriert DbContext-Typen standardmäßig mit einer begrenzten Lebensdauer.

Dies vermeidet in den meisten ASP.NET Core-Anwendungen Probleme durch gleichzeitigen Zugriff, weil es nur einen Thread gibt, der jede Clientanforderung zu einer bestimmten Zeit ausführt, und weil jede Anforderung einen separaten Abhängigkeitsinjektionsbereich (und damit eine separate DbContext-Instanz) erhält. Für das Blazor Server-Hostingmodell wird eine logische Anforderung zum Verwalten der Blazor-Benutzerverbindung verwendet. Daher ist nur eine bereichsbezogene DbContext-Instanz pro Benutzerverbindung verfügbar, wenn der standardmäßige Injektionsbereich verwendet wird.

Jeder Code, der explizit mehrere Threads parallel ausführt, sollte sicherstellen, dass auf DbContext-Instanzen niemals gleichzeitig zugegriffen wird.

Mithilfe von Abhängigkeitsinjektion kann dies erreicht werden, indem der Kontext als bereichsbezogen registriert wird und Bereiche (mit IServiceScopeFactory) für jeden Thread erstellt werden, oder indem DbContext als vorübergehender Wert registriert wird (mithilfe der Überladung von AddDbContext, die einen ServiceLifetime-Parameter annimmt).

Weitere Informationen