无法跟踪实体类型的实例,因为已经跟踪了具有相同键的此类型的另一个实例

Rij*_*rdt 35 c# asp.net entity-framework

我有一个服务对象 Update

public bool Update(object original, object modified)
{
    var originalClient = (Client)original;
    var modifiedClient = (Client)modified;
    _context.Clients.Update(originalClient); //<-- throws the error
    _context.SaveChanges();
    //Variance checking and logging of changes between the modified and original
}
Run Code Online (Sandbox Code Playgroud)

这是我从以下方法调用此方法的地方:

public IActionResult Update(DetailViewModel vm)
{
    var originalClient = (Client)_service.GetAsNoTracking(vm.ClientId);
    var modifiedClient = (Client)_service.Fetch(vm.ClientId.ToString());
    // Changing the modifiedClient here
    _service.Update(originalClient, modifiedClient);
}
Run Code Online (Sandbox Code Playgroud)

这是GetAsNotTracking方法:

public Client GetAsNoTracking(long id)
{
    return GetClientQueryableObject(id).AsNoTracking().FirstOrDefault();
}
Run Code Online (Sandbox Code Playgroud)

Fetch 方法:

public object Fetch(string id)
{
   long fetchId;
   long.TryParse(id, out fetchId);
   return GetClientQueryableObject(fetchId).FirstOrDefault();
}
Run Code Online (Sandbox Code Playgroud)

GetClientQueryableObject:

private Microsoft.Data.Entity.Query.IIncludableQueryable<Client, ActivityType> GetClientQueryableObject(long searchId)
{
    return _context.Clients
        .Where(x => x.Id == searchId)
        .Include(x => x.Opportunities)
        .ThenInclude(x => x.BusinessUnit)
        .Include(x => x.Opportunities)
        .ThenInclude(x => x.Probability)
        .Include(x => x.Industry)
        .Include(x => x.Activities)
        .ThenInclude(x => x.User)
        .Include(x => x.Activities)
        .ThenInclude(x => x.ActivityType);
 }
Run Code Online (Sandbox Code Playgroud)

有任何想法吗?

我看过以下文章/讨论.无济于事:ASP.NET GitHub问题3839

更新:

以下是对以下内容的更改GetAsNoTracking:

public Client GetAsNoTracking(long id)
{
    return GetClientQueryableObjectAsNoTracking(id).FirstOrDefault();
}
Run Code Online (Sandbox Code Playgroud)

GetClientQueryableObjectAsNoTracking:

private IQueryable<Client> GetClientQueryableObjectAsNoTracking(long searchId)
{
    return _context.Clients
        .Where(x => x.Id == searchId)
        .Include(x => x.Opportunities)
        .ThenInclude(x => x.BusinessUnit)
        .AsNoTracking()
        .Include(x => x.Opportunities)
        .ThenInclude(x => x.Probability)
        .AsNoTracking()
        .Include(x => x.Industry)
        .AsNoTracking()
        .Include(x => x.Activities)
        .ThenInclude(x => x.User)
        .AsNoTracking()
        .Include(x => x.Activities)
        .ThenInclude(x => x.ActivityType)
        .AsNoTracking();
}
Run Code Online (Sandbox Code Playgroud)

sab*_*sab 51

对我来说这只是解决了问题。在任何更新之前添加此代码

_context.ChangeTracker.Clear()
Run Code Online (Sandbox Code Playgroud)

来自微软文档

停止跟踪所有当前跟踪的实体。

DbContext 被设计为具有较短的生命周期,其中为每个工作单元创建一个新实例。这种方式意味着当上下文在每个工作单元结束时被处理时,所有跟踪的实体都将被丢弃。但是,在创建新上下文实例不切实际的情况下,使用此方法清除所有跟踪的实体可能会很有用。

这种方法应该始终优先于分离每个被跟踪的实体。分离实体是一个缓慢的过程,可能会产生副作用。此方法在从上下文中清除所有跟踪的实体方面更加有效。

请注意,此方法不会生成 StateChanged 事件,因为实体不是单独分离的。

更新

微软对简单工作单元的解释

更新

最好不要调用该Update()方法。只需在查询需要修改的对象时使用跟踪功能,然后调用SaveChanges()方法,该方法将以优化的方式仅更新已编辑的字段。这就是 EfCore 跟踪的工作原理。如果使用得当,跟踪是一个很好的功能。

如果您的项目中或由于任何其他原因禁用了跟踪,则使用我创建的方法。它是针对 EFCore 更新对象的优化解决方案。调用它而不是Update()方法DbContext,它将检查对象是否被跟踪。如果它被跟踪,那么它将什么也不做。如果对象未被跟踪,那么它将手动执行跟踪工作。只需调用SaveChanges()之后的方法,它将创建优化的 SQL 语句,该语句将仅更新已更改的字段。如果您想为应用程序创建审核功能以轻松跟踪所有数据每行的更改,这也将为您提供帮助。

public void UpdateIfNoTracking(TEntity entityToUpdate) where TEntity : class
{
     var keys = GetPrimaryKeys(context, entityToUpdate);

     bool tracked = context.Entry(entityToUpdate).State != EntityState.Detached;

     if (tracked)
         return;

     if (keys != null)
     {
            
         var oldValues = context.Set<TEntity>().Find(keys);

            context.Entry(oldValues).CurrentValues.SetValues(entityToUpdate);
     }
     else
     {
         context.Set<TEntity>().Attach(entityToUpdate);
         context.Entry(entityToUpdate).State = EntityState.Modified;
     }
 }
Run Code Online (Sandbox Code Playgroud)

我在 Github 上发布了完整的解决方案库:Solid.DataAccess,它很好地应用了工作单元和存储库模式。我在我所有的项目中都使用它,它的作用就像魅力一样。请随意克隆并为使其变得更好做出贡献。

GetPrimaryKeys()方法用于动态获取对象的所有主键值,因为我使用的是通用对象。当对实体使用复合主键或多个主键时,这很有帮助。

 private static object[] GetPrimaryKeys<T>(DbContext context, T value)
 {
     var keyNames = context.Model.FindEntityType(typeof(T)).FindPrimaryKey().Properties
               .Select(x => x.Name).ToArray();
     var result = new object[keyNames.Length];
     for (int i = 0; i < keyNames.Length; i++)
     {
         result[i] = typeof(T).GetProperty(keyNames[i])?.GetValue(value);
     }
     return result;
 }
Run Code Online (Sandbox Code Playgroud)


And*_*era 34

如果不覆盖EF轨道系统,您还可以在保存之前分离"本地"条目并附加更新的条目:

// 
var local = _context.Set<YourEntity>()
    .Local
    .FirstOrDefault(entry => entry.Id.Equals(entryId));

// check if local is not null 
if (!local.IsNull()) // I'm using a extension method
{
    // detach
    _context.Entry(local).State = EntityState.Detached;
}
// set Modified flag in your entry
_context.Entry(entryToUpdate).State = EntityState.Modified;

// save 
_context.SaveChanges();
Run Code Online (Sandbox Code Playgroud)

更新: 为避免代码冗余,您可以执行扩展方法:

public static void DetachLocal<T>(this DbContext context, T t, string entryId) 
    where T : class, IIdentifier 
{
    var local = context.Set<T>()
        .Local
        .FirstOrDefault(entry => entry.Id.Equals(entryId));
    if (!local.IsNull())
    {
        context.Entry(local).State = EntityState.Detached;
    }
    context.Entry(t).State = EntityState.Modified;
}
Run Code Online (Sandbox Code Playgroud)

我的IIdentifier界面只有一个Id字符串属性.

无论您的实体是什么,您都可以在上下文中使用此方法:

_context.DetachLocal(tmodel, id);
_context.SaveChanges();
Run Code Online (Sandbox Code Playgroud)


SHU*_*ATA 24

public async Task<Product> GetValue(int id)
    {
        Product Products = await _context.Products.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id);
        return Products;
    }
Run Code Online (Sandbox Code Playgroud)

AsNoTracking()


dav*_*sit 10

就我而言,表的 id 列未设置为 Identity 列。


Cob*_*byC 8

我在设置 xUnit 测试时遇到了同样的问题(EF Core)。在测试中为我“修复”的是在设置种子数据后循环遍历更改跟踪器实体。

  • 在 SeedAppDbContext() 方法的底部。

我设置了一个测试模拟上下文:

/// <summary>
/// Get an In memory version of the app db context with some seeded data
/// </summary>
public static AppDbContext GetAppDbContext(string dbName)
{
    //set up the options to use for this dbcontext
    var options = new DbContextOptionsBuilder<AppDbContext>()
        .UseInMemoryDatabase(databaseName: dbName)
        //.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)
        .Options;

    var dbContext = new AppDbContext(options);
    dbContext.SeedAppDbContext();
    return dbContext;
}
Run Code Online (Sandbox Code Playgroud)

添加一些种子数据的扩展方法

  • 并在foreach方法底部的循环中分离实体。
    public static void SeedAppDbContext(this AppDbContext appDbContext)
    {
       // add companies
       var c1 = new Company() { Id = 1, CompanyName = "Fake Company One", ContactPersonName = "Contact one", eMail = "one@caomp1.com", Phone = "0123456789", AdminUserId = "" };
       c1.Address = new Address() { Id = 1, AddressL1 = "Field Farm", AddressL2 = "Some Lane", City = "some city", PostalCode = "AB12 3CD" };
       appDbContext.CompanyRecords.Add(c1);
                        
       var nc1 = new Company() { Id = 2, CompanyName = "Test Company 2", ContactPersonName = "Contact two", eMail = "two@comp2.com", Phone = "0123456789", Address = new Address() { }, AdminUserId = "" };
       nc1.Address = new Address() { Id = 2, AddressL1 = "The Barn", AddressL2 = "Some Lane", City = "some city", PostalCode = "AB12 3CD" };
       appDbContext.CompanyRecords.Add(nc1);

       //....and so on....
            
       //last call to commit everything to the memory db
       appDbContext.SaveChanges();

       //and then to detach everything 
       foreach (var entity in appDbContext.ChangeTracker.Entries())
       {
           entity.State = EntityState.Detached;
       }
    }
Run Code Online (Sandbox Code Playgroud)

控制器 put 方法

.ConvertTo<>()方法是从一个扩展方法ServiceStack

 [HttpPut]
public async Task<IActionResult> PutUpdateCompany(CompanyFullDto company)
{
    if (0 == company.Id)
        return BadRequest();
    try
    {
        Company editEntity = company.ConvertTo<Company>();
        
        //Prior to detaching an error thrown on line below (another instance with id)
        var trackedEntity = _appDbContext.CompanyRecords.Update(editEntity);
        

        await _appDbContext.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException dbError)
    {
        if (!CompanyExists(company.Id))
            return NotFound();
        else
            return BadRequest(dbError);
    }
    catch (Exception Error)
    {
        return BadRequest(Error);
    }
    return Ok();
}
Run Code Online (Sandbox Code Playgroud)

和测试:

    [Fact]
    public async Task PassWhenEditingCompany()
    {
        var _appDbContext = AppDbContextMocker.GetAppDbContext(nameof(CompaniesController));
        var _controller = new CompaniesController(null, _appDbContext);

        //Arrange
        const string companyName = "Fake Company One";
        const string contactPerson = "Contact one";

        const string newCompanyName = "New Fake Company One";
        const string newContactPersonName = "New Contact Person";

        //Act
        var getResult = _controller.GetCompanyById(1);
        var getEntity = (getResult.Result.Result as OkObjectResult).Value;
        var entityDto = getEntity as CompanyFullDto;


        //Assert
        Assert.Equal(companyName, entityDto.CompanyName);
        Assert.Equal(contactPerson, entityDto.ContactPersonName);
        Assert.Equal(1, entityDto.Id);

        //Arrange
        Company entity = entityDto.ConvertTo<Company>();
        entity.CompanyName = newCompanyName;
        entity.ContactPersonName = newContactPersonName;
        CompanyFullDto entityDtoUpd = entity.ConvertTo<CompanyFullDto>();

        //Act
        var result = await _controller.PutUpdateCompany(entityDtoUpd) as StatusCodeResult;

        //Assert           
        Assert.True(result.StatusCode == 200);

        //Act
        getResult = _controller.GetCompanyById(1);
        getEntity = (getResult.Result.Result as OkObjectResult).Value;
        
        entityDto = getEntity as CompanyFullDto;
        
        //Assert
        Assert.Equal(1, entityDto.Id); // didn't add a new record
        Assert.Equal(newCompanyName, entityDto.CompanyName); //updated the name
        Assert.Equal(newContactPersonName, entityDto.ContactPersonName); //updated the contact

//make sure to dispose of the _appDbContext otherwise running the full test will fail.
_appDbContext.Dispose();
    }
Run Code Online (Sandbox Code Playgroud)


smo*_*nes 6

听起来您确实只是想跟踪对模型所做的更改,而不是真正将未跟踪的模型保留在内存中。我是否可以建议完全替代该问题的替代方法?

EF会自动为您跟踪更改。如何利用内置逻辑?

Ovverride SaveChanges()在你的DbContext

    public override int SaveChanges()
    {
        foreach (var entry in ChangeTracker.Entries<Client>())
        {
            if (entry.State == EntityState.Modified)
            {
                // Get the changed values.
                var modifiedProps = ObjectStateManager.GetObjectStateEntry(entry.EntityKey).GetModifiedProperties();
                var currentValues = ObjectStateManager.GetObjectStateEntry(entry.EntityKey).CurrentValues;
                foreach (var propName in modifiedProps)
                {
                    var newValue = currentValues[propName];
                    //log changes
                }
            }
        }

        return base.SaveChanges();
    }
Run Code Online (Sandbox Code Playgroud)

很好的例子可以在这里找到:

实体框架6:审核/跟踪更改

使用MVC和实体框架实施审核日志/更改历史记录

编辑: Client可以轻松更改为界面。比方说ITrackableEntity。这样,您可以集中逻辑并自动将所有更改记录到实现特定接口的所有实体。接口本身没有任何特定的属性。

    public override int SaveChanges()
    {
        foreach (var entry in ChangeTracker.Entries<ITrackableClient>())
        {
            if (entry.State == EntityState.Modified)
            {
                // Same code as example above.
            }
        }

        return base.SaveChanges();
    }
Run Code Online (Sandbox Code Playgroud)

另外,看看eranga的很棒的订阅建议,而不是实际覆盖SaveChanges()。


小智 5

对我来说,我在使用 AutoMapper 和 .NET 6 时遇到了这个问题。为了解决这个问题,我将代码更改为:

DbItem? result = await _dbContext.DbItems.FirstOrDefaultAsync(t => t.Id == id);
if (result == null)
{
    return null;
}
DbItem mappedItem = _mapper.Map<DbItem>(dto);  //problematic line
var updatedItem = _dbContext.DbItems.Update(mappedItem);
Run Code Online (Sandbox Code Playgroud)

到:

DbItem? result = await _dbContext.DbItems.FirstOrDefaultAsync(t => t.Id == id);
if (result == null)
{
    return null;
}
_mapper.Map(dto, result);  //the fix
var updatedItem = _dbContext.DbItems.Update(result);
Run Code Online (Sandbox Code Playgroud)

有问题的行创建了一个具有相同键值的新 DbItem,从而导致了问题。修复线将 DTO 中的字段映射到原始 DbItem。


小智 5

也有这个问题..通过在保存之前先取消跟踪旧实体来解决它。

 public async Task<int> Update<T>(T entity) where T : BaseEntity
{
    entity.UpdatedAt = DateTime.UtcNow;

    // Untrack previous entity version
    var trackedEntity = this.context.Set<T>()
        .SingleOrDefaultAsync(e => e.Id == entity.Id);
    this.context.Entry<T>(await trackedEntity).State = EntityState.Detached;

    // Track new version
    this.context.Set<T>().Attach(entity);
    this.context.Entry<T>(entity).State = EntityState.Modified;

    await this.context.SaveChangesAsync();

    return entity.Id;
}
Run Code Online (Sandbox Code Playgroud)


归档时间:

查看次数:

47356 次

最近记录:

6 年 前