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