Skip to content

No longer able to use eventData.DbContext within IDbTransactionInterceptor.TransactionCommittedAsync #3718

@pgelardi-motor

Description

@pgelardi-motor

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions