Update/Insert/Delete or ignore

Open
#19,508 2 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
25/100
Issue type
Bug
Clarity
Needs clarification
Activity status
Stale
Tech stack
csharp
Domain
backend, database

Research direction

Start with the attached UserLoginUpdateLoginSessionCommandHandler.cs snippet and the SaveChangesAsync stack trace, then review the EF Core 3.1 SQL Server update behavior described in the issue. Done would require a maintainer-approved determination of whether the concurrency exception is expected and, if a change is needed, a defined test or documentation target; no repository test or implementation file is named.

Written by the indexing model from the issue text.

Description

area-save-changes customer-reported needs-design

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

Dominant language
C#
Stars
14.8k
Forks
3.4k
Avg merge
2d 5h
Merged PRs (30d)
134

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from dotnet/efcore

All issues in dotnet/efcore

Similar issues

More C# issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.