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抛出并发异常吗?
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)
| 归档时间: |
|
| 查看次数: |
944 次 |
| 最近记录: |