C# 跨作用域DbContext的并发性

C# 跨作用域DbContext的并发性,c#,entity-framework,asynchronous,asp.net-core,concurrency,C#,Entity Framework,Asynchronous,Asp.net Core,Concurrency,我有一个标准的ASP.NET Core 2.0 Web API,其中包含标准的DI DbContext: services.AdDbContext<TestContext>( ... ); 所以它基本上只是以1异步递增列中的值。ITestRepository被添加到国际奥委会的服务中,并在需要时注入 情景: 用户A调用API,获取TestContext的新实例,现在调用IncrementCounter方法 在之前,savechangessync被称为a用户B调用了API,获得了新的

我有一个标准的ASP.NET Core 2.0 Web API,其中包含标准的DI DbContext:

services.AdDbContext<TestContext>( ... );
所以它基本上只是以1异步递增列中的值。ITestRepository被添加到国际奥委会的服务中,并在需要时注入

情景:

用户A调用API,获取TestContext的新实例,现在调用IncrementCounter方法

之前,savechangessync被称为a用户B调用了API,获得了新的TestContext,并调用了IncrementCounter方法

现在用户A将值1保存到列中,用户B也认为它应该保存值1,但实际上应该是2

即使在阅读了相关文档之后,我仍然不确定如何保证IncrementCounter方法正确递增,即使多个用户使用自己的TestContext实例调用API

我可能比我应该弄糊涂了,但有人能澄清一下如何处理同一上下文的不同实例之间的并发性吗

相关文件:

更新1


正如相关文档所解释的,我曾考虑在一个新的“rowVersion”属性上实现[TimeStamp]数据注释,然后捕获DbConcurrencyException,但这是否解决了一个问题,即它是一个有范围的DbContext,因此是试图保存ChangesAsync()的不同上下文?

这实际上与对象生命周期、作用域或其他方面没有任何关系。并发就是并发。执行诸如递增计数器之类的操作本质上不是线程安全的,因此必须使用锁或乐观并发,就像执行任何其他线程不安全的任务一样。数据库锁通常不受欢迎,因此乐观并发肯定是首选方法

这在数据库级别上的工作原理是,您实际上有一个timestamp列,它会随着每次更改而更新。EF非常聪明,可以监视此列,如果它在更新时的值与第一次查询行时的值不同,则它将中止更新。此时,代码捕获异常并通过重新查询行并尝试再次更新来处理它

例如,用户A和用户B试图同时保存值1。假设用户B提前几毫秒到达并成功更新了列。然后还将更新时间戳列。当用户A的更新到达时,更新失败,因为时间戳列不再匹配。这会跳回EF,在EF中抛出一个
DbUpdateConcurrencyException
。您的代码捕获此异常,并通过重新查询行(其中列现在是1,而不是0(并获取最新的时间戳))来响应,递增为2,然后再次尝试保存。这一次,没有其他用户做任何事情,所以它成功地通过了。但是,您还可能有另一个并发异常。在这种情况下,您需要冲洗并重复,最终尝试将3保存到列中,以此类推

您已经链接到问题本身中的所有相关文档,因此我建议您只需花一些时间仔细阅读所有这些内容并真正理解它。您需要向存储递增值的实体添加新属性:

[Timestamp]
public byte[] Version { get; set; }

这样(显然在您迁移之后),当发生冲突时,EF将开始如上所述在save上抛出异常。为了捕捉和处理这些,我建议使用一个类似的库。它允许您设置重试策略,使处理重复查询增量保存逻辑变得简单。

您可能应该使用数据库事务来锁定正在修改的行:)是的,这或多或少就是我的更新1所描述的,但我只是对框架在不同的上下文调用时如何不同感到困惑。您为什么不使用表中的自动递增字段?@DrewB非常好的一点-我创建了一个记录功能,可以统计不同的统计数据。因此,我希望每个“统计”都有一个计数器,例如更新列值,而不是有大量的行(我需要通过其他SQL进行检索)。感谢您的全面回答。我同意你所说的一切——我很可能把自己和上下文范围及其对并发性的影响搞混了。所以我的问题似乎完全在数据库级别上。我怎样才能引发这个DBUpdateConcurrencyException呢?我可以创建一个小的控制台应用程序来执行一些线程。在打第二个电话时睡觉吗?这不是你要做的事情。您只需将timestamp属性添加到实体类中,并将更改迁移到数据库中。一旦该列存在于表中,EF就会自动开始采用乐观并发检查。在尝试保存和处理此异常时,您只需做好接收此异常的准备。我想在收到异常的地方做一个测试,以便正确理解它。这可能超出了这个问题的范围。啊。好的,如果您正在进行单元测试,您可以简单地存根
SaveChanges
来抛出异常。为了确保最终成功,您需要使用首选模拟框架的某些功能,该功能允许您根据调用的数量改变存根。例如,Moq具有
回调
,您可以使用它仅在第一次抛出异常(通过保留调用计数器),否则返回适当的值(对于
SaveChanges
,它是成功操作的数量)。
[Timestamp]
public byte[] Version { get; set; }