Update/Insert/Delete or ignore
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 25/100
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
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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from dotnet/efcore
-
Difficulty 4/5 3-5 days Newbie friendliness 55/100
-
customer-reported
Difficulty 5/5 Over a week Newbie friendliness 38/100
-
area-cosmos area-vector-search
Difficulty 5/5 Over a week Newbie friendliness 25/100
-
area-cosmos
Difficulty 5/5 Over a week Newbie friendliness 25/100
-
area-tools needs-design
Difficulty 4/5 3-5 days Newbie friendliness 25/100
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
-
:watch: Not Triaged 11.0 fundamentals/subsvc
Difficulty 2/5 1-3 hours Newbie friendliness 92/100
dotnet/AspNetCore.Docs#37699 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
SubtitleEdit/subtitleedit#15108 · 1 comment ·
-
area/docs-content Bug pulumi/docs
Difficulty 1/5 1-3 hours Newbie friendliness 94/100
-
agentic-workflows untriaged
Difficulty 2/5 1-3 hours Newbie friendliness 76/100