Benutzerdefinierte Code First-Konventionen

Hinweis

Nur EF6 und höher: Die Features, APIs usw., die auf dieser Seite erläutert werden, wurden in Entity Framework 6 eingeführt. Wenn Sie eine frühere Version verwenden, gelten manche Informationen nicht.

Bei Verwendung von Code First wird Ihr Modell anhand einer Reihe von Konventionen aus Ihren Klassen berechnet. Die standardmäßigen Code First-Konventionen bestimmen Dinge wie z. B. welche Eigenschaft zum Primärschlüssel einer Entität wird, den Namen der Tabelle, der eine Entität zugeordnet ist, und welche Genauigkeit und Skalierung eine Dezimalspalte standardmäßig aufweist.

Manchmal sind diese Standardkonventionen für Ihr Modell nicht ideal, und Sie müssen sie umgehen, indem Sie viele einzelne Entitäten mithilfe von Datenanmerkungen oder der Fluent-API konfigurieren. Mit benutzerdefinierten Code First-Konventionen können Sie eigene Konventionen definieren, die Konfigurationsstandardwerte für Ihr Modell bereitstellen. In dieser exemplarischen Vorgehensweise werden wir die verschiedenen Arten von benutzerdefinierten Konventionen untersuchen und wie sie zu erstellen sind.

Modellbasierte Konventionen

Diese Seite behandelt die DbModelBuilder-API für benutzerdefinierte Konventionen. Diese API sollte für die Erstellung der meisten benutzerdefinierten Konventionen ausreichend sein. Es gibt jedoch auch die Möglichkeit, modellbasierte Konventionen zu erstellen – Konventionen, die das endgültige Modell nach der Erstellung manipulieren –, um erweiterte Szenarien zu behandeln. Weitere Informationen finden Sie unter Modellbasierte Konventionen.

 

Unser Modell

Lassen Sie uns mit der Definition eines einfachen Modells beginnen, das wir mit unseren Konventionen verwenden können. Fügen Sie ihrem Projekt die folgenden Klassen hinzu.

    using System;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Linq;

    public class ProductContext : DbContext
    {
        static ProductContext()
        {
            Database.SetInitializer(new DropCreateDatabaseIfModelChanges<ProductContext>());
        }

        public DbSet<Product> Products { get; set; }
    }

    public class Product
    {
        public int Key { get; set; }
        public string Name { get; set; }
        public decimal? Price { get; set; }
        public DateTime? ReleaseDate { get; set; }
        public ProductCategory Category { get; set; }
    }

    public class ProductCategory
    {
        public int Key { get; set; }
        public string Name { get; set; }
        public List<Product> Products { get; set; }
    }

 

Einführung in benutzerdefinierte Konventionen

Lassen Sie uns eine Konvention schreiben, die jede Eigenschaft mit dem Namen „Schlüssel“ als Primärschlüssel für ihren Entitätstyp konfiguriert.

Konventionen sind für den Modell-Generator aktiviert, auf den durch Überschreiben von OnModelCreating im Kontext zugegriffen werden kann. Aktualisieren Sie die ProductContext-Klasse wie folgt:

    public class ProductContext : DbContext
    {
        static ProductContext()
        {
            Database.SetInitializer(new DropCreateDatabaseIfModelChanges<ProductContext>());
        }

        public DbSet<Product> Products { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Properties()
                        .Where(p => p.Name == "Key")
                        .Configure(p => p.IsKey());
        }
    }

Jetzt wird jede Eigenschaft in unserem Modell mit dem Namen „Schlüssel“ als Primärschlüssel der Entität konfiguriert, zu der sie gehört.

Wir könnten unsere Konventionen auch spezifischer gestalten, indem wir nach dem Typ der Eigenschaft filtern, die wir konfigurieren werden:

    modelBuilder.Properties<int>()
                .Where(p => p.Name == "Key")
                .Configure(p => p.IsKey());

Dadurch werden alle Eigenschaften mit Namen „Schlüssel“ so konfiguriert, dass sie der Primärschlüssel ihrer Entität sind, aber nur, wenn sie Integer sind.

Ein interessantes Feature der IsKey-Methode ist, dass es additiv ist. Das heißt, wenn Sie IsKey für mehrere Eigenschaften aufrufen, werden sie alle Teil eines zusammengesetzten Schlüssels. Die einzige Einschränkung dabei ist, dass Sie bei der Angabe mehrerer Eigenschaften für einen Schlüssel auch eine Reihenfolge für diese Eigenschaften angeben müssen. Sie können dies tun, indem Sie die HasColumnOrder-Methode wie folgt aufrufen:

    modelBuilder.Properties<int>()
                .Where(x => x.Name == "Key")
                .Configure(x => x.IsKey().HasColumnOrder(1));

    modelBuilder.Properties()
                .Where(x => x.Name == "Name")
                .Configure(x => x.IsKey().HasColumnOrder(2));

Mit diesem Code werden die Typen in unserem Modell so konfiguriert, dass sie über einen zusammengesetzter Schlüssel verfügen, der aus der internen Spalte „Schlüssel“ und der Zeichenfolgenspalte „Name“ besteht. Wenn wir das Modell im Designer anzeigen, würde es wie folgt aussehen:

composite Key

Ein weiteres Beispiel für Eigenschaftskonventionen ist die Konfiguration aller DateTime-Eigenschaften in meinem Modell, damit sie in SQL Server dem Typ „datetime2“ statt „datetime“ zugeordnet werden. Sie können dies mit den folgenden Schritten erreichen:

    modelBuilder.Properties<DateTime>()
                .Configure(c => c.HasColumnType("datetime2"));

 

Konventionsklassen

Eine weitere Möglichkeit zum Definieren von Konventionen besteht darin, eine Konventionsklasse zu verwenden, um Ihre Konvention zu kapseln. Wenn Sie eine Konventionsklasse verwenden, dann erstellen Sie einen Typ, der von der Konventionsklasse im Namespace System.Data.Entity.ModelConfiguration.Conventions erbt.

Wir können eine Konventionsklasse mit der datetime2-Konvention erstellen, die wir zuvor gezeigt haben, indem wir Folgendes tun:

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

Um EF anweisen, diese Konvention zu verwenden, fügen Sie diese der Konventionssammlung in OnModelCreating hinzu. Wenn Sie der exemplarischen Vorgehensweise gefolgt sind, sieht dies wie folgt aus:

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Properties<int>()
                    .Where(p => p.Name.EndsWith("Key"))
                    .Configure(p => p.IsKey());

        modelBuilder.Conventions.Add(new DateTime2Convention());
    }

Wie Sie sehen können, fügen wir der Konventionssammlung eine Instanz unserer Konvention hinzu. Das Erben von Konventionen bietet eine bequeme Möglichkeit zum Gruppieren und Freigeben von Konventionen über Teams oder Projekte hinweg. Sie könnten z. B. eine Klassenbibliothek mit einer gemeinsamen Gruppe von Konventionen haben, die von allen Projekten Ihrer Organisationen verwendet wird.

 

Benutzerdefinierte Attribute

Eine weitere großartige Verwendung von Konventionen besteht darin, beim Konfigurieren eines Modells neue Attribute zu verwenden. Um dies zu veranschaulichen, erstellen wir ein Attribut, mit dem wir Zeichenfolgeneigenschaften als Nicht-Unicode markieren können.

    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public class NonUnicode : Attribute
    {
    }

Als Nächstes erstellen wir eine Konvention, um dieses Attribut auf unser Modell anzuwenden:

    modelBuilder.Properties()
                .Where(x => x.GetCustomAttributes(false).OfType<NonUnicode>().Any())
                .Configure(c => c.IsUnicode(false));

Mit dieser Konvention können wir das NonUnicode-Attribut zu jeder unserer Zeichenfolgeneigenschaften hinzufügen, was bedeutet, dass die Spalte in der Datenbank als „varchar“ anstatt „nvarchar“ gespeichert wird.

Beachten Sie bei dieser Konvention folgendes: Wenn Sie das NonUnicode-Attribut auf einen anderen Wert als eine Zeichenfolgeneigenschaft setzen, dann wird eine Ausnahme ausgelöst. Dies geschieht, weil Sie IsUnicode für keinen anderen Typ als eine Zeichenfolge konfigurieren können. Wenn dies geschieht, können Sie Ihre Konvention spezifischer gestalten, sodass sie alles herausfiltert, was keine Zeichenfolge ist.

Während die obige Konvention für die Definition von benutzerdefinierten Attributen funktioniert, gibt es eine andere API, die viel einfacher zu verwenden ist, insbesondere dann, wenn Sie Eigenschaften aus der Attributklasse verwenden möchten.

Für dieses Beispiel werden wir unser Attribut aktualisieren und es in ein IsUnicode-Attribut umwandeln, so dass es wie folgt aussieht:

    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    internal class IsUnicode : Attribute
    {
        public bool Unicode { get; set; }

        public IsUnicode(bool isUnicode)
        {
            Unicode = isUnicode;
        }
    }

Sobald wir dies haben, können wir einen booleschen Wert auf unser Attribut setzen, um der Konvention mitzuteilen, ob eine Eigenschaft Unicode sein soll oder nicht. Dies könnte in der Konvention geschehen, die wir bereits haben, indem wir wie folgt auf die ClrProperty der Konfigurationsklasse zugreifen:

    modelBuilder.Properties()
                .Where(x => x.GetCustomAttributes(false).OfType<IsUnicode>().Any())
                .Configure(c => c.IsUnicode(c.ClrPropertyInfo.GetCustomAttribute<IsUnicode>().Unicode));

Dies ist einfach genug, aber es gibt einen prägnanteren Weg, dies mithilfe der Having-Methode der Konventionen-API zu erreichen. Die Having-Methode verfügt über einen Parameter vom Typ Func<PropertyInfo, T>, der die PropertyInfo genauso akzeptiert wie die Where-Methode, aber ein Objekt zurückgeben sollte. Wenn das zurückgegebene Objekt NULL ist, wird die Eigenschaft nicht konfiguriert, d. h., Sie können Eigenschaften genau wie mit Where herausfiltern, aber es unterscheidet sich darin, dass es auch das zurückgegebene Objekt erfasst und an die Configure-Methode übergeben wird. Dies funktioniert wie folgt:

    modelBuilder.Properties()
                .Having(x => x.GetCustomAttributes(false).OfType<IsUnicode>().FirstOrDefault())
                .Configure((config, att) => config.IsUnicode(att.Unicode));

Benutzerdefinierte Attribute sind nicht der einzige Grund für die Verwendung der Having-Methode. Sie ist überall dort nützlich, wo Sie beim Konfigurieren Ihrer Typen oder Eigenschaften über etwas nachdenken müssen, nach dem Sie filtern.

 

Konfigurieren von Typen

Bisher wurden alle unsere Konventionen für Eigenschaften verwendet, aber es gibt einen weiteren Bereich der Konventionen-API zum Konfigurieren der Typen in Ihrem Modell. Die Erfahrung ähnelt den bisher gezeigten Konventionen, aber die Optionen innerhalb der Konfiguration befinden sich auf der Entitätsebene und nicht auf der Eigenschaftsebene.

Konventionen auf Typebene können unter anderem sehr nützlich sein, um die Tabellennamenskonvention zu ändern, entweder um sie einem bestehenden Schema zuzuordnen, das sich vom EF-Standard unterscheidet, oder um eine neue Datenbank mit einer anderen Namenskonvention zu erstellen. Dazu benötigen wir zuerst eine Methode, die TypeInfo für einen Typ in unserem Modell annehmen und den Tabellennamen für diesen Typ zurückgeben kann:

    private string GetTableName(Type type)
    {
        var result = Regex.Replace(type.Name, ".[A-Z]", m => m.Value[0] + "_" + m.Value[1]);

        return result.ToLower();
    }

Diese Methode verwendet einen Typ und gibt eine Zeichenfolge zurück, die Kleinbuchstaben mit Unterstrichen anstelle von CamelCase verwendet. In unserem Modell bedeutet dies, dass die ProductCategory-Klasse einer Tabelle namens product_category anstelle von ProductCategories zugeordnet wird.

Sobald wir über diese Methode verfügen, können wir sie in einer Konvention wie folgt aufrufen:

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

Diese Konvention konfiguriert jeden Typ in unserem Modell so, dass er dem Tabellennamen zugeordnet ist, der von unserer GetTableName-Methode zurückgegeben wird. Diese Konvention entspricht dem Aufrufen der ToTable-Methode für jede Entität im Modell mithilfe der Fluent-API.

Dabei ist zu beachten, dass EF beim Aufruf von ToTable die von Ihnen angegebene Zeichenkette als genauen Tabellennamen verwenden wird, ohne die Pluralisierung, die normalerweise bei der Ermittlung von Tabellennamen vorgenommen wird. Aus diesem Grund lautet der Tabellenname unserer Konvention product_category statt product_categories. Wir können dies in unserer Konvention auflösen, indem wir selber den Pluralisierungsdienst aufrufen.

Im folgenden Code verwenden wir das Feature Abhängigkeitsauflösung, das in EF6 hinzugefügt wurde, um den Pluralisierungsdienst abzurufen, den EF verwendet hätte, und unseren Tabellennamen zu pluralisieren.

    private string GetTableName(Type type)
    {
        var pluralizationService = DbConfiguration.DependencyResolver.GetService<IPluralizationService>();

        var result = pluralizationService.Pluralize(type.Name);

        result = Regex.Replace(result, ".[A-Z]", m => m.Value[0] + "_" + m.Value[1]);

        return result.ToLower();
    }

Hinweis

Die generische Version von GetService ist eine Erweiterungsmethode im System.Data.Entity.Infrastructure.DependencyResolution-Namespace. Sie müssen ihrem Kontext eine „using“-Anweisung hinzufügen, um sie zu verwenden.

ToTable und Vererbung

Ein weiterer wichtiger Aspekt von ToTable ist, dass Sie, wenn Sie einen Typ explizit einer bestimmten Tabelle zuordnen, die Zuordnungsstrategie ändern können, die EF verwenden wird. Wenn Sie ToTable für jeden Typ in einer Vererbungshierarchie aufrufen, indem Sie den Typnamen wie oben beschrieben als Namen der Tabelle übergeben, dann werden Sie die Standardzuordnungsstrategie „Tabelle pro Hierarchie (TPH)“ in „Tabelle pro Typ (TPT)“ ändern. Dies lässt sich am besten anhand eines konkreten Beispiels beschreiben:

    public class Employee
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

    public class Manager : Employee
    {
        public string SectionManaged { get; set; }
    }

Standardmäßig werden sowohl Mitarbeiter als auch Manager*innen der gleichen Tabelle (Mitarbeiter) in der Datenbank zugeordnet. Die Tabelle wird sowohl Mitarbeiter als auch Manager*innen mit einer Diskriminatorspalte enthalten, die Ihnen sagt, welche Art von Instanz in jeder Zeile gespeichert ist. Dies ist die TPH-Zuordnung, da es eine einzelne Tabelle für die Hierarchie gibt. Wenn Sie jedoch ToTable für beide Klassen aufrufen, dann wird jeder Typ stattdessen seiner eigenen Tabelle zugeordnet, auch als TPT bezeichnet, da jeder Typ über seine eigene Tabelle verfügt.

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

Der obige Code wird einer Tabellenstruktur zugeordnet, die wie folgt aussieht:

tpt Example

Sie können dies vermeiden und die standardmäßige TPH-Zuordnung auf verschiedene Arten beibehalten:

  1. Rufen Sie ToTable mit demselben Tabellennamen für jeden Typ in der Hierarchie auf.
  2. Rufen Sie ToTable nur auf der Basisklasse der Hierarchie auf, in unserem Beispiel währe dies Mitarbeiter.

 

Ausführungsreihenfolge

Konventionen funktionieren auf die gleiche Weise wie die Fluent-API, nämlich „die letzte gewinnt“. Dies bedeutet, dass wenn Sie zwei Konventionen schreiben, die dieselbe Option derselben Eigenschaft konfigurieren, dann gewinnt die zuletzt auszuführende. Beispiel: Im nachfolgenden Code wird die maximalen Länge aller Zeichenfolgen auf 500 festgelegt, aber dann konfigurieren wir alle Eigenschaften namens „Name“ im Modell so, dass sie eine maximale Länge von 250 aufweisen.

    modelBuilder.Properties<string>()
                .Configure(c => c.HasMaxLength(500));

    modelBuilder.Properties<string>()
                .Where(x => x.Name == "Name")
                .Configure(c => c.HasMaxLength(250));

Da sich die Konvention zum Festlegen der maximalen Länge auf 250 hinter derjenigen befindet, welche alle Zeichenfolgen auf 500 festlegt, werden alle Eigenschaften, die in unserem Modell „Name“ genannt werden, eine MaxLength von 250 haben, während alle anderen Zeichenfolgen, z. B. Beschreibungen, eine Länge von 500 haben würden. Die Verwendung von Konventionen auf diese Weise bedeutet, dass Sie eine allgemeine Konvention für Typen oder Eigenschaften in Ihrem Modell bereitstellen und diese dann für abweichende Teilmengen überschreiben können.

Die Fluent-API und Datenanmerkungen können auch verwendet werden, um eine Konvention in bestimmten Fällen außer Kraft zu setzen. Wenn wir in unserem obigen Beispiel die Fluent-API zum Festlegen der maximalen Länge einer Eigenschaft verwendet hätten, dann hätten wir sie vor oder nach der Konvention einfügen können, da die spezifischere Fluent-API über die allgemeinere Konfigurationskonvention gewinnen wird.

 

Integrierte Konventionen

Da benutzerdefinierte Konventionen von den Standardkonventionen für Code First betroffen sein können, kann es hilfreich sein, Konventionen hinzuzufügen, die vor oder nach einer anderen Konvention ausgeführt werden sollen. Dazu können Sie die Methoden AddBefore und AddAfter der Konventionssammlung für Ihren abgeleiteten DbContext verwenden. Der folgende Code würde die Konventionsklasse, die wir zuvor erstellt haben, so hinzufügen, dass sie vor der integrierten Schlüsselermittlungskonvention ausgeführt wird.

    modelBuilder.Conventions.AddBefore<IdKeyDiscoveryConvention>(new DateTime2Convention());

Dies wird am nützlichsten sein, wenn Konventionen hinzugefügt werden, die vor oder nach den integrierten Konventionen ausgeführt werden müssen. Eine Liste der integrierten Konventionen finden Sie hier: System.Data.Entity.ModelConfiguration.Conventions-Namespace.

Sie können auch Konventionen entfernen, die nicht auf Ihr Modell angewendet werden sollen. Verwenden Sie die Remove-Methode, um eine Konvention zu entfernen. Hier sehen Sie ein Beispiel für das Entfernen der PluralizingTableNameConvention.

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
    }