Massenkonfiguration

Wenn ein Aspekt auf die gleiche Weise für mehrere Entitätstypen konfiguriert werden muss, können die folgenden Techniken Codeduplizierung reduzieren und die Logik konsolidieren.

Sehen Sie sich das vollständige Beispielprojekt an, das die unten dargestellten Codeausschnitte enthält.

Massenkonfiguration in ErstellungImModell

Jedes Generatorobjekt, das von ModelBuilder zurückgegeben wird, macht eine Model- oder Metadata-Eigenschaft verfügbar, die Zugriff auf die Objekte des Modells bietet. Insbesondere gibt es Methoden, mit denen Sie bestimmte Objekte im Modell durchlaufen und allgemeine Konfigurationen darauf anwenden können.

Im folgenden Beispiel enthält das Modell einen benutzerdefinierten Werttyp Currency:

public readonly struct Currency
{
    public Currency(decimal amount)
        => Amount = amount;

    public decimal Amount { get; }

    public override string ToString()
        => $"${Amount}";
}

Eigenschaften dieses Typs werden nicht standardmäßig ermittelt, da der aktuelle EF-Anbieter nicht weiß, wie sie einem Datenbanktyp zugeordnet werden. Dieser Codeausschnitt von OnModelCreating fügt alle Eigenschaften des Typs Currency hinzu und konfiguriert einen Wertkonverter zu einem unterstützten Typ decimal:

foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
    foreach (var propertyInfo in entityType.ClrType.GetProperties())
    {
        if (propertyInfo.PropertyType == typeof(Currency))
        {
            entityType.AddProperty(propertyInfo)
                .SetValueConverter(typeof(CurrencyConverter));
        }
    }
}
public class CurrencyConverter : ValueConverter<Currency, decimal>
{
    public CurrencyConverter()
        : base(
            v => v.Amount,
            v => new Currency(v))
    {
    }
}

Nachteile der Metadaten-API

  • Im Gegensatz zur Fluent-API muss jede Änderung des Modells explizit erfolgen. Wenn beispielsweise einige der Currency-Eigenschaften nach einer Konvention als Navigation konfiguriert wurden, müssen Sie zuerst die Navigation entfernen, die auf die CLR-Eigenschaft verweist, bevor Sie ihr einen Entitätstypeigenschaft hinzufügen. #9117 wird dies verbessern.
  • Die Konventionen werden nach jeder Änderung ausgeführt. Wenn Sie eine von einer Konvention ermittelte Navigation entfernen, wird die Konvention erneut ausgeführt und könnte sie wieder hinzufügen. Um dies zu verhindern, müssen Sie entweder die Konventionen verzögern, bis die Eigenschaft hinzugefügt wurde, indem Sie DelayConventions() aufrufen und später das zurückgegebene Objekt löschen oder die CLR-Eigenschaft mit AddIgnored als ignoriert kennzeichnen.
  • Entitätstypen können nach dieser Iteration hinzugefügt werden, aber die Konfiguration wird nicht auf sie angewendet. Dies kann in der Regel verhindert werden, indem sie diesen Code am Ende von OnModelCreating platzieren, aber wenn Sie zwei zusammenhängende Sätze von Konfigurationen haben, gibt es möglicherweise keine Reihenfolge, mit der sie konsistent angewendet werden können.

Konfiguration der Präkonvention

EF Core ermöglicht die einmalige Angabe der Zuordnungskonfiguration für einen bestimmten CLR-Typ; diese Konfiguration wird dann auf alle Eigenschaften dieses Typs im Modell angewendet, sobald sie ermittelt werden. Dies wird als „Konfiguration des Präkonventionsmodells“ bezeichnet, da Aspekte des Modells konfiguriert werden, die dann von den Modellerstellungskonventionen verwendet werden. Diese Konfiguration wird durch Außerkraftsetzung von ConfigureConventions auf den Typ angewendet, der aus DbContext abgeleitet wurde.

In diesem Beispiel wird gezeigt, wie alle Eigenschaften des Typs Currency so konfiguriert werden, dass sie einen Wertkonverter verwenden:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder
        .Properties<Currency>()
        .HaveConversion<CurrencyConverter>();
}

In diesem Beispiel wird gezeigt, wie einige Facetten für alle Eigenschaften des Typs string konfiguriert werden:

configurationBuilder
    .Properties<string>()
    .AreUnicode(false)
    .HaveMaxLength(1024);

Hinweis

Der in einem Aufruf ConfigureConventions angegebene Typ kann ein Basistyp, eine Schnittstelle oder eine generische Typdefinition sein. Alle übereinstimmenden Konfigurationen werden in der Reihenfolge beginnend mit der am wenigsten spezifischen angewendet:

  1. Schnittstelle
  2. Basistyp
  3. Generische Typdefinition
  4. Nicht auf NULL festlegbarer Werttyp
  5. Exakter Typ

Wichtig

Die Konfiguration der Präkonvention entspricht der expliziten Konfiguration, die angewendet wird, sobald dem Modell ein übereinstimmendes Objekt hinzugefügt wird. Sie überschreibt alle Konventionen und Datenanmerkungen. Mit der oben genannten Konfiguration werden beispielsweise alle Zeichenfolgen-Fremdschlüsseleigenschaften als Nicht-Unicode mit MaxLength von 1024 erstellt, auch wenn dies nicht mit dem Prinzipalschlüssel übereinstimmt.

Ignorieren von Typen

Die Konfiguration der Präkonvention ermöglicht es auch, einen Typ zu ignorieren und zu verhindern, dass er von Konventionen entweder als Entitätstyp oder als Eigenschaft für einen Entitätstyp erkannt wird:

configurationBuilder
    .IgnoreAny(typeof(IList<>));

Zuordnung von Standardtypen

Im Allgemeinen kann EF Abfragen mit Konstanten eines Typs übersetzen, die vom Anbieter nicht unterstützt werden, solange Sie einen Wertkonverter für eine Eigenschaft dieses Typs angegeben haben. In Abfragen, die keine Eigenschaften dieses Typs umfassen, gibt es jedoch keine Möglichkeit, dass EF den richtigen Wertkonverter finden kann. In diesem Fall ist es möglich DefaultTypeMapping aufzurufen, um eine Anbietertypzuordnung hinzuzufügen oder außer Kraft zu setzen:

configurationBuilder
    .DefaultTypeMapping<Currency>()
    .HasConversion<CurrencyConverter>();

Einschränkungen der Konfiguration der Präkonvention

  • Viele Aspekte können mit diesem Ansatz nicht konfiguriert werden. #6787 erweitert sie auf weitere Typen.
  • Derzeit wird die Konfiguration nur vom CLR-Typ bestimmt. #20418 würde benutzerdefinierte Prädikate zulassen.
  • Diese Konfiguration wird ausgeführt, bevor ein Modell erstellt wird. Wenn es Konflikte gibt, die beim Anwenden auftreten, enthält die Ausnahmestapelablaufverfolgung nicht die Methode ConfigureConventions, sodass es möglicherweise schwieriger ist, die Ursache zu finden.

Konventionen

Hinweis

Benutzerdefinierte Modellbaukonventionen wurden in EF Core 7.0 eingeführt.

EF Core-Modellbaukonventionen sind Klassen, die Logik enthalten, die basierend auf Änderungen am Modell ausgelöst wird, während sie erstellt werden. Dadurch bleibt das Modell auf dem neuesten Stand, da eine explizite Konfiguration vorgenommen wird, Zuordnungsattribute angewendet und andere Konventionen ausgeführt werden. Um daran teilzunehmen, implementiert jede Konvention eine oder mehrere Schnittstellen, die bestimmen, wann die Konvention ausgelöst wird. Eine Konvention, die implementiert wird, wird beispielsweise ausgelöst, wenn dem Modell ein neuer Entitätstyp IEntityTypeAddedConvention hinzugefügt wird. Ebenso wird eine Konvention, die sowohl IForeignKeyAddedConvention also auch IKeyAddedConvention implementiert, ausgelöst, wenn dem Modell entweder ein Schlüssel oder ein Fremdschlüssel hinzugefügt wird.

Modellbaukonventionen sind eine leistungsfähige Möglichkeit, die Modellkonfiguration zu steuern, aber sie können komplex und schwer zu erreichen sein. In vielen Fällen kann stattdessen die vorhandene Konfiguration des Präkonventionsmodells verwendet werden, um einfach eine gemeinsame Konfiguration für Eigenschaften und Typen anzugeben.

Hinzufügen einer neuen Konvention

Beispiel: Einschränken der Länge von Diskriminatoreigenschaften

Für die TPH-Vererbungszuordnungsstrategie pro Hierarchie ist eine Diskriminatorspalte erforderlich, um anzugeben, welcher Typ in einer Zeile dargestellt wird. Standardmäßig verwendet EF eine ungebundene Zeichenfolgenspalte für den Diskriminator, wodurch sichergestellt wird, dass sie für jede Diskriminatorlänge funktioniert. Das Einschränken der maximalen Länge von Diskriminatorzeichenfolgen kann jedoch eine effizientere Speicherung und Abfragen ermöglichen. Lassen Sie uns eine neue Konvention erstellen, die dies tut.

EF Core-Modellbaukonventionen werden basierend auf Änderungen am Modell während der Erstellung ausgelöst. Dadurch bleibt das Modell auf dem neuesten Stand, da eine explizite Konfiguration vorgenommen wird, Zuordnungsattribute angewendet und andere Konventionen ausgeführt werden. Um daran teilzunehmen, implementiert jede Konvention eine oder mehrere Schnittstellen, die bestimmen, wann die Konvention ausgelöst wird. Eine Konvention, die implementiert wird, wird beispielsweise ausgelöst, wenn dem Modell ein neuer Entitätstyp IEntityTypeAddedConvention hinzugefügt wird. Ebenso wird eine Konvention, die sowohl IForeignKeyAddedConvention also auch IKeyAddedConvention implementiert, ausgelöst, wenn dem Modell entweder ein Schlüssel oder ein Fremdschlüssel hinzugefügt wird.

Zu Wissen, welche Schnittstellen implementiert werden sollen, kann schwierig sein, da die Konfiguration, die an einem Punkt an dem Modell vorgenommen wurde, zu einem späteren Zeitpunkt geändert oder entfernt werden kann. Beispielsweise kann ein Schlüssel durch Konvention erstellt, aber später ersetzt werden, wenn ein anderer Schlüssel explizit konfiguriert wird.

Lassen Sie uns dies etwas konkreter machen, indem wir zunächst versuchen, die Diskriminatorlängen-Konvention zu implementieren:

public class DiscriminatorLengthConvention1 : IEntityTypeBaseTypeChangedConvention
{
    public void ProcessEntityTypeBaseTypeChanged(
        IConventionEntityTypeBuilder entityTypeBuilder,
        IConventionEntityType? newBaseType,
        IConventionEntityType? oldBaseType,
        IConventionContext<IConventionEntityType> context)
    {
        var discriminatorProperty = entityTypeBuilder.Metadata.FindDiscriminatorProperty();
        if (discriminatorProperty != null
            && discriminatorProperty.ClrType == typeof(string))
        {
            discriminatorProperty.Builder.HasMaxLength(24);
        }
    }
}

Diese Konvention implementiert IEntityTypeBaseTypeChangedConvention, was bedeutet, dass sie ausgelöst wird, wenn die zugeordnete Vererbungshierarchie für einen Entitätstyp geändert wird. Die Konvention sucht und konfiguriert dann die Zeichenfolgendiskriminatoreigenschaft für die Hierarchie.

Diese Konvention wird dann durch Aufrufen von Add in ConfigureConventions verwendet:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Conventions.Add(_ =>  new DiscriminatorLengthConvention1());
}

Hinweis

Anstatt eine Instanz der Konvention direkt hinzuzufügen, akzeptiert die Methode Add eine Factory zum Erstellen von Instanzen der Konvention. Dadurch kann die Konvention Abhängigkeiten vom internen EF Core-Dienstanbieter verwenden. Da diese Konvention keine Abhängigkeiten aufweist, wird der Dienstanbieterparameter _ genannt, um anzugeben, dass er nie verwendet wird.

Wenn Sie das Modell erstellen und den Entitätstyp Post betrachten, wird gezeigt, dass dies funktioniert hat – die Diskriminatoreigenschaft ist jetzt mit einer maximalen Länge von 24 konfiguriert:

 Discriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(24)

Aber was geschieht, wenn wir jetzt explizit eine andere Diskriminatoreigenschaft konfigurieren? Beispiel:

modelBuilder.Entity<Post>()
    .HasDiscriminator<string>("PostTypeDiscriminator")
    .HasValue<Post>("Post")
    .HasValue<FeaturedPost>("Featured");

In der Debugansicht des Modells stellen wir fest, dass die Diskriminatorlänge nicht mehr konfiguriert ist.

 PostTypeDiscriminator (no field, string) Shadow Required AfterSave:Throw

Dies liegt daran, dass die Diskriminatoreigenschaft, die wir in unserer Konvention konfiguriert haben, später entfernt wurde, als der benutzerdefinierte Diskriminator hinzugefügt wurde. Wir könnten versuchen, dies zu beheben, indem wir eine andere Schnittstelle in unserer Konvention implementieren, um auf die Diskriminatoränderungen zu reagieren, aber herauszufinden, welche Schnittstelle implementiert werden soll, ist nicht einfach.

Glücklicherweise gibt es einen einfacheren Ansatz. Oft spielt es keine Rolle, wie das Modell aussieht, während es erstellt wird, solange das endgültige Modell korrekt ist. Darüber hinaus muss die Konfiguration, die wir anwenden möchten, häufig keine anderen Konventionen auslösen, um zu reagieren. Daher kann unsere Konvention IModelFinalizingConvention umsetzen. Die Abschlusskonventionen des Modells werden ausgeführt, nachdem alle anderen Modellerstellungskonventionen abgeschlossen sind und somit Zugriff auf den endgültigen Zustand des Modells haben. Dies steht im Gegensatz zu interaktiven Konventionen, die auf jede Modelländerung reagieren und sicherstellen, dass das Modell an jedem Punkt der Methodenausführung OnModelCreating auf dem neuesten Stand ist. Eine Abschlusskonvention für ein Modell durchläuft in der Regel das gesamte Modell und konfiguriert währenddessen Modellelemente. In diesem Fall finden wir also jeden Diskriminator im Modell und konfigurieren ihn:

public class DiscriminatorLengthConvention2 : IModelFinalizingConvention
{
    public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
    {
        foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()
                     .Where(entityType => entityType.BaseType == null))
        {
            var discriminatorProperty = entityType.FindDiscriminatorProperty();
            if (discriminatorProperty != null
                && discriminatorProperty.ClrType == typeof(string))
            {
                discriminatorProperty.Builder.HasMaxLength(24);
            }
        }
    }
}

Nach dem Erstellen des Modells mit dieser neuen Konvention stellen wir fest, dass die Diskriminatorlänge jetzt richtig konfiguriert ist, obwohl sie angepasst wurde:

PostTypeDiscriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(24)

Lassen Sie uns einfach einen Schritt weitergehen und die maximale Länge so konfigurieren, dass sie die Länge des längsten Diskriminatorwerts ist:

public class DiscriminatorLengthConvention3 : IModelFinalizingConvention
{
    public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
    {
        foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()
                     .Where(entityType => entityType.BaseType == null))
        {
            var discriminatorProperty = entityType.FindDiscriminatorProperty();
            if (discriminatorProperty != null
                && discriminatorProperty.ClrType == typeof(string))
            {
                var maxDiscriminatorValueLength =
                    entityType.GetDerivedTypesInclusive().Select(e => ((string)e.GetDiscriminatorValue()!).Length).Max();

                discriminatorProperty.Builder.HasMaxLength(maxDiscriminatorValueLength);
            }
        }
    }
}

Jetzt beträgt die maximale Länge der Diskriminatorspalte 8, also der Länge von "Featured", dem längsten verwendeten Diskriminatorwert.

PostTypeDiscriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(8)

Beispiel: Standardlänge für alle Zeichenfolgeneigenschaften

Sehen wir uns ein weiteres Beispiel an, in dem eine abschließende Konvention verwendet werden kann: Festlegen einer Standardlänge für eine beliebige Zeichenfolgeneigenschaft. Die Konvention sieht dem vorherigen Beispiel ziemlich ähnlich:

public class MaxStringLengthConvention : IModelFinalizingConvention
{
    public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
    {
        foreach (var property in modelBuilder.Metadata.GetEntityTypes()
                     .SelectMany(
                         entityType => entityType.GetDeclaredProperties()
                             .Where(
                                 property => property.ClrType == typeof(string))))
        {
            property.Builder.HasMaxLength(512);
        }
    }
}

Diese Konvention ist ziemlich einfach. Es findet jede Zeichenfolgeneigenschaft im Modell und legt die maximale Länge auf 512 fest. Wenn Sie in der Debugansicht nach den Eigenschaften von Post suchen, sehen wir, dass alle Zeichenfolgeneigenschaften jetzt eine maximale Länge von 512 haben.

EntityType: Post
  Properties:
    Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
    AuthorId (no field, int?) Shadow FK Index
    BlogId (no field, int) Shadow Required FK Index
    Content (string) Required MaxLength(512)
    Discriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(512)
    PublishedOn (DateTime) Required
    Title (string) Required MaxLength(512)

Hinweis

Dasselbe kann durch die Konfiguration der Präkonvention erreicht werden, aber die Verwendung einer Konvention ermöglicht es, die anwendbaren Eigenschaften weiter zu filtern und für Datenanmerkungen die Konfiguration außer Kraft zu setzen.

Was geschieht schließlich, bevor wir dieses Beispiel verlassen, wenn wir sowohl MaxStringLengthConvention als auch DiscriminatorLengthConvention3 gleichzeitig verwenden? Die Antwort lautet, dass es von der Reihenfolge abhängt, in der sie hinzugefügt werden, da modellbasierte Konventionen in der Reihenfolge ausgeführt werden, in der sie hinzugefügt werden. Wenn MaxStringLengthConvention also zuletzt hinzugefügt wird, wird es zuletzt ausgeführt, und legt die maximale Länge der Diskriminatoreigenschaft auf 512 fest. Daher ist es in diesem Fall besser, DiscriminatorLengthConvention3 als letztes hinzuzufügen, damit sie die Standardlänge für nur Diskriminatoreigenschaften außer Kraft setzen kann, während alle anderen Zeichenfolgeneigenschaften als 512 verbleiben.

Entfernen einer vorhandenen Konvention

Manchmal wollen wir anstatt eine vorhandene Konvention vollständig zu entfernen sie stattdessen durch eine Konvention ersetzen, die im Grunde dasselbe tut, aber mit einem geänderten Verhalten. Dies ist nützlich, da die vorhandene Konvention bereits die benötigten Schnittstellen implementiert, um entsprechend ausgelöst zu werden.

Beispiel: Opt-In-Eigenschaftszuordnung

EF Core ordnet alle öffentlichen Lese-/Schreibeigenschaften nach Konvention zu. Dies eignet sich möglicherweise nicht für die Art und Weise, wie die Entitätstypen definiert sind. Um dies zu ändern, können wir die PropertyDiscoveryConvention mit unserer eigenen Implementierung ersetzen, die keine Eigenschaft zuordnet, es sei denn, sie wird explizit in OnModelCreating einem neuen Attribut zugeordnet oder mit einem neuen Attribut namens Persist gekennzeichnet:

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public sealed class PersistAttribute : Attribute
{
}

Hier ist die neue Konvention:

public class AttributeBasedPropertyDiscoveryConvention : PropertyDiscoveryConvention
{
    public AttributeBasedPropertyDiscoveryConvention(ProviderConventionSetBuilderDependencies dependencies)
        : base(dependencies)
    {
    }

    public override void ProcessEntityTypeAdded(
        IConventionEntityTypeBuilder entityTypeBuilder,
        IConventionContext<IConventionEntityTypeBuilder> context)
        => Process(entityTypeBuilder);

    public override void ProcessEntityTypeBaseTypeChanged(
        IConventionEntityTypeBuilder entityTypeBuilder,
        IConventionEntityType? newBaseType,
        IConventionEntityType? oldBaseType,
        IConventionContext<IConventionEntityType> context)
    {
        if ((newBaseType == null
             || oldBaseType != null)
            && entityTypeBuilder.Metadata.BaseType == newBaseType)
        {
            Process(entityTypeBuilder);
        }
    }

    private void Process(IConventionEntityTypeBuilder entityTypeBuilder)
    {
        foreach (var memberInfo in GetRuntimeMembers())
        {
            if (Attribute.IsDefined(memberInfo, typeof(PersistAttribute), inherit: true))
            {
                entityTypeBuilder.Property(memberInfo);
            }
            else if (memberInfo is PropertyInfo propertyInfo
                     && Dependencies.TypeMappingSource.FindMapping(propertyInfo) != null)
            {
                entityTypeBuilder.Ignore(propertyInfo.Name);
            }
        }

        IEnumerable<MemberInfo> GetRuntimeMembers()
        {
            var clrType = entityTypeBuilder.Metadata.ClrType;

            foreach (var property in clrType.GetRuntimeProperties()
                         .Where(p => p.GetMethod != null && !p.GetMethod.IsStatic))
            {
                yield return property;
            }

            foreach (var property in clrType.GetRuntimeFields())
            {
                yield return property;
            }
        }
    }
}

Tipp

Beim Ersetzen einer integrierten Konvention sollte die neue Konventionsimplementierung von der vorhandenen Konventionsklasse erben. Beachten Sie, dass einige Konventionen relationale oder anbieterspezifische Implementierungen aufweisen. In diesem Fall sollte die neue Konventionsimplementierung von der spezifischen vorhandenen Konventionsklasse des verwendeten Datenbankanbieters erben.

Die Konvention wird dann mit der Replace-Methode in ConfigureConventions verwendet:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Conventions.Replace<PropertyDiscoveryConvention>(
        serviceProvider => new AttributeBasedPropertyDiscoveryConvention(
            serviceProvider.GetRequiredService<ProviderConventionSetBuilderDependencies>()));
}

Tipp

Dies ist ein Fall, in dem die vorhandene Konvention Abhängigkeiten aufweist, die durch das Abhängigkeitsobjekt ProviderConventionSetBuilderDependencies dargestellt werden. Diese werden vom internen Dienstanbieter abgerufen mit GetRequiredService und an den Konventionskonstruktor übergeben.

Beachten Sie, dass diese Konvention die Zuordnung von Feldern (zusätzlich zu Eigenschaften) ermöglicht, solange sie mit [Persist] gekennzeichnet sind. Dies bedeutet, dass wir private Felder als ausgeblendete Schlüssel im Modell verwenden können.

Berücksichtigen Sie beispielsweise folgende Entitätstypen:

public class LaundryBasket
{
    [Persist]
    [Key]
    private readonly int _id;

    [Persist]
    public int TenantId { get; init; }

    public bool IsClean { get; set; }

    public List<Garment> Garments { get; } = new();
}

public class Garment
{
    public Garment(string name, string color)
    {
        Name = name;
        Color = color;
    }

    [Persist]
    [Key]
    private readonly int _id;

    [Persist]
    public int TenantId { get; init; }

    [Persist]
    public string Name { get; }

    [Persist]
    public string Color { get; }

    public bool IsClean { get; set; }

    public LaundryBasket? Basket { get; set; }
}

Das Modell, das aus diesen Entitätstypen erstellt wurde, ist:

Model:
  EntityType: Garment
    Properties:
      _id (_id, int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      Basket_id (no field, int?) Shadow FK Index
      Color (string) Required
      Name (string) Required
      TenantId (int) Required
    Navigations:
      Basket (LaundryBasket) ToPrincipal LaundryBasket Inverse: Garments
    Keys:
      _id PK
    Foreign keys:
      Garment {'Basket_id'} -> LaundryBasket {'_id'} ToDependent: Garments ToPrincipal: Basket ClientSetNull
    Indexes:
      Basket_id
  EntityType: LaundryBasket
    Properties:
      _id (_id, int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      TenantId (int) Required
    Navigations:
      Garments (List<Garment>) Collection ToDependent Garment Inverse: Basket
    Keys:
      _id PK

Normalerweise wäre IsClean zugeordnet worden, aber da sie nicht mit [Persist] markiert ist, wird sie jetzt als nicht zugeordnete Eigenschaft behandelt.

Tipp

Diese Konvention konnte nicht als Abschlusskonvention für ein Modell implementiert werden, da es vorhandene Konventionen zum Abschließen von Modellen gibt, die ausgeführt werden müssen, nachdem die Eigenschaft zugeordnet wurde, um sie weiter zu konfigurieren.

Überlegungen zur Implementierung von Konventionen

EF Core verfolgt nach, wie jede Konfiguration vorgenommen wird. Dies wird durch die ConfigurationSource-Enumeration dargestellt. Die verschiedenen Konfigurationsarten sind:

  • Explicit: Das Modellelement wurde explizit in OnModelCreating konfiguriert
  • DataAnnotation: Das Modellelement wurde mithilfe eines Zuordnungsattributs (auch Datenanmerkung genannt) für den CLR-Typ konfiguriert
  • Convention: Das Modellelement wurde durch eine Modellbaukonvention konfiguriert

Konventionen sollten niemals die Konfiguration überschreiben, die als DataAnnotation oder Explicit markiert ist. Dies wird mithilfe eines Konventionsgenerators erreicht, z. B. der IConventionPropertyBuilder, der aus der Eigenschaft Builder abgerufen wird. Beispiel:

property.Builder.HasMaxLength(512);

Durch Aufrufen von HasMaxLength des Konventionsgenerators wird nur die maximale Länge festgelegt, wenn sie noch nicht durch ein Zuordnungsattribut oder in OnModelCreating konfiguriert wurde.

Generatormethoden wie diese haben auch einen zweiten Parameter: fromDataAnnotation. Legen Sie diese Einstellung auf true fest, wenn die Konvention die Konfiguration im Auftrag eines Zuordnungsattributs vornimmt. Beispiel:

property.Builder.HasMaxLength(512, fromDataAnnotation: true);

Dadurch wird ConfigurationSource auf DataAnnotation festgelegt, was bedeutet, dass der Wert jetzt durch explizite Zuordnungen auf OnModelCreating überschrieben werden kann, aber nicht durch Nichtzuordnungsattributekonventionen.

Wenn die aktuelle Konfiguration nicht überschrieben werden kann, wird die Methode null zurückgegeben. Dies muss berücksichtigt werden, wenn Sie eine weitere Konfiguration durchführen müssen:

property.Builder.HasMaxLength(512)?.IsUnicode(false);

Beachten Sie, dass die maximale Länge weiterhin festgelegt wird, wenn die Unicode-Konfiguration nicht außer Kraft gesetzt werden kann. Wenn Sie die Facetten nur konfigurieren müssen, wenn beide Aufrufe erfolgreich ausgeführt werden, können Sie dies vorab durch Aufrufen von CanSetMaxLength und CanSetIsUnicode überprüfen:

public class MaxStringLengthNonUnicodeConvention : IModelFinalizingConvention
{
    public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
    {
        foreach (var property in modelBuilder.Metadata.GetEntityTypes()
                     .SelectMany(
                         entityType => entityType.GetDeclaredProperties()
                             .Where(
                                 property => property.ClrType == typeof(string))))
        {
            var propertyBuilder = property.Builder;
            if (propertyBuilder.CanSetMaxLength(512)
                && propertyBuilder.CanSetIsUnicode(false))
            {
                propertyBuilder.HasMaxLength(512)!.IsUnicode(false);
            }
        }
    }
}

Hier können wir sicher sein, dass der Aufruf von HasMaxLength nicht null zurückgegeben wird. Es wird weiterhin empfohlen, die Generator-Instanz zu verwenden, die von HasMaxLength zurückgegeben wird, da sie sich möglicherweise von propertyBuilder unterscheidet.

Hinweis

Andere Konventionen werden nicht sofort ausgelöst, nachdem eine Konvention eine Änderung vorgenommen hat. Sie werden verzögert, bis alle Konventionen die Verarbeitung der aktuellen Änderung abgeschlossen haben.

IKonventionsKontext

Alle Konventionsmethoden verfügen auch über einen Parameter IConventionContext<TMetadata>. Er stellt Methoden bereit, die in bestimmten Fällen nützlich sein könnten.

Beispiel: NichtZugeordnetesAttribut-Konvention

Diese Konvention sucht nach NotMappedAttribute in einem Typ, der dem Modell hinzugefügt wird, und versucht, diesen Entitätstyp aus dem Modell zu entfernen. Wenn der Entitätstyp jedoch aus dem Modell entfernt wird, müssen alle anderen Konventionen, die ProcessEntityTypeAdded implementieren, nicht mehr ausgeführt werden. Dies kann durch Aufrufen on StopProcessing() erreicht werden:

public virtual void ProcessEntityTypeAdded(
    IConventionEntityTypeBuilder entityTypeBuilder,
    IConventionContext<IConventionEntityTypeBuilder> context)
{
    var type = entityTypeBuilder.Metadata.ClrType;
    if (!Attribute.IsDefined(type, typeof(NotMappedAttribute), inherit: true))
    {
        return;
    }

    if (entityTypeBuilder.ModelBuilder.Ignore(entityTypeBuilder.Metadata.Name, fromDataAnnotation: true) != null)
    {
        context.StopProcessing();
    }
}

IKonventionsModell

Jedes an die Konvention übergebene Generatorobjekt macht eine Metadata-Eigenschaft verfügbar, die einen Zugriff auf die Objekte bietet, die das Modell umfassen. Insbesondere gibt es Methoden, mit denen Sie bestimmte Objekte im Modell durchlaufen und allgemeine Konfigurationen anwenden können, wie im Beispiel: Standardlänge für alle Zeichenfolgeneigenschaftendargestellt. Diese API ähnelt IMutableModel aus der Massenkonfiguration.

Achtung

Es wird empfohlen, immer eine Konfiguration durchzuführen, indem Methoden für den Generator aufgerufen werden, die als Eigenschaft Builder verfügbar sind, da die Generatoren überprüfen, ob die angegebene Konfiguration etwas außer Kraft setzen würde, das bereits mithilfe der Fluent-API oder Datenanmerkungen angegeben wurde.

Gründe für die Verwendung der einzelnen Methoden für die Massenkonfiguration

Verwenden Sie die Metadaten-API in folgenden Fällen:

  • Die Konfiguration muss zu einem bestimmten Zeitpunkt angewendet werden und nicht auf spätere Änderungen im Modell reagieren.
  • Die Modellbaugeschwindigkeit ist sehr wichtig. Die Metadaten-API verfügt über weniger Sicherheitsüberprüfungen und kann daher etwas schneller sein als andere Ansätze, aber die Verwendung eines kompilierten Modells würde zu noch besseren Startzeiten führen.

Verwenden Sie die Konfiguration des Präkonventionenmodells in folgenden Fällen:

  • Die Anwendbarkeitsbedingung einfach ist, da sie nur vom Typ abhängt.
  • Die Konfiguration muss an jedem Punkt angewendet werden, an dem eine Eigenschaft des angegebenen Typs im Modell hinzugefügt und Datenanmerkungen und Konventionen außer Kraft gesetzt wird.

Verwenden Sie abschließende Konventionen in den folgenden Fällen:

  • Die Anwendbarkeitsbedingung ist komplex.
  • Die Konfiguration sollte nicht überschreiben, was durch Datenanmerkungen angegeben wird.

Verwenden Sie interaktive Konventionen in den folgenden Fällen:

  • Mehrere Konventionen hängen voneinander ab. Die Abschlusskonventionen werden in der Reihenfolge ausgeführt, in der sie hinzugefügt wurden, und können daher nicht auf Änderungen reagieren, die durch spätere Abschlusskonventionen vorgenommen wurden.
  • Die Logik wird von mehreren Kontexten gemeinsam verwendet. Interaktive Konventionen sind sicherer als andere Ansätze.