EF Core 6迁移导航属性报错:无法添加'ReadJob'导航属性
问题描述
从NHibernate迁移至基于.NET 6与SQL Server的EF Core 6,采用仓储模式逐个迁移仓储时,遇到导航属性相关错误:
System.InvalidOperationException: 无法向实体类型'ProductRead'添加属性或导航'ReadJob',因为该实体类型已存在同名属性或导航。
模型构建代码
modelBuilder.Entity<ProductRead>(entity => { entity.Property(e => e.Id).ValueGeneratedNever(); entity.Property(e => e.ImageUrl).IsRequired(false); entity.Property(e => e.AbsDeviation) .HasColumnName("absDeviation") .HasComputedColumnSql("(abs([Quantity]-[ExpectedQuantity]))", false); entity.Property(e => e.ImageUrl).HasMaxLength(255); entity.Property(e => e.NegativeDeviation) .HasColumnName("negativeDeviation") .HasComputedColumnSql("(case when [ExpectedQuantity]>[Quantity] then [Quantity]-[ExpectedQuantity] else (0) end)", false); entity.Property(e => e.PositiveDeviation) .HasColumnName("positiveDeviation") .HasComputedColumnSql("(case when [Quantity]>[ExpectedQuantity] then [Quantity]-[ExpectedQuantity] else (0) end)", false); entity.Property(e => e.Sku).HasMaxLength(36); entity.Property(e => e.Version) .IsRowVersion() .IsConcurrencyToken(); entity.HasOne(e => e.ReadJob) .WithMany(e => e.ProductReads) .HasForeignKey(e => e.ReadJobId) .HasConstraintName("FK_ProductReads_ReadJobId_ReadJob_Id") .IsRequired(false) .OnDelete(DeleteBehavior.Cascade); entity.HasIndex(e => e.Created, "IX_ProductReads_Created_Quantity_ReadJobId_Sku") .HasFillFactor(90); entity.HasIndex(e => e.ReadJob, "IX_ProductReads_ReadJobId_CreatedSkuQuantityExpectedQuantityImageUrlNoStockTransferVersion") .HasFillFactor(90); entity.HasIndex(e => e.ReadJob, "IX_ProductReads_ReadJobId_Incl_Sku") .HasFillFactor(80); entity.HasIndex(e => new { e.ReadJob, e.Sku }, "IX_ProductReads_ReadJobId_Sku_cover") .HasFillFactor(80); }); modelBuilder.Entity<ReadJob>(entity => { entity.HasIndex(e => new { e.Assignment, e.ReadType, e.State }, "IX_ReadJobs_AssignmentId_ReadType_State") .HasFillFactor(80); entity.HasIndex(e => new { e.ReadType, e.Completed }, "IX_ReadJobs_ReadType_Completed_Created_UserId") .HasFillFactor(90); entity.HasIndex(e => e.State, "IX_ReadJobs_State_includes") .HasFillFactor(90); entity.HasIndex(e => new { e.UserId, e.ReadType, e.Completed, e.State }, "IX_ReadJobs_UserId_ReadType_Completed_State_includes") .HasFillFactor(90); entity.HasIndex(e => new { e.UserId, e.State }, "IX_ReadJobs_UserId_State_cover") .HasFillFactor(90); entity.HasIndex(e => e.Assignment, "NonClusteredIndex-20230503-081413") .HasFillFactor(80); entity.HasIndex(e => new { e.State, e.UserId }, "UX_ReadJobs_AssignmentId_IsCompleted_filtered") .IsUnique() .HasFilter("([State]='InProgress')") .HasFillFactor(85); entity.HasIndex(e => e.Assignment, "idx_ReadJob_Cover") .HasFillFactor(80); entity.HasIndex(e => new { e.ReadType, e.State }, "idx_readtype_state") .HasFillFactor(90); entity.Property(e => e.Id).ValueGeneratedNever(); entity.Property(e => e.AssignmentId).IsRequired(false); entity.Ignore(e => e.ProductReads); entity.Property(e => e.ReadType) .HasMaxLength(20) .HasConversion( v => v.ToString(), v => (ReadType)Enum.Parse(typeof(ReadType), v)); entity.Property(e => e.State) .HasMaxLength(255) .IsRequired(true) .HasConversion( v => v.ToString(), v => (ReadJobState)Enum.Parse(typeof(ReadJobState), v)); entity.Property(e => e.Version) .IsRowVersion() .IsConcurrencyToken(); entity.Ignore(e => e.Assignment); entity.HasOne(e => e.Assignment) .WithMany(e => e.ReadJobs) .HasForeignKey(e => e.AssignmentId) .HasConstraintName("FK_ReadJob_Assignment"); });
ProductRead实体类代码
public class ProductRead : EntityBase<Guid> { public ProductRead() { this.ProductEntries = new List<ProductEntry>(); } public virtual Guid? ReadJobId { get; set; } public virtual ReadJob ReadJob { get; set; } public virtual DateTime Created { get; set; } public virtual string Sku { get; set; } public virtual int? Quantity { get; set; } public virtual int ExpectedQuantity { get; set; } public virtual bool NoStockTransfer { get; set; } public virtual int? Deviation { get { if (this.Quantity != null) { return this.Quantity.Value - this.ExpectedQuantity; } return null; } } public virtual IList<ProductEntry> ProductEntries { get; protected set; } public virtual string ImageUrl { get; set; } public virtual int? AbsDeviation { get; } public virtual int? PositiveDeviation { get; } public virtual int? NegativeDeviation { get; } }
ReadJob实体类代码
public class ReadJob : EntityBase<Guid> { public ReadJob() { this.ProductReads = new List<ProductRead>(); } public virtual DateTime Created { get; set; } public virtual DateTime? Completed { get; set; } public virtual DateTime? Started { get; set; } public virtual int UserId { get; set; } public virtual ReadJobState State { get; set; } public virtual Guid? AssignmentId { get; set; } public virtual Assignment Assignment { get; set; } public virtual IList<ProductRead> ProductReads { get; set; } public virtual ReadType ReadType { get; set; } }
问题原因
ReadJob配置中的矛盾Ignore语句:先调用entity.Ignore(e => e.ProductReads);标记集合导航被忽略,但后续在ProductRead的配置中又通过HasOne(e => e.ReadJob).WithMany(e => e.ProductReads)关联该导航,EF Core处理双向关联时会尝试注册ProductReads,之前的Ignore操作导致元数据冲突,间接引发ReadJob导航的重复注册错误。同时ReadJob配置中先Ignore(e => e.Assignment)又立即配置HasOne(e => e.Assignment),进一步扰乱了EF Core的模型构建逻辑。- 索引配置使用导航属性而非外键:在
ProductRead的索引配置中,多次使用e.ReadJob(导航属性)创建索引,EF Core处理这类索引时会自动尝试注册或验证导航属性,重复触发的逻辑导致系统认为ReadJob导航被重复添加。
解决方案
移除
ReadJob配置中多余的Ignore语句
删除以下两行代码:entity.Ignore(e => e.ProductReads); entity.Ignore(e => e.Assignment);这两个导航属性需要用于建立关联,
Ignore会和后续的关联配置产生冲突。修改
ProductRead的索引配置,使用外键属性
将所有使用e.ReadJob的索引配置替换为使用外键e.ReadJobId:// 替换后 entity.HasIndex(e => e.ReadJobId, "IX_ProductReads_ReadJobId_CreatedSkuQuantityExpectedQuantityImageUrlNoStockTransferVersion") .HasFillFactor(90); entity.HasIndex(e => e.ReadJobId, "IX_ProductReads_ReadJobId_Incl_Sku") .HasFillFactor(80); entity.HasIndex(e => new { e.ReadJobId, e.Sku }, "IX_ProductReads_ReadJobId_Sku_cover") .HasFillFactor(80);验证模型一致性
确保实体类中的导航属性与Fluent API配置完全匹配:ProductRead的ReadJob导航和ReadJob的ProductReads导航是双向关联,HasOne.WithMany的配置要准确对应实体类中的属性。
内容的提问来源于stack exchange,提问作者KnApP993
相关产品推荐
相关产品推荐

