III Teil III – Entity Framework Core

9. Für Umsteiger von EF 6

Leichtgewichtigkeit, Flexibilität, Erweiterbarkeit und ein vereinheitlichter Modellierungsprozess sind die wichtigen Motivationen bei der Entwicklung der neuen Version. Dabei wird vor allem auf die komplexen Designer und die Modellierung mit XML verzichtet. “Code Only” ist die Devise, wobei wie bisher vorhandene und neue Datenbanken gleichermaßen unterstützt werden.

9.1 Was ist neu?

Die Top-Level APIs ändern sich mit Entity Framework Core nur wenig. Es gibt aber lang erwartete, signifikante Verbesserungen:

  • Batch CUD
    Erstmals wird das EF die Ausführung von multiplen Abfragen mit nur einem Datenbank-Roundtrip unterstützen. In den bisherigen Versionen des Frameworks wird pro INSERT-, UPDATE- und DELETE-Statement eine Abfrage zur Datenbank geschickt. Dies führt häufig zu enorm vielen Abfragen, die von einer Anwendung an die Datenbank gesendet werden, was natürlich zu Lasten der Performance geht. Jetzt werden in vielen Situationen Abfragen vom Framework zusammengefasst, um diese mit einem Roundtrip zur Datenbank zu schicken.
  • Unique Constraints
    Hiermit erfüllt das EF-Team einen weiteren Wunsch der Community, mit 3196 Stimmen in der User Voice genannten Plattform die am häufigsten geforderte Verbesserung. Mit diesem Feature ist es möglich, Fremdschlüsselbeziehungen auf eine definierte Menge von Spalten einer referenzierten Tabelle festzulegen, wobei die Menge der Spalten nicht der Primärschlüssel der referenzierten Tabelle sein muss. Damit können unabhängig vom Datenbankmanagementsystem referentielle Abhängigkeiten und Einschränkungen im Model definiert werden.
  • Generierung von SQL-Abfragen
    Bisher generiert Entity Framework aus einer Linq-Abfrage eine komplette SQL-Abfrage und sendet diese an die Datenbank. Dabei kommt es in vielen Situationen zu sehr komplexen Abfragen, die nur langsam von den Datenbankmanagementsystemen ausgeführt werden können. Um diese Problematik zu entschärfen, betrachtet EF Core Linq-Abfragen etwas differenzierter: Ein neu eingeführtes Model überlässt es dem Linq-Provider zu entscheiden, welche Teile der Abfrage auf welche Art und Weise in der Datenbank ausgeführt werden. Damit können die Provider noch weiter auf das spezifische DB-System hin optimiert werden. Individuelle Features einer Datenbank können so überhaupt erst genutzt und die Ausführung von Abfragen damit verbessert werden.
  • Leichtgewichtiger Unterbau
    Microsoft wird den OR-Mapper an vielen Stellen neu implementieren, das EF-Team möchte den kompletten Mapper auf einen flexiblen, leichtgewichtigen Unterbau setzen. Als Beispiel wird die Verwaltung der Metadaten eines Modells angeführt. Der MetadataWorkspace ist bisher eine sehr träge Komponente, was den vielen Spielarten der Modellierung geschuldet ist. Beispielsweise benötigt man zum Herausfinden einer Tabelle, die zu einer bestimmten Entität gemappt ist, bisher sieben Zeilen Code; vier davon sind selbst komplexere Linq-Abfragen. Mit dem neuen Unterbau des EF Core wird dazu nur noch eine Zeile Code benötigt. Dies ist natürlich weniger fehleranfällig, die Performance muss aber an dieser Stelle sicher noch einmal genauer betrachtet werden.

Der OR-Mapper wurde von Grund auf überarbeitet. Einige in alten Versionen des Entity Frameworks vorhandene Funktionen sind dem zum Opfer gefallen:

  • Multiple Entity Sets per Type (MEST)
    In Entity Framework Core wird es nicht mehr möglich sein, verschiedene Tabellen auf dieselbe Entität zu mappen.
  • Komplexes Mapping zwischen Tabellen und Typen
    Das Kombinieren von TPH, TPT und TPC in einer Vererbungshierarchie wird zukünftig nicht mehr unterstützt. Diese Möglichkeit in alten Versionen des Frameworks ist ein weiterer Grund für die Komplexität des Metadata-Workspaces, das Eliminieren also eine Konsequenz aus der neuen Leichtgewichtigkeit.
  • EDMX
    Das EDMX-Modell-Format wurde gänzlich abgeschafft. Das Speichern der Metadaten zu einem Modell in XML-Form gibt es nicht mehr. Die Code-basierte Modellierung ist nun der einzige Ansatz.
  • ObjectContext
    Auch dem ObjectContext geht es an den Kragen. Der mit dem EF 4.1 eingeführte DbContext ist häufig die erste Wahl bei Entwicklern. ObjectContext-APIs werden in den DbContext verschoben, und der ObjectContext damit überflüssig gemacht – in Hinblick auf existierende Anwendungen wohl eine der gravierendsten Änderungen in Entity Framework Core.

9.2 Primäre Rolle in ASP.NET Core

Das Entity Framework als primärer Datenbankprovider kommt allein durch die Wahl des Authentifizierungsverfahrens in die ASP.NET-Applikation. Grundlage ist die Klasse IdentityDbContext, die die Benutzerverwaltung liefert, zugleich aber selbst von DbContext abstammt – der Basisklasse des Datenbankzugriffs. Wird also mit dem individuellen Authentifizierungsverfahren gearbeitet, geht dies nur mit Entity Framework. Es ist naheliegend, dies dann auch für alle anderen Datenbankaufgaben zu nutzen. Dabei spielt es keine Rolle, ob die Datenbank erst durch das Objektmodell erzeugt wird oder eine bereits vorhandene Datenbank benutzt wird. Auch ein Aufsplitten auf zwei Datenbanken ist denkbar, wenngleich dies die Abfragen an einigen Stellen etwas komplizierter macht, da die Objekte dann in zwei getrennten Kontexten existieren.

Entity Framework ist datenbankunabhängig. Die hier gezeigten Beispiele basieren jedoch ausnahmslos auf dem Microsoft SQL Server. Zum Test habe ich hier MS SQL 2014 benutzt, es sollte aber auch mit den vorherigen Versionen funktionieren. EF Core 1.0 ist sehr jung – Erfahrungen mit anderen Datenbanken und der Qualität der Provider, soweit überhaupt verfügbar, gibt es noch nicht.

Installation ohne die Authentifizierung

Um sicherzustellen, dass der SQL Server-Provider vorliegt, installieren Sie zuerst das passende Paket via Nuget:

Tools > NuGet Package Manager > Package Manager Console

Install-Package EntityFramework.MicrosoftSqlServer –Pre

Achten Sie hier auf die Angabe -Pre – andernfalls installieren Sie die aktuelle Version EF 6 (Stand dieser Aussage: Januar 2016). EF 6 läuft allerdings nicht auf dem Core-Framework, was das gesamte Projekt ad absurbum führt. Die Installation erfolgt im Hintergrund. Nun installieren Sie die Kommandos:

Install-Package EntityFramework.Commands –Pre

Öffnen Sie nun die Datei project.json im Wurzelverzeichnis des Projekts. Suchen Sie den Abschnitt “commands” und ändern Sie den Inhalt wie folgt:

1 "commands": {
2   "web": "Microsoft.AspNet.Server.Kestrel",
3   "ef": "EntityFramework.Commands"
4 },

10. Grundlagen

In diesem Abschnitt soll es vor allem um architektonische und konzeptionelle Aspekte rund um das Entity Framework gehen.

Primär geht es darum, modellbasiert zu entwickeln, nicht Datenbankschema-basiert. Mit dem Einsatz von ADO.NET verwenden Sie viel Zeit auf die Abfrage von Daten aus einer Datenbank: das Durchlaufen der Ergebnisse und Herausfiltern der relevanten Felder sowie deren Inhalte. Mit dem Entity Framework ist das anders. Hier erfolgt die Abfrage nicht gegen ein Datenbankschema. Sie entwickeln Ihre Software gegen ein der Geschäftslogik entsprechendes konzeptionelles Objektmodell.

Die Daten aus der Datenbank werden automatisch in die Geschäftsobjekte übertragen. Änderungen werden gespeichert, indem die geänderten Objekte gespeichert werden, das Entity Framework erledigt den Rest automatisch. So werden Sie als Entwickler von der Aufgabe “befreit”, die Eigenschaften der Geschäftsobjekte in Reihen und Spalten der Datenbank zu konvertieren.

10.1 Entitäten

Der wichtigste Bestandteil des Entity Frameworks sind Entitäten. Es ist der Kern, um den sich die weitere Funktionalität anordnet. Das Entitäten-Modell beschreibt die Geschäftsobjekte, wie sie von der Geschäftsprogrammlogik verwendet werden sollen. Es gehört, im Gegensatz zu dem logischen Datenmodell, nicht zur verwendeten Datenbank.

Wichtige Begriffe im Zusammenhang mit dem Entity Framework sind:

  • Entity Set
    Eine Sammlung zusammengehörender Entities, denen ein Entity Key gemeinsam ist. Das bildet eine Tabelle im relationalen Modell ab.
  • Entity Container
    Gruppierung aus Entity Set, Beziehungen und Funktionen. Dies muss mindestens einmal im Modell vorhanden sein und bildet eine Datenbank im relationalen Modell ab. Es wird auch als Kontext (Context) bezeichnet.
  • Association Type
    Eine Beziehung von einem Entity Typy zu einem anderen. Die Abbildung erfolgt durch eine Eigenschaft (“Navigation Property” genannt). Sie hat eine Multiplizität: 1, 0..1, * und bildet Relationen im relationalen Modell ab.
  • Datentypen
    Dies ist die Abbildung des relationalen Modells auf Typen des Objektmodells. Es werden viele .NET-Datentypen wie Boolean, String, Int32, Guid, Byte[] und DateTime direkt abgebildet. Komplexere Typen werden durch Klassen abgebildet.
Abbildung: Schematische Darstellung eines Entity Containers
Abbildung: Schematische Darstellung eines Entity Containers

10.2 Bausteine

Die folgende Abbildung zeigt, wie sich das Entity Framework in die Applikation einordnet.

Abbildung: Einordnung des Entity Frameworks in die Applikation
Abbildung: Einordnung des Entity Frameworks in die Applikation

Taucht man tiefer ein, so werden die einzelnen Dienste sichtbar.

Abbildung: Innerer Aufbau des Entity Frameworks
Abbildung: Innerer Aufbau des Entity Frameworks

Wichtig ist der Umgang mit den bereits erwähnten Schnittstellen IEnumerable<T> und IQueryable<T>.

IEnumerable<T>

Diese Schnittstelle unterstützt Iterationen, also speziell den Zugriff mittels foreach. Basis ist die Methode GetEnumerator, die einen Enumerator mit den folgenden Mitgliedern zurückgibt:

  • Reset
  • MoveNext
  • Current<T>

Objekte von diesem Type sind immer Speicherobjekte. Man spricht im Entity Framework davon, dass diese “materialisiert” sind. Abfragen auf solchen Objekten werden nunmehr im Speicher und nicht mehr auf der Datenbank ausgeführt.

IQueryable<T>

Diese Schnittstelle liefert IEnumerable<T> und zusätzlich folgende Eigenschaften:

  • Type vom Typ ElementType
  • Expression vom Typ Expression
  • IQueryProvider vom Typ Provider

Abfragen stehen in Expression und werden vom Provider ausgeführt. Damit ist dies der Schlüssel, um die Abfrage weiter zu verfeinern (Expression wird erweitert) und dann auf der Datenbank ausgeführt zu werden.

Soll die Abfrage auf der Datenbank ausgeführt und dann materialisiert werden, wird folgendes geschrieben:

1 MyDataSource.Where(d => d.Name.Contains("Test").ToList()

Soll die Datenbank komplett abgerufen und dann materialisiert und die Abfrage danach im Speicher ausgeführt werden, wird folgendes geschrieben:

1 MyDataSource.ToList().Where(d => d.Name.Contains("Test")

Der Aufruf von ToList() erzwingt die Materialisierung und gibt (unter anderem) IEnumerable<T>zurück.

Abbildung: Vergleich IEnumerable und IQueryable
Abbildung: Vergleich IEnumerable und IQueryable

10.3 Kontext

Der Kontext ist eine zusätzliche Schicht, welche mit Hilfe des Entity Readers C#-Objekte zur Verfügung stellt. Die Dienste des Entity Frameworks unterstützen Entity SQL oder LINQ-To-Entities durch Implementierung der entsprechenden Schnittstellen mittels Providern. Die Provider sorgen dafür, dass datenbankunabhängig gearbeitet wird.

Die Klasse DbContext ist der zentrale Bestandteil der Dienste. Diese Klasse stellt Funktionen für die Interaktion mit CLR-Objekten zur Verfügung. Abfragen werden in Form eines Ausdrucksbaums (Befehlsstruktur) gesendet. Als Rückgabewert kommt ein Auszählungstyp der Schnittstelle IQueryable<T> zurück.

Die Abfragesprache Entity-SQL ist eine eigens für das Entity Framework entwickelte Abfragesprache, welche T-SQL sehr ähnlich ist. Sie erlaubt SQL-Anweisungen die vor allem dann nützlich sind, wenn der Zugriff mit LINQ nicht gelingt oder Funktionen erreicht werden sollen, die der LINQ-Provider nicht kennt. Entity-SQL stellt eine generische, von der verwendeten Datenquelle unabhängige, abstrakte Abfrage zur Verfügung. Ausgeführt wird diese Abfrage unter Verwendung von datenquellenspezifischen Abfrageprovidern, beispielsweise ADO.NET. Zwar kann EntitySQL direkt verwendet werden, jedoch ist es oft leichter den LINQ-Provider zu verwenden, weil LINQ für Abfragen auf Objekten und Strukturen optimiert wurde.

Dieses Verfahren wird dann LINQ-To-Entities genannt. Der Datenclient LINQ-To-Entities greift direkt auf die clientseitigen Komponenten des Entity Frameworks zurück. Dabei wird der Objektdienst als LINQ-Provider verwendet. Die LINQ-Abfrage wird in einen Ausdrucksbaum gewandelt, welcher an den Entity-Datenprovider übergeben wird. Hier erfolgt eine weitere Anpassung in einen ADO.NET kompatiblen Ausdrucksbaum. Der ADO.NET-Provider erzeugt das entsprechende SQL für die Abfrage und liefert das Ergebnis an den Provider. Im letzten Schritt wandelt der LINQ-Provider die im EntityReader enthaltenen Informationen in Objekte, auf die über die Schnittstelle IQueryable<T> zugegriffen werden kann.

10.4 Eigenschaften

Entity Framework hat einige grundlegend Eigenschaften, die bei jedem Zugriff berücksichtigt werden sollten.

Lazy Loading

Lazy Loading heißt, das Daten nur dann geladen werden, wenn Sie auch gebraucht werden. In der aktuellen Version unterstützt Entity Framework dieses Verfahren nicht.

Eager Loading

Hier wird das Laden explizit gesteuert. Nutzen Sie folgenden Aufruf:

1 var usersWithClaim = Users.Include(u => u.Claims);

Weitere Ladevorgänge werden mit .ThenInclude ausgeführt.

Explicit Loading

Hier wird der gesamte Objektgraph geladen:

1 context.Entry(Person)       
2        .Reference(p => p.Bookings)       
3        .Load()

Smart Loading

Hier erfolgt kein Laden, wenn nur eine Aggregation auf den Objekten erfolgt:

1 context.Entry(Person)
2        .Collection(p => p.Bookings)
3        .Query()      
4        .Count()

Dies lässt sich mit dem expliziten Laden kombinieren:

1 context.Entry(sess)
2        .Collection(b => b.Speakers)
3        .Query()
4        .Where(p => p.Name.Contains("EF")
5        .Load();

10.5 Umgang mit Multiplizität

Per Konvention werden Navigationseigenschaften (lies: Relationen) erstellt, wenn der Name auf “ID” endet. Eine explizite Deklaration ist mit dem Attribut [Key] möglich. Folgende Multiplizitäten gibt es:

  • 0..1: Eigenschaftentyp ist Entity Type
  • 1: Eigenschaftentyp ist Entity Type + [Required]
  • n: ICollection<Entity Type> + [Required]
  • 0..n: ICollection<Entity Type>

Assoziationen wirken sich auf das Ladeverhalten aus. Die “n”-Seite der Beziehungen wird durch drei Schnittstellen bestimmt:

  • IEnumerable<T>: Erlaubt nur Lesen, es gibt keine Änderungsverfolgung (Tracking) und kein Lazy Loading.
  • ICollection<T>: Mit Änderungsverfolgung (Add/Remove) und Lazy Loading
  • IList<T>: Zusätzlich mit Sortieren, Filtern und weiteren Abfragen mit LINQ to Objects

Fremdschlüssel können, wenn keine Navigationseigenschaften möglich sind, mit dem Attribut [ForeignKey] bestimmt:

1 public class Project { 
2 
3   public int ProjectId { get; set; } 
4   public string Name { get; set; }    
5   public int ManagerId { get; set; }    
6   
7   [ForeignKey("ManagerId")]    
8   public Person Manager { get; set; }
9 } 

10.6 Umgang mit dem Kontext

Die wichtigsten Funktionen sind:

  • SaveChanges: Speichern, auslösen des Datenbankzugriffs
  • ExecuteStoreQuery: Ausführen von Entity-SQL-Abfragen
  • Entry: Zugriff auf die Objekttabelle

SaveChanges

Mit SaveChanges erfolgt das Speichern, also das Auslösen des Datenbankzugriffs. Die Funktion gibt es synchron und asynchron als SaveChangesAsync. Wenn Sie global das Speicherverhalten anpassen möchten, überschreiben Sie die Methode und fügen eigene Aktionen davor ein. Insbesondere besteht hier Zugriff auf den ChangeTracker, der alle vom Entity Framework überwachten Objekte enthält. Damit lassen sich typische Aktionen steuern:

  • Setzen eines Löschkennzeichens für gelöschte Objekte statt physischem Löschen
  • Setzen von globalen Feldern wie CreatedBy, CreatedAt, ModifiedBy, ModifiedAt

Der Aufruf ist darüber hinaus atomar und kapselt alle anstehenden SQL-Abfragen in eine Transaktion. Sie müssen sich nur dann um Transaktionen kümmern, wenn die betroffenen Aktionen in mehrere Aufrufe von SaveChanges zerfallen.

ExecuteStoreQuery

Hiermit erfolgt das Ausführen von Entity-SQL-Abfragen. Die Funktion gibt es synchron und asynchron als ExecuteStoreQueryAsync. Entity-SQL-Abfragen ist ein plattformunabhängiger SQL-Dialekt, der es erlaubt, die Abfragestruktur selbst zu schreiben. Dies ist sinnvoll, wenn es Entity Framework nicht gelingt, eine brauchbare SQL-Abfrage zu erstellen.

Entry und DbSet/IDbSet

Mit Entry erfolgt der Zugriff auf die Objekttabelle der vom Tracker behandelten Objekte. Objekte in der Objekttabelle haben einen definierten Zustand (State, vom Typ EntityState), der darüber entscheidet, welche Art von SQL beim Speichern erzeugt wird.

Objekte lassen sich folgendermaßen der Objekttabelle hinzufügen und in einen bestimmten Zustand versetzen:

1 using (var context = new SessionContext())
2 {
3   var session = new Session { Name = "EF Core 1.0" };
4   context.Entry(session).State = EntityState.Added;   
5   context.SaveChanges();
6 }

Mit Hilfe dieses Verfahrens ist es möglich, generische Repositories aufzubauen, die nicht auf bestimmte DbSet-Methoden angewiesen sind. Der gezeigte Code-Block entspricht folgender Vorgehensweise:

1 using (var context = new SessionContext())
2 {
3   var session = new Session { Name = "EF Core 1.0" };
4   context.Sessions.Add(session);   
5   context.SaveChanges();
6 }

Die Eigenschaft Sessions ist hier im Kontext folgendermaßen definiert:

1 public DbSet<Session> Sessions { get; set; }

Weitere Methoden des Kontexts

Einige weitere Methoden werden nicht immer benötigt, helfen aber dabei, eine leistungsfähigere API für den Datenbankzugriff aufzubauen.

  • Attach
    Hiermit erfolgt das Anhängen eines vorher abgehängten Objekts. Dieses muss in der Datenbank bereits existieren, hat aber möglicherweise Tracking verloren, weil ein Webdienst oder ein ähnlicher Serialisierungsvorgang benutzt worden ist.
  • Find
    Hiermit erfolgt die Suche nach einem Primärschlüssel. Als Parameter wird auch ein Array akzeptiert, falls es sich um einen zusammengesetzten Primärschlüssel handelt.
  • Remove
    Hier erfolgt das Entfernen (DELETE) aus der Objekttabelle und damit das Löschen beim nächsten Speichervorgang. Dies setzt voraus, dass das Objekt vorher geladen wurde. Das können Sie sich sparen, wenn folgendermaßen vorgegangen wird:
1    Ctx.Entry(cust).State = EntityState.Deleted; 
  • Local
    Dies erzeugt eine ObservableCollection<T>, deren Änderungen Teil des Tracking sind und bei SaveChanges beachtet werden
  • Create
    Erzeugt einen Proxy (sehen Sie sich dazu den Abschnitt Proxies an). Der Befehl fügt das Objekt nicht sofort ein; dies würde ein weiteres Add/Attach erfordern.
  • Include
    Dient der Erweiterung des Objektgraphen zum Abruf folgender Stufen verbundener Objekte. Gesteuert wird damit das Eager Loading. Ohne diese Maßnahme wird immer nur eine Stufe geladen und dies auch nur dann, wenn dies mit dem Schlüsselwort virtual bei der Navigationseingenschaft angezeigt wird.
  • SqlQuery
    Erlaubt das direkte Abfragen mit SQL. Das Ergebnis wird Teil des Trackings.

Der Status einer Entität

Der Status steuert das Tracking – das Verfolgen von Änderungen an Proxy-Objekten. Er kann folgende Zustände haben:

  • Added
  • Unchanged
  • Modified
  • Deleted
  • Detached

Dies kann man nutzen, um mit Hilfe der bereits gezeigten Methode Entry eine Funktion zu entwickeln, die gleichzeitig Aktualisieren und Erstellen kann. Als Kriterium wird der Primärschüssel benutzt.

 1 public void InsertOrUpdate(Session session)
 2 {
 3   using (var context = new SessionContext())
 4   {
 5     context.Entry(session).State = session.Id == 0 ?
 6                                    EntityState.Added :
 7                                    EntityState.Modified;
 8  
 9     context.SaveChanges();
10   }
11 }