Ich habe viele Artikel gelesen, in denen erklärt wurde, wie Entity Frameworks DbContext
so eingerichtet werden, dass nur eines pro HTTP-Webanforderung mit verschiedenen DI-Frameworks erstellt und verwendet wird.
Warum ist das überhaupt eine gute Idee? Welche Vorteile erzielen Sie mit diesem 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 nicht tun können, wenn Sie DbContext
s pro Repository-Methodenaufruf instanziieren ?
Antworten:
Beginnen wir mit dem Echo von Ian: Eine einzige
DbContext
für die gesamte Anwendung zu haben, ist eine schlechte Idee. Dies ist nur dann sinnvoll, wenn Sie über eine Single-Threaded-Anwendung und eine Datenbank verfügen, die ausschließlich von dieser einzelnen Anwendungsinstanz verwendet wird. DasDbContext
ist nicht threadsicher und da dieDbContext
Daten zwischengespeichert werden, 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 ist). Aber ich gehe davon aus, dass Sie das bereits wissen und nur wissen möchten, warum Sie nicht einfachDbContext
jedem, der es braucht , eine neue Instanz (dh mit einem vorübergehenden Lebensstil) von injizieren . (Weitere Informationen darüber, warum eine einzelneDbContext
oder sogar im Kontext pro Thread schlecht ist, finden Sie in dieser Antwort. )Lassen Sie mich zunächst sagen, dass die Registrierung eines
DbContext
als vorübergehend 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 Lebensstil pro Webanfrage. Auf diese Weise können Sie eine ganze Reihe von Objekten im selben Kontext arbeiten lassen. Mit anderen Worten, sie arbeiten innerhalb derselben Geschäftstransaktion.Wenn Sie nicht das Ziel haben, dass eine Reihe von Operationen im selben Kontext ausgeführt werden, ist in diesem Fall der vorübergehende Lebensstil in Ordnung, aber es gibt einige Dinge zu beachten:
_context.SaveChanges()
ändert , aufrufen (andernfalls gehen Änderungen verloren). Dies kann Ihren Code komplizieren und dem Code eine zweite Verantwortung hinzufügen (die Verantwortung für die Kontrolle des Kontexts). Dies verstößt gegen das Prinzip der Einzelverantwortung .DbContext
] 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 enorm komplizieren, da Sie diese Entitäten, wenn Sie sie benötigen, erneut über die ID laden müssen, was ebenfalls zu Leistungsproblemen führen kann.DbContext
ImplementierungIDisposable
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 direkt nach dem Aufruf auf dieselbe Weise entsorgen.context.SaveChanges()
In diesem Fall übernimmt die Geschäftslogik jedoch 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 benötigen Sie jedoch noch einen Bereich, um den Container darüber zu informieren, wann diese Instanzen entsorgt werden müssen.Eine andere Möglichkeit ist, überhaupt keine zu injizieren
DbContext
. Stattdessen fügen Sie eine einDbContextFactory
, die eine neue Instanz erstellen kann (ich habe diesen Ansatz in der Vergangenheit verwendet). Auf diese Weise steuert die Geschäftslogik den Kontext explizit. Wenn es so aussehen könnte:Das Plus daran ist, dass Sie das Leben der
DbContext
explizit verwalten und es einfach einzurichten ist. Außerdem können Sie einen einzelnen Kontext in einem bestimmten Bereich verwenden, was klare Vorteile bietet, z. B. das Ausführen von Code in einem einzelnen Geschäftsvorgang und das Weitergeben von Entitäten, da diese von demselben stammenDbContext
.Der Nachteil ist, dass Sie die
DbContext
Methode von Methode zu Methode weitergeben müssen (was als Methodeninjektion bezeichnet wird). Beachten Sie, dass diese Lösung in gewissem Sinne mit dem "Scoped" -Ansatz identisch ist, der Umfang jedoch jetzt im Anwendungscode selbst gesteuert wird (und möglicherweise viele Male wiederholt wird). Es ist die Anwendung, die für die Erstellung und Entsorgung der Arbeitseinheit verantwortlich ist. Da dasDbContext
erstellt wird, nachdem das Abhängigkeitsdiagramm erstellt wurde, ist die Konstruktorinjektion nicht im Bild und Sie müssen auf die Methodeninjektion zurückgreifen, wenn Sie den Kontext von einer Klasse zur anderen weitergeben müssen.Die Methodeninjektion ist nicht so schlecht, aber wenn die Geschäftslogik komplexer wird und mehr Klassen involviert sind, müssen Sie sie von Methode zu Methode und von Klasse zu Klasse übergeben, was den Code sehr komplizieren kann (wie ich gesehen habe) dies in der Vergangenheit). Für eine einfache Anwendung ist dieser Ansatz jedoch ausreichend.
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 Sie den Container und / oder die Infrastruktur damit umgehen lassen, wird Ihr Anwendungscode nicht durch das Erstellen, (optional) Festschreiben und Entsorgen einer UoW-Instanz verschmutzt, wodurch die Geschäftslogik einfach und sauber bleibt (nur eine einzige Verantwortung). Bei diesem Ansatz gibt es einige Schwierigkeiten. Haben Sie beispielsweise die Instanz 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 einfach nicht sicher bestimmen, ob die Arbeitseinheit tatsächlich festgeschrieben werden soll. Beispiel: Wenn der Business-Layer-Code eine Ausnahme ausgelöst hat, die höher im Callstack abgefangen wurde, möchten Sie definitiv kein Commit durchführen.
Die eigentliche Lösung besteht wiederum darin, einen Bereich explizit zu verwalten, diesmal jedoch im Composition Root. Wenn Sie die gesamte Geschäftslogik hinter dem Befehls- / Handler-Muster abstrahieren , können Sie einen Dekorator schreiben, der um jeden Befehlshandler gewickelt werden kann, der dies ermöglicht. Beispiel:
Dadurch wird sichergestellt, dass Sie diesen Infrastrukturcode nur einmal schreiben müssen. Mit jedem festen DI-Container können Sie einen solchen Dekorator so konfigurieren, dass er alle
ICommandHandler<T>
Implementierungen auf konsistente Weise umschließt.quelle
CreateCommand<TEnity>
und ein generisches erstellenCreateCommandHandler<TEntity> : ICommandHandler<CreateCommand<TEntity>>
(und dasselbe für Aktualisieren und Löschen tun und hatten eine einzigeGetByIdQuery<TEntity>
Abfrage). Sie sollten sich jedoch fragen, ob dieses Modell eine nützliche Abstraktion für CRUD-Operationen ist oder nur die Komplexität erhöht. Dennoch können Sie von der Möglichkeit profitieren, mit diesem Modell auf einfache Weise Querschnittsthemen (durch Dekorateure) hinzuzufügen. Sie müssen die Vor- und Nachteile abwägen.TransactionCommandHandlerDecorator
? Wenn die dekorierte Klasse beispielsweise eineInsertCommandHandler
Klasse ist, wie kann sie dann die Einfügeoperation im Kontext registrieren (DbContext in EF)?Es gibt zwei widersprüchliche Empfehlungen von Microsoft, und viele Menschen verwenden DbContexts auf völlig unterschiedliche Weise.
Diese widersprechen sich, denn wenn Ihre Anfrage viel mit dem Db-Zeug zu tun hat, wird Ihr DbContext ohne Grund beibehalten. Daher ist es eine 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 folgen, haben ihre DbContexts in ihrem "Repository-Muster" und erstellen eine neue Instanz pro Datenbankabfrage, also X * DbContext pro Anfrage
Sie erhalten nur ihre Daten und entsorgen den Kontext so schnell wie möglich. Dies wird von vielen Menschen als akzeptable Praxis angesehen. Dies hat zwar den Vorteil , dass Sie Ihre Datenbankressourcen für die minimale Zeit belegen, aber es opfert eindeutig alle UnitOfWork- und Caching- Süßigkeiten, die EF zu bieten hat.
Wenn Sie eine einzelne Mehrzweckinstanz von DbContext am Leben erhalten, werden die Vorteile von Caching maximiert. Da DbContext jedoch nicht threadsicher ist und jede Webanforderung in einem eigenen Thread ausgeführt wird, ist ein DbContext pro Anforderung die längste, die Sie behalten können.
Die Empfehlung des EF-Teams zur Verwendung von 1-dB-Kontext pro Anforderung basiert eindeutig auf der Tatsache, dass sich eine UnitOfWork in einer Webanwendung höchstwahrscheinlich innerhalb einer Anforderung befindet und diese Anforderung einen Thread hat. Ein DbContext pro Anfrage ist also der ideale Vorteil von UnitOfWork und Caching.
Aber in vielen Fällen ist dies nicht wahr. Ich halte Protokollierung über einen separaten UnitOfWork damit einen neuen DbContext für in Post-Request Logging mit Asynchron - Gewinde ist völlig akzeptabel
Schließlich stellt sich heraus, dass die Lebensdauer eines DbContext auf diese beiden Parameter beschränkt ist. UnitOfWork und Thread
quelle
Keine einzige Antwort hier beantwortet tatsächlich die Frage. Das OP fragte nicht nach einem DbContext-Design für Singleton / pro Anwendung, sondern nach einem Design pro (Web-) Anfrage und nach möglichen Vorteilen.
Ich verweise auf http://mehdi.me/ambient-dbcontext-in-ef6/, da Mehdi eine fantastische Ressource ist:
Denken Sie daran, dass es auch Nachteile gibt. Dieser Link enthält viele andere Ressourcen, die Sie zu diesem Thema lesen können.
Posten Sie dies einfach, falls jemand anderes auf diese Frage stößt und sich nicht in Antworten vertieft, die die Frage nicht wirklich beantworten.
quelle
Ich bin mir ziemlich sicher, dass der DbContext überhaupt nicht threadsicher ist. Das Ding zu teilen ist also nie eine gute Idee.
quelle
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 Sie können den Änderungsbaum nicht löschen. Wenn Sie also einen Kontext pro Anforderung verwenden, haben Sie kein Glück, wenn Sie Änderungen aus irgendeinem Grund wegwerfen müssen.
Persönlich erstelle ich bei Bedarf Instanzen von DbContext - normalerweise angehängt an Geschäftskomponenten, die den Kontext bei Bedarf neu erstellen können. Auf diese Weise habe ich die Kontrolle über den Prozess, anstatt mir eine einzelne Instanz aufzwingen zu lassen. Ich muss den DbContext auch nicht bei jedem Controller-Start erstellen, unabhängig davon, ob er tatsächlich verwendet wird. Wenn ich dann immer noch Instanzen pro Anforderung haben möchte, kann ich sie im CTOR erstellen (über DI oder manuell) oder sie nach Bedarf in jeder Controller-Methode erstellen. Persönlich verfolge ich normalerweise den letzteren Ansatz, um zu vermeiden, dass DbContext-Instanzen erstellt werden, wenn sie nicht wirklich benötigt werden.
Es hängt davon ab, aus welchem Blickwinkel Sie es auch betrachten. Für mich hat die Instanz pro Anfrage nie Sinn gemacht. Gehört der DbContext wirklich zur HTTP-Anfrage? 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 und müssen sich nie um die Lebensdauer des Kontexts sorgen.
quelle
Ich stimme früheren Meinungen zu. Es ist gut zu sagen, dass Sie mehr Speicher benötigen, wenn Sie DbContext in einer Single-Thread-App freigeben möchten. Zum Beispiel benötigt meine Webanwendung in Azure (eine extra kleine Instanz) weitere 150 MB Speicher und ich habe ungefähr 30 Benutzer pro Stunde.
Hier ist ein echtes Beispielbild: Die Anwendung wurde um 12:00 Uhr bereitgestellt
quelle
Was ich daran mag, ist, dass es die Arbeitseinheit (wie der Benutzer es sieht - dh eine Seite einreichen) mit der Arbeitseinheit im ORM-Sinne ausrichtet.
Daher können Sie die gesamte Seitenübermittlung transaktional durchführen, was Sie nicht tun könnten, wenn Sie CRUD-Methoden bei jeder Erstellung eines neuen Kontexts verfügbar machen würden.
quelle
Ein weiterer untertriebener Grund dafür, dass kein Singleton-DbContext verwendet wird, selbst in einer Einzelbenutzeranwendung mit einem Thread, ist das verwendete Identitätszuordnungsmuster. Dies bedeutet, dass jedes Mal, wenn Sie Daten mithilfe einer Abfrage oder einer 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 zu mehreren verschiedenen Entitätsinstanzen desselben Datenbankdatensatzes führt. Andernfalls müsste der Kontext die Daten aller dieser Entitätsinstanzen irgendwie zusammenführen.
Der Grund für das Problem ist, dass ein Singleton-DbContext zu einer Zeitbombe werden kann, die möglicherweise die gesamte Datenbank + den Overhead von .NET-Objekten im Speicher zwischenspeichert.
Es gibt Möglichkeiten, dieses Verhalten zu umgehen, indem nur Linq-Abfragen mit der
.NoTracking()
Erweiterungsmethode verwendet werden. Auch heutzutage haben PCs viel RAM. Aber normalerweise ist das nicht das gewünschte Verhalten.quelle
Ein weiteres Problem, auf das Sie bei Entity Framework besonders achten sollten, ist die Verwendung einer Kombination aus dem Erstellen neuer Entitäten, dem verzögerten Laden und dem anschließenden Verwenden dieser neuen Entitäten (aus demselben Kontext). Wenn Sie IDbSet.Create nicht verwenden (im Gegensatz zu nur neu), funktioniert das verzögerte Laden dieser Entität nicht, wenn sie aus dem Kontext abgerufen wird, in dem sie erstellt wurde. Beispiel:
quelle