Identity Usermanager DeleteAsync DbUpdateConcurrencyException - PullRequest
0 голосов
/ 28 августа 2018

Я пытаюсь удалить пользователя с помощью aspnetcore.identity UserManager за webapi.

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

Я знаю, что это исключение обычно указывает на условия гонки, но я не понимаю, почему это происходит.

Я что-то не так делаю?

EDIT

Пользовательский объект, который я публикую, выглядит примерно так:

"User": {
 "Email": "",
 "FirstName": "",
 "LastName": "",
 "Gender": "",
 "Affiliation": {
     "isStudent": true,
     "isEmployee": false
   }
   ...
}

1 Ответ

0 голосов
/ 28 августа 2018

Entity Framework Core использует Оптимистичный параллелизм :

В модели оптимистичного параллелизма считается, что нарушение имело место, если после того, как пользователь получил значение из базы данных, другой пользователь изменил значение до того, как первый пользователь попытался его изменить.

Сравните это с пессимистическим параллелизмом:

... в пессимистической модели параллелизма пользователь, который обновляет строку, устанавливает блокировку. Пока пользователь не закончит обновление и не снимет блокировку, никто не сможет изменить эту строку.

Для достижения оптимистического параллелизма класс IdentityUser содержит свойство ConcurrencyStamp (и соответствующий столбец в базе данных), которое является строковым представлением GUID:

public virtual string ConcurrencyStamp { get; set; } = Guid.NewGuid().ToString();

Каждый раз, когда пользователь сохраняется в базе данных, ConcurrencyStamp получает новый GUID.

На примере удаления пользователя упрощенная версия оператора SQL DELETE, отправляемого на сервер, может выглядеть примерно так:

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 Model Model Binding сначала создает новый экземпляр 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);
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...