c# - 身份用户管理器 DeleteAsync DbUpdateConcurrencyException
问题描述
我正在尝试通过 webapi 后面的 aspnetcore.identity UserManager 删除用户。
[HttpPost("Delete", Name = "DeleteRoute")]
[Authorize(Roles = "SuperUser")]
public async Task<IActionResult> DeleteAsync([FromBody] User user)
{
Console.WriteLine("Deleting user: " + user.Id);
try {
await _userManager.DeleteAsync(user);
return Ok();
} catch(Exception e) {
return BadRequest(e.Message);
}
}
这抛出一个DbUpdateConcurrencyException
Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.
at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.ThrowAggregateUpdateConcurrencyException(Int32 commandIndex, Int32 expectedRowsAffected, Int32 rowsAffected)
at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.ConsumeResultSetWithoutPropagationAsync(Int32 commandIndex, RelationalDataReader reader, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.ConsumeAsync(RelationalDataReader reader, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(DbContext _, ValueTuple`2 parameters, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(IReadOnlyList`1 entriesToSave, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.
at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.ThrowAggregateUpdateConcurrencyException(Int32 commandIndex, Int32 expectedRowsAffected, Int32 rowsAffected)
at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.ConsumeResultSetWithoutPropagationAsync(Int32 commandIndex, RelationalDataReader reader, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.ConsumeAsync(RelationalDataReader reader, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(DbContext _, ValueTuple`2 parameters, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(IReadOnlyList`1 entriesToSave, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
我知道这个异常通常表示竞争条件,但我不明白为什么会发生这种情况。
难道我做错了什么?
编辑
我发布的用户对象如下所示:
"User": {
"Email": "",
"FirstName": "",
"LastName": "",
"Gender": "",
"Affiliation": {
"isStudent": true,
"isEmployee": false
}
...
}
解决方案
Entity Framework Core 使用乐观并发:
在乐观并发模型中,如果在用户从数据库接收到值之后,另一个用户在第一个用户尝试修改该值之前修改了该值,则认为发生了违规。
将此与悲观并发进行对比:
...在悲观并发模型中,更新行的用户会建立锁。在用户完成更新并释放锁之前,没有其他人可以更改该行。
为了实现乐观并发,IdentityUser
该类包含一个ConcurrencyStamp
属性(以及数据库中的相应列),它是 GUID 的字符串表示形式:
public virtual string ConcurrencyStamp { get; set; } = Guid.NewGuid().ToString();
每次将用户保存到数据库时,ConcurrencyStamp
都会将其设置为新的 GUID。
以删除用户为例,DELETE
发送到服务器的 SQL 语句的简化版本可能如下所示:
DELETE FROM dbo.AspNetUsers
WHERE Id = '<USER_ID>' AND ConcurrencyStamp = '<CONCURRENCY_STAMP>'
CONCURRENCY_STAMP
当上述 SQL 语句中的值与给定用户在数据库中存储的值不匹配时,您会收到错误消息。这确保了如果您从数据库中检索用户(其中包含特定的ConcurrencyStamp
),则只有在其他地方没有进行其他更改时(因为您提供的ConcurrencyStamp
值与数据库中存在的相同),您才能将更改保存到数据库中。
从ConcurrencyStamp
上面的定义可以看出,该属性默认为一个新的GUID
——每次IdentityUser
创建一个(或子类)时,它都会获得一个新ConcurrencyStamp
值。在您的示例中,通过将User
传递给您的DeleteAsync
操作,ASP.NET Core 模型绑定首先创建一个新实例,User
然后设置 JSON 有效负载中存在的属性。ConcurrencyStamp
由于有效负载中没有值,User
最终将得到一个与数据库中的值不匹配的新值。 ConcurrencyStamp
为避免此问题,您可以将该ConcurrencyStamp
值添加到从客户端发送的有效负载中。但是,我不会推荐这个。解决此问题的最简单和最安全的方法是将Id
的User
作为有效负载发送,使用该实例检索User
自身_userManager.FindByIdAsync
,然后使用该实例执行删除。这是一个例子:
[HttpPost("Delete/{id}", Name = "DeleteRoute")]
[Authorize(Roles = "SuperUser")]
public async Task<IActionResult> DeleteAsync(string id)
{
Console.WriteLine("Deleting user: " + id);
try {
var user = await _userManager.FindByIdAsync(id);
if(user == null)
// ...
await _userManager.DeleteAsync(user);
return Ok();
} catch(Exception e) {
return BadRequest(e.Message);
}
}
推荐阅读
- tornadofx - replaceWith() 方法的 sizeToScene 属性切割顶部并增加窗口的底部
- javascript - Wordpress - JS 仅选择我点击关注的当前用户
- react-native - 如何使不规则设计响应原生反应?
- python - 如何通过 Python 脚本运行 sikuli 脚本
- python-3.x - 是否可以选择在数组中查找数组?
- vue.js - 在本地 https 上运行 nuxt – nuxt.config.js 的问题
- azerothcore - 未找到艾泽拉斯核心模块配置
- python - Context_processor 未定义“”类/属性(错误)
- sql - 在postgres中如何找到最短的字符串数组
- c - 在包含字符串的 typedef 结构中使用指针