Abfangjäger für Entitäten

Entity Framework 6.1: Neue Interceptors und mehr
Kommentare

Dank einiger neuer Interceptors kann der Entwickler Entity Framework 6.1 an vielen Stellen an seine Bedürfnisse anpassen. Das Offenlegen des bis dato internen Mapping-API gibt ihm weitere Freiheiten an die Hand.

Ohne viel Aufsehen zu erregen, hat Microsoft Mitte März 2014 Entity Framework 6.1 veröffentlicht. Sie neue Version beinhaltet einige neue Möglichkeiten, um das Datenzugriffsframework aus Redmond an die eigenen Bedürfnisse anzupassen. Dazu gehört neben einigen neuen Interceptors auch die Möglichkeit, das Entity Data Model mit dem Mapping-API zu manipulieren. Dies gestattet es dem Entwickler, beim Einsatz von Code First auch jene Aspekte zu nutzen, die bis dato nur bei einer modellbasierten Vorgehensweise möglich waren. Ein Beispiel dafür ist die Nutzung von Stored Functions innerhalb von Abfragen. Basierend auf einigen neuen Interceptor-Typen bietet Entity Framework nun auch eine neue Möglichkeit, um im Zuge der Bestätigung einer Transaktion auftretende Fehler zu kompensieren. Damit erweitert das Produktteam die mit Version 6 eingeführten Konzepte zur Schaffung von Fehlertoleranz. Daneben kann der Entwickler auch Interceptors über die Konfigurationsdatei aktivieren. Abgesehen davon gibt es ein paar kleine Abrundungen. Dazu zählt ein Indexattribut zum Definieren von Indizes, die Unterstützung von zusätzlichen Methoden in LINQ-Abfragen, Performanceverbesserungen und eine verbesserte Erkennung von Änderungen am Datenmodell beim Einsatz von Migrationen. Unter dem Begriff „Tooling Consolidation“ stellt Microsoft darüber hinaus eine aktualisierte Werkzeugunterstützung für Visual Studio bereit. Diese erlaubt nun auch das Generieren von Klassen für Code First aus einer bestehenden Datenbank.

Entity Framework 6.1: Interceptors

Das in Entity Framework 6 eingeführte Interceptor-Konzept gibt dem Entwickler die Möglichkeit, die von Entity Framework beabsichtigten Aktionen abzufangen sowie stattdessen bzw. davor und/oder danach eigene Routinen zu platzieren. Somit kann er das Standardverhalten des Entity Frameworks anpassen. Neben dem bereits in Version 6 vorhandenen CommandInterceptor wartet Version 6.1 mit einer Reihe weiterer Interceptors auf (Übersicht in Tabelle 1). Für jede Interceptor-Art existiert ein Interface, das seinerseits vom leeren Marker-Interface IDbInterceptor erbt. Um einen Interceptor zu implementieren, realisiert der Entwickler das jeweilige Interface. Damit Entity Framework diese Implementierung nutzt, registriert er es im Rahmen der Konfiguration.

Tabelle 1: Interceptors in Entity Framework 6.1

Interceptor/Interface Beschreibung
IDbCommandInterceptor Wird aktiv, bevor und nachdem eine SQL-Anweisung zur Datenbank gesendet wurde. Der Interceptor kann die SQL-Anweisung einsehen, abändern oder die Ausführung dieser Anweisung unterdrücken und stattdessen auf eigene Weise ein Ergebnis bereitstellen. Auch das Reagieren auf sowie Unterdrücken von Exceptions ist möglich.
IDbCommandTreeInterceptor Wie IDbCommandInterceptor, allerdings stellt dieser Interceptor die SQL-Anweisung als Syntaxbaum dar. Das vereinfacht die Inspektion und Abänderung der SQL-Anweisung.
IDbConfigurationInterceptor Ermöglicht das Erweitern der Konfiguration. Beispielsweise kann ein solcher Interceptor weitere Interceptors registrieren oder die von Entity Framework ausgewählten Komponenten austauschen.
IDbConnectionInterceptor Wird bei Ereignissen auf Ebene der Datenbankverbindung aktiv. Beispiele hierfür sind das Öffnen und Schließen der Datenbankverbindung, das Erhalten von Verbindungs-Time-outs oder der Start von neuen sowie die Teilnahme an bestehenden Transaktionen.
IDbTransactionInterceptor Während der IDbConnectionInterceptor über den Start von neuen und die Teilnahme an bestehenden Transaktionen informiert, wird dieser Interceptor bei anderen transaktionsbezogenen Ereignissen aktiv. Beispiele sind die Bestätigung oder das Zurückrollen einer Transaktion (Commit bzw. Rollback), aber auch das Abrufen des aktuellen Isolationslevels.

Ein Beispiel für einen einfachen ConnectionInterceptor findet sich in Listing 1. Er implementiert das Interface IDbConnectionInterceptor und somit auch alle davon vorgegebenen Methoden. Nicht mehr benötigte Methoden implementiert er mit einem leeren Rumpf. Aus Platzgründen werden hier die meisten dieser leeren Methoden nicht abgebildet. Entity Framework stößt jede dieser Methoden zu einem bestimmten Zeitpunkt an. Opened kommt z. B. zur Ausführung, nachdem eine Datenbankverbindung geöffnet wurde; Opening hingegen bevor Entity Framework eine Datenbankverbindung öffnet. Dieses Muster findet sich in sämtlichen Interceptors wieder: Für jedes unterstützte Ereignis existiert eine Methode, die davor ausgeführt wird und/oder eine Methode, die danach zur Ausführung kommt.

Jede dieser Methoden nimmt ein Kontextobjekt entgegen. Es beinhaltet z. B. eine eventuell aufgetretene Exception oder das ermittelte Ergebnis. Der Interceptor hat die Möglichkeit, diese Informationen mit eigenen zu überschreiben und so seine eigene Exception oder sein eigenes Ergebnis festzulegen. Kompensiert er eine Exception, kann er sie auch durch null ersetzen, sodass Entity Framework sie nicht an die Anwendung weiterreicht. Legt der Interceptor eine eigene Exception oder ein eigenes Ergebnis in einer Methode fest, die Entity Framework vor dem Ausführen eines Ereignisses anstößt, wird das eigentliche Ereignis unterdrückt. Beispiele für solche Methoden sind BeginningTransaction und Opening.

Der betrachtete Interceptor in Listing 1 führt nach dem Öffnen einer Datenbankverbindung eine eigene SQL-Anweisung aus. Diese hinterlegt den aktuellen Benutzernamen zusammen mit Informationen zur aktuellen Datenbanksitzung in einer Tabelle session_info, sodass beispielsweise auch Trigger den Benutzernamen in Erfahrung bringen können. Auch wenn dies nicht zwangsläufig auf ein gutes Programmdesign schließen lässt, handelt es sich dabei dennoch um eine Anforderung, die ab und an bei Legacy-Anwendungen aufkommt. Das Schema der Tabelle session_info findet sich in Form eines Kommentars ebenfalls im betrachteten Quellcode. Die für den SQL Server geschriebene SQL-Anweisung verwendet die globale Variable @@SPID, um die ID der aktuellen Datenbanksitzung zu erhalten. Da der SQL Server Sitzungs-IDs für spätere Sitzungen wiederverwendet, wird auch der Zeitpunkt, zu dem die Sitzung gestartet wurde, aus der Systemtabelle sys.dm_exec_sessions entnommen.

class CustomDbConnectionInterceptor : IDbConnectionInterceptor
{
    public void BeganTransaction(
            DbConnection connection, 
            BeginTransactionInterceptionContext interceptionContext) { }

    public void BeginningTransaction(
            DbConnection connection, 
            BeginTransactionInterceptionContext interceptionContext) { }

    public void Closed(
            DbConnection connection, 
            DbConnectionInterceptionContext interceptionContext) { }

    public void Closing(
            DbConnection connection, 
            DbConnectionInterceptionContext interceptionContext) { }

    public void Opened(
            DbConnection connection, 
            DbConnectionInterceptionContext interceptionContext)
    {
        /*
            create table session_info(
                  session_id int, 
                  login_time datetime, 
                  applicationUser varchar(255), 
                  primary key(session_id, login_time) 
            ); 
        */

        var cmd = connection.CreateCommand();
        {
            var sql = "if not exists( n"
                        + "   select * n"
                        + "   from sys.dm_exec_sessions  a n"
                        + "       inner join session_info b n"
                        + "   on a.session_id = b.session_id n"
                        + "   and a.login_time = b.login_time n"
                        + "   where a.session_id = @@SPID)  n"
                        + "begin n"
                        + "   insert into session_info n"
                        + "   select session_id, login_time, @appUserName n"
                        + "   from sys.dm_exec_sessions n"
                        + "   where session_id = @@SPID n"
                        + "end;";
                
            cmd.CommandText = sql;

            var param = cmd.CreateParameter();
            param.Direction = System.Data.ParameterDirection.Input;
            param.ParameterName = "appUserName";
            param.Value = Thread.CurrentPrincipal.Identity.Name;
            cmd.Parameters.Add(param);

            cmd.ExecuteNonQuery();
        }
    }

    public void Opening(
            DbConnection connection, 
            DbConnectionInterceptionContext interceptionContext) { }

    [...]
}

Damit Entity Framework einen Interceptor auch verwendet, muss der Entwickler ihn registrieren. Dies geschieht in unserem Beispiel unter Verwendung der Methode AddInterceptor innerhalb des Konstruktors einer Klasse, die von DbConfiguration ableitet:

class MyConfiguration: DbConfiguration
{
    public MyConfiguration()
    {
        AddInterceptor(new CustomDbConnectionInterceptor());
    }
}

Diese Klassen, von denen es per definitionem pro Projekt nur eine einzige geben darf, werden von Entity Framework im Zuge des Initialisierens herangezogen.

Aufmacherbild: Layered collage of electronic pathways of a motherboard von Shutterstock / Urheberrecht: Kamil Hajek

Soft-Delete mit „IDbCommandTreeInterceptor“

Wie weiter oben erwähnt, kann der Entwickler mit CommandTreeInterceptor SQL-Anweisungen abfangen und abändern. Im Gegensatz zu CommandInterceptor stellen CommandTreeInterceptors die SQL-Anweisung nicht in Form eines Strings, sondern in Form eines Syntaxbaums dar. Dies vereinfacht das Abändern der Anweisung. Auf diese Weise lassen sich globale Filter definieren. Ein solcher globaler Filter könnte z. B. festlegen, dass nur Datensätze mit einem bestimmten Status oder nur Datensätze eines bestimmten Mandanten geladen werden. Dazu müsste der Interceptor jeder Abfrage um eine entsprechende Einschränkung erweitern.

Auf der TechEd 2014 hat Entity-Framework-Programmmanager Rowan Miller ein Beispiel präsentiert, in dem er einen CommandTreeInterceptor zur Implementierung von Soft Deletes nutzt. Hierunter versteht man Fälle, in denen Anwendungen obsolete Datensätze nicht tatsächlich löschen, sondern nur als gelöscht markieren. Den Quellcode dieses Beispiels finden Sie hier. Da es sich bei Soft Deletes um ein häufig anzutreffendes Muster im Bereich der Datenbankprogrammierung handelt und Entity Framework hierfür keine Bordmittel bietet, demonstriert dieser Abschnitt den Einsatz von CommandTreeInterceptor anhand von Auszügen dieses Beispiels. Um den Einsatz von Soft Delete für ausgewählte Entitäten zu aktivieren, sieht das besprochene Beispiel ein Attribut SoftDeleteAttribute vor (Listing 2). Damit werden die jeweiligen Entitäten annotiert. Über die Eigenschaft ColumnName gibt der Entwickler den Namen der Spalte an, die Auskunft darüber gibt, ob der Datensatz als gelöscht gilt. Standardmäßig wird hierfür der Name IsDeleted angenommen.

public class SoftDeleteAttribute: Attribute
{
    public SoftDeleteAttribute()
    {
        ColumnName = "IsDeleted";
    }

    public string ColumnName { get; set; }

    public static string GetColumnName(EdmType type)
    {
        var prop = type
                    .MetadataProperties
                    .Where(p => 
                           p.Name.EndsWith("customannotation:SoftDeleteColumnName"))
                    .FirstOrDefault();

        string columnName = (prop != null) ? (string) prop.Value : null;

        return columnName;
    }
}

Die statische Hilfsmethode GetColumnName nimmt einen EdmType, der im Entity Data Model eine Entität repräsentiert, entgegen und liefert den zuvor erwähnten Spaltennamen für diese Entität zurück. Sie geht davon aus, dass diese Information unter dem Schlüssel customannotation:SoftDeleteColumnName in den im Modell für diesen Typ hinterlegten Metadaten zu finden ist. Damit dies auch der Fall ist, konfiguriert der Entwickler für den verwendeten DbContext innerhalb der Methode OnModelCreating die Konvention AttributeToTableAnnotationConvention:

var conv = 
        new AttributeToTableAnnotationConvention<SoftDeleteAttribute, string>(
        "SoftDeleteColumnName",
        (type, attributes) => attributes.Single().ColumnName);
modelBuilder.Conventions.Add(conv);

Mit dieser Konvention legt er fest, dass Entity Framework die Eigenschaft ColumnName des Attributs SoftDeleteAttribute in die Metadaten der Entität aufnehmen soll. Als Schlüssel gibt er hierfür SoftDeleteColumnName an; das zuvor betrachtete Präfix customannotation wird von dieser Konvention standardmäßig vergeben. Richtig spannend wird das betrachtete Beispiel, wenn der CommandTreeInterceptor ins Spiel kommt (Listing 3), der eine Methode TreeCreated anbietet. Diese führt Entity Framework aus, nachdem es den Syntaxbaum für eine SQL-Anweisung erstellt hat. Die betrachtete Implementierung prüft zunächst, ob TreeCreated für das Store Model ausgeführt wird und somit einen Syntaxbaum beinhaltet, aus dem später direkt SQL abgeleitet wird. Ist dem nicht so, wird die Methode abgebrochen. Anschließend greift sie auf die Eigenschaft Result des von Entity Framework übergebenen Kontexts zu. Diese Eigenschaft beinhaltet den Syntaxbaum und ist vom abstrakten Typ DbCommandTree. Die hier tatsächlich verwendete Subklasse gibt Auskunft über die Art der zugrunde liegenden SQL-Anweisung. Handelt es sich um ein SELECT, kommt die konkrete Subklasse DbQueryCommandTree zum Einsatz. Für den Aufruf einer Stored Procedure oder Stored Function verwendet Entity Framework hingegen das Derivat DbFunctionCommandTree und für DML-Anweisungen (INSERT, UPDATE, DELETE) die abstrakte Klasse DbModificationCommandTree, von der die konkreten Klassen DbInsertCommandTree, DbUpdateCommandTree und DbDeleteCommandTree ableiten. Die Namen sind dabei Programm.

Da das hier behandelte Beispiel jedes SELECT erweitern muss, sodass nur Datensätze geladen werden, die nicht als gelöscht markiert wurden, prüft es, ob es sich beim Baum um einen DbQueryCommandTree handelt. Ist dem so, wendet es auf den Baum einen SoftDeleteQueryVisitor an, indem es ihn an dessen Methode Accept übergibt. Dies geht konform mit dem für solche Aufgaben häufig eingesetzten Entwurfsmuster Visitor (Besucher): Implementierungen dieses Musters durchlaufen die Knoten eines Baums und übergeben diese an eine als Visitor bezeichnete Komponente. Der Visitor besucht demnach die einzelnen Knoten und kann entscheiden, ob er die Daten des erhaltenen Knotens auswertet bzw. den Knoten abändert. Im betrachteten Fall kümmert sich der verwendete Visitor (weiter unten genauer beschrieben) um das Hinzufügen der besprochenen Einschränkung. Das Ergebnis von Accept verpackt die betrachtete Implementierung in einem neuen DbQueryCommandTree. Diesen hinterlegt es in der Eigenschaft Result des Kontexts, was zur Folge hat, dass Entity Framework damit und nicht mit dem ursprünglichen Baum vorliebnimmt. Die hier zu findende Implementierung prüft auch, ob der an TreeCreated übergebene Syntaxbaum einem DELETE entspricht. In diesem Fall formt es dieses zu einem UPDATE um, das den Datensatz als gelöscht markiert. Aus Platzgründen wird dieser Aspekt hier nicht abgedruckt.

class CustomCommandTreeInterceptor : IDbCommandTreeInterceptor
{
    public void TreeCreated(
            DbCommandTreeInterceptionContext interceptionContext)
    {
        if (interceptionContext.OriginalResult.DataSpace 
                                          != DataSpace.SSpace) return;

        var tree = interceptionContext.Result as DbQueryCommandTree;

        if (tree != null)
        {
            var newQuery = tree.Query.Accept(new SoftDeleteQueryVisitor());

            interceptionContext.Result = new DbQueryCommandTree(
                tree.MetadataWorkspace,
                tree.DataSpace,
                newQuery);
        }
    }
}

Die Umsetzung des SoftDeleteQueryVisitors findet sich in Listing 4. Sie leitet von der Klasse DefaultExpressionVisitor ab, die Entity Framework als Basisklasse für Visitor-Implementierungen anbietet und überschreibt die Methode Visit. Da es von Visit zahlreiche Überladungen gibt, ist zu betonen, dass es sich hier um jene Überladung handelt, die eine DbScanExpression entgegennimmt. Diese Methode ruft Entity Framework immer dann auf, wenn der jeweils besuchte Knoten das Abfragen von Daten einer Entität repräsentiert. Die Methode Visit ermittelt mit der zuvor besprochenen Methode GetColumnName die für die betroffene Entität hinterlegte Spalte. Ist dieser Wert null, geht Visit davon aus, dass für die betrachtete Entität keine Soft Deletes zum Einsatz kommen sollen und delegiert lediglich an die geerbte Basisimplementierung. Ansonsten erweitert Visit die DbScanExpression um einen Filter, aus dem hervor geht, dass nur Datensätze zu laden sind, bei denen die festgelegte Spalte den Wert true hat, und liefert sie zurück.

public class SoftDeleteQueryVisitor : DefaultExpressionVisitor
{
    public override DbExpression Visit(DbScanExpression expression)
    {
        var columnName = 
                SoftDeleteAttribute.GetColumnName(expression.Target.ElementType);

        if (columnName != null)
        {
            var binding = DbExpressionBuilder.Bind(expression);

            return DbExpressionBuilder.Filter(
                binding,
                DbExpressionBuilder.NotEqual(
                    DbExpressionBuilder.Property(
                        DbExpressionBuilder.Variable(
                                binding.VariableType, binding.VariableName),
                        columnName),
                    DbExpression.FromBoolean(true)));
        }
        else
        {
            return base.Visit(expression);
        }
    }
}

Die hinter diesem Beispiel stehende Idee wurde mittlerweile auch von der Community aufgegriffen. Hier finden sie beispielsweise ein Projekt, das das Definieren von globalen Filtern für Entity Framework erlaubt. Es verbirgt die Komplexität der benötigten Interceptors und Visitators vor dem Entwickler, indem er die Möglichkeit bekommt, Filter mit ein paar Zeilen Code zu hinterlegen.

Interceptors per Konfiguration aktivieren

Seit Entity Framework 6.1 muss der Entwickler Interceptors nicht mehr über den Code konfigurieren, sondern kann sie auch in der Konfigurationsdatei hinterlegen. Somit kann er Interceptors bei Bedarf auch nach dem Ausliefern einer Anwendung aktivieren. Besonders nützlich erscheint dies bei solchen Interceptors zu sein, die sich um das Protokollieren von Informationen kümmern. Ein Beispiel, das sich auf dieses Szenario bezieht, finden Sie hier.

Fehler beim Commit mit „CommitFailureHandler“ kompensieren

Während bereits Entity Framework 6.0 die Möglichkeit bietet, fehlgeschlagene SQL-Anweisungen automatisch zu wiederholen, schließt das mit Version 6.1 eingeführte Konzept des TransactionHandlers genau in diesem Bereich eine Lücke, die dann entsteht, wenn beim Bestätigen einer Transaktion (Commit) ein Fehler auftritt. In diesem Fall ist es nicht zulässig, die betroffene Transaktion ohne Weiteres zu wiederholen. Vielmehr muss ein Weg gefunden werden, um herauszufinden, ob der Fehler vor oder nach dem Commit aufgetreten ist. Ist der Fehler vor dem Commit aufgetreten, muss die Transaktion wiederholt werden. Ist er hingegen danach aufgetreten, wurden die Änderungen erfolgreich übernommen, und der Fehler kann ignoriert werden. Diese Situation kann sich unter anderem ergeben, wenn unmittelbar nach dem Commit die Datenbankverbindung unterbrochen wird. Gerade in Cloud-Umgebungen ist solch ein Fall realistisch, zumal der Entwickler hier jederzeit damit rechnen muss, dass die Verbindung im Zuge eines Failovers, zur Lastverteilung oder einfach, da sie länger nicht mehr zum Übertragen von Daten verwendet wurde, geschlossen wird. Letzteres ergibt sich etwa, wenn die Verarbeitung der übermittelten Daten in der Datenbank länger dauert.

Die von Entity Framework angebotene TransactionHandler-Implementierung nennt sich CommitFailerHandler. Um erkennen zu können, ob eine Transaktion vor dem Auftreten eines Fehlers erfolgreich abgeschlossen wurde, fügt der CommitFailerHandler zu Beginn jeder Transaktion einen Datensatz in eine Protokolltabelle namens __Transactions ein. Diese Tabelle legt er bei Bedarf an. Existiert dieser Datensatz im Fehlerfall noch, kann er davon ausgehen, dass die Transaktion vor dem Auftreten des Fehlers erfolgreich abgeschlossen wurde. Existiert er nicht mehr, konnte die Transaktion nicht erfolgreich abgeschlossen werden und ist somit zurückgerollt worden. Obwohl der CommitFailerHandler versucht, nicht mehr benötigte Datensätze aus dieser Tabelle zu löschen, kann es dennoch vorkommen, dass hier alte Einträge zurückbleiben. Auf diesen Umstand weist das Produktteam auch hin. Somit liegt die endgültige Verantwortung im Entfernen nicht mehr benötigter Einträge beim Entwickler. Zum Aktivieren des CommitFailerHandlers verwendet der Entwickler die Methode SetTransactionHandler innerhalb des Konstruktors eines DbConfiguration-Derivates:

SetTransactionHandler(
        SqlProviderServices.ProviderInvariantName,
        () => new CommitFailureHandler());

Neben dieser von Entity Framework bereitgestellten Implementierung kann der Entwickler auch eine eigene Implementierung erstellen, indem er von der abstrakten Basisklasse TransactionHandler ableitet. Diese Klasse realisiert die Interfaces IDbTransactionInterceptor und IDbConnectionInterceptor (vgl. Abschnitt über Interceptors) und kann sich somit über transaktionsrelevante Ereignisse informieren lassen. Der abstrakte TransactionHandler implementiert die Methoden dieser Interfaces mit leeren Rümpfen, sodass sich der Entwickler nur mehr um die wirklich benötigten Methoden kümmern muss.

Um seine Aufgabe zu erfüllen, überschreibt ein TransactionHandler die Methode Committed, die Entity Framework nach dem Abschluss einer Transaktion aufruft. In dieser Methode ist zu prüfen, ob im Zuge des Commits eine Exception aufgetreten ist, sowie ob die Transaktion (trotzdem) erfolgreich abgeschlossen wurde. Ist Letzteres der Fall, kann der TransactionHandler die Exception unterdrücken, indem er die des übergebenen Kontextobjekts auf null setzt. Um prüfen zu können, ob die Transaktion erfolgreich beendet wurde, muss ein TransactionHandler in der Regel weitere Methoden der erwähnten Interceptor-Interfaces überschreiben. Der CommitFailerHandler überschreibt beispielsweise die Methode BeganTransaction, um nach dem Start einer Transaktion den erwähnten Eintrag in die Protokolltabelle zu schreiben.

Neben den von den Interceptor-Interfaces vorgegebenen Methoden weist die abstrakte Klasse TransactionHandler auch eine eigene Methode namens BuildDatabaseInitializationScript auf. Diese ist ebenfalls abstrakt und muss vom Entwickler überschrieben werden. Ihre Aufgabe besteht darin, SQL-Anweisungen zum Initialisieren der Datenbank als String zurückzugeben. Der CommitFailerHandler gibt auf diesem Weg z. B. den Befehl zum Erzeugen der benötigten Tabelle __Transactions bekannt.

Öffentliches Mapping-API

Zum Abbilden von Entitäten auf Tabellen stützt sich Entity Framework bekannterweise auf ein so genanntes Entity Data Model. Dieses Modell liegt entweder explizit als XML-Datei vor oder es wird – im Falle von Code First – aus dem Quellcode abgeleitet. Genau genommen handelt es sich dabei nicht um ein einziges Modell, sondern um gleich drei Modelle: Das Conceptual Model beschreibt die Entitätsklassen, das Store Model die verwendeten Datenbankobjekte wie Tabellen oder Stored Procedures, und das Mapping beschreibt, wie Elemente aus dem einen Modell auf Elemente des anderen Modells abzubilden sind. Da das Entity Data Model beim Einsatz von Code First nicht explizit vorliegt, konnten Entwickler bei der Wahl dieser Spielart vor Version 6.1 darauf keinen direkten Einfluss nehmen. Ab Version 6.1 erhalten Entwickler über eigene Konventionen sowohl lesenden als auch schreibenden Zugriff auf das Entity Data Model. Prinzipiell kann der Entwickler hierzu eine ConceptualModelConvention oder eine StoreModelConvention verwenden. Da das Entity Data Model aber erst beim Ausführen der StoreModelConvention-Implementierung vollständig zur Verfügung steht, ist es ratsam, sich auf diese Art der Konventionen zu beschränken.

Listing 5 zeigt, wie der Entwickler auf die drei Modelle des Entity Data Models innerhalb einer StoreModelConvention zugreifen kann. Die Arbeit mit diesem vor Version 6.1 internen Objektmodell gestaltet sich recht schwierig, zumal man zum einen herausfinden muss, wo sich die gewünschten Aspekte in den drei Modellen befinden und zum anderen dann auch noch wissen muss, wie das Objektmodell diese Stellen widerspiegelt. Informationen darüber finden sich in der Dokumentation des Entity Data Models sowie in der Dokumentation der in Listing 5 gezeigten Klassen, die das Entity Data Model repräsentieren. Allerdings sind künftig Erweiterungen zu erwarten, die basierend auf der Möglichkeit, das Entity Data Model zu bearbeiten, die Grenzen von Entity Framework ausweiten. Eine solche Erweiterung ist hier zu finden. Sie gibt dem Entwickler die Möglichkeit, Stored Functions beim Einsatz von Code First zu verwenden. Dabei handelt es sich um ein Feature von Entity Framework, das bis dato noch nicht den Weg in die Welt von Code First gefunden hat. Wer wissen möchte, wie dieses Projekt das Mapping-API für diese Aufgabe nutzt, findet hier eine umfangreiche Beschreibung.

public class CustomFunctionsConvention : IStoreModelConvention
{
    public void Apply(EntityContainer item, DbModel model)
    {
        EdmModel conceptualModel = model.ConceptualModel;
        EdmModel storeModel = model.StoreModel;
        EntityContainerMapping mapping = model.ConceptualToStoreMapping;

        // Modell lesen und abändern
    }
}

Tooling Consolidation

Mit dem Schlagwort „Tooling Consolidation“ bezeichnet Microsoft die Tatsache, dass der Entwickler beim Hinzufügen eines neuen Entity Data Models nun auch die Möglichkeit hat, Code First zu nutzen (Abb. 1). Während die neue Option zur Erzeugung eines neuen Code-First-Modells lediglich das NuGet-Paket von Entity Framework einbindet und ein initiales DbContext-Derivat zur Verfügung stellt, generiert die Option „Code First aus Datenbank“ aus bestehenden Tabellen auch alle benötigten Artefakte für den Zugriff via Code First. Dazu gehören Entitätsklassen, ein DbContext-Derivat sowie Klassen, die das Mapping beschreiben. Um in den Genuss dieser beiden neuen Optionen zu kommen, muss der Entwickler die Entity Framework 6 Tools for Visual Studio 2012 & 2013 installieren.

Abb. 1: Assistent zum Erzeugen von Code-First-Modellen

Sonstige Neuerungen in EF 6.1

Entity Framework 6.1 bringt auch ein paar kleinere Abrundungen, die nachfolgend kurz zusammengefasst werden:

  • Mit einem neuen Indexattribut kann der Entwickler beim Einsatz von Code First angeben, welche Attribute zu indizieren sind. Pro Index gibt er einen Namen an sowie die Tatsache, ob es sich hierbei um einen eindeutigen Index handelt. Werden mehrere Spalten in einen Index einbezogen, ist auch eine Ordnungszahl anzugeben, aus der hervorgeht, an welcher Stelle im Index die jeweilige Spalte vorkommt.
  • Die Methoden ToString, String.Concat sowie HasFlags von Enums können nun in LINQ-Abfragen genutzt werden.
  • Die Erkennung von Änderungen beim Einsatz von Migrations wurde laut Aussagen von Microsoft verbessert.
  • Die Performance wurde ebenfalls laut Aussagen von Microsoft mit Version 6.1 verbessert.

Fazit

Mit Version 6.1 hat das Produktteam weitere Einsprungspunkte hinzugefügt. Damit stehen Entwicklern mehr Möglichkeiten zur Verfügung, um Entity Framework an die eigenen Bedürfnisse anzupassen. Somit wird eine weitere Lücke gegenüber Konkurrenten wie NHibernate geschlossen. Auch Entwickler, die sich nicht mit diesen Einsprungspunkten und den damit einhergehenden Interna von Entity Framework belasten möchten, profitieren hiervon, zumal sie auch als Basis für Erweiterungen durch die Open-Source-Szene gelten. Als Beispiel wurden in diesem Artikel die freien Projekte „EntityFramework.Filters“ sowie „Store Functions for Entity Framework Code First“ genannt. Dass aber auch das Produktteam selbst von den geschaffenen Einsprungspunkten profitiert, wird am Beispiel der TransactionHandler deutlich, die sich auf mit Version 6.1 eingeführte Interceptor-Arten stützen.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -