在某些情况下,OptimisticConcurrencyException在entity framework中不起作用

更新(2010-12-21):根据我一直在做的测试完全重写了这个问题。 此外,这曾经是一个POCO特定的问题,但事实certificate我的问题不一定是POCO特定的。

我正在使用entity framework,我的数据库表中有一个时间戳列,应该用于跟踪乐观并发的更改。 我已将实体设计器中此属性的并发模式设置为“已修复”,并且我得到的结果不一致。 以下是一些简化的场景,它们演示了并发检查在一个场景中工作但在另一个场景中不起作用。

成功抛出OptimisticConcurrencyException:

如果我附加一个断开连接的实体,那么如果存在时间戳冲突,SaveChanges将抛出一个OptimisticConcurrencyException:

[HttpPost] public ActionResult Index(Person person) { _context.People.Attach(person); var state = _context.ObjectStateManager.GetObjectStateEntry(person); state.ChangeState(System.Data.EntityState.Modified); _context.SaveChanges(); return RedirectToAction("Index"); } 

不抛出OptimisticConcurrencyException:

另一方面,如果我从数据库中检索我的实体的新副本,并对某些字段进行部分更新,然后调用SaveChanges(),那么即使存在时间戳冲突,我也不会得到OptimisticConcurrencyException :

  [HttpPost] public ActionResult Index(Person person) { var currentPerson = _context.People.Where(x => x.Id == person.Id).First(); currentPerson.Name = person.Name; // currentPerson.VerColm == [0,0,0,0,0,0,15,167] // person.VerColm == [0,0,0,0,0,0,15,166] currentPerson.VerColm = person.VerColm; // in POCO, currentPerson.VerColm == [0,0,0,0,0,0,15,166] // in non-POCO, currentPerson.VerColm doesn't change and is still [0,0,0,0,0,0,15,167] _context.SaveChanges(); return RedirectToAction("Index"); } 

基于SQL事件探查器,看起来entity framework忽略了新的VerColm(这是时间戳属性),而是使用最初加载的VerColm。 因此,它永远不会抛出OptimisticConcurrencyException。


更新:根据Jan的要求添加其他信息:

请注意,我还在上面的代码中添加了注释,以便与我在执行此示例时在控制器操作中看到的内容一致。

这是更新前我的DataBase中VerColm的值:0x0000000000000FA7

以下是SQL Profiler在执行更新时显示的内容:

 exec sp_executesql N'update [dbo].[People] set [Name] = @0 where (([Id] = @1) and ([VerColm] = @2)) select [VerColm] from [dbo].[People] where @@ROWCOUNT > 0 and [Id] = @1',N'@0 nvarchar(50),@1 int,@2 binary(8)',@0=N'hello',@1=1,@2=0x0000000000000FA7 

请注意,@ 2应该是0x0000000000000FA6,但它是0x0000000000000FA7

这是更新后我的DataBase中的VerColm:0x0000000000000FA8


有谁知道如何解决这个问题? 当我更新现有实体并且存在时间戳冲突时,我希望entity framework抛出exception。

谢谢

说明

您没有在第二个代码示例上获得预期的OptimisticConcurrencyException的原因是由于EF检查并发的方式:

通过查询数据库检索实体时,EF会在查询时记住所有使用ConcurrencyMode.Fixed标记属性的值作为原始未修改的值。

然后更改一些属性(包括Fixed标记的属性)并在DataContext上调用SaveChanges()

EF通过将所有已Fixed标记的db列的当前值与Fixed标记属性的原始未修改值进行比较来检查并发更新。 这里的关键点是EF将时间戳属性的更新视为普通数据属性更新。 你看到的行为是设计的。

解决方案/解决方法

要解决此问题,您有以下选择:

  1. 使用您的第一种方法:不要为您的实体重新查询数据库,而是将重新创建的实体附加到您的上下文。

  2. 将您的时间戳值伪造为当前的db值,以便EF并发检查使用您提供的值,如下所示(另请参阅类似问题的答案 ):

     var currentPerson = _context.People.Where(x => x.Id == person.Id).First(); currentPerson.VerColm = person.VerColm; // set timestamp value var ose = _context.ObjectStateManager.GetObjectStateEntry(currentPerson); ose.AcceptChanges(); // pretend object is unchanged currentPerson.Name = person.Name; // assign other data properties _context.SaveChanges(); 
  3. 您可以通过将时间戳值与已获取时间戳值进行比较来自行检查并发性:

     var currentPerson = _context.People.Where(x => x.Id == person.Id).First(); if (currentPerson.VerColm != person.VerColm) { throw new OptimisticConcurrencyException(); } currentPerson.Name = person.Name; // assign other data properties _context.SaveChanges(); 

这是另一种更通用的方法,适合数据层:

 // if any timestamps have changed, throw concurrency exception var changed = this.ChangeTracker.Entries<>() .Any(x => !x.CurrentValues.GetValue("Timestamp").SequenceEqual( x.OriginalValues.GetValue("Timestamp"))); if (changed) throw new OptimisticConcurrencyException(); this.SaveChanges(); 

它只是检查TimeStamp是否已更改并引发并发exception。

如果首先是EF代码,则使用类似于以下代码的代码。 这会将从db加载的原始TimeStamp更改为UI中的一个,并确保发生OptimisticConcurrencyEception

 db.Entry(request).OriginalValues["Timestamp"] = TimeStamp; 

我修改了@JarrettV解决方案以使用Entity Framework Core。 现在,它正在遍历上下文中的所有已修改条目,并查找标记为并发令牌的属性中的任何不匹配。 适用于TimeStamp(RowVersion):

 private void ThrowIfInvalidConcurrencyToken() { foreach (var entry in _context.ChangeTracker.Entries()) { if (entry.State == EntityState.Unchanged) continue; foreach (var entryProperty in entry.Properties) { if (!entryProperty.IsModified || !entryProperty.Metadata.IsConcurrencyToken) continue; if (entryProperty.OriginalValue != entryProperty.CurrentValue) { throw new DbUpdateConcurrencyException( $"Entity {entry.Metadata.Name} has been modified by another process", new List() { entry.GetInfrastructure() }); } } } } 

我们只需要在保存EF上下文中的更改之前调用此方法:

 public async Task SaveChangesAsync(CancellationToken cancellationToken) { ThrowIfInvalidConcurrencyToken(); await _context.SaveChangesAsync(cancellationToken); }