EF Core 7.0.15使用TPC映射与AutoInclude时引发无效转换异常
使用.NET 6.0 + PostgreSQL,通过TPC映射策略让所有实体继承DbBase抽象类,相关代码及遇到的问题如下:
DbContext配置
public MyDbContext(DbContextOptions<MyDbContext> options) : base(options) { } // devices tracking public DbSet<Device> Devices { get; set; } public DbSet<DeviceLending> DevicesLendings { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { // Default values from db functions modelBuilder .Entity<DbBase>() .UseTpcMappingStrategy() .Property(e => e.Created) .HasDefaultValueSql("now()"); // Auto Includes modelBuilder .Entity<Device>() .Navigation(e => e.Lendings) .AutoInclude(); modelBuilder .Entity<DeviceLending>() .Navigation(e => e.Device) .AutoInclude(); }
DbBase类定义
public abstract class DbBase { [Key] public int Id { get; set; } public Instant Created { get; set; } public Instant Updated { get; set; } [Timestamp] public uint Version { get; set; } }
实体类定义
public class Device : DbBase { public ICollection<DeviceLending> Lendings { get; } = new List<DeviceLending>(); public string Comment { get; set; } = string.Empty; } public class DeviceLending : DbBase { public string Comment { get; set; } = String.Empty; public int DeviceId { get; set; } public Device Device { get; set; } = null!; }
加载包含Lendings属性的Device数据时抛出异常:
System.InvalidCastException: Unable to cast object of type 'devices.Entities.Device' to type 'devices.Entities.DeviceLending'
注释AutoInclude配置后可正常运行,但需要自动加载关联的Lendings数据,添加Lendings属性的setter也无效果。完整异常信息:
fail: Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost[111]
Unhandled exception in circuit '5xqWOiz4ASoSDGW8EUUVSgQvQ-XQTuwqp0axa7fVeFk'.
System.InvalidCastException: Unable to cast object of type 'devices.Entities.Device' to type 'devices.Entities.DeviceLending'.
at lambda_method90(Closure , QueryContext , DbDataReader , ResultContext , SingleQueryResultCoordinator )
at Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.g__ProcessCurrentElementRow|24_0[TIncludingEntity,TIncludedEntity](<>c__DisplayClass24_02& ) at Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.PopulateIncludeCollection[TIncludingEntity,TIncludedEntity](Int32 collectionId, QueryContext queryContext, DbDataReader dbDataReader, SingleQueryResultCoordinator resultCoordinator, Func3 parentIdentifier, Func3 outerIdentifier, Func3 selfIdentifier, IReadOnlyList1 parentIdentifierValueComparers, IReadOnlyList1 outerIdentifierValueComparers, IReadOnlyList1 selfIdentifierValueComparers, Func5 innerShaper, INavigationBase inverseNavigation, Action2 fixup, Boolean trackingQuery) at lambda_method92(Closure , QueryContext , DbDataReader , ResultContext , SingleQueryResultCoordinator ) at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable1.AsyncEnumerator.MoveNextAsync()
at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable1 source, CancellationToken cancellationToken) at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable1 source, CancellationToken cancellationToken)
at devices.Services.DbService.GetDevicesAsync(CancellationToken ct) in [redacted]/Services/DbService.cs:line 68
at devices.Shared.DeviceGrid.LoadDevices() in [redacted]/Shared/DeviceGrid.razor:line 40
at devices.Shared.DeviceGrid.OnInitializedAsync() in [redacted]/Shared/DeviceGrid.razor:line 45
at Microsoft.AspNetCore.Components.ComponentBase.RunInitAndSetParametersAsync()
at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle, ComponentState owningComponentState)
这个异常确实和TPC映射策略直接相关:
- TPC(Table-Per-Concrete-Type)策略会为每个具体实体创建独立数据表,每个表包含实体所有属性(包括继承自基类的字段)。但你直接给抽象基类
DbBase配置TPC,导致EF Core在处理关联时,错误地将基类的Id字段当作关联匹配键,混淆了Device和DeviceLending表的结果行,触发类型转换错误。 - 同时配置双向
AutoInclude(Device.Lendings和DeviceLending.Device互相自动包含),会生成循环关联的JOIN查询,进一步加剧结果塑形逻辑的混乱。
1. 修正TPC配置方式
不要直接给抽象基类配置TPC,而是为每个具体实体单独配置:
protected override void OnModelCreating(ModelBuilder modelBuilder) { // 为每个具体实体配置TPC策略 modelBuilder.Entity<Device>().UseTpcMappingStrategy(); modelBuilder.Entity<DeviceLending>().UseTpcMappingStrategy(); // 分别为每个实体设置基类属性的默认值 modelBuilder.Entity<Device>() .Property(e => e.Created) .HasDefaultValueSql("now()"); modelBuilder.Entity<DeviceLending>() .Property(e => e.Created) .HasDefaultValueSql("now()"); }
2. 显式配置关联关系
明确指定外键,避免EF Core混淆基类Id:
// 在OnModelCreating中添加 modelBuilder.Entity<DeviceLending>() .HasOne(dl => dl.Device) .WithMany(d => d.Lendings) .HasForeignKey(dl => dl.DeviceId) .OnDelete(DeleteBehavior.Cascade);
3. 移除双向AutoInclude
保留单向自动包含即可,避免循环查询:
// 只保留Device到Lendings的自动包含 modelBuilder.Entity<Device>() .Navigation(e => e.Lendings) .AutoInclude(); // 注释掉反向的AutoInclude // modelBuilder.Entity<DeviceLending>() // .Navigation(e => e.Device) // .AutoInclude();
4. 验证查询SQL
通过EF Core日志查看生成的SQL语句,确认JOIN逻辑是通过DeviceId关联两张表,而非基类的Id字段。
内容的提问来源于stack exchange,提问作者atcheckmate

