it-swarm.com.de

Ein DbContext pro Webanfrage ... warum?

Ich habe viele Artikel gelesen, in denen erklärt wird, wie das DbContext von Entity Framework eingerichtet wird, sodass nur eines pro HTTP-Webanforderung unter Verwendung verschiedener DI-Frameworks erstellt und verwendet wird.

Warum ist das überhaupt eine gute Idee? Welche Vorteile bringt Ihnen dieser Ansatz? Gibt es bestimmte Situationen, in denen dies eine gute Idee wäre? Gibt es Dinge, die Sie mit dieser Technik tun können, die Sie beim Instanziieren von DbContexts pro Repository-Methodenaufruf nicht tun können?

374
Andrew

HINWEIS: Diese Antwort bezieht sich auf das DbContext des Entity Framework, ist jedoch auf jede Art von Unit of Work-Implementierung anwendbar, z. B. auf LINQ to SQL DataContext und NHibernates ISession.

Beginnen wir mit Ian: Ein einziges DbContext für die gesamte Anwendung zu haben, ist eine schlechte Idee. Dies ist nur dann sinnvoll, wenn Sie eine Anwendung mit einem Thread und eine Datenbank haben, die nur von dieser einzelnen Anwendungsinstanz verwendet wird. Das DbContext ist nicht threadsicher und da das DbContext Daten zwischenspeichert, wird es ziemlich bald veraltet. Dies bringt Sie in alle möglichen Schwierigkeiten, wenn mehrere Benutzer/Anwendungen gleichzeitig an dieser Datenbank arbeiten (was natürlich sehr häufig vorkommt). Aber ich gehe davon aus, dass Sie das bereits wissen und einfach wissen möchten, warum Sie nicht einfach eine neue Instanz (d. H. Mit einem vorübergehenden Lebensstil) von DbContext in jeden injizieren, der sie benötigt. (Für weitere Informationen darüber, warum ein einzelnes DbContext - oder sogar der Kontext pro Thread - schlecht ist, lesen Sie diese Antwort ).

Lassen Sie mich zunächst sagen, dass das Registrieren eines DbContext als transient funktionieren könnte, aber normalerweise möchten Sie eine einzelne Instanz einer solchen Arbeitseinheit in einem bestimmten Bereich haben. In einer Webanwendung kann es praktisch sein, einen solchen Bereich an den Grenzen einer Webanforderung zu definieren. somit ein Per Web Request Lebensstil. Auf diese Weise können Sie eine ganze Reihe von Objekten im selben Kontext betreiben. Mit anderen Worten, sie arbeiten innerhalb desselben Geschäftsvorfalls.

Wenn Sie nicht das Ziel haben, eine Reihe von Operationen im selben Kontext auszuführen, ist in diesem Fall der vorübergehende Lebensstil in Ordnung, aber es gibt ein paar Dinge, die Sie beachten sollten:

  • Da jedes Objekt eine eigene Instanz erhält, muss jede Klasse, die den Status des Systems ändert, _context.SaveChanges() aufrufen (andernfalls gehen Änderungen verloren). Dies kann Ihren Code komplizieren und fügt dem Code eine zweite Verantwortung hinzu (die Verantwortung für die Steuerung des Kontexts) und verstößt gegen das Prinzip der einfachen Verantwortung .
  • Sie müssen sicherstellen, dass Entitäten [die von einem DbContext geladen und gespeichert werden] niemals den Gültigkeitsbereich einer solchen Klasse verlassen, da sie nicht in der Kontextinstanz einer anderen Klasse verwendet werden können. Dies kann Ihren Code erheblich komplizieren, da Sie diese Entitäten, wenn Sie sie benötigen, erneut per ID laden müssen, was ebenfalls zu Leistungsproblemen führen kann.
  • Da DbContextIDisposable implementiert, möchten Sie wahrscheinlich immer noch alle erstellten Instanzen entsorgen. Wenn Sie dies tun möchten, haben Sie grundsätzlich zwei Möglichkeiten. Sie müssen sie nach dem Aufruf von context.SaveChanges() auf dieselbe Weise entsorgen, aber in diesem Fall übernimmt die Geschäftslogik den Besitz eines Objekts, das von außen weitergegeben wird. Die zweite Option besteht darin, alle erstellten Instanzen an der Grenze der HTTP-Anforderung zu entsorgen. In diesem Fall müssen Sie jedoch noch einen Bereich festlegen, um dem Container mitzuteilen, wann diese Instanzen entsorgt werden müssen.

Eine andere Möglichkeit ist, überhaupt kein DbContext zu injizieren . Stattdessen injizieren Sie ein DbContextFactory, mit dem eine neue Instanz erstellt werden kann (ich habe diesen Ansatz in der Vergangenheit verwendet). Auf diese Weise steuert die Geschäftslogik den Kontext explizit. Wenn könnte so aussehen:

public void SomeOperation()
{
    using (var context = this.contextFactory.CreateNew())
    {
        var entities = this.otherDependency.Operate(
            context, "some value");

        context.Entities.InsertOnSubmit(entities);

        context.SaveChanges();
    }
}

Das Plus daran ist, dass Sie das Leben des DbContext explizit verwalten und es einfach ist, dies einzurichten. Außerdem können Sie einen einzelnen Kontext in einem bestimmten Bereich verwenden, was klare Vorteile hat, z. B. das Ausführen von Code in einem einzelnen Geschäftsvorgang und das Weitergeben von Entitäten, da diese aus demselben DbContext stammen.

Der Nachteil ist, dass Sie die DbContext von Methode zu Methode (die als Method Injection bezeichnet wird) weitergeben müssen. Beachten Sie, dass diese Lösung in gewissem Sinne mit dem "Scoped" -Ansatz identisch ist, der Geltungsbereich jedoch jetzt im Anwendungscode selbst gesteuert wird (und möglicherweise mehrmals wiederholt wird). Es ist die Anwendung, die für das Erstellen und Entsorgen der Arbeitseinheit verantwortlich ist. Da das DbContext nach der Erstellung des Abhängigkeitsgraphen erstellt wird, ist die Konstruktorinjektion nicht im Bild und Sie müssen die Methodeninjektion verlassen, wenn Sie den Kontext von einer Klasse zur anderen weitergeben möchten.

Method Injection ist nicht so schlimm, aber wenn die Geschäftslogik komplexer wird und mehr Klassen involviert werden, müssen Sie sie von Methode zu Methode und von Klasse zu Klasse weitergeben, was den Code sehr komplizieren kann (wie ich gesehen habe) dies in der Vergangenheit). Für eine einfache Anwendung reicht dieser Ansatz jedoch aus.

Aufgrund der Nachteile, die dieser Factory-Ansatz für größere Systeme hat, kann ein anderer Ansatz nützlich sein, bei dem Sie den Container oder den Infrastrukturcode/ Composition Root die Arbeitseinheit verwalten lassen. Dies ist der Stil, um den es in Ihrer Frage geht.

Indem der Container und/oder die Infrastruktur dies handhaben, wird Ihr Anwendungscode nicht dadurch verschmutzt, dass Sie eine UoW-Instanz erstellen, (optional) festschreiben und entsorgen müssen, wodurch die Geschäftslogik einfach und sauber bleibt (nur eine einzige Verantwortung). Bei diesem Ansatz gibt es einige Schwierigkeiten. Wurde die Instanz beispielsweise festgeschrieben und entsorgt?

Die Entsorgung einer Arbeitseinheit kann am Ende der Webanforderung erfolgen. Viele Menschen gehen jedoch fälschlicherweise davon aus, dass dies auch der Ort ist, an dem die Arbeitseinheit festgelegt wird. Zu diesem Zeitpunkt in der Anwendung können Sie jedoch nicht sicher feststellen, ob die Arbeitseinheit tatsächlich festgeschrieben werden soll. z.B. Wenn der Business-Layer-Code eine Ausnahme ausgelöst hat, die höher im Callstack aufgefangen wurde, möchten Sie definitiv kein Commit durchführen .

Die eigentliche Lösung besteht wiederum darin, einen Bereich explizit zu verwalten, diesmal jedoch innerhalb des Kompositionsstamms. Wenn Sie die gesamte Geschäftslogik hinter dem Befehls-/Handlermuster zusammenfassen, können Sie einen Decorator schreiben, der um jeden Befehlshandler gewickelt werden kann, der dies zulässt. Beispiel:

class TransactionalCommandHandlerDecorator<TCommand>
    : ICommandHandler<TCommand>
{
    readonly DbContext context;
    readonly ICommandHandler<TCommand> decorated;

    public TransactionCommandHandlerDecorator(
        DbContext context,
        ICommandHandler<TCommand> decorated)
    {
        this.context = context;
        this.decorated = decorated;
    }

    public void Handle(TCommand command)
    {
        this.decorated.Handle(command);

        context.SaveChanges();
    } 
}

Dies stellt sicher, dass Sie diesen Infrastrukturcode nur einmal schreiben müssen. In jedem DI-Container können Sie einen solchen Decorator so konfigurieren, dass er alle ICommandHandler<T> konsequent umsetzen.

537
Steven

Keine einzige Antwort hier beantwortet tatsächlich die Frage. Das OP erkundigte sich nicht nach einem DbContext-Entwurf für Singleton/pro Anwendung, sondern nach einem Entwurf für Per Web-) Anforderungen und nach den potenziellen Vorteilen.

Ich verweise auf --- (http://mehdi.me/ambient-dbcontext-in-ef6/ , da Mehdi eine fantastische Ressource ist:

Mögliche Leistungssteigerungen.

Jede DbContext-Instanz verwaltet einen Cache der ersten Ebene aller Entitäten, die sie aus der Datenbank lädt. Wann immer Sie eine Entität anhand ihres Primärschlüssels abfragen, versucht DbContext zunächst, sie aus ihrem Cache der ersten Ebene abzurufen, bevor standardmäßig eine Abfrage von der Datenbank durchgeführt wird. Abhängig von Ihrem Datenabfragemuster kann die Wiederverwendung desselben DbContext für mehrere sequenzielle Geschäftstransaktionen dazu führen, dass dank des DbContext-Cache der ersten Ebene weniger Datenbankabfragen durchgeführt werden.

Ermöglicht verzögertes Laden.

Wenn Ihre Services persistente Entitäten zurückgeben (im Gegensatz zum Zurückgeben von Ansichtsmodellen oder anderen Arten von DTOs) und Sie das verzögerte Laden dieser Entitäten nutzen möchten, muss die Lebensdauer der DbContext-Instanz, von der diese Entitäten abgerufen wurden, über diesen Zeitraum hinausgehen den Umfang des Geschäftsvorfalls. Wenn die Dienstmethode die DbContext-Instanz entsorgt, die sie vor der Rückgabe verwendet hat, schlägt jeder Versuch fehl, Eigenschaften für die zurückgegebenen Entitäten zu verzögern (ob die Verwendung von verzögertem Laden eine gute Idee ist oder nicht, ist eine andere Debatte, auf die wir überhaupt nicht eingehen werden Hier). In unserem Webanwendungsbeispiel wird das verzögerte Laden normalerweise in Controller-Aktionsmethoden für Entitäten verwendet, die von einer separaten Service-Schicht zurückgegeben werden. In diesem Fall muss die DbContext-Instanz, die von der Dienstmethode zum Laden dieser Entitäten verwendet wurde, für die Dauer der Webanforderung (oder zumindest bis zum Abschluss der Aktionsmethode) aktiv bleiben.

Denken Sie daran, es gibt auch Nachteile. Dieser Link enthält viele andere Ressourcen, die Sie zu diesem Thema lesen können.

Dies nur für den Fall posten, dass jemand anderes über diese Frage stolpert und nicht in Antworten vertieft wird, die die Frage nicht wirklich ansprechen.

31
user4893106

Es gibt zwei widersprüchliche Empfehlungen von Microsoft, und viele Leute verwenden DbContexts auf völlig unterschiedliche Weise.

  1. Eine Empfehlung ist "Dispose DbContexts so bald wie möglich" , da ein DbContext Alive wertvolle Ressourcen wie DB-Verbindungen usw. belegt.
  2. Der andere besagt, dass ein DbContext pro Anforderung dringend empfohlen wird

Diese widersprechen sich gegenseitig, denn wenn Ihre Anfrage viel nicht mit dem Db-Material zu tun hat, wird Ihr DbContext ohne Grund beibehalten. Daher ist es Verschwendung, Ihren DbContext am Leben zu erhalten, während Ihre Anfrage nur darauf wartet, dass zufällige Dinge erledigt werden ...

So viele Leute, die Regel 1 befolgen , haben ihre DbContexts in ihrem "Repository-Muster" und erstellen eine neue Instanz pro Datenbankabfrage also X * DbContext auf Anfrage

Sie erhalten nur ihre Daten und entsorgen den Kontext so schnell wie möglich. Dies wird von [~ # ~] vielen [~ # ~] Personen als akzeptable Praxis angesehen. Dies hat zwar den Vorteil, dass Sie Ihre Datenbankressourcen nur für eine minimale Zeit belegen, jedoch werden alle UnitOfWork- und Caching-Vorgänge Süßigkeiten, die EF zu bieten hat.

Wenn Sie eine einzelne Mehrzweckinstanz von DbContext am Leben erhalten, werden die Vorteile von Caching aber maximiert Da DbContext nicht threadsicher ist und jede Webanforderung auf einem eigenen Thread ausgeführt wird, ist ein DbContext pro Anforderung am längsten Sie können es behalten.

Die Teamempfehlung von EF zur Verwendung von 1-Db-Kontext pro Anforderung basiert eindeutig auf der Tatsache, dass in einer Webanwendung ein UnitOfWork höchstwahrscheinlich in einer Anforderung enthalten ist und diese Anforderung einen Thread enthält. Ein DbContext pro Anfrage ist also der ideale Vorteil von UnitOfWork und Caching.

Aber in vielen Fällen ist das nicht wahr. Ich betrachte Logging eine separate UnitOfWork und habe damit einen neuen DbContext für das Post-Request Logging in asynchrone Threads ist völlig akzeptabel

Schließlich stellt sich heraus, dass die Lebensdauer eines DbContext auf diese beiden Parameter beschränkt ist. UnitOfWork und Thread

29

Ich bin mir ziemlich sicher, dass dies daran liegt, dass der DbContext überhaupt nicht threadsicher ist. Es ist also nie eine gute Idee, das Ding zu teilen.

22
Ian

Eine Sache, die in der Frage oder Diskussion nicht wirklich angesprochen wird, ist die Tatsache, dass DbContext Änderungen nicht abbrechen kann. Sie können Änderungen einreichen, aber den Änderungsbaum nicht löschen. Wenn Sie also einen Kontext pro Anforderung verwenden, haben Sie Pech, wenn Sie Änderungen aus irgendeinem Grund wegwerfen müssen.

Persönlich erstelle ich bei Bedarf Instanzen von DbContext - normalerweise verknüpft mit Geschäftskomponenten, die den Kontext bei Bedarf neu erstellen können. Auf diese Weise habe ich die Kontrolle über den Prozess, anstatt mir eine einzige Instanz aufzwingen zu lassen. Ich muss den DbContext auch nicht bei jedem Start des Controllers erstellen, unabhängig davon, ob er tatsächlich verwendet wird. Wenn ich dann noch Instanzen pro Anfrage haben möchte, kann ich sie im CTOR erstellen (über DI oder manuell) oder sie nach Bedarf in jeder Controller-Methode erstellen. Persönlich gehe ich normalerweise so vor, dass DbContext-Instanzen nicht erstellt werden, wenn sie tatsächlich nicht benötigt werden.

Es kommt darauf an, aus welchem ​​Blickwinkel Sie es auch betrachten. Für mich hat die Per-Request-Instanz nie Sinn gemacht. Gehört der DbContext wirklich in die HTTP-Anforderung? In Bezug auf das Verhalten ist das der falsche Ort. Ihre Geschäftskomponenten sollten Ihren Kontext erstellen, nicht die HTTP-Anforderung. Dann können Sie Ihre Geschäftskomponenten nach Bedarf erstellen oder wegwerfen, ohne sich Gedanken über die Lebensdauer des Kontexts zu machen.

14
Rick Strahl

Ich stimme früheren Meinungen zu. Es ist gut zu sagen, dass Sie mehr Speicher benötigen, wenn Sie DbContext in einer Einzelthread-App freigeben möchten. Zum Beispiel benötigt meine Webanwendung auf Azure (eine extra kleine Instanz) weitere 150 MB Arbeitsspeicher und ich habe ungefähr 30 Benutzer pro Stunde. Application sharing DBContext in HTTP Request

Hier ist ein echtes Beispielbild: Die Anwendung wurde in 12.00 Uhr bereitgestellt

9
Miroslav Holec

Was ich daran mag, ist, dass es die Arbeitseinheit (wie der Benutzer es sieht - d. H. Eine übermittelte Seite) mit der Arbeitseinheit im ORM-Sinne ausrichtet.

Aus diesem Grund können Sie die gesamte Übermittlung einer Seite transaktional machen, was Sie nicht tun könnten, wenn Sie CRUD-Methoden mit jedem Erstellen eines neuen Kontexts verfügbar machen würden.

3
RB.

Ein weiterer Grund, warum ein Singleton-DbContext nicht verwendet wird, selbst in einer Single-Threaded-Einzelbenutzeranwendung, ist das verwendete Identitätszuordnungsmuster. Dies bedeutet, dass jedes Mal, wenn Sie Daten mit query oder by id abrufen, die abgerufenen Entitätsinstanzen im Cache bleiben. Wenn Sie das nächste Mal dieselbe Entität abrufen, erhalten Sie die zwischengespeicherte Instanz der Entität, sofern verfügbar, mit allen Änderungen, die Sie in derselben Sitzung vorgenommen haben. Dies ist erforderlich, damit die SaveChanges-Methode nicht mit mehreren unterschiedlichen Entitätsinstanzen desselben Datenbankdatensatzes bzw. derselben Datenbankdatensätze endet. Andernfalls müsste der Kontext die Daten aus all diesen Entitätsinstanzen irgendwie zusammenführen.

Der Grund, der ein Problem darstellt, ist, dass ein Singleton-DbContext zu einer Zeitbombe werden kann, die schließlich die gesamte Datenbank + den Overhead von .NET-Objekten im Speicher zwischenspeichern kann.

Es gibt Möglichkeiten, dieses Verhalten zu umgehen, indem nur Linq-Abfragen mit der Erweiterungsmethode .NoTracking() verwendet werden. Auch heutzutage haben PCs viel RAM. Aber normalerweise ist das nicht das gewünschte Verhalten.

2
Dmitry S.

Ein weiteres Problem, auf das Sie bei Entity Framework besonders achten sollten, ist die Kombination aus Erstellen neuer Entitäten, verzögertem Laden und anschließender Verwendung dieser neuen Entitäten (aus demselben Kontext). Wenn Sie IDbSet.Create nicht verwenden (statt nur new), funktioniert das verzögerte Laden dieser Entität nicht, wenn sie aus dem Kontext abgerufen wird, in dem sie erstellt wurde. Beispiel:

 public class Foo {
     public string Id {get; set; }
     public string BarId {get; set; }
     // lazy loaded relationship to bar
     public virtual Bar Bar { get; set;}
 }
 var foo = new Foo {
     Id = "foo id"
     BarId = "some existing bar id"
 };
 dbContext.Set<Foo>().Add(foo);
 dbContext.SaveChanges();

 // some other code, using the same context
 var foo = dbContext.Set<Foo>().Find("foo id");
 var barProp = foo.Bar.SomeBarProp; // fails with null reference even though we have BarId set.
1
Ted Elliott