模型大量設定

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

請參閱包含以下程式碼片段 的完整範例專案

OnModel 中的大量設定建立

從 傳回的每個 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. 確切類型

重要

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

忽略類型

預先慣例組態也允許忽略類型,並防止其被慣例當做實體類型或實體類型的屬性來探索:

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

預設類型對應

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

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

預先慣例設定的限制

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

慣例

注意

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

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

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

新增慣例

範例:限制鑒別子屬性的長度

每個階層的資料表繼承對應策略需要一個辨別子資料行,以指定任何指定資料列中所代表的類型。 根據預設,EF 會針對鑒別子使用未系結的字串資料行,以確保其適用于任何鑒別子長度。 不過,限制鑒別子字串的最大長度,可能會產生更有效率的儲存和查詢。 讓我們建立會執行此動作的新慣例。

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

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

讓我們先嘗試實作辨別長度慣例,讓這更具體:

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 最後一個,以便只覆寫鑒別子屬性的預設長度上限,同時將所有其他字串屬性保留為 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:模型專案是由模型建置慣例所設定

慣例不應覆寫標示為 或 ExplicitDataAnnotation 組態。 這可藉由使用 慣例產生器來達成,例如 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 的安全性檢查較少,因此比其他方法更快,不過使用 編譯的模型 會產生更好的啟動時間。

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

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

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

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

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

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