Freigeben über


Erste Konventionen für benutzerdefinierten Code

Hinweis

Nur EF6 und weiter – Die Funktionen, APIs und mehr, die auf dieser Seite besprochen werden, wurden in Entity Framework 6 eingeführt. Wenn Sie eine frühere Version verwenden, gelten einige oder alle 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, welche Eigenschaft zum Primärschlüssel einer Entität wird, dem 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 untersuchen wir die verschiedenen Arten von benutzerdefinierten Konventionen und erfahren, wie Sie jede dieser Konventionen erstellen.

Model-Based Konventionen

Diese Seite behandelt die DbModelBuilder-API für benutzerdefinierte Konventionen. Diese API sollte für die Erstellung der meisten benutzerdefinierten Konventionen ausreichen. Es gibt jedoch auch die Möglichkeit, modellbasierte Konventionen zu erstellen – Konventionen, die das endgültige Modell bearbeiten, sobald es erstellt wurde - um erweiterte Szenarien zu behandeln. Weitere Informationen finden Sie in Modellbasierte Konventionen.

 

Unser Modell

Beginnen wir mit der Definition eines einfachen Modells, 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 benutzerdefinierter Konventionen

Lassen Sie uns eine Konvention schreiben, die jede Eigenschaft mit dem Namen "Key" als Primärschlüssel für den Entitätstyp konfiguriert.

Konventionen sind für den Modell-Generator aktiviert, auf die durch Außerkraftsetzung 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, die den Namen Key trägt, als Primärschlüssel der jeweiligen Entität konfiguriert, zu der sie gehört.

Wir könnten unsere Konventionen auch genauer 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 namens "Key" so konfiguriert, dass sie der Primärschlüssel ihrer Entität sind, aber nur, wenn sie eine ganze Zahl sind.

Ein interessantes Merkmal der IsKey-Methode ist, dass es additiv ist. Dies bedeutet, dass, wenn Sie IsKey für mehrere Eigenschaften aufrufen, diese alle Teil eines zusammengesetzten Schlüssels werden. Eine Einschränkung hierfür ist, dass Sie beim Angeben 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 ein zusammengesetzter Schlüssel besteht, der aus der Spalte "Int-Schlüssel" und der Spalte "Zeichenfolgenname" besteht. Wenn wir das Modell im Designer anzeigen, würde es wie folgt aussehen:

zusammengesetzter Schlüssel

Ein weiteres Beispiel für Eigenschaftskonventionen besteht darin, alle DateTime-Eigenschaften in meinem Modell so zu konfigurieren, dass sie dem Datetime2-Typ in SQL Server anstelle von 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, erstellen Sie einen Typ, der von der Convention-Klasse im Namespace System.Data.Entity.ModelConfiguration.Conventions erbt.

Wir können eine Convention Klasse 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 sie der Conventions-Auflistung in OnModelCreating hinzu. Wenn Sie mit der exemplarischen Vorgehensweise folgen, 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 eine Instanz unserer Konvention der Sammlung von Konventionen hinzu. Das Erben von Convention bietet eine bequeme Möglichkeit zum Gruppieren und Teilen von Konventionen über Teams oder Projekte hinweg. Sie können z. B. eine Klassenbibliothek mit einer gemeinsamen Konvention haben, die von allen Projekten Ihrer Organisation 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 einer unserer Zeichenfolgeneigenschaften hinzufügen, was bedeutet, dass die Spalte in der Datenbank anstelle von nvarchar als varchar gespeichert wird.

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

Während die oben genannte Konvention zum Definieren von benutzerdefinierten Attributen funktioniert, gibt es eine weitere API, die viel einfacher zu verwenden ist, insbesondere, wenn Sie Eigenschaften aus der Attributklasse verwenden möchten.

In diesem Beispiel aktualisieren wir unser Attribut und ändern es in ein IsUnicode-Attribut, sodass 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 Bool für unser Attribut festlegen, um der Konvention mitzuteilen, ob eine Eigenschaft Unicode sein soll. 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 eine prägnantere Möglichkeit, dies mithilfe der Having-Methode der Konventions-API zu erreichen. Die Having-Methode verfügt über einen Parameter vom Typ Func<PropertyInfo, T> , der die PropertyInfo akzeptiert, die mit der Where-Methode identisch ist, aber erwartet wird, dass ein Objekt zurückgegeben wird. Wenn das zurückgegebene Objekt null ist, wird die Eigenschaft nicht konfiguriert, d. h., Sie können Eigenschaften wie 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 logisch über etwas argumentieren müssen, das Sie beim Konfigurieren Ihrer Typen oder Eigenschaften filtern.

 

Konfigurieren von Typen

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

Eine der Konventionen auf Typebene kann wirklich nützlich sein, um die Benennungskonvention für Tabellen zu ändern, entweder um einem vorhandenen Schema zuzuordnen, das sich von der EF-Standardeinstellung unterscheidet, oder um eine neue Datenbank mit einer anderen Benennungskonvention zu erstellen. Dazu benötigen wir zunächst eine Methode, die typeInfo für einen Typ in unserem Modell akzeptiert und den Tabellennamen für diesen Typ zurückgeben soll:

    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 diese Methode haben, 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, um dem Tabellennamen zuzuordnen, 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.

Beachten Sie, dass EF beim Aufruf von ToTable die Zeichenfolge, die Sie bereitstellen, als exakten Tabellennamen verwendet und dabei keine der normalerweise vorgenommenen Pluralisierungen vornimmt. Deshalb lautet der Tabellenname gemäß unserer Konvention product_category statt product_categories. Wir können dies in unserer Konvention lösen, indem wir den Pluralisierungsdienst selbst aufrufen.

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

    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 Variante 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 verwendet. Wenn Sie "ToTable" für jeden Typ in einer Vererbungshierarchie aufrufen, indem Sie den Typnamen wie oben beschrieben als Namen der Tabelle übergeben, ändern Sie die Standardzuordnungsstrategie "Table-Per-Hierarchy(TPH)" in "Table-Per-Type "TPT". Die beste Möglichkeit, dies zu beschreiben, ist ein konkretes Beispiel:

    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 Mitarbeiter und Vorgesetzte der gleichen Tabelle (Mitarbeiter) in der Datenbank zugeordnet. Die Tabelle enthält sowohl Mitarbeiter als auch Vorgesetzte mit einer Diskriminatorspalte, in der Sie feststellen, 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, wird jeder Typ stattdessen einer eigenen Tabelle zugeordnet, auch als TPT bezeichnet, da jeder Typ über eine 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-Beispiel

Sie können dies vermeiden und die Standard-TPH-Zuordnung auf verschiedene Weise beibehalten:

  1. Aufrufen von "ToTable" mit demselben Tabellennamen für jeden Typ in der Hierarchie.
  2. Rufen Sie ToTable nur auf der Basisklasse der Hierarchie auf, in unserem Beispiel wäre das die Mitarbeiter.

 

Ausführungsreihenfolge

Konventionen funktionieren nach dem Prinzip 'Wer zuletzt kommt, gewinnt', genauso wie die Fluent-API. Dies bedeutet, dass wenn Sie zwei Konventionen schreiben, die dieselbe Option derselben Eigenschaft konfigurieren, dann gewinnt die letzte auszuführende. Im folgenden Beispiel ist im Code die maximale Länge aller Zeichenfolgen auf 500 festgelegt. Anschließend werden alle Eigenschaften im Modell, die "Name" genannt werden, so konfiguriert, dass sie eine maximale Länge von 250 haben.

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

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

Da die Konvention, die maximale Länge auf 250 festzulegen, nach der erfolgt, die alle Zeichenfolgen auf 500 setzt, haben alle Eigenschaften, die in unserem Modell "Name" genannt werden, eine `MaxLength` von 250, während alle anderen Zeichenfolgen, z. B. Beschreibungen, eine Länge von 500 haben. Die Verwendung von Konventionen bedeutet, dass Sie eine allgemeine Konvention für Typen oder Eigenschaften in Ihrem Modell bereitstellen und diese dann für Untermengen außer Kraft setzen können, die unterschiedlich sind.

Die Fluent-API und Datenanmerkungen können auch verwendet werden, um eine Konvention in bestimmten Fällen außer Kraft zu setzen. In unserem obigen Beispiel, wenn wir die Fluent-API zum Festlegen der maximalen Länge einer Eigenschaft verwendet haben, könnten wir sie vor oder nach der Konvention platzieren, da die spezifischere Fluent-API die allgemeinere Konfigurationskonvention gewinnen wird.

 

Eingebaute 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 Conventions-Auflistung für den abgeleiteten DbContext verwenden. Der folgende Code würde die Konventionsklasse hinzufügen, die wir zuvor erstellt haben, damit sie vor der integrierten Schlüsselermittlungskonvention ausgeführt wird.

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

Dies wird am häufigsten verwendet, 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 ist ein Beispiel für das Entfernen der PluralizingTableNameConvention.

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