Entity Framework 6保存复杂对象时重复实体跟踪错误求助
背景
正在为C#/SQL Server应用编写单元测试,采用SQLite做非生产环境测试,已将生产样本数据导出为JSON用于填充测试库。
错误现象
解析JSON得到复杂对象的IEnumerable后,调用DbContext.AddRange()时抛出以下错误:
System.InvalidOperationException : The instance of entity type 'RequestType' cannot be tracked because another instance with the same key value for {'RequestTypeId'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.
问题根源是重复保存查找表数据:RequestType是与Request关联的单值查找表,Status是多值查找表,ChangeDetail是关联Request的唯一ID详情对象。曾尝试将RequestType和Statuses设为Null,未解决问题。
实体类及配置
Request类定义
public class Request { public int RequestId { get; set; } /// <summary>changed from "navarchar(max)" to "text" for sqlite testing db </summary> [Required] [MinLength(50)] [Column(TypeName = "text")] public string Justification { get; set; } = string.Empty; [Required] public int SubmitUser { get; set; } [Required] public DateTime SubmitDate { get; set; } public int? ModifyUser { get; set; } public DateTime? ModifyDate { get; set; } public virtual IEnumerable<ChangeDetail> ChangeDetail { get; set; } = new List<ChangeDetail>(); public virtual IEnumerable<Status> Statuses { get; set; } = new List<Status>(); public virtual RequestType RequestType { get; set; } = new RequestType(); }
实体关系配置
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Request>() .HasOne(x => x.RequestType) .WithMany(x => x.Requests); modelBuilder.Entity<Request>() .HasMany(x => x.ChangeDetail) .WithOne(x => x.Request); modelBuilder.Entity<Request>() .HasMany(x => x.Statuses) .WithMany(x => x.Requests); modelBuilder.Entity<Status>() .HasOne(x => x.RequestStatusType) .WithMany(x => x.Statuses); }
已尝试的无效方法
- 使用LINQ对查找表实体去重
- 设置实体状态为
EntityState.Unmodified
解决方案建议
1. 预插入并复用查找表实体
在插入Request之前,先提取所有唯一的RequestType和Status实体插入上下文,确保每个键仅被跟踪一次,之后将Request中的引用替换为上下文已跟踪的实例:
// 提取唯一RequestType并插入 var uniqueRequestTypes = requests.Select(r => r.RequestType) .DistinctBy(rt => rt.RequestTypeId) .ToList(); context.RequestTypes.AddRange(uniqueRequestTypes); context.SaveChanges(); // 提取唯一Status并插入 var uniqueStatuses = requests.SelectMany(r => r.Statuses) .DistinctBy(s => s.StatusId) .ToList(); context.Statuses.AddRange(uniqueStatuses); context.SaveChanges(); // 替换Request中的引用 foreach (var request in requests) { request.RequestType = uniqueRequestTypes.First(rt => rt.RequestTypeId == request.RequestType.RequestTypeId); request.Statuses = uniqueStatuses.Where(s => request.Statuses.Any(rs => rs.StatusId == s.StatusId)).ToList(); } context.Requests.AddRange(requests); context.SaveChanges();
2. 手动Attach查找表实体
遍历Request时,检查上下文是否已跟踪对应键的查找表实体,已存在则复用,不存在则Attach而非Add:
foreach (var request in requests) { // 处理RequestType var existingRequestType = context.RequestTypes.Local .FirstOrDefault(rt => rt.RequestTypeId == request.RequestType.RequestTypeId); if (existingRequestType != null) { request.RequestType = existingRequestType; } else { context.Attach(request.RequestType); } // 处理Statuses var updatedStatuses = new List<Status>(); foreach (var status in request.Statuses) { var existingStatus = context.Statuses.Local .FirstOrDefault(s => s.StatusId == status.StatusId); if (existingStatus != null) { updatedStatuses.Add(existingStatus); } else { context.Attach(status); updatedStatuses.Add(status); } } request.Statuses = updatedStatuses; } context.Requests.AddRange(requests); context.SaveChanges();
3. 清除查找表实体跟踪状态
如果查找表在测试中为只读,插入后可清除上下文跟踪,避免后续重复跟踪:
// 插入查找表后清除跟踪 context.RequestTypes.AddRange(uniqueRequestTypes); context.SaveChanges(); context.ChangeTracker.Clear(); context.Statuses.AddRange(uniqueStatuses); context.SaveChanges(); context.ChangeTracker.Clear(); // 再插入Request context.Requests.AddRange(requests); context.SaveChanges();
4. 修复实体默认初始化问题
Request类中默认初始化RequestType = new RequestType()可能生成空键实例,建议改为可空类型避免多余实例:
public virtual RequestType? RequestType { get; set; }
内容的提问来源于stack exchange,提问作者USMC6072

