在某些情况下,OptimisticConcurrencyException在实体框架中不起作用

时间:2010-12-09 20:02:50

标签: c# asp.net entity-framework concurrency poco

更新(2010-12-21):根据我一直在做的测试完全重写了这个问题。此外,这曾经是一个POCO特定的问题,但事实证明我的问题不一定是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 Profiler,看起来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


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

由于

4 个答案:

答案 0 :(得分:30)

<强>解释

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

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

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

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

<强>解决方案/解决方法

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

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

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

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

答案 1 :(得分:6)

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

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

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

答案 2 :(得分:3)

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

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

答案 3 :(得分:0)

我修改了@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<IUpdateEntry>()
                    {
                        entry.GetInfrastructure()
                    });
            }
        }
    }
}

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

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