Hello, I stumbled upon this weird issue when was implementing optimistic concurrency scenario.
When I update multiple enity types in one context save changes operation and each of those updates should cause DbUpdateConcurrencyException, the mentioned exception is not triggering for all of entity types in ThrowingConcurrencyExceptionAsync interceptor method. This issue happens only when the tables for these enteties are marked as Temporal.
Repro scenario
I have four identical simple entity types:
public class FirstEntry
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Key { get; set; }
public int Token { get; set;}
}
public class SecondEntry
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Key { get; set; }
public int Token { get; set; }
}
// ... same ThirdEntry/FourthEntry
And model configuration:
modelBuilder.Entity<FirstEntry>(builder =>
{
// the issue doesn't occur when table is not temporal
builder.ToTable(nameof(FirstEntry), b => b.IsTemporal());
builder.HasKey(e => e.Key);
builder.Property(e => e.Token).IsConcurrencyToken(true);
});
// same for SecondEntry/ThirdEntry/FourthEntry
Then simple creating these entitites:
using (AppDbContext appContext = new AppDbContext())
{
await appContext.Database.EnsureDeletedAsync();
await appContext.Database.EnsureCreatedAsync();
FirstEntry firstEntry = new FirstEntry() { Key = 1, Token = 1 };
appContext.Add(firstEntry);
// same for SecondEntry/ThirdEntry/FourthEntry
await appContext.SaveChangesAsync();
}
Then I simulate concurrency exception triggering by updating concurrency token original value (worth mentitoning that I get the same behaviour when causing concurrency exception from multiple threads scenario):
using (AppDbContext appContext = new AppDbContext())
{
FirstEntry? dbFirst = await appContext.Set<FirstEntry>().Where(f => f.Key == 1).FirstOrDefaultAsync();
if (dbFirst != null)
{
appContext.Entry(dbFirst).Property(e => e.Token).OriginalValue = 0;
}
// same for SecondEntry/ThirdEntry/FourthEntry
await appContext.SaveChangesAsync();
}
Then I have ThrowingConcurrencyExceptionAsync method from SaveChangesInterceptor where I have a breakpoint and simply suppress InterceptionResult to let it go through and observe the issue:
public override ValueTask<InterceptionResult> ThrowingConcurrencyExceptionAsync(ConcurrencyExceptionEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default)
{
Console.WriteLine("ThrowingConcurrencyExceptionAsync for type {0}", eventData.Entries.FirstOrDefault()?.Entity.GetType().ToString());
return new(InterceptionResult.Suppress());
}
In this case the ThrowingConcurrencyExceptionAsync triggers 2/4 times for FirstEntity and SecondEntity. When the tables are not marked as temporal it is triggering 4/4 times for all the entety types.
Log
In the log I have this warning printed RelationalEventId.UnexpectedTrailingResultSetWhenSaving
dbug: 05/03/2024 09:19:04.621 CoreEventId.OptimisticConcurrencyException[10006] (Microsoft.EntityFrameworkCore.Update)
Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: The database operation was expected to affect 1 row(s), but actually affected 0 row(s); data may have been modified or deleted since entities were loaded. See https://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.
ThrowingConcurrencyExceptionAsync for type efconcurrency.FirstEntry
dbug: 05/03/2024 09:19:15.468 CoreEventId.OptimisticConcurrencyException[10006] (Microsoft.EntityFrameworkCore.Update)
Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: The database operation was expected to affect 1 row(s), but actually affected 0 row(s); data may have been modified or deleted since entities were loaded. See https://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.
ThrowingConcurrencyExceptionAsync for type efconcurrency.SecondEntry
warn: 05/03/2024 09:19:17.052 RelationalEventId.UnexpectedTrailingResultSetWhenSaving[20705] (Microsoft.EntityFrameworkCore.Update)
An unexpected trailing result set was found when reading the results of a SaveChanges operation; this may indicate that a stored procedure returned a result set without being configured for it in the EF model. Check your stored procedure definitions.
dbug: 05/03/2024 09:19:17.055 RelationalEventId.DataReaderClosing[20301] (Microsoft.EntityFrameworkCore.Database.Command)
Closing data reader to 'efconcurrency' on server 'localhost\SQLEXPRESS'.
Verbose Output
...
Finding DbContext classes...
Finding IDesignTimeDbContextFactory implementations...
Finding application service provider in assembly 'efconcurrency'...
Finding Microsoft.Extensions.Hosting service provider...
No static method 'CreateHostBuilder(string[])' was found on class 'Program'.
No application service provider was found.
Finding DbContext classes in the project...
Found DbContext 'AppDbContext'.
### Provider and version information
EF Core version: 8.0.2
Database provider: Microsoft.EntityFrameworkCore.SqlServer
Target framework: .NET 8.0
Operating system: Windows 10
IDE: Visual Studio 2022 17.8.2
Issue description
Hello, I stumbled upon this weird issue when was implementing optimistic concurrency scenario. When I update multiple enity types in one context save changes operation and each of those updates should cause DbUpdateConcurrencyException, the mentioned exception is not triggering for all of entity types in ThrowingConcurrencyExceptionAsync interceptor method. This issue happens only when the tables for these enteties are marked as Temporal.
Repro scenario
I have four identical simple entity types:
And model configuration:
Then simple creating these entitites:
Then I simulate concurrency exception triggering by updating concurrency token original value (worth mentitoning that I get the same behaviour when causing concurrency exception from multiple threads scenario):
Then I have ThrowingConcurrencyExceptionAsync method from SaveChangesInterceptor where I have a breakpoint and simply suppress InterceptionResult to let it go through and observe the issue:
In this case the ThrowingConcurrencyExceptionAsync triggers 2/4 times for FirstEntity and SecondEntity. When the tables are not marked as temporal it is triggering 4/4 times for all the entety types.
Log
In the log I have this warning printed
RelationalEventId.UnexpectedTrailingResultSetWhenSaving
Verbose Output
... Finding DbContext classes... Finding IDesignTimeDbContextFactory implementations... Finding application service provider in assembly 'efconcurrency'... Finding Microsoft.Extensions.Hosting service provider... No static method 'CreateHostBuilder(string[])' was found on class 'Program'. No application service provider was found. Finding DbContext classes in the project... Found DbContext 'AppDbContext'.