Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
BenjaminAbt opened this issue Jan 7, 2020 · 1 comment
Milestone

Comments

@BenjaminAbt
Copy link

@BenjaminAbt BenjaminAbt commented Jan 7, 2020

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

The expected behavior of the method is:

  • If the entity exists and the update could be performed, the method should return a true
  • If not, false

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.

  • 0 if no entity was found and updated
  • 1 (or higher) when found and 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

@BenjaminAbt BenjaminAbt added the type-bug label Jan 7, 2020
@ajcvickers

This comment has been minimized.

Copy link
Member

@ajcvickers ajcvickers commented Jan 8, 2020

@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.

@ajcvickers ajcvickers added this to the Backlog milestone Jan 10, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
2 participants
You can’t perform that action at this time.