You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

EF双上下文添加评论时ApplicationUser跟踪冲突问题求助

双DbContext下EF Core跟踪冲突问题解决思路

问题描述

应用基于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的实体跟踪:

  1. _userManager.GetUserAsync(User)从生产库的AppDbContext获取并跟踪了当前用户实例;
  2. 当将该实例赋值给Comment的User属性,再调用_demoDbContext.Ticket.Update(ticket)时,EF会尝试把整个对象图(包括Comment和关联的ApplicationUser)附加到DemoDbContext的跟踪器;
  3. 第一次操作后,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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 03:27:04