Sto cercando di eliminare un utente tramite il UserManager aspnetcore.identity dietro una 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);
}
}
Questo genera una 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)
Sono consapevole che questa eccezione di solito indica le condizioni di gara, ma non capisco perché ciò accada.
Sto facendo qualcosa di sbagliato?
MODIFICARE
L'oggetto utente che invio è simile al seguente:
"User": {
"Email": "",
"FirstName": "",
"LastName": "",
"Gender": "",
"Affiliation": {
"isStudent": true,
"isEmployee": false
}
...
}
Entity Framework Core utilizza la concorrenza ottimistica :
In un modello di concorrenza ottimista, si ritiene che si sia verificata una violazione se, dopo che un utente riceve un valore dal database, un altro utente modifica il valore prima che il primo utente abbia tentato di modificarlo.
Contrasta con la concorrenza pessimistica:
... in un modello di concorrenza pessimistica, un utente che aggiorna una riga stabilisce un blocco. Fino a quando l'utente non ha completato l'aggiornamento e rilasciato il blocco, nessun altro può modificare quella riga.
Per ottenere la concorrenza ottimistica, la classe IdentityUser
contiene una proprietà ConcurrencyStamp
(e la colonna corrispondente nel database), che è una rappresentazione in formato stringa di un GUID:
public virtual string ConcurrencyStamp { get; set; } = Guid.NewGuid().ToString();
Ogni volta che un utente viene salvato nel database, ConcurrencyStamp
viene impostato su un nuovo GUID.
Prendendo l'esempio dell'eliminazione di un utente, una versione semplificata DELETE
SQL inviata al server potrebbe essere simile alla seguente:
DELETE FROM dbo.AspNetUsers
WHERE Id = '<USER_ID>' AND ConcurrencyStamp = '<CONCURRENCY_STAMP>'
Il messaggio di errore che viene visualizzato si verifica quando il valore CONCURRENCY_STAMP
SQL precedente non corrisponde al valore memorizzato nel database per l'utente specificato. Ciò garantisce che se si recupera un utente dal database (che contiene uno specifico ConcurrencyStamp
), è possibile salvare le modifiche al database solo se non sono state apportate altre modifiche altrove (poiché si fornisce lo stesso valore ConcurrencyStamp
esistente nel database ).
Come puoi vedere dalla definizione di ConcurrencyStamp
sopra, la proprietà imposta automaticamente un nuovo GUID
- ogni volta che viene creato un IdentityUser
(o sottoclasse), ottiene un nuovo valore di ConcurrencyStamp
. Nel tuo esempio, con l' User
che viene passato all'azione DeleteAsync
, ASP.NET Core Model-Binding crea prima una nuova istanza di User
e quindi imposta le proprietà esistenti nel payload JSON. Poiché non vi è alcun valore ConcurrencyStamp
nel payload, l' User
finirà con un nuovo valore ConcurrencyStamp
che non corrisponderà a quello nel database.
Per evitare questo problema, è possibile aggiungere il valore ConcurrencyStamp
nel payload inviato dal client. Tuttavia, non lo consiglierei. L'approccio più semplice e più sicuro per risolvere questo problema sarebbe quella di inviare il Id
del User
come il carico utile, recuperare l' User
stesso utilizzando _userManager.FindByIdAsync
e quindi utilizzando tale istanza per eseguire la cancellazione. Ecco un esempio:
[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);
}
}