繁体   English   中英

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

[英]The instance of entity type cannot be tracked because another instance of this type with the same key is already being tracked

我有一个服务 Object 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 问题 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();
}

在不覆盖 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();
public async Task<Product> GetValue(int id)
{
    Product Products = await _context.Products
        .AsNoTracking().FirstOrDefaultAsync(x => x.Id == id);
    return Products;
}

AsNoTracking()

重要提示:在许多情况下,使用AsNoTracking()就像一种魅力。 但是,当对Moq等框架使用模拟或存根策略时,单元测试将失败。 MSDN官方解释:

但是,无法正确DbSet查询功能,因为查询是通过 LINQ 运算符表示的,这些运算符是对IQueryable的静态扩展方法调用。 结果,当有些人谈论“ DbSet ”时,他们真正的意思是他们创建了一个由内存中集合支持的DbSet ,然后在内存中针对该集合评估查询运算符,就像一个简单的IEnumerable一样。 而不是模拟,这实际上是一种假的,其中内存中的集合取代了真实的数据库。

尝试使用模拟策略进行单元测试时避免将来出现问题。 Microsoft 文档中所述,还有其他单元测试方法,例如实现存储库模式。

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

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

_context.ChangeTracker.Clear()

来自微软文档

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

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

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

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

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

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

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

实体框架 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();
    }

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

我在设置 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<>()方法是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();
    }

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

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

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

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

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

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

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

 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;

        }

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

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

[Collection("Sequential")]

这就是我的解决方法: 串行执行单元测试(而不是并行执行)


我用 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 在内存数据库中不更新记录
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
在单元测试中使用 EF Core SqlLite 时防止跟踪问题

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

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

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

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

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

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

如果您设置了两个或多个具有“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");
            });
}

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

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

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

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

重要提醒:

不要一起注册AddDbContextAddDbContextFactory因为你会得到一个System.AggregateException: 'Some services are not able to be constructed...' exception 请改用AddDbContextFactory

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

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 应用程序。

我希望这可以帮助面临这个问题的人。 干杯!

我自己也有这个问题。 实体框架跟踪您插入数据库的每个对象。 因此,当您插入同一对象的重复记录并更改了几个字段时,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();

我刚刚遇到了这个问题,我正在使用具有工作单元模式的存储库。 经过大量搜索,我找到了解决方案。 在开始编码之前,请阅读此参考。 它对于理解很多基本概念很有用,然后你就不需要阅读本节中的所有评论。 一个简单的解释是:创建对象后,如果你已经做了context.SaveChanges() ,它的EntityState将是Unchanged ,如果没有,它的 EntityState 将是Added 无论如何,该实体将在“TrackedEntitiesList”中进行跟踪,这些可以在context.ChangeTracker.Entries()中访问。 当您尝试更新时,EF Core 会尝试(再次)在此“TrackedEntitiesList”上添加实体,其中EntityStateModified 但是已经有另一个实体,具有相同的[Key]Id 因此,它无法更新实体。 因此,解决起来很简单:只需分离您的实体或清除所有内容。 正如MSDN中提到的,清除所有轨道是推荐的方式:

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

在调用dbSet.Update(updatedEntity)之前,请执行: context.ChangeTracker.Clear() 直到现在,它就像一个魅力!

通用存储库中的 FullCode 示例:

private readonly AppDbContext _context;
private readonly DbSet<TEntity> _dbSet;

public ThisIsAConstructor(AppDbCOntext context)
{
    _context = context;
    _dbSet = context.Set<TEntity>();
}

public async Task UpdateAsync(TEntity updatedEntity)
{
    _context.ChangeTracker.Clear();

    _dbSet.Update(updatedEntity);
    await Task.CompletedTask;
}

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM