dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.82k stars 3.2k forks source link

Update of an non-existing, optional entity ends in DbUpdateConcurrencyException #19508

Open BenjaminAbt opened 4 years ago

BenjaminAbt commented 4 years ago

I would like to update a property of an entity without having to perform a round-trip.

The expected behavior of the method is:

For this I use the following code snippet:

int userId = request.UserId;
Guid loginId = request.LoginId;

UserLoginEntity userLogin = new UserLoginEntity
            {
                Id = loginId,
                UserAccountId = userId,
                LatestActivityOn = DateTimeOffset.UtcNow
            };

_dbContext.Attach(userLogin);
_dbContext.Entry(userLogin).Property(x => x.LatestActivityOn).IsModified = true;
int updated = await _dbContext.SaveChangesAsync(cancellationToken);

return updated > 0;

My assumption here is that SaveChanges returns the number of entries that have been updated.

This works fine if a corresponding entity exists (>0) If an entity does not exist (=0) and therefore nothing could be found, then a DBConcurrencyException is thrown.

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.

Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.ThrowAggregateUpdateConcurrencyException(int commandIndex, int expectedRowsAffected, int rowsAffected)
Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.ConsumeResultSetWithoutPropagationAsync(int commandIndex, RelationalDataReader reader, CancellationToken cancellationToken)
Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.ConsumeAsync(RelationalDataReader reader, CancellationToken cancellationToken)
Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable<ModificationCommandBatch> commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable<ModificationCommandBatch> commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(IList<IUpdateEntry> entriesToSave, CancellationToken cancellationToken)
Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(DbContext _, bool acceptAllChangesOnSuccess, CancellationToken cancellationToken)
Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.ExecuteAsync<TState, TResult>(TState state, Func<DbContext, TState, CancellationToken, Task<TResult>> operation, Func<DbContext, TState, CancellationToken, Task<ExecutionResult<TResult>>> verifySucceeded, CancellationToken cancellationToken)
Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken)
MyApp.Portal.Engine.Commands.UserLoginUpdateLoginSessionCommandHandler.Handle(UserLoginUpdateLoginSessionCommand request, CancellationToken cancellationToken) in UserLoginUpdateLoginSessionCommandHandler.cs
+
            int updated = await _dbContext.SaveChangesAsync(cancellationToken);
MediatR.Pipeline.RequestPostProcessorBehavior<TRequest, TResponse>.Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
MediatR.Pipeline.RequestPreProcessorBehavior<TRequest, TResponse>.Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
MyApp.Portal.Engine.Behaviors.ApplicationInsightsBehavior<TRequest, TResponse>.Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next) in ApplicationInsightsBehavior.cs
+
                response = await next().ConfigureAwait(false);
MyApp.Portal.Engine.MediatorDispatcher.Send<TResponse>(ICommand<TResponse> command, CancellationToken cancellationToken) in MediatorDispatcher.cs
+
            return await _mediator.Send(command, cancellationToken).ConfigureAwait(false);
MyApp.Portal.AspNetCore.Handlers.PortalUserLoginCookieValidationHandler.ValidatePrincipal(CookieValidatePrincipalContext context) in PortalUserLoginCookieValidationHandler.cs
+
                bool valid2 = await eventDispatcher.Send(new UserLoginUpdateLoginSessionCommand(userId, sessionId));
Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler.HandleAuthenticateAsync()
Microsoft.AspNetCore.Authentication.AuthenticationHandler<TOptions>.AuthenticateAsync()
Microsoft.AspNetCore.Authentication.AuthenticationService.AuthenticateAsync(HttpContext context, string scheme)
Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.ResponseCaching.ResponseCachingMiddleware.Invoke(HttpContext httpContext)
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

My temporary solution is that I have to do the roundtrip (select first, then update, if exists)

Is this behavior by design or unintentional? I could not find any documented behavior.

Further technical details

EF Core version: 3.1.0 Database provider: Microsoft.EntityFrameworkCore.SqlServer 3.1.0 Target framework: (e.g. .NET Core 3.1) Operating system: Windows 10.0.18363.535 IDE: VS2019 Version 16.5.0 Preview 1.0

ajcvickers commented 4 years ago

@BenjaminAbt See #10443 which is about disabling the concurrency check.

Note for team: I haven's seen requests for "update or ignore" behavior before. (As opposed to the common request for "update or insert".) We should discuss.

AndriySvyryd commented 3 years ago

Also consider https://github.com/dotnet/efcore/issues/16949 when doing this as well as disabling other constraints (NOCHECK CONSTRAINT), there could be an overarching API for suppressing checks on SaveChanges