-
Notifications
You must be signed in to change notification settings - Fork 256
Description
The below code worked using dotnet/efcore 9, but fails after upgrading to 10. It is now throwing a System.InvalidOperationException: 'Transaction is already completed' from within TransactionCommittedAsync
using System.Data.Common;
using Demo;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddScoped<DemoInterceptor>();
services.AddDbContext<DemoDbContext>((serviceProvider, options) =>
{
options.UseNpgsql("Host=localhost;Port=5432;User ID=postgres;Password=password;Database=postgres;Pooling=true;");
options.AddInterceptors(serviceProvider.GetRequiredService<DemoInterceptor>());
});
var serviceProvider = services.BuildServiceProvider();
var dbContext = serviceProvider.GetRequiredService<DemoDbContext>();
dbContext.Demos.Add(new() { Value = "interceptor works during SaveChangesAsync" });
await dbContext.SaveChangesAsync();
using var transaction = await dbContext.Database.BeginTransactionAsync();
dbContext.Demos.Add(new() { Value = "interceptor fails during CommitAsync" });
await dbContext.SaveChangesAsync();
await transaction.CommitAsync(); // Boom!
public class DemoInterceptor : ISaveChangesInterceptor, IDbTransactionInterceptor
{
public async ValueTask<int> SavedChangesAsync(SaveChangesCompletedEventData eventData, int result, CancellationToken cancellationToken = default)
{
// Make arbitrary query - this consistently always works
await ((DemoDbContext)eventData.Context!).Demos.AnyAsync(d => d.Value == "five");
return result;
}
public async Task TransactionCommittedAsync(DbTransaction transaction, TransactionEndEventData eventData, CancellationToken cancellationToken = default)
{
// Make arbitrary query - this now fails with System.InvalidOperationException: 'Transaction is already completed'
await ((DemoDbContext)eventData.Context!).Demos.AnyAsync(d => d.Value == "six");
}
}
The above code works with this:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
</ItemGroup>
</Project>
but fails with this
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
</ItemGroup>
</Project>
The database is an extremely simple single table for demonstration purposes, scaffolded using dotnet ef dbcontext scaffold
CREATE TABLE demo (
id int GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
value text
);
For context about the use case, we have an interceptor hooked up for capturing auditing information - capturing changes to entities and submitting this to a separate data store. There are scenarios where we want to include related information to the data that was changed to include in this submission, so we query for it as needed.
When within an explicit transaction, SaveChangesAsync doesn't actually change antyhing yet, so we defer these submissions to the TransactionCommitted method,
Wondering if this change in behavior was intentional or accidental. If intentional, I can probably work around it with some changes to application code - just curious about the rationale for the change.