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

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

返回文章页面

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

有什么想法吗?

我看了下面的文章/讨论。没有用: NET GitHub 第3839期

更新:

以下是对 GetAsNoTracking的修改:

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

返回文章页面

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();
}
247035 次浏览

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

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 ()的伟大建议。

无需覆盖 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();

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

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

啊,这让我很不爽,我花了很多时间来解决这个问题。问题是我的测试是在 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);
}
}

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

[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 时防止跟踪问题

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

追踪()

重要提示: 在许多情况下,使用 AsNoTracking()就像一种魅力。然而,当使用模仿或者使用像 Moq这样的框架的存根策略时,单元测试将会失败。来自 MSDN 的官方解释:

但是,正确模仿 DbSet查询功能是不可能的,因为查询是通过 LINQ 操作符表示的,LINQ 操作符是 IQueryable上的静态扩展方法调用。因此,当一些人谈论“模仿 DbSet”时,他们真正的意思是,他们创建一个由内存集合支持的 DbSet,然后根据内存集合评估查询操作符,就像简单的 IEnumerable一样。这实际上是一种赝品,内存中的集合取代了真正的数据库。

当尝试使用模仿策略进行单元测试时,要避免将来出现的问题。还有其他的单元测试方法,如 Microsoft 文档中提到的实现存储库模式。

我面临同样的问题,但问题是非常愚蠢的,我错误地给出了错误的关系,我给出了两个身份之间的关系。

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

控制器的 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();
}

还有测试:

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

我制作了这两个扩展方法,它们工作得非常好。

我从后台服务中得到了这个错误,我解决了这个问题,创建了一个新的范围。

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

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

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

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

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

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

如果已经设置了两个或多个具有“ Id”的表或具有相同列名的列名,那么最简单的方法是在上下文类中更改 OnModelCreate 方法。

在这种情况下,我必须将‘ 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 应用程序。

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

对我来说,这只是修复了问题。在任何更新之前添加这个代码

_context.ChangeTracker.Clear()

来自 Microsoft 文档

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

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

这种方法应该总是优先于分离每个跟踪 分离实体是一个缓慢的过程 这种方法更有效地清除所有跟踪 来自上下文的实体。

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

更新

简单工作单元的微软解释

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

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

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

也有这个问题. . 解决了它通过解除跟踪旧的实体之前保存。

 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;


}


方法分离任何键类型的实体。

public static void Detach<TEntry, TId>(this DbContext context, Func<TEntry, TId> idReader, TId id)
where TEntry : class
where TId : IEquatable<TId>
{
var local = context.Set<TEntry>()
.Local
.FirstOrDefault(entry => idReader(entry).Equals(id));
if (local != null)
{
context.Entry(local).State = EntityState.Detached;
}
}

Guid 键的用法:

dbContext.Detach<EntryType, Guid>(e => e.Id, myEntity.Id);
dbContext.Attach(myEntity);