EF Core派生类配置报错:无法在RegisteredDevice上配置主键的解决方法
问题背景
现有以下类架构与EF Core配置:
基类定义
public abstract class BaseDevice<TDeviceSync> : IPosition, IChainByPreviousId where TDeviceSync : BaseDeviceSync { public virtual TDeviceSync AddedSync { get; set; } public virtual TDeviceSync? DeletedSync { get; set; } public int Id { get; set; } public string AddedSyncId { get; set; } = string.Empty; public string? DeletedSyncId { get; set; } public int? Position { get; set; } public int? PreviousDeviceId { get; set; } public virtual BaseDevice<TDeviceSync>? PreviousDevice { get; set; } } public abstract class BaseDeviceSync { public string Id { get; set; } = Guid.NewGuid().ToString(); public DateTime SyncDate { get; set; } = DateTime.Now; public string Filename { get; set; } } public abstract class BaseDeviceHistory<TBaseDevice, TDeviceSync> where TBaseDevice : BaseDevice<TDeviceSync> where TDeviceSync : BaseDeviceSync { public int DeviceId { get; set; } public virtual TBaseDevice Device { get; set; } public string SyncId { get; set; } public string OldValue { get; set; } public string NewValue { get; set; } }
派生类定义
public class RegisteredDevice : BaseDevice<RegisteredDeviceSync> { public string DeviceName { get; set; } public string ModelNumber { get; set; } public string SoftwareVersion { get; set; } public string EldIdentifier { get; set; } public string CompanyName { get; set; } public string Email { get; set; } public string Website { get; set; } public string? DataTransferOptions { get; set; } public virtual RegisteredDeviceSync AddedSync { get; set; } public virtual RegisteredDeviceSync? DeletedSync { get; set; } private ICollection<RegisteredDeviceHistory> _history; public virtual ICollection<RegisteredDeviceHistory> History { get => _history ??= new List<RegisteredDeviceHistory>(); protected set => _history = value; } } public class RegisteredDeviceHistory : BaseDeviceHistory<RegisteredDevice, RegisteredDeviceSync> { public virtual RegisteredDeviceSync RegisteredDeviceSync { get; set; } public RegisteredDeviceFieldName FieldName { get; set; } } public class RegisteredDeviceSync : BaseDeviceSync { private ICollection<RegisteredDeviceHistory> _history; public virtual ICollection<RegisteredDeviceHistory> History { get => _history ??= new List<RegisteredDeviceHistory>(); protected set => _history = value; } private ICollection<RegisteredDevice> _addedDevices; public virtual ICollection<RegisteredDevice> AddedDevices { get => _addedDevices ??= new List<RegisteredDevice>(); protected set => _addedDevices = value; } private ICollection<RegisteredDevice> _deletedDevices; public virtual ICollection<RegisteredDevice> DeletedDevices { get => _deletedDevices ??= new List<RegisteredDevice>(); protected set => _deletedDevices = value; } }
EF配置代码
public class RegisteredDeviceMap : IEntityTypeConfiguration<RegisteredDevice> { public void Configure(EntityTypeBuilder<RegisteredDevice> builder) { builder.ToTable(nameof(RegisteredDevice)); builder.HasKey(p => p.Id); builder.Property(p => p.EldIdentifier).HasMaxLength(6).IsFixedLength(); builder.Property(p => p.Phone).HasMaxLength(128); builder.Property(p => p.Email).HasMaxLength(256); builder.Property(p => p.AddedSyncId).HasMaxLength(64); builder.Property(p => p.DeletedSyncId).HasMaxLength(64); builder.HasOne(a => a.AddedSync) .WithMany(a => a.AddedDevices) .HasForeignKey(p => p.AddedSyncId) .OnDelete(DeleteBehavior.Restrict); builder.HasOne(a => a.DeletedSync) .WithMany(a => a.DeletedDevices) .HasForeignKey(p => p.DeletedSyncId) .OnDelete(DeleteBehavior.Restrict); builder.HasOne(a => a.Vendor) .WithMany(a => a.RegisteredDevices) .HasForeignKey(a => a.VendorId) .OnDelete(DeleteBehavior.SetNull); builder.HasOne(a => a.PreviousDevice) .WithOne() .HasForeignKey<BaseDevice<RegisteredDeviceSync>>(p => p.PreviousDeviceId) .OnDelete(DeleteBehavior.NoAction); } }
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<BaseDevice<BaseDeviceSync>>().UseTpcMappingStrategy(); modelBuilder.ApplyConfigurationsFromAssembly(typeof(RegisteredDeviceMap).Assembly); }
错误信息
创建迁移时抛出错误:
System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---
System.InvalidOperationException: A key cannot be configured on 'RegisteredDevice' because it is a derived type. The key must be configured on the root type 'BaseDevice'. If you did not intend for 'BaseDevice' to be included in the model, ensure that it is not referenced by a DbSet property on your context, referenced in a configuration call to ModelBuilder, or referenced from a navigation on a type that is included in the model.
错误原因
- TPC映射策略约束:使用TPC(每个具体类对应一张表)继承映射时,EF Core要求主键必须在根实体类上配置,派生类不能重复定义主键。
RegisteredDevice是BaseDevice<RegisteredDeviceSync>的派生类,基类已包含Id主键字段,在派生类配置中调用builder.HasKey(p => p.Id)违反了该规则。 - 泛型基类配置错误:
OnModelCreating中配置的是BaseDevice<BaseDeviceSync>的TPC策略,但实际根类是泛型封闭类型BaseDevice<RegisteredDeviceSync>,类型不匹配导致EF无法正确识别继承关系。
解决方法
1. 移除派生类中的主键配置
删除RegisteredDeviceMap中的builder.HasKey(p => p.Id);行,主键配置由基类负责。
2. 正确配置泛型基类的TPC策略
创建泛型基类配置类,统一处理基类的主键与TPC策略:
public class BaseDeviceConfiguration<TDeviceSync> : IEntityTypeConfiguration<BaseDevice<TDeviceSync>> where TDeviceSync : BaseDeviceSync { public void Configure(EntityTypeBuilder<BaseDevice<TDeviceSync>> builder) { builder.HasKey(p => p.Id); // 在基类上配置主键 builder.UseTpcMappingStrategy(); // 应用TPC映射策略 // 配置基类公共属性 builder.Property(p => p.AddedSyncId).HasMaxLength(64); builder.Property(p => p.DeletedSyncId).HasMaxLength(64); // 配置PreviousDevice关联 builder.HasOne(a => a.PreviousDevice) .WithOne() .HasForeignKey<BaseDevice<TDeviceSync>>(p => p.PreviousDeviceId) .OnDelete(DeleteBehavior.NoAction); } }
更新OnModelCreating,替换原有的错误配置:
protected override void OnModelCreating(ModelBuilder modelBuilder) { // 为具体泛型封闭类型应用基类配置 modelBuilder.ApplyConfiguration(new BaseDeviceConfiguration<RegisteredDeviceSync>()); // 应用派生类的配置 modelBuilder.ApplyConfigurationsFromAssembly(typeof(RegisteredDeviceMap).Assembly); }
3. 清理派生类配置中的重复属性
基类已配置的属性(如AddedSyncId、DeletedSyncId)无需在派生类中重复配置,仅保留派生类独有的属性配置:
public class RegisteredDeviceMap : IEntityTypeConfiguration<RegisteredDevice> { public void Configure(EntityTypeBuilder<RegisteredDevice> builder) { builder.ToTable(nameof(RegisteredDevice)); // 仅配置RegisteredDevice独有的属性 builder.Property(p => p.EldIdentifier).HasMaxLength(6).IsFixedLength(); builder.Property(p => p.Phone).HasMaxLength(128); builder.Property(p => p.Email).HasMaxLength(256); // 配置关联关系 builder.HasOne(a => a.AddedSync) .WithMany(a => a.AddedDevices) .HasForeignKey(p => p.AddedSyncId) .OnDelete(DeleteBehavior.Restrict); builder.HasOne(a => a.DeletedSync) .WithMany(a => a.DeletedDevices) .HasForeignKey(p => p.DeletedSyncId) .OnDelete(DeleteBehavior.Restrict); builder.HasOne(a => a.Vendor) .WithMany(a => a.RegisteredDevices) .HasForeignKey(a => a.VendorId) .OnDelete(DeleteBehavior.SetNull); } }
4. 确保基类的正确使用
- 基类
BaseDevice<TDeviceSync>保持抽象类,不在DbContext中定义DbSet<BaseDevice<TDeviceSync>>,仅定义DbSet<RegisteredDevice>。 - 避免在其他实体或配置中直接引用基类,确保EF仅处理具体的派生类实体。
内容的提问来源于stack exchange,提问作者Oleg Sh

