自定义EF Core影子主键约定时遇基类型被重置问题
问题描述
我正在编写EF Core模型批量配置约定,要给实体基类添加uint类型主键(替代默认的long)及其他配置,同时不想逐个配置泛型实体类型。但遇到EF Core报错:
无法创建类型为'Context'的'DbContext'。创建实例时引发异常:“实体类型'DirectoryReference<Entry_Foo>'需要定义主键。如果要使用无键实体类型,请在'OnModelCreating'中调用'HasNoKey'。”
已实现的约定代码如下:
public class IdShadowPropertyConvention: IEntityTypeBaseTypeChangedConvention, IEntityTypeAddedConvention { static IdShadowPropertyConvention() { File.Delete("efcore.log"); } public void ProcessEntityTypeAdded(IConventionEntityTypeBuilder builder, IConventionContext<IConventionEntityTypeBuilder> context) { File.AppendAllText("efcore.log", $"[ProcessEntityTypeAdded] Entity `{builder.Metadata.Name}`, {builder.Metadata.IsInModel}, {builder.Metadata.HasSharedClrType}, `{builder.Metadata.BaseType?.Name}`.\n"); using var t = context.DelayConventions(); if(builder.Metadata is { IsInModel: true, HasSharedClrType: false, BaseType: null } && !builder.Metadata.IsOwned()) { var type = builder.Metadata.ClrType; if(type.BaseType != typeof(object)) { var entity = builder; do { type = type.BaseType!; var baseEntity = builder.ModelBuilder.Metadata.FindEntityType(type)?.Builder ?? builder.ModelBuilder.Entity(type)!; if(!entity.Metadata.HasSharedClrType && !entity.Metadata.IsOwned()) { File.AppendAllText("efcore.log", $" Set a new base type (transitive): `{entity.Metadata.Name}` => `{baseEntity.Metadata.Name}`.\n"); _ = entity.HasBaseType(baseEntity.Metadata); } entity = baseEntity; } while(type.BaseType != typeof(object)); } IConventionKeyBuilder? pk1 = null; { var baseEntity = builder.ModelBuilder.Entity(type); File.AppendAllText("efcore.log", $" Base type: {type.ShortDisplayName()}, `{baseEntity?.Metadata.HasSharedClrType}`, `{baseEntity?.Metadata.IsKeyless}`.\n"); if(baseEntity == builder) { if(baseEntity.Metadata is { HasSharedClrType: false } meta) { if(!baseEntity.Metadata.IsKeyless) { var prop = baseEntity.Property(typeof(uint), "Id"); File.AppendAllText("efcore.log", $" Added an ID property: `{prop}`.\n"); var pk = prop is null ? baseEntity.PrimaryKey(["Id"]) : baseEntity.PrimaryKey([prop.Metadata]); pk1 = pk; File.AppendAllText("efcore.log", $" Added a primary key: `{pk?.Metadata.GetName()}`.\n"); } _ = baseEntity.UseMappingStrategy(RelationalAnnotationNames.TphMappingStrategy)?.HasDiscriminator(); File.AppendAllText("efcore.log", $" Configured as a base type.\n"); } } } _ = builder.Property(typeof(uint), "RowVersion")?.ValueGenerated(ValueGenerated.OnAddOrUpdate)?.IsConcurrencyToken(true); var pk2 = builder.Metadata.FindDeclaredPrimaryKey(); File.AppendAllText("efcore.log", $" PKs: `{pk1?.Metadata.Properties.Select((p) => p.Name).Join(", ")}`, `{pk2?.Properties.Select((p) => p.Name).Join(", ")}`.\n"); } } public void ProcessEntityTypeBaseTypeChanged(IConventionEntityTypeBuilder entityTypeBuilder, IConventionEntityType? newBaseType, IConventionEntityType? oldBaseType, IConventionContext<IConventionEntityType> context) { File.AppendAllText("efcore.log", $"[ProcessEntityTypeBaseTypeChanged] Entity `{entityTypeBuilder.Metadata.Name}`, {entityTypeBuilder.Metadata.IsInModel}, {entityTypeBuilder.Metadata.HasSharedClrType}, BT: `{entityTypeBuilder.Metadata.BaseType?.Name}`, OT: `{oldBaseType?.Name}`, NT: {newBaseType?.Name}`, OBT: `{oldBaseType?.BaseType?.Name}`, NBT: `{newBaseType?.BaseType?.Name}`.\n"); if(newBaseType?.IsInModel is true) { var type = newBaseType; while(type.BaseType is not null) { type = type.BaseType; } File.AppendAllText("efcore.log", $" Base type: {type.Name}, `{type.HasSharedClrType}`, `{type.IsKeyless}`.\n"); if(type.BaseType is null) { if(!type.IsKeyless) { var prop = type.Builder.Property(typeof(uint), "Id"); File.AppendAllText("efcore.log", $" Added an ID property: `{prop}`.\n"); var pk = prop is null ? type.Builder.PrimaryKey(["Id"]) : type.Builder.PrimaryKey([prop.Metadata]); File.AppendAllText("efcore.log", $" Added a primary key: `{pk?.Metadata.GetName()}`.\n"); } _ = type.Builder.Property(typeof(uint), "RowVersion")?.ValueGenerated(ValueGenerated.OnAddOrUpdate)?.IsConcurrencyToken(true); _ = type.Builder.UseMappingStrategy(RelationalAnnotationNames.TphMappingStrategy)?.HasDiscriminator(); } } } }
日志显示EF Core后续将实体基类型重置为空:
[ProcessEntityTypeAdded] Entity `DirectoryReference<Entry_Foo>`, True, False, ``. Set a new base type (transitive): `DirectoryReference<Entry_Foo>` => `DirectoryReference<Directory, Entry_Foo>`. Base type: DirectoryReference<Directory, Entry_Foo>, `False`, `False`. PKs: ``, ``. [ProcessEntityTypeAdded] Entity `DirectoryReference<Directory, Entry_Foo>`, True, False, ``. Base type: DirectoryReference<Directory, Entry_Foo>, `False`, `False`. Added an ID property: `Microsoft.EntityFrameworkCore.Metadata.Internal.InternalPropertyBuilder`. Added a primary key: `PK_directory_reference_directory_entry_foo`. Configured as a base type. PKs: `Id`, `Id`. [ProcessEntityTypeBaseTypeChanged] Entity `DirectoryReference<Entry_Foo>`, True, False, BT: `DirectoryReference<Directory, Entry_Foo>`, OT: ``, NT: `DirectoryReference<Directory, Entry_Foo>`, OBT: ``, NBT: ``. Base type: DirectoryReference<Directory, Entry_Foo>, `False`, `False`. Added an ID property: `Microsoft.EntityFrameworkCore.Metadata.Internal.InternalPropertyBuilder`. Added a primary key: `PK_directory_reference_directory_entry_foo`. [ProcessEntityTypeBaseTypeChanged] Entity `DirectoryReference<Entry_Foo>`, True, False, BT: ``, OT: `DirectoryReference<Directory, Entry_Foo>`, NT: ``, OBT: ``, NBT: ``.
尝试将基类型修改逻辑移至模型最终化约定,但此时外键已配置完成,无法修改基类型,也难以重新关联外键。请问如何阻止EF Core覆盖约定配置?
解决方案
1. 调整约定执行顺序
EF Core内置约定可能在你的约定之后执行,覆盖了基类型配置。需要将你的约定注册在内置约定之前:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) { // 插入到内置派生类型发现约定前面 configurationBuilder.Conventions.AddBefore(typeof(DerivedTypeDiscoveryConvention), new IdShadowPropertyConvention()); }
DerivedTypeDiscoveryConvention是EF Core自动处理继承关系的核心内置约定,让自定义约定先执行可抢占优先级。
2. 修复基类型设置的延迟机制
当前代码中仅在方法开头使用一次延迟约定,后续设置基类型时可能被其他约定打断。修改基类型设置逻辑,每次变更都启用延迟:
if(type.BaseType != typeof(object)) { var entity = builder; do { type = type.BaseType!; var baseEntityType = builder.ModelBuilder.Metadata.FindEntityType(type) ?? builder.ModelBuilder.Entity(type).Metadata; if(!entity.Metadata.HasSharedClrType && !entity.Metadata.IsOwned()) { // 每次设置基类型都启用延迟,避免中途被其他约定覆盖 using var delay = context.DelayConventions(); entity.HasBaseType(baseEntityType); File.AppendAllText("efcore.log", $" Set a new base type (transitive): `{entity.Metadata.Name}` => `{baseEntityType.Name}`.\n"); } entity = baseEntityType.Builder; } while(type.BaseType != typeof(object)); }
3. 提前预配置泛型基类的主键
不要等到派生实体被添加时才配置基类,直接在约定初始化阶段预配置泛型基类的主键:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) { // 提前为泛型基类配置主键 configurationBuilder.DefaultTypeMapping(typeof(DirectoryReference<,>)) .HasKey("Id") .Property<uint>("Id") .ValueGeneratedOnAdd(); configurationBuilder.Conventions.AddBefore(typeof(DerivedTypeDiscoveryConvention), new IdShadowPropertyConvention()); }
这样EF Core处理派生实体时,基类已有完整的主键配置,不会因继承关系丢失而报错。
4. 禁用自动派生类型发现
如果不需要EF Core自动处理继承逻辑,可以直接移除内置的派生类型发现约定:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) { configurationBuilder.Conventions.Remove(typeof(DerivedTypeDiscoveryConvention)); configurationBuilder.Conventions.Add(new IdShadowPropertyConvention()); }
完全由自定义约定控制继承关系,避免内置逻辑干扰配置。
内容的提问来源于stack exchange,提问作者ZzZombo
相关产品推荐
相关产品推荐

