# Lifecycle-Management für DbContext-Instanzen - 1 (30.10.2024) EF Core ist ein Framework welches viel mehr bietet als nur grundlegende ORM-Funktionalität. Viele dieser Features ermöglichen nicht nur eine bessere Interaktion mit der Datenbank, sondern verbessern auch die Performance stark. Damit diese Vorteile aber auch genutzt werden können, gibt es vieles zu beachten. Einer der wichtigsten Aspekte hierbei ist die Verwaltung der DbContext-Instanzen. In diesem Blog werden wir über verschiedene Vorgehensweisen gehen und uns die Vorteile wie auch die Nachteile anschauen. Beim Lifecycle-Management von DbContext-Instanzen gibt es viele bewegende Komponenten und es ist wichtig zu beachten, dass keine der präsentierten Vorgehensweisen für alle Szenarien sinnvoll ist. Genau deswegen ist es wichtig, die Gedanken hinter jeder Vorgehensweise zu verstehen, um eine möglichst optimale Entscheidung treffen zu können. > [!Note] Bedeutung von Service > Folgend werde ich viel auf den Begriff "Service" zurückgreifen. Damit sind keine externen Services (z.B.: REST) gemeint sondern sogenannte *Service Objects*, also die Teile, in denen die Geschäftslogik umgesetz ist. Je nach Design Pattern / Architektur der Applikation kann das eine eigene Schicht (*Layer*) aber auch ein nicht so klar definierter Bereich sein. ## Aspekte die beachtet werden müssen Wenn wir eine Lifecycle-Management Strategie für DbContext-Instanzen auswerten wollen gibt es bestimmte Aspekte, die beachtet werden müssen: ### Die Lebensdauer einer DbContext-Instanz muss nicht an die Lebensdauer einer Geschäftstransaktion gebunden sein, Services müssen diese jedoch kontrollieren können > [!Hint]- Geschäftstransaktion (*Business Transaction*) > Unter Geschäftstransaktion versteht man die zusammenhängenden Schritte, welche für eine gewünschte Änderung nötig sind. Angenommen wir haben eine Web-Applikation für Bestellungen und benötigen die Möglichkeit, eine neue Bestellung zu erstellen: Ein Beispiel für eine Geschäftstransaktion wäre also eine Methode, welche alle Schritte für das umwandeln der Daten in den korrekten Typ und das anschliessende hinzufügen zu den Bestellungen enthaltet. Einer der vielleicht grössten Faktoren, der zu Verwirrung bei der Verwaltung con DbContext-Lifecycles führen kann, ist der Unterschied zwischen der Lebensdauer der DbContext-Instanz und der Lebensdauer einer Geschäftstransaktion. > [!Note] DbContext setzt das Unit-of-Work-Muster ein > "Unit of Work - Maintains a list of objects affected by a business transaction and coordinates the writing out of changes and the resolution of concurrency problems."[^2] > > Das bedeutet, dass Änderungen einer Geschäftstransaktion zuerst in einer Liste gespeichert werden und erst anschliessend in die Datenbank geschrieben werden. > In EF Core heisst das, dass eine DbContext-Instanz genutzt wird um Objekte zu laden, die Instanz merkt sich anschliessend die Änderungen. Diese speichert es erst in der Datenbank, sobald die `SaveChanges()` Methode aufgerufen wird. Eine Service-Methode ist verantwortlich dafür, die Abgrenzungen einer Geschäftstransaktion zu definieren. Die Konsequenzen davon sind: - Eine Service-Methode muss die gleiche DbContext-Instanz über die Dauer der Geschäftstransaktion nutzen. Der DbContext ist dafür zuständig, die Units of Work zu speichern. Wenn innerhalb einer Geschäftstransaktion der DbContext wechselt, werden die bisherigen Änderungen nicht mehr gespeichert. - Die Services sollten als einzige die `SaveChanges()` Methode aufrufen. Wird die Methode auch in anderen Teilen der Applikation aufgerufen (z.B.: in einem Repository), endet man mit teilweise gespeicherten Änderungen, was die Daten in einem inkonsistenten Zustand lässt. - Die `SaveChanges()` Methode sollte genau einmal am ende einer Geschäftstransaktion aufgerufen werden. Wird sie während der Transaktion bereits aufgerufen, endet man wieder mit inkonsistenten, teilweise gespeicherten Änderungen. Eine DbContext-Instanz kann jedoch problemlos über mehrere Geschäftstransaktionen verwendet werden. Sobald die Änderungen einer Geschäftstransaktion gespeichert sind, besteht keine Gefahr mehr, diese Daten zu beschädigen. #### Vor- und Nachteile einer Geschäftstransaktionen-unabhängigen Lebensdauer der DbContext-Instanz Der Hauptgrund für eine getrennte Lebensdauer der DbContext-Instanz ist: - **Bessere Performance**: Jede DbContext-Instanz verwaltet einen sogenannten *First-Level Cache* für all die Entitäten, die von der Datenbank geladen werden. Wenn eine Abfrage auch den DbContext gemacht wird und keine Änderungen geschehen sind, können die gleichen Objekte aus dem Cache genutzt werden. Dies minimiert die Anfragen an die Datenbank, was viele Ressourcen einsparen kann. Es werden aber auch weitere Optionen wie beispielsweise Lazy-Loading ermöglicht. > [!note] DbContext Pooling > Seit EF Core 2.0 gibt es die Möglichkeit, einen DbContext Pool zu implementieren. Mit diesem werden entsorgte Instanzen nicht gelöscht sondern zurückgesetzt, was sie ohne grosse Kosten wieder verwendbar macht. Die Performance-Unterschiede zu dieser Vorgehensweise sind also nicht mehr sehr relevant. Will man jedoch auf Features wie Lazy-Loading zurückgreifen, bietet es sich onehin an.[^6] Während es unproblematisch sein kann, eine DbContext-Instanz über mehrere Geschäftstransaktionen zu nutzen, sollte die Lebensdauer trotzdem möglichst kurz gehalten werden. Nach einer weile wird der First-Level Cache abgestanden, was auch wieder zu *Concurrency*-Problemen führen kann. Auch eine DbContext-Instanz über mehrere Services zu nutzen ist möglich, bindet die Applikation jedoch an eine Infrastruktur, was die Flexibilität stark einschränkt. ### Die Vorgehensweise bei der Verwaltung vom DbContext sollte unabhängig von der Architektur der Applikation sein Die Architektur einer Applikation und die verwendeten Design-Patterns entwickeln sich mit der Zeit und dem Projekt. Die Strategie des DbContext sollte nicht an eine bestimmte Architektur gebunden sein. So kann sich alles Unabhängig voneinander nach Bedarf entwickeln. ### Die Vorgehensweise bei der Verwaltung vom DbContext sollte unabhängig vom Typ der Applikation sein Die Strategie für die Verwaltung der Lebensdauer des DbContext sollte keine Ansprüche an das Umfeld haben, in welchem die Service-Methode aufgerufen wird. Es sollte beispielsweise nicht davon ausgegangen werden, dass eine Service-Methode immer in einem Web-Request aufgerufen wird. Stattdessen sollte die Service Schicht (sofern vorhanden) immer unabhängig von den darüberliegenden Schichten entwickelt werden. ### Die DbContext-Verwaltungsstrategie sollte verschiedene von DbContext erbende Typen unterstützen Wenn die Applikation mehrere Datenbanken nutzt (beispielsweise eine eigene für Logging), muss man in der Lage sein, mehrere von DbContext abgeleitete Typen zu verwalten. Die gewählte Strategie sollte also in der Lage sein, den richtigen DbContext-Typ zu wählen. ## Das Normalverhalten von DbContext Es gibt viele Verhaltensweisen die bei der Nutzung von einem DbContext beachtet werden müssen: ### DbContext sind nicht Thread-sicher EF Core unterstützt keine parallele Operationen in der gleichen DbContext-Instanz. Eine DbContext-Instanz sollte **niemals** also über mehrere Threads zeitgleich verwendet werden. Asynchrone Aufrufe müssen immer entweder abgewartet (`await`) oder eine neue DbContext-Instanz für die parallele Arbeit muss verwendet werden. [^4] ### Änderungen sind nur persistent, wenn `SaveChanges()` aufgerufen wird Änderungen welche gemacht wurden werden erst mit der Datenbank synchronisiert, sobald die Methode aufgerufen wird. Wenn eine DbContext-Instanz vor dem Aufruf entsorgt (*disposed*) wird gehen die Änderungen verloren.[^1] ### DbContext-Instanzen *sollten* entsorgt werden Die `DbContext` Klasse implementiert das `IDisposable` Interface. Entsprechend *sollten* Instanzen entsorgt werden, sobald sie nicht mehr benötigt werden. Wie sich herausstellt ist dies jedoch nicht unbedingt nötig. EF Core ist so implementiert, dass die Verbindung automatisch geöffnet wird sobald man sie braucht und geschlossen wird sobald man sie nicht mehr braucht. Ein Aufruf von `SaveChanges()` wird also die Entsorgung vornehmen bevor die Methode endet.[^5] [^1]: vgl. Managing DbContext the right way with Entity Framework 6: an in-depth guide, in: mehdi.com, 30.10.2024, https://mehdi.me/ambient-dbcontext-in-ef6/ [^2]: vgl. Unit of Work, in: martinFowler.com, 30.10.2024, https://martinfowler.com/eaaCatalog/unitOfWork.html [^3]: vgl. Make the DbContext Ambient with UnitOfWorkScope, in: PASSION FOR CODING, 30.10.2024, https://coding.abel.nu/2012/10/make-the-dbcontext-ambient-with-unitofworkscope/?ref=mehdi.me [^4]: vgl. DbContext Lifetime, Configuration, and Initialization, in: Microsoft Learn, 30.10.2024, https://learn.microsoft.com/en-us/ef/core/dbcontext-configuration/ [^5]: vgl. Do I always have to call Dispose() on my DbContext objects? Nope, in: Jon Gallant, 30.10.2024, https://blog.jongallant.com/2012/10/do-i-have-to-call-dispose-on-dbcontext/ [^6]: vgl. Advanced Performance Topics, in: Microsoft Learn, 30.10.2024, https://learn.microsoft.com/en-us/ef/core/performance/advanced-performance-topics#dbcontext-pooling