将具有相同主键的相同实体附加到 Entity Framework Core 中的 DbContext 时出现问题

Problem attaching same Entity with same Primary Key to DbContext in Entity Framework Core

我正在开发 .NET Core 3.1 Razor 页面应用程序。我正在使用 Entity Framework 核心和带有通用存储库的工作单元模式。我还使用 AddScoped 来注册我所有的服务,例如UnitOfWork 和 Repositories 等,即每个 HttpRequest 在存储库之间共享一个 DbContext。

services.AddDbContextPool<MyContext>(opt => opt.UseSqlServer(Configuration.GetConnectionString("MyConnection"))
         .EnableSensitiveDataLogging());

services.AddScoped<IUnitOfWork, UnitOfWork>();
services.AddScoped<IGenericRepository<Domain.List>, ListRepository>();
services.AddScoped<IGenericRepository<Domain.ListItem>, ListItemRepository>();
//etc...

我已经为用户编写了一些代码来更新他们的姓名和电子邮件。用户输入数据后,我调用函数 UserAlreadyExists 来验证电子邮件地址是否已存在于数据库中。此验证码用于在应用程序上注册的新用户,也用于已经注册并正在更新其详细信息的用户。当现有用户尝试更新其详细信息时,将使用以下代码。

public IUnitOfWork UoW { get; set; }

[BindProperty]
public Domain.User UserObj { get; set; }

public IActionResult OnPost()
{
    if(ModelState.IsValid)
    {
        if(UserObj.Id > 0)
        {
           //Update user

           //Validation to ensure if email updated, it's not already in use
           if(UserAlreadyExists(UserObj))
           {
              TempData["Message"] = "Email address already exists within database.";
              return Page();
           }

           UoW.UserRepository.Update(UserObj);
           UoW.SaveChanges();
           TempData["Message"] = "User updated.";

        }
        else
        {
          //Add user code here               
        }

        return RedirectToPage("List");
    }

   return Page();
}
    

这是检查用户是否存在的代码

private bool UserAlreadyExists(Domain.User user)
{
   bool alreadyExists = true;

   var existingUser = UoW.UserRepository
            .Find(u => u.Email.Trim().ToUpper() == user.Email.Trim().ToUpper())
            .FirstOrDefault();
        
    // Existing user
    if (existingUser == null)
    {
        alreadyExists = false;
    }
    else if (user.Email == existingUser.Email)
    {
        if (user.Id == existingUser.Id)
        {
            //User updating their details, but not their email
            alreadyExists = false;
        }               
    }

   return alreadyExists;
}

当现有用户尝试更新他们的姓名时,出现此错误:

The instance of entity type 'User' cannot be tracked because another instance with the key value '{Id: 1}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached

我理解错误,它发生在 UserAlreadyExists 函数内部。当现有用户尝试更新他们的姓名时,User 实体将传递到函数中并且该实体包含主键。然后我在名为 existingUser 的验证函数中创建了另一个 User 实体,这就是问题所在 - 我现在有 2 个具有相同主键值的用户实体。

我想知道我是否应该在保存更改之前将 existingUser 实体从上下文中分离出来,或者也许有更好的方法?

感谢任何反馈。

谢谢。

请记住,归根结底,实体的数据库上下文既是一个工作单元(注意你的注入),也是上下文中定义的所有模型的通用存储库,所以..你实际上可以不用重新- 将所有东西包装在你以后必须维护的东西上。

对于这种情况,由于您可能没有可用的“AsNoTracking”(因为不直接使用 ef 作为您的存储库),您可以通过两种方式更改代码:

  1. 用你想要的过滤器对数据库进行计数,这样你 return 逻辑上的“是否已经有用户”而不是实际的 用户行
  2. 对 DTO 对象执行 select,这是一个对象 实际上并没有被跟踪。做个投影,select 您想要 select 的属性,并将它们映射到普通的 class, 特定于任务

不清楚 UoWUserRepository 类 的作用。错误是因为 UserAlreadyExists 加载了实体但没有更新它。从错误看来,UserRepository.Update 正在尝试再次附加 DTO,即使具有相同 ID 的实体已经加载。

有两种选择:

  • 只加载 ID 而不是整个实体
  • 更新加载的对象

只加载ID

如果直接使用 EF Core,UserAlreadyExists 可以重写为使用 LINQ 检查是否存在:

private bool UserAlreadyExists(Domain.User user)
{
    var id=await _context.Users.Where(u=>u.Email==user.Email.Trim())
                         .Select(u=>u.Id)
                         .FirstOrDefault();
    return (user.Id==id);
}

仅此而已,因为已知 user.Id 大于 0,并且电子邮件已经匹配。

u.Email 应该 被修改,因为这会阻止数据库服务器使用任何索引来加速搜索电子邮件。在 SQL 服务器中,通常使用 case-insensitive 排序规则,因此没有理由使用 ToLower().

在那之后,无论 Update 做什么都应该有效,因为没有实体被跟踪。如果使用 EF Core,我们可以这样写:

if(!UserAlreadyExists(UserObj))
{
    _context.Users.Update(UserObj);
    _context.SaveChanges();
}

更新加载的实体

Razor CRUD tutorial shows how to use the ControllerBase.TryUpdateModelAsync 更新已加载实体的方法。我们可以直接加载实体并更新它,而不是检查用户是否存在:

var existingUser= _context.Users.Where(u=>u.Email==UserObj.Email.Trim())
                               .FirstOrDefault();
if(existingUser==null)
{
    _context.Users.Add(UserObj);
    _context.SaveChanges();
}
else if(existingUser.Id==UserObj.Id)
{
    if (await TryUpdateModelAsync<User>(existingUser))
    {
        _context.SaveChanges();
    }
}

TryUpdateModelAsync 将使用模型属性,因此它不需要访问 UserObj 对象。

这个问题的答案是使用 Entity Framework 的 .AsNoTracking

下面的初始查询由 EF 跟踪,但没有必要这样做,该实体仅用于比较目的,因此 DbContext 不需要跟踪它。

var existingUser = UoW.UserRepository
            .Find(u => u.Email.Trim().ToUpper() == user.Email.Trim().ToUpper())
            .FirstOrDefault();

相反,我在我的用户存储库中创建了一个方法(如下),它利用了 .AsNoTracking 功能:

public User GetUserByIdNoTracking(int Id)
{
   return context.Users.AsNoTracking().Where(u => u.Id ==Id).FirstOrDefault();
}

然后在我的 UserAlreadyExists 方法中使用了它。

//This entity is not tracked by the DbContext, there is no need, used only for comparison check
var existingUser = UserRepository
.GetUserByEmailNoTracking(email);