【问题标题】:The instance of entity type cannot be tracked because another instance of this type with the same key is already being tracked无法跟踪实体类型的实例,因为已在跟踪具有相同键的该类型的另一个实例
【发布时间】:2016-08-19 18:13:00
【问题描述】:

我有一个服务对象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
}

这是我调用这个方法的地方:

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);
}

这里是GetAsNotTracking 方法:

public Client GetAsNoTracking(long id)
{
    return GetClientQueryableObject(id).AsNoTracking().FirstOrDefault();
}

Fetch方法:

public object Fetch(string id)
{
   long fetchId;
   long.TryParse(id, out fetchId);
   return GetClientQueryableObject(fetchId).FirstOrDefault();
}

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);
 }

有什么想法吗?

我查看了以下文章/讨论。无济于事:ASP.NET GitHub Issue 3839

更新:

以下是对GetAsNoTracking 的更改:

public Client GetAsNoTracking(long id)
{
    return GetClientQueryableObjectAsNoTracking(id).FirstOrDefault();
}

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();
}

【问题讨论】:

  • 我认为您将AsNoTracking() 放在哪里可能有点晚了,因为_context.Clients 已经被跟踪了。尝试将您的AsNoTracking() 直接放在_context.Clients 电话上。你所有的lncludes 也应该是AsNoTracking(),除非你打算跟踪它们。
  • @RobertHarvey 正在尝试。会让你知道的。
  • 我能问一下你为什么要得到一个原始的和修改过的吗?为什么不能直接获取原件,进行修改,然后调用更新?我看不出修改的意义?
  • @garethb 我必须做一个审计日志。所以我有一个方差检查器来检查两个对象的变化并将其记录到数据库中。
  • 如果您对修改后的和原始的都调用 GetAsNoTracking 会发生什么?

标签: c# asp.net entity-framework


【解决方案1】:

在不覆盖 EF 跟踪系统的情况下,您还可以分离“本地”条目并在保存之前附加更新的条目:

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

// check if local is not null 
if (local != null)
{
    // detach
    _context.Entry(local).State = EntityState.Detached;
}
// set Modified flag in your entry
_context.Entry(entryToUpdate).State = EntityState.Modified;

// save 
_context.SaveChanges();

更新: 为了避免代码冗余,你可以做一个扩展方法:

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;
}

我的IIdentifier 接口只有一个Id 字符串属性。

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

_context.DetachLocal(tmodel, id);
_context.SaveChanges();

【讨论】:

  • 我试图在一个通用存储库中执行此操作,在该存储库中我无权访问实体的主键(可以具有任何名称和任何类型)。在这种情况下有没有办法找到本地跟踪的实体?显然 EF 可以在内部完成,但我找不到它的公共 API。
【解决方案2】:
public async Task<Product> GetValue(int id)
    {
        Product Products = await _context.Products.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id);
        return Products;
    }

AsNoTracking()

【讨论】:

  • 为我工作...!
  • 工作就像一个魅力。
【解决方案3】:

在我的例子中,表的 id 列没有设置为 Identity 列。

【讨论】:

  • 你救了我 :) 我有我的密钥集,但是错误(Id 和 Level 而不是只有 Id),因此我得到了这个错误异常。
  • 你是救生员!
【解决方案4】:

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

EF 将自动为您跟踪更改。使用内置逻辑怎么样?

在您的DbContext 中覆盖SaveChanges()。

    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();
    }

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

Entity Framework 6: audit/track changes

Implementing Audit Log / Change History with MVC & Entity Framework

编辑: 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();
    }

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

【讨论】:

  • 我实际上看过这些例子;我遇到的一些问题是需要在某些表上启用它。那里使用的用例有一个广泛的实现。感谢您的反馈。真的很感激。
  • 这可能会到达某个地方,感谢您的输入。不过,只有一个查询——我如何获得授权用户?我需要它用于审核日志。我可以使用User.GetUserId()吗?
  • 这取决于您的应用程序以及您如何验证用户身份。是 WPF、ASP、MVC 等吗?我通常将其抽象为可以检索当前用户的管理器或实用程序。 User.GetUserId() 听起来像是 Controller 中可用的东西,它是 HttpContext.User 的缩写。所以你需要HttpContext。这里有一些可以帮助你的东西:stackoverflow.com/questions/1918283/…, stackoverflow.com/questions/5898170/…
  • HttpContext.Current.User.Identity.GetUserId();
【解决方案5】:

我在设置 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;
}

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

  • 并在方法底部的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;
       }
    }

控制器放置方法

.ConvertTo&lt;&gt;()方法是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();
}

和测试:

    [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();
    }

【讨论】:

    【解决方案6】:

    在 EF 核心中 - 还要确保您没有同时设置外键和外键导航属性。设置键和属性时出现此错误。

    例如

            new VerificationAccount()
            {
                Account = konto_1630,
                VerificationRowType = VerificationRowType.Template,
                // REMOVED THE LINE BELOW AND THE ERROR WENT AWAY
                //VerificationAccount = verificationAccounts.First(x => x.Account == konto_1630),
                VerificationId = verificationId
            }
    

    【讨论】:

      【解决方案7】:

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

      _context.ChangeTracker.Clear()
      

      来自Microsoft Documentation

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

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

      这种方法总是比分离每个被跟踪的 实体。分离实体是一个缓慢的过程,可能有副作用 效果。这种方法在清除所有跟踪的 上下文中的实体。

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

      【讨论】:

        【解决方案8】:

        您可以在保存后将实体设置为分离,如下所示:

        public async Task<T> Update(int id, T entity)
            {
                entity.Id = id;
                _ctx.Set<T>().Update(entity);
                await _ctx.SaveChangesAsync();
                _ctx.Entry(entity).State = EntityState.Detached; //detach saved entity
                return entity;
            }
        

        【讨论】:

        • 为我工作。我在这个应用程序中使用 .Net Core 3.0。我还必须在我的存储库的构造函数中设置它以使其工作:DbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
        【解决方案9】:

        我的后台服务收到此错误。我解决了创建新范围的问题。

                        using (var scope = serviceProvider.CreateScope())
                        {
                              // Process
                        }
        

        【讨论】:

          【解决方案10】:

          啊,这让我很着急,我花了很多时间进行故障排除。问题是我的测试是在 Parellel(XUnit 的默认设置)中执行的。

          为了让我的测试按顺序运行,我用这个属性装饰了每个类:

          [Collection("Sequential")]
          

          我就是这样解决的: Execute unit tests serially (rather than in parallel)


          我用 GenFu 模拟了我的 EF In Memory 上下文:

          private void CreateTestData(TheContext dbContext)
          {
              GenFu.GenFu.Configure<Employee>()
                 .Fill(q => q.EmployeeId, 3);
              var employee = GenFu.GenFu.ListOf<Employee>(1);
          
              var id = 1;
              GenFu.GenFu.Configure<Team>()
                  .Fill(p => p.TeamId, () => id++).Fill(q => q.CreatedById, 3).Fill(q => q.ModifiedById, 3);
              var Teams = GenFu.GenFu.ListOf<Team>(20);
              dbContext.Team.AddRange(Teams);
          
              dbContext.SaveChanges();
          }
          

          在创建测试数据时,据我所知,它存在于两个范围内(一次在团队测试运行时的员工测试中):

          public void Team_Index_should_return_valid_model()
          {
              using (var context = new TheContext(CreateNewContextOptions()))
              {
                  //Arrange
                  CreateTestData(context);
                  var controller = new TeamController(context);
          
                  //Act
                  var actionResult = controller.Index();
          
                  //Assert
                  Assert.NotNull(actionResult);
                  Assert.True(actionResult.Result is ViewResult);
                  var model = ModelFromActionResult<List<Team>>((ActionResult)actionResult.Result);
                  Assert.Equal(20, model.Count);
              }
          }
          

          用这个顺序集合属性包装两个测试类已经清除了明显的冲突。

          [Collection("Sequential")]
          

          其他参考资料:

          https://github.com/aspnet/EntityFrameworkCore/issues/7340
          EF Core 2.1 In memory DB not updating records
          http://www.jerriepelser.com/blog/unit-testing-aspnet5-entityframework7-inmemory-database/
          http://gunnarpeipman.com/2017/04/aspnet-core-ef-inmemory/
          https://github.com/aspnet/EntityFrameworkCore/issues/12459
          Preventing tracking issues when using EF Core SqlLite in Unit Tests

          【讨论】:

            【解决方案11】:

            我遇到了同样的问题,但问题很愚蠢,我错误地给出了错误的关系,我给出了 2 个 ID 之间的关系。

            【讨论】:

              【解决方案12】:
              public static void DetachEntity<T>(this DbContext dbContext, T entity, string propertyName) where T: class, new()
              {
                 try
                 {
                    var dbEntity = dbContext.Find<T>(entity.GetProperty(propertyName));
                    if (dbEntity != null)
                        dbContext.Entry(dbEntity).State = EntityState.Detached;
                    dbContext.Entry(entity).State = EntityState.Modified;
                 }
                 catch (Exception)
                 {
                      throw;
                 }
              }
              
              
               public static object GetProperty<T>(this T entity, string propertyName) where T : class, new()
               {
                  try
                  {
                      Type type = entity.GetType();
                      PropertyInfo propertyInfo = type.GetProperty(propertyName);
                      object value = propertyInfo.GetValue(entity);
                      return value;
                  }
                  catch (Exception)
                  {
                       throw;
                  }
               }
              

              我做了这两种扩展方法,效果很好。

              【讨论】:

                【解决方案13】:

                无法更新数据库行。我面临同样的错误。现在使用以下代码:

                _context.Entry(_SendGridSetting).CurrentValues.SetValues(vm);
                await _context.SaveChangesAsync();
                

                【讨论】:

                  【解决方案14】:

                  如果您有重复的条目/实体并运行 SaveChanges(),则可能会出现此错误消息。

                  【讨论】:

                  • 这不能回答问题。
                  【解决方案15】:

                  我自己也遇到过这个问题。实体框架跟踪您插入数据库的每个对象。因此,当您插入同一对象的重复记录并更改了几个字段时,EF 将通过此错误。我通过对我要重新插入的对象进行深度克隆来解决它,并且它通过了。

                      public static T DeepClone<T>(this T a)
                      {
                          using (MemoryStream stream = new MemoryStream())
                          {
                              BinaryFormatter formatter = new BinaryFormatter();
                              formatter.Serialize(stream, a);
                              stream.Position = 0;
                              return (T)formatter.Deserialize(stream);
                          }
                      }
                  

                  然后:

                  var cloned = objectYouAreTryingToReinsert.deepClone();
                  context.objects.add(cloned);
                  await context.SaveChangesAsync();
                  

                  【讨论】:

                    【解决方案16】:

                    如果您设置了两个或多个具有“Id”的表或具有相同列名的列名,最简单的方法是更改​​上下文类中的 OnModelCreating 方法。

                    在这种情况下,我必须将“Id”更改为“AbandonedCartId”并告诉实体该对象的列名称为“Id”

                    entity.Property(e => e.AbandonedCartId).HasColumnName("Id");
                    

                    例子

                    public partial class AbandonedCart
                        {
                            public int AbandonedCartId { get; set; }
                            public double? CheckoutId { get; set; }
                            public int? AppId { get; set; }
                            public double? CustomerId { get; set; }
                            
                        }
                    
                    protected override void OnModelCreating(ModelBuilder modelBuilder)
                     {
                                modelBuilder.Entity<AbandonedCart>(entity =>
                                {
                                    entity.Property(e => e.AbandonedCartId).HasColumnName("Id");
                                    entity.Property(e => e.CreatedAt).HasColumnType("datetime");
                    
                                    entity.HasOne(d => d.App)
                                        .WithMany(p => p.AbandonedCart)
                                        .HasForeignKey(d => d.AppId)
                                        .HasConstraintName("FK_AbandonedCart_Apps");
                                });
                    }
                    

                    【讨论】:

                      【解决方案17】:

                      我之前遇到过同样的问题,并通过将AddDbContext 替换为服务容器的AddDbContextFactory 来解决它。

                      这就是我解决问题的方法:

                      我没有注册AddDbContext,而是注册了AddDbContextFactory,如下所示:

                      public void ConfigureServices(IServiceCollection services)
                      {
                      services.AddDbContextFactory<YourApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("YourDatabaseConnectionString")));
                      }
                      

                      重要提示:

                      不要同时注册AddDbContext和AddDbContextFactory,因为你会得到一个System.AggregateException: 'Some services are not able to be constructed...'exception。请改用AddDbContextFactory。

                      您的ApplicationDbContext 类必须公开一个带有DbContextOptions&lt;YourApplicationDbContext&gt; 参数的公共构造函数,如下所示:

                      public class YourApplicationDbContext : DbContext
                      {
                          public ApplicationDbContext(DbContextOptions<YourApplicationDbContext> options): base(options){}
                      }
                      

                      DbContextFactory 工厂可以像这样通过构造函数注入来使用:

                      private readonly IDbContextFactory<YourApplicationDbContext> dbContextFactory;
                      
                      public YourConstructor(IDbContextFactory<YourApplicationDbContext> dbContextFactory)
                      {
                          dbContextFactory = dbContextFactory;
                      }
                      

                      或

                      public YourController(IDbContextFactory<YourApplicationDbContext> dbContextFactory)
                      {
                          dbContextFactory = dbContextFactory;
                      }
                      

                      然后可以使用注入的工厂在服务代码中构造DbContext 实例,如下所示:

                         using (var context = dbContextFactory.CreateDbContext())
                              {
                                  // your database CRUD code comes in here... for example:
                                  context.DatabaseTable.Update(suppliedModel);
                                  await context.SaveChangesAsync();
                              }
                      

                      您何时可以考虑此选项: 注册工厂而不是直接注册上下文类型可以让您轻松创建新的 DbContext 实例。也推荐用于 Blazor 应用程序。

                      我希望这对面临此问题的人有所帮助。干杯!

                      【讨论】:

                        【解决方案18】:

                        对我来说,我在使用 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);
                        

                        收件人:

                        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);
                        

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

                        【讨论】:

                          【解决方案19】:

                          如果您的数据每更改一次,您会注意到不要跟踪表。例如,使用 tigger 更新某些表的 id ([key])。如果您跟踪,您将获得相同的 id 并得到问题。

                          【讨论】:

                          • 有意思,你能解释得更好吗?
                          猜你喜欢
                          • 1970-01-01
                          • 1970-01-01
                          • 2022-01-02
                          • 1970-01-01
                          • 2023-01-03
                          • 2018-06-20
                          • 1970-01-01
                          • 1970-01-01
                          相关资源
                          最近更新 更多