Entity Framework Core:手动更改实体的 RowVersion 对并发处理没有影响

Pav*_*vel 0 c# concurrency entity-framework-core angular

我遇到并发问题。当两个用户尝试依次保存同一记录时,EF 允许他们这样做。

我的前端是 Angular,在后端我使用 EF Core 7 和数据库优先的方法。

当两个用户加载该行时,它包含一个名为Rv( ) 的属性,该属性在后端和前端中RowVersion存储为 a 。byte[]ArrayBuffer

C# 后端:

public partial class User
{
    public int Id { get; set; }
    public byte[] Rv { get; set; }
    ...
}
Run Code Online (Sandbox Code Playgroud)

Angular 前端:

export interface User {
    id: number;
    Rv: ArrayBuffer;
    ...
}
Run Code Online (Sandbox Code Playgroud)

当用户A更新该行并单击“保存”时,后端运行以下代码:

Chrome DevTools 显示在 RV 的“网络”选项卡中发送的以下内容:“AAAAAAAAEBE=”

public async Task<ActionResult> Save(User dto)
{
    var entity = await context.Users.FindAsync(id); 
    // Here entity's RowVersion = byte[8] = 0,0,0,0,0,0,16,17

    mapper.Map(dto, entity); // Updates all properties on Entity using AutoMapper
    // Now entity's RowVersion = byte[8] = 0,0,0,0,0,0,16,17

    await context.SaveChangesAsync();
    // SQL GENERATED: UPDATE [Users] SET LastName = 'ABC' WHERE [Id] = 22 AND [RV] = '0x0000000000001011' 

    // Now entity's RowVersion = byte[8] = 0,0,0,0,0,0,16,18 
}
Run Code Online (Sandbox Code Playgroud)

当用户B更新记录并点击保存时,后端运行以下代码:

Chrome DevTools 显示在 RV 的“网络”选项卡中发送的以下内容:“AAAAAAAAEBE=”

public async Task<ActionResult> Save(User dto)
{
    var entity = await context.Users.FindAsync(id); 
    // Here entity's RowVersion = byte[8] = 0,0,0,0,0,0,16,18

    mapper.Map(dto, entity); // Updates all properties on Entity using AutoMapper
    // Now entity's RowVersion = byte[8] = 0,0,0,0,0,0,16,17

    await context.SaveChangesAsync();
    // SQL GENERATED: UPDATE [Users] SET LastName = 'XYZ' WHERE [Id] = 22 AND [RV] = '0x0000000000001012'  

    // Now entity's RowVersion = byte[8] = 0,0,0,0,0,0,16,19
}
Run Code Online (Sandbox Code Playgroud)

当运行DbScaffold对数据库进行逆向工程时,以下 Fluent 配置被添加到DbContext

entity.Property(e => e.Rv)
      .IsRequired()
      .IsRowVersion()
      .IsConcurrencyToken()
      .HasColumnName("RV");
Run Code Online (Sandbox Code Playgroud)

我可以看到 AutoMapper 正确地将Rv实体上的属性映射/更改为来自 DTO(请求)的属性,但它似乎对生成的 SQL 没有影响,因为它没有反映此更改。

这是在 Entity Framework Core 中保存之前更新的错误方法吗RowVersion

EF Core 不应该在更改期间检测SaveChanges()RowVersion抛出并发异常吗?

Iva*_*oev 7

EF Core乐观并发文档主题包含以下说明:

在 EF Core 中,乐观并发是通过将属性配置为并发令牌来实现的。当查询实体时,会加载并跟踪并发令牌 - 就像任何其他属性一样。然后,当在 期间执行更新或删除操作时SaveChanges(),会将数据库上的并发令牌的值与 EF Core 读取的原始值进行比较。

最重要的是“数据库上并发令牌的值与原始值进行比较”

这解释了您看到的生成WHERE条件。然而,“当实体被查询时”“由 EF Core 读取”并不完全正确,因为这些只是原始值更新时的一些情况。

EF Core 更改跟踪器跟踪属性的 2 个值 -当前值原始值当前基本上是对象的属性值。然而,原始版本纯粹在更改跟踪器内维护,并在从数据库读取实体后或成功更新后更新为当前版本,或者可以手动设置。

请注意,原始值必须与数据库值而不是当前值进行比较,因为即使它是从数据库读取的,它也是执行读取查询时的快照,并且此后数据库可能已更改。

综上所述,应该清楚的是,您需要设置并发令牌属性的原始值(在您的情况下是行版本),您发送到前端并从前端接收未经修改的值。

AutoMapper 在这种情况下无法提供帮助,因为它所做的相当于手动设置当前值。此外,它无法访问所需的数据库上下文元数据/更改跟踪器。也无法知道(对于 EF Core 本身也是如此)对象内部的值必须被视为原始值,并且它最终所能做的就是忽略它(正如 EF Core 所IsRowRomber暗示的那样ValueGeneratedOnAddOrUpdate)。

您是唯一拥有该信息的人,因此您必须手动将其提供给 EF Core。它可以通过多种方式完成,所有方式都需要访问实体的更改跟踪器条目,并在设置常规属性之后和“SaveChanges”之前调用。

例如

context.Entry(entity).OriginalValues.SetValues(new { entity.Rv });
Run Code Online (Sandbox Code Playgroud)

或者

context.Entry(entity).OriginalValues[nameof(entity.Rv)] = entity.Rv;
Run Code Online (Sandbox Code Playgroud)

或者(可能是最自然的)

context.Entry(entity).Property(e => e.Rv).OriginalValue = entity.Rv;
Run Code Online (Sandbox Code Playgroud)

甚至(当当前值已经设置为原始值时)

var rv = context.Entry(entity).Property(e => e.Rv);
rv.OriginalValue = rv.CurrentValue;
Run Code Online (Sandbox Code Playgroud)