【问题标题】:Identity Usermanager DeleteAsync DbUpdateConcurrencyException身份用户管理器 DeleteAsync DbUpdateConcurrencyException
【发布时间】:2019-02-02 17:59:18
【问题描述】:

我正在尝试通过 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
   }
   ...
}

【问题讨论】:

  • 当保存期间意外数量的行受到影响时,会发生并发冲突。这通常是因为数据库中的数据自从加载到内存后就被修改了。 (docs.microsoft.com/en-us/ef/core/api/…) 在您的情况下,控制器接收到的 User 对象在数据库中可能没有相同的值。正如@KirkLarkin 所建议的,您可以使用用户ID 获取用户对象,然后调用删除方法。

标签: c# asp.net-core entity-framework-core asp.net-core-identity


【解决方案1】:

Entity Framework Core 使用Optimistic Concurrency:

在乐观并发模型中,如果在用户从数据库接收到一个值后,另一个用户在第一个用户尝试修改该值之前修改了该值,则认为发生了违规。

将此与悲观并发进行对比:

...在悲观并发模型中,更新行的用户建立锁。在用户完成更新并释放锁之前,其他人无法更改该行。

为了实现乐观并发,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>'

当上述 SQL 语句中的 CONCURRENCY_STAMP 值与给定用户在数据库中存储的值不匹配时,您会收到错误消息。这确保了如果您从数据库中检索用户(其中​​包含特定的 ConcurrencyStamp),则只有在其他地方没有进行其他更改时才能将更改保存到数据库(因为您提供的 ConcurrencyStamp 值与存在于数据库中)。

从上面的ConcurrencyStamp 定义可以看出,该属性默认为一个新的GUID - 每次创建IdentityUser(或子类)时,它都会获得一个新的ConcurrencyStamp 值。在您的示例中,使用传递给您的 DeleteAsync 操作的 User,ASP.NET Core 模型绑定首先创建 User 的新实例,然后设置 JSON 有效负载中存在的属性。由于负载中没有 ConcurrencyStamp 值,User 最终将得到一个 new ConcurrencyStamp 值,该值与数据库中的值不匹配。

为避免此问题,您可以ConcurrencyStamp 值添加到从客户端发送的有效负载中。但是,我不会推荐这个。解决此问题的最简单和最安全的方法是将UserId 作为有效负载发送,使用_userManager.FindByIdAsync 检索User 本身,然后使用 that 实例执行删除。这是一个例子:

[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);
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-09-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-15
    • 2017-05-14
    相关资源
    最近更新 更多