# Change-Tracking von Entitäten in EF Core (28.01.2025) Eine wichtige Thematik bei der Nutzung von EF Core als Data Access Technologie ist das Tracking von Änderungen. In diesem Blog möchte ich eine Einführung in die Funktionsweise geben und explizites Tracking genauer erklären. Um die Datenbank mit den serialisierten Objekten in der Applikation synchron zu halten, werden die Entities von EF Core ge-*tracked* (folgend auch "verfolgt" genannt). Das Tracking wird durch den DbContext geführt, wobei alle Änderungen an dem Entity gemerkt und bei einem Aufruf von `SaveChanges` in die Datenbank gespeichert werden. Eine Entity-Instanz wird verfolgt, sobald sie: - von einem Query, welcher auf der Datenbank ausgeführt wird, zurückgegeben wurde. - explizit an einen DbContext angefügt wird. - in Verbindung zu einer bereits verfolgten Entity stehen. Nicht mehr verfolgt werden sie, sobald: - die DbContext-Instanz gelöscht wird. - der *Change Tracker* geleert wird. - die Instanz explizit vom DbContext gelöst wird. Der normale Weg um eine Instanz nicht mehr zu verfolgen wäre über die Löschung der entsprechend DbContext Instanz. EF Core speichert jedoch nicht nur ob eine Entity verfolgt wird, sondern auch den Zustand der Instanz. Der Zustand wird über einen `EntityState` angegeben welcher folgende Werte haben kann: - `Detached`: Entities welche nicht vom DbContext verfolgt werden. - `Added`: Entities, welche neu hinzugefügt wurden und noch nicht in der Datenbank existieren. - `Unchanged`: Der Standard-Wert von Entities welche durch einen Query zurückgegeben werden. Diese Entities wurden noch nicht geändert und repräsentieren die gleichen Daten wie auch in der Datenbank. - `Modified`: Entities welche geändert wurden seit dem letzten Abruf aus der Datenbank. Diese werden aktualisiert, sobald `SaveChanges` ausgeführt wird. - `Deleted`: Diese Entities existieren noch in der Datenbank, wurden jedoch in der Applikation gelöscht. Die Änderungen werden von EF Core pro Eigenschaft verfolgt. Wenn also nur eine Eigenschaft geändert wurde, wird auch nur diese in der Datenbank aktualisiert. ## Tracking von Queries[^1] Da EF Core den Zustand einer Entity, welche aus der Datenbank geholt wurde, automatisch verfolgt, ist das Handling am einfachsten, wenn die gleiche DbContext-Instanz für das Auslesen wie auch für das Aktualisieren genutzt wird. Dieser Vorgang hat mehrere Vorteile: - Entity Zustände müssen selten explizit geändert werden, da EF Core die Änderungen automatisch speichert - Nur die nötigsten Werte werden in der Datenbank geändert - Die ursprünglichen Werte von Eigenschaften werden gespeichert und für effiziente Updates automatisch genutzt Je nach Architektur und Art der Applikation ist dies jedoch nicht möglich. ## Explizites Tracking von Entitäten[^2] Entitäten können auch explizit an einen DbContext angefügt (engl. "attached") werden, um sie zu verfolgen. Dieser Vorgang ist dann vorteilhaft, wenn: - Neue Entitäten erstellt werden welche in die Datenbank gespeichert werden sollten. - Entbundene Entitäten von einer anderen DbContext Instanz von einem neuen Context verfolgt werden sollten. Der erste Fall kommt in den meisten Applikationen vor und wird von der `DbContext.Add` Methode übernommen. Der zweite Fall trifft nur dann ein, wenn eine Entität oder deren Beziehungen geändert wird, während sie nicht verfolgt wird. Ein Beispiel dafür wäre, wenn eine Web Applikation eine Entität an den Client sendet, der Benutzer darin Änderungen vornimmt, und die Entität anschliessend zurückgesendet wird. Diese Entitäten werden entbunden (engl. "disconnected") genannt, da sie ursprünglich von einem DbContext Query zurückgegeben wurden, anschliessend von diesem Context jedoch entbunden wurden. Die Web Applikation muss diese Entität wieder verbinden um die Änderungen in die Datenbank schreiben zu können. ### Neue Entitäten einfügen Eine Entität muss im `Added` Zustand verfolgt werden, damit sie bei einem Aufruf von `SaveChanges` in die Datenbank gespeichert wird. Gemacht wird dies durch einen Aufruf von entweder `DbContext.Add`, `DbContext.AddRange`, `DbContext.AddAsync` oder `DbContext.AddRangeAsync`. > [!Info]- Code-Kontext für die folgenden Beispiele > In den folgenden Code-Beispielen werden wir diese Klasse referenzieren: > > ```csharp > public class Blog > { > public int Id { get; set; } > public string Name { get; set; } > } > ```` > > Von einer entsprechenden DbContext Instanz ist auszugehen. Um beispielsweise einen neuen Blog zu verfolgen, würde der Code folgendermassen aussehen: ```csharp context.Add( new Blog { Id = 1, Name = "Neuer Blog" } ); ``` Wenn anschliessend die `SaveChanges` Methode ausgeführt werden würde, würde der folgende SQL Query generieren werden: ```sql -- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='Neuer Blog' (Size = 10)], CommandType='Text', CommandTimeout='30'] INSERT INTO "Blogs" ("Id", "Name") VALUES (@p0, @p1); ``` Es ist wichtig zu beachten, dass die `Add` Methode (gilt für alle Variationen) nicht nur diese einzelne Entität verfolgt, sondern den ganzen Abhängigkeitsgraphen. Wenn unser Blog beispielsweise mehrere `Post` Objekte hätte, würden auch diese von EF Core verfolgt werden. ### Existierende Entitäten anfügen Entitäten welche von einem DbContext Query zurückgegeben werden, werden im `Unchanged` Zustand verfolgt. Eine entbundene Entität kann wieder in diesen Zustand gelangen, in dem sie über die `DbContext.Attach` oder `DbContext.AttachRange` Methode angefügt wird: ```csharp context.Attach( new Blog { Id = 1, Name = "Beispiel Blog" } ); ``` Wie auch `Add` setzt `Attach` den ganzen verbundenen Graphen in den `Unchanged` Zustand. ### Existierende Entitäten aktualisieren Die `DbContext.Update` (bzw. `DbContext.UpdateRange`) verhält sich wie die `Attach` Methoden, setzt die Entität jedoch in den `Modified` Zustand. Diese Entitäten würden bei einem Aufruf von `SaveChanges` in der Datenbank aktualisiert werden. Auch hier wird wieder der ganze verbundene Graph in den `Modified` Zustand gesetzt. Mit dem Blog Beispiel würde das folgendermassen aussehen: ```csharp context.Update( new Blog { Id = 1, Name = "Aktualisierter Blog" } ); ``` Das würde zu folgendem SQL Query führen: ```sql -- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0='Aktualisierter Blog' (Size = 19)], CommandType='Text', CommandTimeout='30'] UPDATE "Blogs" SET "Name" = @p0 WHERE "Id" = @p1; SELECT changes(); ``` ### Existierende Entitäten löschen Um eine Entität in der Datenbank zu löschen muss diese im `Deleted` Zustand verfolgt werden. Dies kann über einen Aufruf von `DbContext.Remove` oder `DbContext.RemoveRange` erreicht werden. Beispielsweise: ```csharp context.Remove( new Blog { Id = 1, Name = "Beispiel Blog" } ); ``` Was zu folgendem SQL Query führt: ```sql -- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String)], CommandType='Text', CommandTimeout='30'] DELETE FROM "Blogs" WHERE "Id" = @p0; SELECT changes(); ``` Nachdem `SaveChanges` vollständig ausgeführt wurde, wird die Entität vom DbContext entbunden und ist nicht mehr in der Datenbank. ## Fazit EF Core ist eine sehr ausführliche Implementation des Unit-of-Work Musters und kommt mit einer API welche uns einen grossen Teil der Arbeit abnimmt. Im Ideal-Fall müssen wir uns gar keine Gedanken über das Tracking machen. Wenn wir durch eine Architektur jedoch dazu gezwungen sind, haben wir mit den `Update`, `Attach` und `Remove` Methoden eine einfache Schnittstelle [^1]: vgl. Change Tracking in EF Core, in: Microsoft Learn, 22.01.2025, https://learn.microsoft.com/en-us/ef/core/change-tracking/ [^2]: vgl. Explicitly Tracking Entities, in: Microsoft Learn, 28.01.2025, https://learn.microsoft.com/en-us/ef/core/change-tracking/explicit-tracking