HINWEIS: Diese Antwort spricht über die Entity Framework's DbContext
aber es ist auf jede Art von Unit of Work-Implementierung anwendbar, wie z.B. LINQ to SQL's DataContext
und NHibernate's ISession
.
Zunächst möchte ich mich Ian anschließen: Eine einzige DbContext
für die gesamte Anwendung ist eine schlechte Idee. Die einzige Situation, in der dies sinnvoll ist, ist eine Single-Thread-Anwendung und eine Datenbank, die nur von dieser einen Anwendungsinstanz verwendet wird. Die DbContext
nicht thread-sicher ist und da die DbContext
Daten zwischenspeichert, werden sie bald veraltet sein. Dies führt zu allen möglichen Problemen, wenn mehrere Benutzer/Anwendungen gleichzeitig mit dieser Datenbank arbeiten (was natürlich sehr häufig vorkommt). Aber ich nehme an, Sie wissen das bereits und wollen nur wissen, warum Sie nicht einfach eine neue Instanz (d.h. mit einem transienten Lebensstil) der DbContext
für jeden, der es braucht. (für weitere Informationen darüber, warum ein einzelner DbContext
-oder sogar auf Kontext pro Thread- ist schlecht, lesen 本答 ).
Zunächst möchte ich sagen, dass die Registrierung einer DbContext
als transient funktionieren könnte, aber normalerweise möchte man eine einzige Instanz einer solchen Arbeitseinheit innerhalb eines bestimmten Bereichs haben. In einer Web-Anwendung kann es praktisch sein, einen solchen Bereich an den Grenzen einer Web-Anfrage zu definieren, also einen Per Web Request Lifestyle. Dies ermöglicht es Ihnen, eine ganze Reihe von Objekten im selben Kontext zu betreiben. Mit anderen Worten: Sie arbeiten innerhalb desselben Geschäftsvorfalls.
Wenn Sie nicht das Ziel haben, dass eine Reihe von Operationen im gleichen Kontext ablaufen, dann ist der transiente Lebensstil in Ordnung, aber es gibt ein paar Dinge zu beachten:
- Da jedes Objekt seine eigene Instanz erhält, muss jede Klasse, die den Zustand des Systems ändert, die
_context.SaveChanges()
(sonst würden Änderungen verloren gehen). Dies kann Ihren Code verkomplizieren und fügt dem Code eine zweite Verantwortung hinzu (die Verantwortung für die Kontrolle des Kontexts) und ist ein Verstoß gegen die Prinzip der einzigen Verantwortung .
- Sie müssen sicherstellen, dass Entitäten [geladen und gespeichert von einem
DbContext
] verlassen niemals den Geltungsbereich einer solchen Klasse, da sie nicht in der Kontextinstanz einer anderen Klasse verwendet werden können. Dies kann Ihren Code enorm verkomplizieren, denn wenn Sie diese Entitäten benötigen, müssen Sie sie erneut per id laden, was auch zu Leistungsproblemen führen kann.
- Seit
DbContext
implementiert IDisposable
Wenn Sie eine neue Instanz erstellen, möchten Sie wahrscheinlich trotzdem alle erstellten Instanzen entsorgen. Wenn Sie dies tun wollen, haben Sie grundsätzlich zwei Möglichkeiten. Sie müssen sie in der gleichen Methode entsorgen, direkt nach dem Aufruf von context.SaveChanges()
aber in diesem Fall übernimmt die Geschäftslogik das Eigentum an einem Objekt, das von außen weitergegeben wird. Die zweite Möglichkeit besteht darin, alle erstellten Instanzen am Rande der HTTP-Anforderung zu entsorgen, aber in diesem Fall benötigen Sie immer noch eine Art von Scoping, damit der Container weiß, wann diese Instanzen entsorgt werden müssen.
Eine weitere Möglichkeit ist pas einspritzen DbContext
überhaupt nicht. Stattdessen injizieren Sie eine DbContextFactory
die in der Lage ist, eine neue Instanz zu erstellen (in der Vergangenheit habe ich diesen Ansatz verwendet). Auf diese Weise steuert die Geschäftslogik den Kontext explizit. Das könnte dann 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();
}
}
Der Vorteil dabei ist, dass Sie die Lebensdauer der DbContext
und es ist einfach, dies einzurichten. Außerdem können Sie einen einzigen Kontext in einem bestimmten Bereich verwenden, was eindeutige Vorteile hat, z. B. die Ausführung von Code in einem einzigen Geschäftsvorgang und die Möglichkeit, Entitäten weiterzugeben, da sie aus demselben Kontext stammen. DbContext
.
Der Nachteil besteht darin, dass Sie die Informationen weitergeben müssen. DbContext
von Methode zu Methode (dies wird als Methodeninjektion bezeichnet). Beachten Sie, dass diese Lösung in gewissem Sinne dasselbe ist wie der "scoped"-Ansatz, aber jetzt wird der Umfang im Anwendungscode selbst kontrolliert (und möglicherweise viele Male wiederholt). Es ist die Anwendung, die für das Erstellen und Entsorgen der Arbeitseinheit verantwortlich ist. Da die DbContext
erstellt wird, nachdem der Abhängigkeitsgraph erstellt wurde, ist Constructor Injection nicht mehr möglich, und Sie müssen auf Method Injection zurückgreifen, wenn Sie den Kontext von einer Klasse an die andere weitergeben müssen.
Methodeninjektion ist nicht so schlimm, 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 weitergeben, was den Code sehr verkomplizieren kann (ich habe das in der Vergangenheit gesehen). Für eine einfache Anwendung ist dieser Ansatz jedoch genau richtig.
Wegen der Nachteile, die dieser Fabrik-Ansatz für größere Systeme hat, kann ein anderer Ansatz nützlich sein, und zwar der, bei dem man den Container oder den Infrastrukturcode / Zusammensetzung Wurzel die Arbeitseinheit zu verwalten. Das ist der Stil, um den es in Ihrer Frage geht.
Indem Sie dies dem Container und/oder der Infrastruktur überlassen, wird Ihr Anwendungscode nicht dadurch belastet, dass Sie eine UoW-Instanz erstellen, (optional) übertragen und entsorgen müssen, wodurch die Geschäftslogik einfach und sauber bleibt (nur eine einzige Verantwortung). Dieser Ansatz birgt einige Schwierigkeiten. Wo zum Beispiel wird die Instanz festgeschrieben und entsorgt?
Das Entsorgen einer Arbeitseinheit kann am Ende der Webanforderung erfolgen. Viele Menschen jedoch, falsch gehen Sie davon aus, dass dies auch der Ort ist, an dem Sie die Arbeitseinheit verpflichten. An diesem Punkt in der Anwendung können Sie jedoch nicht mit Sicherheit feststellen, ob die Arbeitseinheit tatsächlich übertragen werden sollte. Wenn z. B. der Code der Geschäftsschicht eine Ausnahme ausgelöst hat, die weiter oben im Aufrufstapel abgefangen wurde, können Sie definitiv nicht verpflichten wollen.
Die eigentliche Lösung besteht wieder darin, explizit eine Art von Bereich zu verwalten, aber diesmal innerhalb der Composition Root. Die Abstraktion der gesamten Geschäftslogik hinter dem Befehl/Handler-Muster können Sie einen Dekorator schreiben, der um jeden Befehlshandler gewickelt werden kann, um dies zu ermöglichen. 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. Jeder solide DI-Container erlaubt es Ihnen, einen solchen Dekorator zu konfigurieren, der um alle ICommandHandler<T>
Implementierungen auf konsistente Weise.