當跨多個實體類型以相同方式設定層面時,下列技術可減少程式代碼重複並合併邏輯。
請參閱 完整的範例專案 ,其中包含下面顯示的代碼段。
在 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()並稍後處置傳回的物件,才能將屬性加入;或是使用AddIgnored將CLR屬性標示為忽略。
- 此反覆項目發生之後,可能會新增實體類型,且不會將設定套用至它們。 這通常可以透過將此程式代碼放在
OnModelCreating
的結尾來防止,但如果您有兩組相互依存的組態,則可能無法設定一個順序來一致地套用這些組態。
預先會議組態
EF Core 允許指定一次對應組態來處理特定的 CLR 類型; 一旦在模型中發現該類型的所有屬性,該組態就會套用至這些屬性。 這稱為「慣例前模型組態」,因為它會在允許模型建置慣例執行之前設定模型的各個層面。 透過覆寫 ConfigureConventions 套用到從 DbContext 衍生而來的類型。
此範例示範如何將 類型 Currency
的所有屬性設定為具有值轉換器:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder
.Properties<Currency>()
.HaveConversion<CurrencyConverter>();
}
此範例示範如何在類型為 string
的所有屬性上設定一些特徵:
configurationBuilder
.Properties<string>()
.AreUnicode(false)
.HaveMaxLength(1024);
備註
從 ConfigureConventions
呼叫中指定的型別可以是基底類型、介面或泛型型別定義。 所有相符的組態會依照從最低明確程度開始的順序套用:
- 介面
- 基底類型
- 泛型類型定義
- 不可為 Null 的實值類型
- 精確類型
這很重要
預先慣例組態相當於將相符物件新增至模型后立即套用的明確組態。 它會覆蓋所有慣例和資料註解。 例如,使用上述組態,所有字串外鍵屬性都會以 1024 的非 Unicode MaxLength
建立,即使這不符合主體密鑰也一樣。
忽略類型
預先慣例設定還允許忽略某種類型,並防止其被慣例識別為實體類型或實體類型中的屬性。
configurationBuilder
.IgnoreAny(typeof(IList<>));
預設類型對應
一般而言,只要您已為此類型的屬性指定值轉換器,EF 就能使用提供者不支援的類型常數來轉譯查詢。 不過,在未涉及此類型任何屬性的查詢中,EF 無法尋找正確的值轉換器。 在此情況下,可以呼叫 DefaultTypeMapping 來新增或覆寫提供者類型對應:
configurationBuilder
.DefaultTypeMapping<Currency>()
.HasConversion<CurrencyConverter>();
預先慣例組態的限制
- 此方法無法設定許多層面。 #6787 會將此擴充至更多類型。
- 目前組態只能由 CLR 類型決定。 #20418 會允許自定義述詞。
- 此組態會在建立模型之前執行。 如果套用它時發生任何衝突,例外狀況堆疊追蹤將不會包含
ConfigureConventions
方法,因此可能更難找到原因。
慣例
EF Core 模型建置慣例是類別,其中包含根據建立模型時對模型所做的變更所觸發的邏輯。 這會讓模型 up-to日期保持為明確設定、套用對應屬性,以及執行其他慣例。 為了參與此作業,每個慣例都會實作一或多個介面,以判斷何時觸發對應的方法。 例如,每當新的實體類型被加入到模型時,會觸發與 IEntityTypeAddedConvention 相關的實施慣例。 同樣地,當索引鍵或外鍵被新增至模型時,實作IForeignKeyAddedConvention和IKeyAddedConvention的慣例就會被觸發。
模型建置慣例是控制模型組態的強大方式,但可能很複雜且難以正確。 在許多情況下,可以使用 預先慣例模型組態 ,輕鬆地指定屬性和類型的一般組態。
新增慣例
範例:限制歧視性屬性的長度
表層次結構繼承映射策略 需要一個區分欄位來指定任何資料列所代表的類型。 根據預設,EF 會使用無限制的字串欄位來作為區分器,以確保其能在任何區分器長度下運作。 不過,限制歧視性字串的最大長度,可讓儲存和查詢更有效率。 讓我們建立會執行該作業的新慣例。
EF Core 模型建置慣例會根據模型在建置過程中所做的變更來觸發。 這會讓模型 up-to日期保持為明確設定、套用對應屬性,以及執行其他慣例。 為了參與這項作業,每個慣例都會實作一或多個介面,以判斷何時觸發慣例。 例如,每當新的實體類型被加入到模型時,會觸發與 IEntityTypeAddedConvention 相關的實施慣例。 同樣地,當索引鍵或外鍵被新增至模型時,實作IForeignKeyAddedConvention和IKeyAddedConvention的慣例就會被觸發。
知道要實作的介面可能很棘手,因為某個時間點對模型所做的設定可能會變更或移除。 例如,金鑰可能依慣例建立,但稍後會在明確設定不同的密鑰時加以取代。
讓我們透過嘗試第一次實作區別長度慣例,來讓這個概念更具體化:
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,這表示每當實體類型的對應繼承階層變更時,就會觸發它。 然後,慣例會尋找並設定階層的字串歧視性屬性。
接著,在 Add 中呼叫 ConfigureConventions
,即可使用此慣例。
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
up-to日期。 這個模型完成程序通常會遍歷整個模型,並在過程中設定模型元素。 因此,在此案例中,我們會在模型中尋找每個判別器,並進行配置。
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
:模型元素是由模型建置慣例所設定
慣例不應該覆寫標示為 DataAnnotation
或 Explicit
的組態。 這可藉由使用 慣例產生器來達成,例如 IConventionPropertyBuilder,從屬性取得的 Builder 。 例如:
property.Builder.HasMaxLength(512);
在慣例產生器上呼叫 HasMaxLength
時,只有在對應屬性或在OnModelCreating
中尚未設定最大長度時,才會設定它。
這類產生器方法也有第二個參數: fromDataAnnotation
。 如果慣例是在代表對應屬性進行配置時,請將此值設定為 true
。 例如:
property.Builder.HasMaxLength(512, fromDataAnnotation: true);
這會將 ConfigurationSource
設定為 DataAnnotation
,這表示現在可以透過在 OnModelCreating
上的明確映射來覆寫該值,但不能透過非映射屬性慣例來覆寫。
如果無法覆寫目前的組態,則方法會傳回 null
,如果您需要執行進一步的設定,則必須考慮此設定:
property.Builder.HasMaxLength(512)?.IsUnicode(false);
請注意,如果無法覆寫 Unicode 組態,仍會設定最大長度。 如果您需要僅在兩個呼叫都成功後才進行屬性設定,可以預先透過呼叫 CanSetMaxLength 和 CanSetIsUnicode 來檢查:
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 的安全性檢查較少,因此可能會比其他方法快一些,不過使用 編譯的模型 會產生更佳的啟動時間。
在下列情況下使用 預先慣例模型組態 :
- 適用性條件很簡單,因為它只取決於類型。
- 隨時在模型中新增指定型別的屬性時,都需要套用設定,並覆寫資料註解和慣例。
在下列情況下使用 完成慣例 :
- 適用性條件很複雜。
- 組態不應該覆蓋資料註解所指定的內容。
在下列情況下使用 互動式慣例 :
- 多個慣例彼此相依。 完成慣例會依照新增慣例的順序執行,因此無法回應稍後完成慣例所做的變更。
- 邏輯在多個情境中共享。 互動式慣例比其他方法更安全。