ANMERKUNG: Diese Antwort bezieht sich auf das Entity Framework DbContext
, ist jedoch auf jede Art von Unit of Work-Implementierung anwendbar, z. B. auf LINQ to SQL DataContext
und NHibernate ISession
.
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. Das DbContext
ist nicht threadsicher und da die DbContext
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 einfach DbContext
jedem, der es braucht , eine neue Instanz (dh mit einem vorübergehenden Lebensstil) von injizieren . (Weitere Informationen darüber, warum eine einzelne DbContext
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:
- Da jedes Objekt eine eigene Instanz erhält, muss jede Klasse, die den Status des Systems
_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 .
- Sie müssen sicherstellen, dass Entitäten [geladen und gespeichert von a
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.
- Seit der
DbContext
Implementierung IDisposable
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 ein DbContextFactory
, 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:
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 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 stammen DbContext
.
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 das DbContext
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:
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();
}
}
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.