Skip to content

UnexpectedTrailingResultSetWhenSaving with Temporal Tables and Concurrency Token when updating multiple entity types in context #33243

Open

Description

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:

 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions