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

时间:2016-04-26 05:37:23

标签: 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
}

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

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

15 个答案:

答案 0 :(得分:18)

如果不覆盖EF跟踪系统,您也可以分离本地&#39;输入并在保存之前附上您更新的条目:

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

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

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

答案 1 :(得分:5)

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

EF会自动跟踪您的变化。如何利用内置的逻辑?

SaveChanges()中的Ovverride 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();
    }

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

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()。

答案 2 :(得分:5)

设置xUnit测试时,我遇到了同样的问题(EF内核)。 在测试中,对我来说“修复”的是设置种子数据后遍历变更跟踪器实体。

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

答案 3 :(得分:4)

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

AsNoTracking()

答案 4 :(得分:1)

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

答案 5 :(得分:0)

如果您的数据每隔更改一次,您会发现您没有跟踪表。例如,使用tigger的某些表更新ID([key])。如果进行跟踪,您将获得相同的ID并得到问题。

答案 6 :(得分:0)

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

答案 7 :(得分:0)

嗯,这吸引了我,我花了很多时间对其进行故障排除。问题是我的测试是在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

答案 8 :(得分:0)

我遇到了同样的问题,但问题非常愚蠢,我错误地给了我错误的关系,给了我两个Id之间的关系。

答案 9 :(得分:0)

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

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

答案 10 :(得分:0)

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

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

答案 11 :(得分:0)

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

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

答案 12 :(得分:0)

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

答案 13 :(得分:0)

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

Argument of type 'unknown' is not assignable to parameter of type 'SetStateAction<number | undefined>'.
  Type 'unknown' is not assignable to type '(prevState: number | undefined) => number | undefined'.

然后:

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

答案 14 :(得分:0)

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