You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自定义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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 01:24:56