模型大量設定

當跨多個實體類型以相同方式設定層面時,下列技術可減少程式碼重複併合並邏輯。

請參閱完整的範例專案 ,其中包含下面顯示的程式碼片段。

OnModelCreating 中的大量設定

ModelBuilder 傳回的每個建立器物件都會 Model 公開 或 Metadata 屬性,以提供對組成模型之物件的低階存取。 特別是,有一些方法可讓您逐一查看模型中的特定物件,並將一般設定套用至它們。

在下列範例中,模型包含自訂實數值型別 Currency

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

    public decimal Amount { get; }

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

預設不會探索此類型的屬性,因為目前的 EF 提供者不知道如何將它對應至資料庫類型。 此程式碼片段 OnModelCreating 會將型 Currency 別的所有屬性加入,並將值轉換器設定為支援的型別 - 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))
    {
    }
}

中繼資料 API 的缺點

  • 與 Fluent API 不同 ,必須明確完成模型的每個修改。 例如,如果某些屬性是依照慣例設定為導覽, Currency 則必須先移除參考 CLR 屬性的導覽,再為其新增實體類型屬性。 #9117 將會改善此狀況。
  • 慣例會在每次變更之後執行。 如果您移除慣例所探索的導覽,則慣例會再次執行,並可以將其加回。 若要避免這種情況發生,您必須延遲慣例,直到在屬性加入 DelayConventions() 之後,呼叫 和稍後處置傳回的物件,或將 CLR 屬性標示為忽略。 AddIgnored
  • 此反復專案發生之後,可能會新增實體類型,且不會將設定套用至它們。 這通常可以藉由將此程式碼放在 的 OnModelCreating 結尾來防止,但如果您有兩組相互依存的組態,則可能無法一致地套用這些組態。

預先慣例組態

EF Core 允許指定 CLR 類型的對應組態指定一次;該組態接著會套用至模型中該類型的所有屬性,因為探索到這些屬性。 這稱為「慣例前模型組態」,因為它會在允許模型建置慣例執行之前設定模型的各個層面。 透過覆 ConfigureConventions 寫衍生自 DbContext 的型別,套用這類組態。

此範例示範如何將 類型 Currency 的所有屬性設定為具有值轉換器:

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

此範例示範如何在 類型 string 的所有屬性上設定一些 Facet:

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

注意

ConfigureConventions 呼叫中指定的型別可以是基底類型、介面或泛型型別定義。 所有相符的組態都會依最低特定順序套用:

  1. 介面
  2. 基底類型
  3. 泛型型別定義
  4. 不可為 Null 的實值型別
  5. 精確類型

重要

預先慣例組態相當於將相符物件新增至模型後立即套用的明確組態。 它會覆寫所有慣例和資料批註。 例如,使用上述組態,所有字串外鍵屬性都會以 1024 的非 Unicode MaxLength 建立,即使這不符合主體金鑰也一樣。

忽略類型

預先慣例設定也允許忽略類型,並防止慣例探索為實體類型或實體類型上的屬性:

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

預設類型對應

一般而言,只要您已為此類型的屬性指定值轉換器,EF 就能使用提供者不支援的類型常數來轉譯查詢。 不過,在未涉及此類型任何屬性的查詢中,EF 無法尋找正確的值轉換器。 在此情況下,可以呼叫 DefaultTypeMapping 來新增或覆寫提供者類型對應:

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

預先慣例組態的限制

  • 此方法無法設定許多層面。 #6787 會將此擴充至更多類型。
  • 目前組態只能由 CLR 類型決定。 #20418 會允許自訂述詞。
  • 此組態會在建立模型之前執行。 如果套用它時發生任何衝突,例外狀況堆疊追蹤將不會包含 ConfigureConventions 方法,因此可能更難找到原因。

慣例

注意

EF Core 7.0 中引進了自訂模型建置慣例。

EF Core 模型建置慣例是類別,其中包含根據建立模型時對模型所做的變更所觸發的邏輯。 這會讓模型保持最新狀態,因為已進行明確設定、套用對應屬性,以及執行其他慣例。 為了參與此作業,每個慣例都會實作一或多個介面,以判斷何時觸發對應的方法。 例如,每當將新的實體類型加入模型時,就會觸發實作的慣例 IEntityTypeAddedConvention 。 同樣地,每當將索引鍵或外鍵新增至模型時,都會觸發實作 和 的 IKeyAddedConvention 慣例 IForeignKeyAddedConvention

模型建置慣例是控制模型組態的強大方式,但可能很複雜且難以正確。 在許多情況下, 可以使用預先慣例模型組態 ,輕鬆地指定屬性和類型的一般組態。

新增慣例

範例:限制歧視性屬性的長度

每個 階層的資料表繼承對應策略 需要一個歧視性資料行來指定任何指定資料列中所代表的類型。 根據預設,EF 會針對歧視性使用未系結的字串資料行,以確保它會在任何歧視性長度上運作。 不過,限制歧視性字串的最大長度,可讓儲存和查詢更有效率。 讓我們建立會執行該作業的新慣例。

EF Core 模型建置慣例會根據正在建置的模型所做的變更來觸發。 這會讓模型保持最新狀態,因為已進行明確設定、套用對應屬性,以及執行其他慣例。 為了參與這項作業,每個慣例都會實作一或多個介面,以判斷何時觸發慣例。 例如,每當將新的實體類型加入模型時,就會觸發實作的慣例 IEntityTypeAddedConvention 。 同樣地,每當將索引鍵或外鍵新增至模型時,都會觸發實作 和 的 IKeyAddedConvention 慣例 IForeignKeyAddedConvention

知道要實作的介面可能很棘手,因為某個時間點對模型所做的設定可能會變更或移除。 例如,金鑰可能依慣例建立,但稍後會在明確設定不同的金鑰時加以取代。

讓我們先嘗試實作歧視性長度慣例,讓這更具體一點:

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);
        }
    }
}

此慣例會實作 IEntityTypeBaseTypeChangedConvention ,這表示每當實體類型的對應繼承階層變更時,就會觸發它。 然後,慣例會尋找並設定階層的字串歧視性屬性。

接著,在 中 ConfigureConventions 呼叫 Add ,即可使用此慣例:

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

注意

方法不直接新增慣例的實例, Add 而是接受處理站來建立慣例的實例。 這可讓慣例使用 EF Core 內部服務提供者的相依性。 由於此慣例沒有相依性,所以服務提供者參數會命名為 _ ,表示永遠不會使用。

建置模型並查看 Post 實體類型會顯示已正常運作 - 歧視性屬性現在已設定為最大長度為 24:

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

但是,如果我們現在明確設定不同的歧視性屬性,會發生什麼事? 例如:

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

查看 模型的偵錯檢視 ,我們發現不再設定歧視性長度。

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

這是因為我們稍後在慣例中設定的歧視性屬性會在新增自訂歧視性時移除。 我們可以嘗試藉由在慣例上實作另一個介面來回應歧視性變更來修正此問題,但找出要實作的介面並不容易。

幸運的是,有一個更簡單的方法。 很多時候,只要最終模型正確,模型在建置時看起來就無關緊要。 此外,我們想要套用的設定通常不需要觸發其他慣例來做出反應。 因此,我們的慣例可以實作 IModelFinalizingConvention模型完成慣例 會在所有其他模型建置完成之後執行,因此可以存取模型的接近最終狀態。 這與回應每個模型變更的互動式慣例 相反 ,並確保模型在方法執行的任何時間點 OnModelCreating 都是最新的。 模型完成慣例通常會逐一查看整個模型,以設定模型元素。 因此,在此案例中,我們會在模型中尋找每個歧視性,並加以設定:

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);
            }
        }
    }
}

使用這個新慣例建置模型之後,我們發現即使已自訂,仍可正確設定歧視性長度:

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

我們可以更進一步,並將最大長度設定為最長的歧視性值的長度:

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);
            }
        }
    }
}

現在,歧視性資料行最大長度是 8,也就是 「Featured」 的長度,這是使用中最長的歧視性值。

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

範例:所有字串屬性的預設長度

讓我們看看另一個範例,其中可以使用完成慣例 - 設定 任何 字串屬性的預設最大長度。 慣例看起來與上一個範例相當類似:

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);
        }
    }
}

這個慣例很簡單。 它會尋找模型中的每個字串屬性,並將其最大長度設定為 512。 查看 的偵 Post 錯檢視,我們看到所有字串屬性現在的長度上限為 512。

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)

注意

同樣可以透過預先慣例組態來完成,但使用慣例可進一步篩選適用的屬性,以及讓 資料批註覆寫設定

最後,在我們離開此範例之前,如果我們 MaxStringLengthConvention 同時使用 和 DiscriminatorLengthConvention3 ,會發生什麼情況? 答案是,這取決於新增的順序,因為模型完成慣例會依照新增的循序執行。 因此,如果 MaxStringLengthConvention 最後加入,則會執行最後一個,並將歧視性屬性的最大長度設定為 512。 因此,在此情況下,最好 DiscriminatorLengthConvention3 新增 last,以便只覆寫歧視性屬性的預設最大長度,同時將所有其他字串屬性保留為 512。

取代現有的慣例

有時候,我們不想完全移除現有的慣例,而是想要將它取代為基本上相同但行為變更的慣例。 這非常有用,因為現有的慣例已經實作它需要適當觸發的介面。

範例:加入宣告屬性對應

EF Core 會依慣例對應所有公用讀寫屬性。 這可能不適用於您定義實體類型的方式。 若要變更這項功能,我們可以將 取代 PropertyDiscoveryConvention 為不會對應任何屬性的自有實作,除非它在 中 OnModelCreating 明確對應或標示為名為 Persist 的新屬性:

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

以下是新的慣例:

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;
            }
        }
    }
}

提示

取代內建慣例時,新的慣例實作應該繼承自現有的慣例類別。 請注意,某些慣例具有關系型或提供者特定的實作,在此情況下,新的慣例實作應該繼承自使用中資料庫提供者最特定的現有慣例類別。

接著會使用 Replace 中的 ConfigureConventions 方法註冊慣例:

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

提示

這是現有慣例具有相依性,由相依性物件表示的 ProviderConventionSetBuilderDependencies 案例。 這些是使用 GetRequiredService 從內部服務提供者取得,並傳遞至慣例建構函式。

請注意,此慣例允許對應欄位(除了屬性之外),只要欄位標示為 [Persist] 即可。 這表示我們可以使用私用欄位作為模型中的隱藏金鑰。

例如,請考慮下列實體類型:

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; }
}

從這些實體類型建置的模型如下:

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

一般而言, IsClean 會進行對應,但由於未標示 [Persist] 為 ,因此現在會被視為未對應的屬性。

提示

此慣例無法實作為模型完成慣例,因為現有的模型完成慣例需要在屬性對應之後執行,才能進一步設定它。

慣例實作考慮

EF Core 會持續追蹤每個組態的製作方式。 這會以 ConfigurationSource 列舉表示。 不同類型的組態包括:

  • Explicit:已在 中明確設定模型專案 OnModelCreating
  • DataAnnotation:模型專案是使用 CLR 類型上的對應屬性 (也稱為資料批註) 來設定
  • Convention:模型專案是由模型建置慣例所設定

慣例不應該覆寫標示為 DataAnnotation 或 的 Explicit 組態。 這可藉由使用 慣例產生器 來達成,例如 IConventionPropertyBuilder ,從 屬性取得的 Builder 。 例如:

property.Builder.HasMaxLength(512);

在慣例產生器上呼叫 HasMaxLength 時,只有在對應屬性或 中 OnModelCreating 尚未設定它時,才會設定最大長度

這類產生器方法也有第二個參數: fromDataAnnotation 。 如果慣例代表對應屬性進行組態,請將此值 true 設定為 。 例如:

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

這會將 設定 ConfigurationSourceDataAnnotation ,這表示值現在可以透過明確對應來 OnModelCreating 覆寫,但不能透過非對應屬性慣例來覆寫。

如果無法覆寫目前的組態,則方法會傳回 null ,如果您需要執行進一步的設定,則必須考慮此設定:

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

請注意,如果無法覆寫 Unicode 組態,仍會設定最大長度。 如果您只有在兩個呼叫都成功時才需要設定 Facet,則您可以藉由呼叫 CanSetMaxLengthCanSetIsUnicode 來預先檢查:

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);
            }
        }
    }
}

我們在這裡可以確定 對 HasMaxLength 的呼叫不會傳回 null 。 仍建議使用從 HasMaxLength 傳回的產生器實例,因為它可能與 不同 propertyBuilder

注意

在慣例進行變更之後,不會立即觸發其他慣例,這些慣例會延遲到所有慣例都完成處理目前的變更為止。

IConventionCoNtext

所有慣例方法也有 參數 IConventionContext<TMetadata> 。 它提供在某些特定案例中可能很有用的方法。

範例:NotMappedAttribute 慣例

此慣例會尋找 NotMappedAttribute 新增至模型的類型,並嘗試從模型中移除該實體類型。 但是,如果實體類型已從模型中移除,則實作的任何其他慣例 ProcessEntityTypeAdded 不再需要執行。 呼叫 即可完成這項作業 StopProcessing()

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();
    }
}

IConventionModel

傳遞至慣例的每個產生器物件都會 Metadata 公開屬性,該屬性會提供對組成模型之物件的低階存取。 特別是,有一些方法可讓您逐一查看模型中的特定物件,並將萬用群組態套用至它們,如範例:所有字串屬性 的預設長度所示 。 此 API 類似于 IMutableModel 大量 設定中 所示。

警告

建議您一律在公開為 Builder 屬性的產生器上呼叫方法來執行設定,因為產生器會檢查指定的組態是否會覆寫已使用 Fluent API 或資料批註指定的專案。

何時使用每個方法進行大量設定

在下列情況下使用 中繼資料 API

  • 設定必須在某個時間套用,而不會對模型稍後的變更做出反應。
  • 模型建置速度非常重要。 中繼資料 API 的安全性檢查較少,因此可能會比其他方法快一些,不過使用 編譯的模型 會產生更佳的啟動時間。

在下列情況下使用 預先慣例模型組態

  • 適用性條件很簡單,因為它只取決於類型。
  • 必須在模型中加入指定型別的屬性的任何時間點套用組態,並覆寫資料批註和慣例

在下列情況下使用 完成慣例

  • 適用性條件很複雜。
  • 組態不應該覆寫資料批註所指定的專案。

在下列情況下使用 互動式慣例

  • 多個慣例彼此相依。 完成慣例會依照新增慣例的循序執行,因此無法回應稍後完成慣例所做的變更。
  • 邏輯會在數個內容之間共用。 互動式慣例比其他方法更安全。