EF双上下文添加评论时ApplicationUser跟踪冲突问题求助
问题描述
应用基于Entity Framework配置了两个DbContext:一个连接SQL Server生产数据库,另一个使用InMemory数据库做用户演示(不修改生产数据)。向表中添加新条目时,第二次尝试会触发跟踪冲突错误:
InvalidOperationException: The instance of entity type 'ApplicationUser' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values.
现有配置与代码
DbContext配置
services.AddDbContextPool<AppDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); var databaseName = new Guid().ToString(); services.AddDbContext<DemoDbContext>(options => options.UseInMemoryDatabase(databaseName), ServiceLifetime.Scoped); services.AddIdentity<ApplicationUser, IdentityRole>(config => { config.Password.RequiredLength = 6; config.Password.RequireDigit = false; config.Password.RequireUppercase = false; config.Password.RequireNonAlphanumeric = false; config.User.RequireUniqueEmail = true; }) .AddDefaultTokenProviders() .AddEntityFrameworkStores<AppDbContext>();
演示数据查询(带AsNoTracking)
public IEnumerable<Ticket> GetTicketsForUserRole(ApplicationUser user) { return _demoDbContext.Ticket.AsNoTracking() .Include(ticket => ticket.Author) .Include(ticket => ticket.Priority) .Include(ticket => ticket.State) .Include(ticket => ticket.Project) .Include(ticket => ticket.Type) .Include(ticket => ticket.AssignedUser) .Include(ticket => ticket.Comments) .ThenInclude(c => c.User) .Include(ticket => ticket.Attachments) .ThenInclude(a => a.User); }
出错的添加评论逻辑
[HttpPost] public async Task<IActionResult> AddComment(TicketsViewModel viewModel) { if(viewModel.TicketId != null) { var applicationUser = await _userManager.GetUserAsync(User); var roleObject = _roleFactory.GetRole(applicationUser); var ticket = roleObject.GetTicketsForUserRole(applicationUser) .First(t => t.Id == viewModel.TicketId); viewModel.Ticket = ticket; var comment = new Comment { Date = DateAndTime.Now, TextComment = viewModel.TextComment, User = applicationUser }; ticket.Comments.Add(comment); roleObject.UpdateTicket(ticket); return RedirectToAction("Details", new { id = viewModel.Ticket.Id }); } return NoContent(); }
UpdateTicket方法
public void UpdateTicket(Ticket ticket) { _demoDbContext.Ticket.Update(ticket); _demoDbContext.SaveChanges(); }
移除AsNoTracking后的报错
An unhandled exception occurred while processing the request. ArgumentException: An item with the same key has already been added. Key: 63940036-1c2c-4f8c-8db7-d55f79ce40aa
System.Collections.Generic.Dictionary<TKey, TValue>.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
临时修复(不适用)
移除Comment类的ApplicationUser导航属性可修复错误,但会丢失用户与评论的关联,不符合需求:
public class Comment { [Key] public int Id { get; set; } [Display(Name = "Post Date")] [DisplayFormat(DataFormatString = "{ 0:dd/MM/yyyy HH:mm }")] [DataType(DataType.Date)] public DateTime Date { get; set; } [ForeignKey("User")] public string UserId { get; set; } [Required] public ApplicationUser User { get; set; } [Required] public string TextComment { get; set; } }
问题根源
核心冲突来自跨DbContext的实体跟踪:
_userManager.GetUserAsync(User)从生产库的AppDbContext获取并跟踪了当前用户实例;- 当将该实例赋值给Comment的
User属性,再调用_demoDbContext.Ticket.Update(ticket)时,EF会尝试把整个对象图(包括Comment和关联的ApplicationUser)附加到DemoDbContext的跟踪器; - 第一次操作后,
DemoDbContext已跟踪该用户实例;第二次操作时,同一个实例再次被尝试附加,触发跟踪冲突。
可行解决方案
方案1:仅给Comment赋值UserId(保留导航属性)
创建Comment时只设置外键UserId,不直接关联User实例,避免跨DbContext的跟踪冲突:
var comment = new Comment { Date = DateAndTime.Now, TextComment = viewModel.TextComment, UserId = applicationUser.Id // 只赋值外键,不关联User实例 };
方案2:解除AppDbContext对用户实例的跟踪
在使用用户实例前,将其从AppDbContext的跟踪中分离,让DemoDbContext可以正常附加:
var applicationUser = await _userManager.GetUserAsync(User); // 解除生产库上下文的跟踪 _userManager.Context.Entry(applicationUser).State = EntityState.Detached; var roleObject = _roleFactory.GetRole(applicationUser);
注意:分离后若需在当前请求中用AppDbContext操作该用户,需重新附加。
方案3:从DemoDbContext获取用户实例
如果演示库中存在对应用户数据,直接从DemoDbContext查询用户,确保实例只被单个上下文跟踪:
// 替换原有的_userManager获取逻辑 var demoUser = _demoDbContext.ApplicationUsers.First(u => u.Id == User.Identity.Name); var comment = new Comment { Date = DateAndTime.Now, TextComment = viewModel.TextComment, User = demoUser };
方案4:修改UpdateTicket逻辑,仅更新必要字段
不直接调用Update(ticket)(会触发整个对象图附加),而是从DemoDbContext重新查询ticket,手动更新字段:
public void UpdateTicket(Ticket ticket) { var existingTicket = _demoDbContext.Ticket .Include(t => t.Comments) .First(t => t.Id == ticket.Id); // 手动添加新评论 existingTicket.Comments.AddRange(ticket.Comments); _demoDbContext.SaveChanges(); }
内容的提问来源于stack exchange,提问作者ball

