簡體   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