Stabilität und lang Erwartetes

Ausblick auf Entity Framework 6
Kommentare

Entity Framework 6 bringt einige Neuerungen, die das Leben der Entwickler vereinfachen werden, darunter Tracing, benutzerdefinierte Konventionen für Code Only, Unterstützung für Stored Procedures durch Migrations sowie Code Only und asynchrone Operationen.

Vor einigen Wochen hat Microsoft die Version 6 seines O/R Mappers Entity Framework veröffentlicht. Im Zuge dessen wurde das Entity Framework auch aus dem .NET Framework ausgegliedert. Das erfolgte mit dem Ziel, seine Weiterentwicklung von der .NET-Weiterentwicklung zu entkoppeln, um rascher neue Versionen auf den Markt bringen zu können. Die neuesten Versionen des Entity Frameworks findet der geneigte Entwickler nun jeweils über NuGet.

Breaking Changes

Bevor der vorliegende Artikel auf die neuen Möglichkeiten von Entity Framework 6 eingeht, werden an dieser Stelle zwei Breaking Changes erwähnt. Der eingangs erwähnten Ausgliederung aus dem .NET Framework ist es zu schulden, dass einige Klassen, die sich noch in Version 5 in Namensräumen unter der Hoheit des Product-Teams befanden, in einen anderen Namensraum verschoben werden mussten. Bestehender Code muss somit ein wenig angepasst werden, wobei dies in den meisten Fällen über Suchen und Ersetzen zu bewerkstelligen sein dürfte. Darüber hinaus gab es auch Änderungen am Providermodell. Das hat zur Folge, dass sich Entwickler im Zuge der Migration auf Version 6 ggf. die neueste Version des verwendeten Datenbankproviders besorgen müssen.

Konfiguration

Während es in der Vergangenheit vereinzelt über statische Eigenschaften möglich war, Entity Framework an eigene Bedürfnisse anzupassen, sieht Version 6 eine zentrale Klasse hierfür vor. Um solch eine Klasse einzurichten, leitet der Entwickler von DbConfiguration ab und hinterlegt im Konstruktor dieser Klasse Eckdaten für die Konfiguration des Frameworks. Bei diesen Eckdaten handelt es sich in der Regel um die Angabe von austauschbaren Komponenten, die Entity Framework zur Bewerkstelligung der angeforderten Aufgaben verwendet. Listing 1 demonstriert dies, indem es den DatabaseInitializer DropCreateDatabaseAlways für den HotelDbContext sowie die benutzerdefinierte CustomConnectionFactory zum Aufbau von Datenbankverbindungen festlegt.

Listing 1

public class CustomDbConfiguration : DbConfiguration
{
  public CustomDbConfiguration()
  {
    SetDatabaseInitializer(new DropCreateDatabaseAlways());
    SetDefaultConnectionFactory(new CustomConnectionFactory());

Auf diese Art und Weise kann der Entwickler seit Version 6 eine ganze Menge an Komponenten austauschen [1]. Zu den Highlights unter diesen Komponenten zählt der PluralizationService, der festlegt, wie Entity Framework den Plural bzw. Singular aus Tabellen- und Spaltenbezeichnungen bilden soll, sowie der MigrationSqlGenerator, der bestimmt, auf welche Weise SQL-Code für Migrationen zu erzeugen ist.

Ein Derivat von DbConfiguration zu erstellen, ist jedoch nur die halbe Miete bei der Konfiguration des Entity Frameworks. Daneben muss der Entwickler auch noch den DbContext mit der gewünschten Konfiguration in Verbindung setzen. Existiert in der gesamten Anwendung lediglich eine Subklasse von DbConfiguration, so wird diese verwendet. Existieren in der Anwendung mehrere solcher Klassen, in der Assembly mit dem verwendeten DbContext-Derivat jedoch nur eine einzige, kommt diese zum Zug. Ist auch dies nicht der Fall, besteht die Möglichkeit, das DbContext-Derivat mit dem Attribut DbConfigurationType zu annotieren. Über dieses kann der Entwickler die zu verwendende DbConfiguration-Implementierung angeben:

[DbConfigurationType(typeof(CustomDbConfiguration))] public class HotelDbContext : DbContext { [...] } 

Neben der direkten Angabe des Typs der DbConfiguration-Implementierung kann der Entwickler auch einen String angeben, der die vollständige Typbezeichnung inkl. des Namens der sich darin befindlichen Assembly beinhaltet. Wie unter .NET üblich, wird hierzu das Format Namespace.Klassenname, Assymblyname verwendet. Abgesehen davon kann der Entwickler auch auf globaler Ebene eine DbConfiguration angeben und damit alle anderen Angaben überschatten. Dazu verwendet er entweder die statische Methode DbConfiguration.SetConfiguration oder den folgenden Konfigurationseintrag:

    [...]  

Alternativ zur hier beschriebenen codebasierten Konfiguration stehen für ausgewählte Aspekte nach wie vor die mit Entity Framework 4.1 eingeführten Möglichkeiten zur Konfiguration über app.config bzw. web.config zur Verfügung. Dabei gilt die Regel, dass Einstellungen in diesen Dateien analoge Einstellungen überschatten, die per Code getroffen wurden.

Tracing

Gerade wenn es darum geht, eine Datenbankanwendung zu optimieren oder Fehler zu finden, ist es für den Entwickler wichtig zu wissen, welche SQL-Befehle Entity Framework zur Datenbank sendet. In der Vergangenheit war dies mit Board-Mitteln nur sehr schwer möglich, weswegen der Entwickler entweder auf datenbankseitige Tracing-Mechanismen (z. B. SQL Server Profiler) oder auf kostenpflichtige Werkzeuge, die sich auf Treiberebene ins Entity-Framework einklinkten (z. B. Entity Framework Profiler [2]), ausweichen musste. Künftig wird man weniger häufig zu diesen Werkzeugen greifen müssen, da Entity Framework 6 nun über sämtliche an die Datenbank gesendeten SQL-Anweisungen informiert werden kann. Dazu bietet der DbContext über Database.Log ein Delegate vom Typ Actionan. Registriert der Entwickler bei diesem Delegate eine Methode, die einen String entgegennimmt, wird diese immer dann aufgerufen, wenn eine SQL-Anweisung zur Datenbank gesendet wird. Im Zuge dessen übergibt Entity Framework die SQL-Anweisung als String.

Interceptoren

Der im letzten Abschnitt beschriebene Tracing-Mechanismus basiert auf einem viel mächtigeren Konzept, das ebenfalls mit Version 6 eingeführt wurde: Command Interceptoren. Unter einem Interceptor versteht man im Allgemeinen eine Komponente, die Aktionen eines Frameworks abfängt und vor, nach bzw. anstelle von ihnen eigene Aktionen zur Ausführung bringt. Bei den Command Interceptoren ab Entity Framework 6 handelt es sich um solche, die das Abfangen von zur Datenbank gesendeten Anweisungen erlauben. Zur Implementierung eines solchen Interceptors stellt der Entwickler eine Realisierung des Interface IDbCommandInterceptor zur Verfügung. Dieses Interface weist sechs Methoden auf, die Entity Framework jeweils vor bzw. nach dem Ausführen einer SQL-Anweisung zur Ausführung bringt. Einen Überblick über diese Methoden bietet Tabelle 1.

Tabelle 1: Methoden von „IDbCommandInterceptor“

Methode Beschreibung
NonQueryExecuting Wird ausgeführt, bevor Entity Framework eine Anweisung zur Datenbank sendet, die keine Ergebnismenge zurückgibt. Beispiele hierfür sind INSERT-, UPDATE- oder DELETE-Anweisungen.
NonQueryExecuted Wird ausgeführt, nachdem Entity Framework eine Anweisung, die keine Ergebnismenge zurückgibt, zur Datenbank gesendet und diese die Anweisung ausgeführt hat. Innerhalb dieser Methode liegt bereits die Anzahl der Zeilen vor, die von dieser Anweisung betroffen waren.
ReaderExecuting Wird ausgeführt, bevor Entity Framework eine Anweisung zur Datenbank sendet, die eine Ergebnismenge zurückgibt. Ein Beispiel hierfür ist eine SELECT-Anweisung.
ReaderExecuted Wird ausgeführt, nachdem Entity Framework eine Anweisung, die keine Ergebnismenge zurückgibt, zur Datenbank gesendet und diese die Anweisung ausgeführt hat. Innerhalb dieser Methode liegt bereits die abgefragte Ergebnismenge vor.
ScalarExecuting Wird ausgeführt, bevor Entity Framework eine Anweisung zur Datenbank sendet, die einen einzelnen (skalaren) Wert zurückgibt (z. B. SELECT COUNT(*) FROM …).
ScalarExecuted Wird ausgeführt, nachdem Entity Framework eine Anweisung, die einen einzelnen (skalaren) Wert zurückgibt, zur Datenbank gesendet und diese die Anweisung ausgeführt hat. Innerhalb dieser Methode liegt der abgefragte Wert bereits vor.

Sämtliche von IDbCommandInterceptor vorgegebene Methoden bekommen vom Entity Framework zwei Parameter übergeben: ein DbCommand, der die jeweilige SQL-Anweisung repräsentiert und ein Objekt vom Typ DbCommandInterceptionContext, mit Informationen über die Abfrage. Unter diesen Informationen befinden sich das Ergebnis der SQL-Anweisung sowie eine eventuelle Exception, die im Zuge der Ausführung ausgelöst wurde. Setzt ein Command Interceptor diese Eigenschaften vor der Ausführung der damit assoziierten SQL-Anweisung, führt Entity Framework diese SQL-Anweisung nicht aus, sondern behandelt den vom Interceptor gesetzten Wert als Ergebnis dieser Anweisung bzw. als im Rahmen ihrer Ausführung aufgetretene Exception. Auf diese Weise lassen sich zum Beispiel Caching-Mechanismen realisieren.

In den Interceptor-Methoden, die Entity Framework nach dem Ausführen einer SQL-Anweisung anstößt, hat der Entwickler über den DbCommandInterceptionContext Zugriff auf das Ergebnis der Abfrage bzw. auf eine eventuell ausgelöste Exception. Auch hier kann er diese Werte setzen. Dies hat zur Folge, dass der Aufrufer nicht mit dem eigentlichen Ergebnis, sondern mit den vom Interceptor gesetzten Werten konfrontiert wird.

Listing 2 zeigt ein (aus Platzgründen gekürztes) Beispiel für einen einfachen Command Interceptor. Dieser gibt die SQL-Anweisung, deren Ausführung ansteht bzw. gerade abgeschlossen wurde, sowie ausgewählte Informationen, die der DbCommandInterceptionContext parat hält, auf der Konsole aus. Im Falle von NonQueryExecuting wird als Ergebnis der Wert 42 definiert, indem der Entwickler die Eigenschaft Result setzt. Dies hat zur Folge, dass Entity Framework die mit diesem Methodenaufruf assoziierte SQL-Anweisung nicht zur Ausführung bringt und davon ausgeht, dass das Ergebnis dieser Anweisung 42 ist. Da Entity Framework die Methode NonQueryExecuting vor der Ausführung von DML-Anweisungen wie INSERT, UPDATE oder DELETE anstößt, wird dieser Wert als Anzahl der von der Anweisung betroffenen Zeilen gewertet.

Der Datentyp der hier verwendeten Eigenschaft Result wird durch den Typparameter von DbCommandInterceptionContext bestimmt. Im Fall von NonQueryExecuting und NonQueryExecuted handelt es sich dabei, wie im betrachteten Beispiel gezeigt, um einen int, der die Anzahl der betroffenen Zeilen repräsentiert. Bei ReaderExecuting und ReaderExecuted ist Result hingegen vom Typ DbDataReader. Dieser repräsentiert die Ergebnismenge der Abfrage. Bei ScalarExecuting und ScalarExecuted repräsentiert Result den abgefragten Wert und weist deswegen den Datentyp Object auf.

Die im betrachteten Beispiel auskommentierten Zeilen deuten auf weitere interessante Eigenschaften von DbCommandInterceptionContext hin: Bei DbContexts handelt es sich um eine Auflistung der betroffenen DbContext-Instanzen. In der Regel hat diese Liste genau einen einzigen Eintrag. Exception repräsentiert eine eventuell aufgetretene Exception. Da die Eigenschaften Result und Exception vom Interceptor verändert werden können, bieten OriginalResult und OriginalException Zugriff auf das eigentliche Ergebnis bzw. auf die eigentliche Exception. Die Eigenschaft IsExecutionSuppressed kann nur gelesen werden und gibt an, ob die Ausführung der SQL-Anweisung übersprungen wird. Dies ist, wie schon erwähnt, dann der Fall, wenn der Interceptor das Ergebnis oder die Exception vor der Ausführung der SQL-Anweisung direkt setzt.

Listing 2

public class CustomCommandInterceptor: IDbCommandInterceptor {

  public void NonQueryExecuting(
    DbCommand command, 
    DbCommandInterceptionContext interceptionContext) {
    Console.WriteLine("NonQueryExecuting");
    Console.WriteLine(command.CommandText);
    // command.Connection

    interceptionContext.Result = 42;

    // interceptionContext.DbContexts
    // interceptionContext.Exception 
    //        = new Exception("Es ist doch schon Feierabend!");

    // interceptionContext.OriginalException
    // interceptionContext.OriginalResult
    // interceptionContext.IsExecutionSuppressed
  }

  [...]
}

Damit Entity Framework einen Command Interceptor verwendet, muss der Entwickler diesen zuvor registrieren. Das geschieht innerhalb der Konfigurationsklasse mit der Methode AddInterceptor.

[ header = Ausblick auf Entity Framework 6 – Seite 2 ]

Asynchrone Methoden

Während in ADO.NET bereits mit .NET 4.5 asynchrone Gegenstücke für jene Methoden eingerichtet wurden, die auf die Datenbank zugreifen, ist dies nun auch bei Entity Framework der Fall: Der DbContext weist nun neben SaveChanges auch ein asynchrones SaveChangesAsync auf und für IQueryables stehen nun auch Erweiterungsmethoden wie ToListAsync und ToArrayAsync zur Verfügung. Somit kann der Entwickler Datenbankzugriffe, die wie alle Zugriffe auf externe Ressourcen geradezu für eine asynchrone Ausführung prädestiniert sind, im Hintergrund ausführen, ohne zum Beispiel den GUI-Thread damit zu belasten.

Code First und Stored Procedures

Das Programmiermodell Code First schloss in der Vergangenheit den Einsatz von Stored Procedures zum Speichern von Entitäten aus. Diese Einschränkung wird nun aufgehoben und der Entwickler ist in der Lage, über das Fluent-API Code Only mitzuteilen, dass Stored Procedures anstelle von INSERT-, UPDATE- und DELETE-Anweisungen für bestimmte Entitäten zu verwenden sind. Um dies zu bewerkstelligen, ruft der Entwickler MapToStoredProcedures innerhalb der Methode OnModelCreating des verwendeten DbContext-Derivats auf:

modelBuilder.Entity().MapToStoredProcedures()

Wie immer beim Einsatz von Code First gilt auch hier das Prinzip „Convention over Configuration“: Gibt der Entwickler beim Aufruf von MapToStoredProcedures keine Argumente an, geht Entity Framework davon aus, dass Stored Procedures mit den Namen Entität_Insert, Entität_Update und Entität_Delete existieren, wobei „Entität“ für den Namen der konfigurierten Entität steht.

Entity Framework erwartet auch, dass Entität_Update Parameter aufweist, deren Namen denen der Eigenschaften der jeweiligen Entität entsprechen. Eine Ausnahme bilden hier jene Eigenschaften, die der Entwickler über die Konfiguration als solche markiert hat, die von der Datenbank berechnet werden (StoreGeneratedPattern: Computed): Für diese Eigenschaften dürfen keine Parameter existieren.

Für Entität_Insert gilt dasselbe, wobei hier auch für jene Eigenschaften mit dem StoreGeneratedPattern Identity keine Parameter existieren dürfen, zumal deren Werte beim Einfügen in die Datenbank generiert werden. Entität_Delete hingegen muss lediglich Parameter für jene Eigenschaften aufweisen, die den Primärschlüssel bilden.

Um berechnete Spalten zu erhalten, geht Entity Framework davon aus, dass die einzelnen Stored Procedures eine einzeilige Ergebnismenge mit ebendiesen zurückgeben.

Zum Auflösen von 1:N-Beziehungen sind den Stored Procedures auch die Eigenschaften zu übergeben, über die Fremdschlüssel-Mappings realisiert werden. Macht der Entwickler von Fremdschlüssel-Mappings keinen Gebrauch, sollte er einen Parameter für die Primärschlüssel der benachbarten Tabellen einrichten. Die Namen dieser Parameter entsprechen dabei entweder den Namen der adressierten Primärschlüsselspalten oder dem Namensschema Name-der-Navigationseigenschaft_Name-der-Primärschlüsselspalte.

Stored Procedures und Migrations

Migrations generiert ab Entity Framework 6 entsprechend der mit dem Fluent-API definierten Abbildungen Stored Procedures. Dies erleichtert dem Entwickler die Arbeit, da er sich somit weniger mit den vorherrschenden Konventionen belasten muss. Er kann die generierten Stored Procedures entweder direkt einsetzen oder als Ausgangsbasis für eigene Stored Procedures heranziehen.

Optimistic Concurrency beim Einsatz von Stored Procedures unter Code First

Zur Realisierung von Optimistic Concurrency erwartet Entity Framework, dass die Stored Procedures Entität_Update und Entität_Delete für mit ConcurrencyCheck oder Timestamp annotierte Eigenschaften auch einen Parameter Eigenschaft_Original aufweisen. Eigenschaft steht hier für den Namen der jeweiligen Eigenschaft. An diesen Parameter übergibt Entity Framework den ursprünglichen Wert dieser Eigenschaft, sodass die Stored Procedure prüfen kann, ob er sich seit dem Laden der Entität in der Datenbank geändert hat. Ist dem so, hat ein anderer Benutzer den Datensatz aktualisiert und es liegt ein Konflikt vor, worauf Entity Framework durch das Auslösen einer Exception im Rahmen von SaveChanges hinweist.

Damit Entity Framework erkennen kann, ob ein solcher Konflikt vorliegt, sollte die Stored Procedure die übergebenen ursprünglichen Werte innerhalb der WHERE-Klausel verwenden, sodass im Fall eines Konflikts keine Datensätze anstatt des einen betroffenen Datensatzes aktualisiert werden. Entity Framework prüft standardmäßig nach der Ausführung einer Stored Procedure, wie viele Datensätze in ihrem Rahmen geändert wurden. Beim Einsatz des SQL Servers wird dazu z. B. die globale Variable @@ROWCOUNT geprüft. Wurden keine Datensätze geändert, geht Entity Framework von einem Konflikt aus. Alternativ dazu kann, wie weiter unten beschrieben, die Stored Procedure um einen OUT-Parameter erweitert werden, der die Anzahl der geänderten Datensätze retourniert.

Beim Mappen von Stored Procedures unter Code First aus Konventionen ausbrechen

Kann oder möchte sich der Entwickler bei der Namensgebung für die Stored Procedures und deren Parameter nicht an die soeben aufgezeigten Konventionen halten, hat er die Möglichkeit, die gewünschten Namen an die Methode MapToStoredProcedures zu übergeben. Ein Beispiel dafür findet sich in Listing 3. Es legt mit HasName den Namen der jeweiligen Stored Procedure fest, mit Parameter die Namen der jeweiligen Parameter sowie die auf ihn abzubildenden Eigenschaften und mit Result die Namen der zurückgelieferten Spalten sowie die dazugehörigen Eigenschaften. Um solche Fremdschlüssel auf einen Parameter abzubilden, für die es kein Fremdschlüssel-Mapping gibt, wird, wie im Fall des RegionsCodes veranschaulicht, der gegenüberliegende Primärschlüssel angeführt. RowsAffectedParameter definiert den Namen jenes OUT-Parameters, der zur optimistischen Erkennung von Konflikten angibt, wie viele Spalten von der durchgeführten Aktion betroffen sind.

Das hier betrachtete Beispiel zeigt auch am Beispiel einer Beziehung zwischen Hotel und Merkmal, wie der Entwickler M:N-Beziehungen auf Stored Procedures abbilden kann. Hierzu gibt er eine Stored Procedure zum Hinzufügen einer Zuweisung sowie eine weitere zum Aufheben einer Zuweisung an. Beide Stored Procedures erhalten die Primärschlüssel der beiden zueinander zuzuweisenden Entitäten. Übrigens entsprechen die hier verwendeten Namen den Konventionen, die Entity Framework heranzieht, wenn der Entwickler die Methode MapToStoredProcedures ohne zusätzliche Parameter aufruft.

Listing 3

modelBuilder.Entity().MapToStoredProcedures(s =>
    s.Insert(proc => proc
                        .HasName("Hotel_Insert")
                        .Parameter(h => h.Bezeichnung, "Bezeichnung")
                        .Parameter(h => h.Sterne, "Sterne")
                        .Parameter(h => h.Region.RegionCode, "RegionCode")
                        .Result(h => h.HotelId, "HotelId")
                        .Result(h => h.Version, "Version")
                        )
        .Update(proc => proc
                            .HasName("Hotel_Update")
                            .Parameter(h => h.HotelId, "HotelId")
                            .Parameter(h => h.Bezeichnung, "Bezeichnung")
                            .Parameter(h => h.Sterne, "Sterne")
                            .Parameter(h => h.Region.RegionCode, "RegionCode")
                            .Result(h => h.Version, "Version")
                            .RowsAffectedParameter("RowsAffected")
                            )
        .Delete(proc => proc
                            .HasName("Hotel_Delete")
                            .Parameter(h => h.HotelId, "HotelId")
                            .RowsAffectedParameter("RowsAffected")
                            ));

modelBuilder
    .Entity()
    .HasMany(h => h.Merkmale)
    .WithMany(m => m.Hotels)
    .MapToStoredProcedures(s => 
        s.Insert(proc => proc
                    .HasName("Hotel_Merkmale_Insert")
                    .LeftKeyParameter(h => h.HotelId, "Hotel_HotelId")
                    .RightKeyParameter(m => m.MerkmalId, "Merkmal_MerkmalId"))
        .Delete(proc => proc
                    .HasName("Hotel_Merkmale_Delete")
                    .LeftKeyParameter(h => h.HotelId, "Hotel_HotelId")
                    .RightKeyParameter(m => m.MerkmalId, "Merkmal_MerkmalId")));

[ header = Ausblick auf Entity Framework 6 – Seite 3 ]

Benutzerdefinierte Konventionen bei Code First

Bis dato musste der Entwickler beim Einsatz von Entity Framework Code First entweder die von Microsoft vorgegebenen Konventionen verwenden oder bei jeder Abweichung diesen Umstand über die Konfiguration kundtun. Letzteres ist besonders umständlich, wenn die gesamte Anwendung Konventionen verwendet, die von denen Microsofts abweichen. Für solche Fälle bietet Entity Framework 6 dem Entwickler die Möglichkeit, eigene Konventionen zu definieren. Dazu konfiguriert er Entity Framework wie gewohnt unter Verwendung des Fluent-API, ohne sich dabei auf konkrete Typen oder Eigenschaften zu beziehen. Aus diesem Grund ist hierbei auch von Configuration Conventions die Rede.

Ein Beispiel dafür findet sich in Listing 4. Dieses definiert unter anderem, dass sämtliche Eigenschaften in allen Entitäten, deren Namen sich aus dem Namen ihrer Entität (DeclaringType) und der Endung Code zusammensetzen, Primärschlüssel sind. Die zweite Anweisung bezieht sich auf sämtliche mit dem (benutzerdefinierten) Attribut UnicodeAttribute annotierten Eigenschaften und prüft anhand dessen Eigenschaft IsUnicode, ob das annotierte Feld einer unicodebasierten Spalte (nvarchar) in der Datenbank entspricht. Mit dieser Information wird die annotierte Eigenschaft konfiguriert. Im Gegensatz zur ersten Anweisung verwendet die hier betrachtete zur Selektion der gewünschten Eigenschaften nicht die Methode where, sondern having. Während beim Einsatz von where ein bool zurückgegeben wird, der anzeigt, ob mit der vorliegenden Anweisung die jeweilige Eigenschaft konfiguriert werden soll, liefert having ein Objekt zurück. Ist dieses Objekt null, geht Entity Framework davon aus, dass der Entwickler die geprüfte Eigenschaft nicht konfigurieren möchte. Ansonsten führt Entity Framework den in Configure angegebenen Lambda-Ausdruck zum Konfigurieren der vorliegenden Eigenschaft aus und übergibt an diesen neben dem Konfigurationsobjekt das von having zurückgelieferte Objekt als zweiten Parameter.

Die dritte Anweisung in Listing 4 bezieht sich auf Typen anstelle von Eigenschaften und legt fest, dass jede Klasse auf eine gleichnamige Tabelle abgebildet werden soll. Auch hier könnte der Entwickler durch den Einsatz von where und having die Menge der zur Verfügung stehenden Typen einschränken.

Anschließend werden in Listing 4 zwei Konventionen aktiviert, die in eigene Klassen ausgelagert wurden. Bei DateTime2Convention (Listing 5) handelt es sich um eine Klasse, die von Convention erbt und die gewünschte Konvention auf gewohnte Weise in ihrem Konstruktor definiert.

Modellbasierte Konventionen

Bei der am Ende von Listing 4 konfigurierten NoCascadeConvention (Listing 6) handelt es sich um eine so genannte modellbasierte Konvention, die sich von den bis dato betrachteten Configuration Conventions unterscheidet. Modellbasierte Konventionen werden eingesetzt, um das beim Einsatz von Code Only unter Verwendung der Konfiguration aus den vorliegenden Klassen abgeleitete Entity Data Model abzuändern. Dabei wird zwischen zwei Arten modellbasierter Konventionen unterschieden: IStoreModelConventionimplementierende Konventionen ändern das Storage Model des Entity Data Models ab; jene die IConceptualModelConvention implementieren, das konzeptionelle Modell. Der Typparameter dieser Klassen muss vom Typ MetadataItem oder eines Subtypen sein. Durch das Festlegen eines MetadataItemDerivats definiert der Entwickler, welche Aspekte des jeweiligen Models anzupassen sind. Einen Überblick über die verfügbaren MetadataItemDerivate gibt Abbildung 1.

Die in Listing 6 gezeigte NoCascadeConvention implementiert IConceptualModelConvention und fixiert den Typparameter auf AssociationType. Somit setzt Entity Framework diese Klasse zur Modifizierung von Assoziationen (Beziehungen) zwischen Entitäten im konzeptionellen Modell ein. Die im Zuge dessen auszuführende Logik findet sich in der Methode Apply, die sämtliche Enden der Assoziation iteriert und konfigurierte Löschweitergaben (Löschkaskaden) deaktiviert.

Abb. 1: Verfügbare „MetadataItem“-Derivate

Listing 4

modelBuilder
    .Properties()
    .Where(p => p.Name == p.DeclaringType.Name + "Code")
    .Configure(p => p.IsKey());

modelBuilder
    .Properties()
    .Having(p => p.GetCustomAttributes().FirstOrDefault())
    .Configure( (c, attr) =>
        {
            var isUnicode = attr.IsUnicode;
            c.IsUnicode(isUnicode);
        });

modelBuilder.Types().Configure(c => c.ToTable(c.ClrType.Name));

modelBuilder.Conventions.Add();
modelBuilder.Conventions.Add();

Listing 5

public class DateTime2Convention : Convention
{
  public DateTime2Convention()
  {
    this.Properties()
    .Configure(c => c.HasColumnType("datetime2"));

Listing 6

public class NoCascadeConvention : IConceptualModelConvention
{
  public void Apply(AssociationType item, DbModel model)
  {
    foreach(var m in item.AssociationEndMembers) {
      if (m.DeleteBehavior == OperationAction.Cascade)
      {
        m.DeleteBehavior = OperationAction.None; 
      }
    }
  }
}

[ header = Ausblick auf Entity Framework 6 – Seite 4 ]

Wiederanlauf nach Fehler (Connection Resiliency)

Gerade, aber nicht nur in Cloud-Umgebungen sind Entwickler mit so genannten transienten Problemen konfrontiert. Darunter versteht man Probleme, die nur kurzfristig vorherrschen und durch ein erneutes Ausführen der gescheiterten Aktion kompensiert werden können. Ein Beispiel hierfür ist SQL Azures Vorbehalt, im Fall einer hohen Belastung Datenbankverbindungen zu schließen.

Damit Entwickler zur Kompensierung solcher Fälle nicht selbst Retry-Logiken implementieren müssen, übernimmt Entity Framework ab Version 6 diese Aufgabe. Dazu gibt es Ausführungsstrategien. Tabelle 2 zeigt jene Klassen, die diese Strategien implementieren.

Tabelle 2: „ExecutionStrategy“-Implementierungen

Klasse Beschreibung
DefaultExecutionStrategy Führt keine Retrys durch und wird mit Ausnahme von SQL Server bzw. SQL Azure standardmäßig verwendet.
DefaultSqlExecutionStrategy Wie DefaultExecutionStrategy. Allerdings werden transiente Exceptions mit Exceptions, die auf die hier beschriebenen Möglichkeiten hinweisen, ummantelt. Wird standardmäßig für SQL Server verwendet.
DbExecutionStrategy Basisklasse für eigene Ausführungsstrategien. Verwendet standardmäßig eine exponentiell ansteigende zeitliche Verzögerung zwischen den einzelnen Ausführungsversuchen. Dazu nimmt der Konstruktor eine maximale Anzahl an Ausführungsversuchen sowie die maximale zeitliche Verzögerung, die zwischen zwei Versuchen stattfinden darf.
SqlAzureExecutionStrategy Implementierung von DbExecutionStrategy. Berücksichtigt die möglichen transienten Exceptions, die bei der Arbeit mit SQL Azure auftreten können. Wird standardmäßig für SQL Azure verwendet.

Zur Festlegung der gewünschten ExecutionStrategy verwendet der Entwickler innerhalb der mit dem Context assoziierten DbConfiguration-Implementierung die Methode SetExecutionStrategy. Diese nimmt den Namen des jeweiligen ADO.NET-Providers entgegen sowie eine Func, die die gewünschte Strategie zurückgibt:

SetExecutionStrategy("System.Data.SqlClient",
       () => new SqlAzureExecutionStrategy(3, TimeSpan.FromSeconds(1)));

Transaktionen

Als Alternative zum Einsatz von TransactionScope zur Realisierung von Transaktionen bietet Entity Framework 6 die Möglichkeit, Transaktionen direkt über den verwendeten Context zu starten. Dazu ruft der Entwickler die Methode Database.BeginTransaction auf. Im Zuge dessen kann er das gewünschte Transaktions-Isolations-Level übergeben, ansonsten kommt das Standard-Transaktions-Isolations-Level der verwendeten Datenbank zum Einsatz. Das Ergebnis von BeginTransaction ist eine Instanz von DbContextTransaction. Diese bietet eine Methode Commit zur Bestätigung der mit BeginTransaction gestarteten Transaktion sowie eine Methode Rollback zum Rückgängigmachen. Um sicherzustellen, dass beanspruchte Ressourcen freigegeben werden, sollte der Entwickler am Ende der durchgeführten Aktion die Methode Dispose beim DbContextTransaction-Objekt aufrufen oder dieses Objekt im Rahmen eines using-Blocks einsetzen.

Einschränkungen bei der Arbeit mit Transaktionen

Bei der Arbeit mit Transaktionen in Entity Framework 6 sollte der Entwickler die folgenden Einschränkungen berücksichtigen:

Die vorhin beschriebenen Transaktionen, die direkt über den Context gestartet werden, können nicht gemeinsam mit Connection Resiliency (siehe oben) verwendet werden.

Der TransactionScope funktioniert nur gemeinsam mit asynchronen Operationen, wenn an den Konstruktor das Argument TransactionScopeOption.Required übergeben wird. Dies wird jedoch erst ab .NET 4.5.1 unterstützt.

Arbeiten mit bestehenden Datenbankverbindungen

Ab Entity Framework 6 kann der Entwickler den Context anweisen, eine bestehende ADO.NET-Connection zu verwenden. Dazu stellt die Klasse DbContext einen Konstruktor zur Verfügung, der die gewünschte Connection in Form einer Instanz von DbConnection sowie einen Boolean namens contextOwnsConnection entgegennimmt. Wird contextOwnsConnection auf true gesetzt, schließt der Context die Connection nach deren Verwendung. Ansonsten versetzt er sie nach der Verwendung in jenen Zustand, in dem er sie davor vorgefunden hat: War sie vorher geschlossen, schließt er sie, ansonsten lässt er sie offen. Damit der Entwickler bei Einsatz von Code First in den Genuss dieser Methode kommt, muss er sie in seiner DbContext-Implementierung manuell einrichten und an den entsprechenden Konstruktor in DbContext weiterdelegieren.

Neben der Möglichkeit zur Verwendung einer bestehenden Datenbankverbindung bietet DbContext ab Version 6 auch die Option, an einer bestehenden Transaktion dieser Verbindung teilzunehmen, die in Form einer DbTransaction-Instanz vorliegt. Dazu übergibt der Entwickler diese Instanz an die Methode UseTransaction, die nun vom DbContext angeboten wird.

Fazit

Nachdem Entity Framework in den letzten Versionen um einige Spielarten und Programmiermodelle erweitert wurde und im Laufe der Zeit auch einige Programmiermodelle zumindest de facto weggefallen sind, scheint der Übergang auf Version 6 sehr stabil verlaufen zu sein. Zumindest wenn man von den wenigen notwendigen Änderungen im Bereich der Namensräume und am Providermodell absieht. Dazu kommt, dass Aspekte, die man als Entwickler bis dato vermisst hatte, mit der vorliegenden Version nachgereicht wurden. Dazu zählen Connection Resiliency, die Unterstützung von Stored Procedures durch Code Only, die Möglichkeit zur Bereitstellung benutzerdefinierter Konventionen sowie Tracing und Interception. Beide Beobachtungen zeugen für ein stabiles Produkt und unterstreichen die Empfehlung seitens Microsofts für den Einsatz von Entity Framework in neuen Projekten.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -