Viele Breaking Changes in Entity Framework Core 3.0

Der Dotnet-Doktor  –  2 Kommentare

Von Entity Framework Core 3.0 gibt es mittlerweile eine vierte Preview-Version, in der man aber noch nicht keine der unten genannten neuen Funktionen findet. Vielmehr hat Microsoft eine erhebliche Anzahl von Breaking Changes eingebaut. Die Frage ist warum?

Für Entity Framework Core 3.0 hat Microsoft erhebliche funktionale Verbesserungen angekündigt:

  • Bessere LINQ-zu-SQL-Umsetzung (mehr Server-Side-Queries, das heißt weniger Client Evaluation, das heißt Ausführung von Abfragen im RAM, die voraussetzen, dass eine Tabelle komplett geladen wird, was nicht vertretbar ist bei großen Datenmengen)
  • NoSQL-Unterstützung und Cosmos DB-Treiber (*)
  • Unterstützung für C# 8.0 (Nullable Reference Types und Async Streams)
  • Reverse Engineering von Datenbank-Views mit Query Types (*)
  • Property Bag Entities (zur Unterstützung für N:M-Abstraktion wie im klassischen Entity Framework, das heißt Zwischentabellen muss dann im Objektmodell nicht mehr als Klassen existieren)

(*) war schon für 2.0/2.1/2.2 angekündigt.

Preview 4 ohne diese Neuerungen, aber mit Breaking Changes

Die Breaking Changes führen dazu, dass wir alle unseren Programmcode beim Umstieg von Version 1.x oder 2.x oder 3.0 ändern werden müssen.

Wenn man sich die Liste der Breaking Changes ansieht, dann sieht man die Beseitigung einiger Inkonsistenzen, Verbesserungen bei der Namensgebung, und man findet darin Fälle, in denen Microsoft nun die Vorbereitung trifft, um später andere Features zu in Entity Framework Core zu implementieren.

Beispiel für Verhaltensänderungen

Eine zukunftsorientierte Verhaltensänderung ist zum Beispiel die "ToTable()should throw on derived types". Bisher unterstützt Entity Framework Core hauptsächlich die Vererbungsstrategie "Table per Hierarchy" (TPH), in einigen Fällen auch "Table per Concrete Type" (TPCT), auch wenn die Dokumentation etwas anderes behauptet. "Table per Type" (TPT) gibt es bisher nicht in Entity Framework Core.

In Entity Framework Core 1.x und 2.x war es möglich, auch bei Entitätsklassen, die von einer anderen Klasse ableiten, einen expliziten Tabellennamen mit der Datenannotation [Table] oder in der Fluent-API per ToTable() anzugeben, auch wenn diese Entitätsklassen nach dem TPH-Prinzip nicht in eine eigenen Datenbanktabelle, sondern in die Tabelle der Basisklasse gewandert sind. Die Namensangaben bei [Table] oder ToTable() wurden einfach ignoriert.

Nun in Entity Framework Core 3.0 löst Microsoft hier plötzlich einen Laufzeitfehler (Invalid Operation Exception: Only base entity types can be mapped to a table) aus. Das mag auf den ersten Blick verwundern, auf den zweiten Blick wird der Sinn aber klar: Microsoft will hiermit die Voraussetzung schaffen, in einer späteren Version [Table] und ToTable() die Bedeutung zu geben, eine TPT-Strategie zu erzwingen. Da gemäß Semantic Versioning Breaking Changes nur bei der Änderung der Hauptversionsnummer erlaubt sind, führt Microsoft diese Verhaltensänderung nun mit Version 3.0 ein; somit könnte dann das TPT-Feature zum Beispiel. in Version 3.1 oder 3.2 erscheinen.

Diese Änderung kann sich auch an Stellen auswirken, die nicht direkt offensichtlich sind. Ich habe bereits eine Anwendung testweise auf Entity Framework Core 3.0 umgestellt und lief auf den Fehler "Invalid Operation Exception: Only base entity types can be mapped to a table". Ich habe in Visual Studio per "Find all references" nach der Annotation [Table] auf der Klasse beziehungsweise einem Aufruf von ToTable() auf der Klasse in der Fluent API gesucht, aber nichts gefunden.

Erst auf den zweiten Blick dachte ich an meine Massenkonfiguration, die dafür sorgt, die Standardkonvention von Microsoft bei den Datenbanktabellen auszuhebeln. Hier ist nun die ergänzende Bedingung && entity.BaseType == null notwendig:

   #region Bulk configuration via model class for all table names
foreach (IMutableEntityType entity in mb.Model.GetEntityTypes())
{
// All table names = class names (~ EF 6.x),
// except the classes that have a [Table] annotation
varannotation = entity.ClrType?.GetCustomAttribute<TableAttribute>();
if (annotation == null && entity.BaseType == null)
{
entity.Relational().TableName = entity.DisplayName();
}
}
#endregion

Beispiele für Namensänderungen

Während obige Änderung erst zur Laufzeit auffällt, gibt es weitere Änderungen, die schon der Compiler bemerkt. So hat Microsoft einige Namen geändert, zum Beispiel:

  • IEntityType.QueryFilter --> GetQueryFilter()
  • IEntityType.DefiningQuery --> GetDefiningQuery()
  • IProperty.IsShadowProperty --> IsShadowProperty()
  • IProperty.BeforeSaveBehavior --> GetBeforeSaveBehavior()
  • IProperty.AfterSaveBehavior --> GetAfterSaveBehavior()
  • ForSqlServerHasIndex().ForSqlServerInclude() --> HasIndex().ForSqlServerInclude()

Leistungsverbesserungen

Weitere Änderungen dienen dazu, die Performance zu steigern. So führt ein Zugriff auf die Entry()-Methode nicht mehr dazu, dass DetectChanges() auf allen Objekten in der Kontextklasse ausgeführt wird, sondern nur noch lokal für das betroffene Objekt und direkt zuordnet Objekte. Bei einigen asynchronen Methoden wie

  • DbContext.FindAsync()
  • DbSet.FindAsync()
  • DbContext.AddAsync()
  • DbSet.AddAsync()
  • ValueGenerator.NextValueAsync()

verwendet Microsoft nun als Rückgabetyp ValueTask<T> statt Task<T>, was die Anzahl der Speicherallokationen auf dem Heap verringert. Die Klasse ValueTask<T> gibt es noch nicht so lange in .NET (erst seit C# 7.0), vorher war nur Task<T> möglich.

Warum nicht schon früher?

Ich kann mir vorstellen, dass zu diesem Beitrag nun ein Kommentar erscheint mit der Frage, warum Microsoft sich in den bisherigen Versionen von Entity Framework Core an diese Dinge gedacht hat beziehungsweise die Namen direkt konsistent festgelegt hat. Natürlich wäre das besser gewesen, aber – ohne jetzt speziell die Entwickler von Entity Framework Core in Schutz nehmen zu wollen – es gibt ihn nicht, den "perfekten Programmierer". Wir alle machen Fehler, die wir später korrigieren müssen. Besser wir korrigieren unsere Fehler spät als nie.

Weniger Breaking Changes wäre auch möglich gewesen, wenn Microsoft von Anfang an mehr Funktionen in den ORM eingebaut hätte. Dann wären Design-Fehler direkt aufgefallen. Aber dann wäre Entity Framework Core 1.0 viel später erst erschienen. Dann sind wir wieder bei der Frage, wie agil wir die Softwareentwicklung wollen.