You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Pomelo EF Core MySql时释放事务随机触发空引用异常求助

排查Pomelo.EntityFrameworkCore.MySql释放事务时的随机NullReferenceException问题

在使用Pomelo.EntityFrameworkCore.MySql时,释放IDbContextTransaction实例会随机触发System.NullReferenceException,以下是相关代码、异常栈及排查分析:

相关代码

SampleContext 类

public class SampleContext : DbContext, ISampleContext 
{
    private readonly AuditableEntitySaveChangesInterceptor _auditableEntitySaveChangesInterceptor;
    private IDbContextTransaction dbContextTransaction;
    public SampleContext(DbContextOptions<SampleContext > options,
            AuditableEntitySaveChangesInterceptor auditableEntitySaveChangesInterceptor
            ) : base(options)
    {
        this._auditableEntitySaveChangesInterceptor = auditableEntitySaveChangesInterceptor;
    }

    public DbSet<Accommodation> Accommodations => Set<Accommodation>();

    public DbSet<AccommodationLevel> AccommodationLevels => Set<AccommodationLevel>();

    public DbSet<City> Cities => Set<City>();

    public DbSet<CityPopulationType> CityPopulationTypes => Set<CityPopulationType>();

    public DbSet<Contact> Contacts => Set<Contact>();


    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
        base.OnModelCreating(modelBuilder);
    }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.LogTo(message => Debug.WriteLine(message))
            .EnableSensitiveDataLogging();
        optionsBuilder.UseLazyLoadingProxies();
        optionsBuilder.AddInterceptors(_auditableEntitySaveChangesInterceptor);
    }
    public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        return base.SaveChangesAsync(cancellationToken);
    }

    public async Task BeginTransactionAsync(CancellationToken cancellationToken)
    {
        dbContextTransaction ??= await Database.BeginTransactionAsync(cancellationToken);
    }

    public async Task CommitTransactionAsync(CancellationToken cancellationToken)
    {
        try
        {
            await SaveChangesAsync(cancellationToken);
            dbContextTransaction?.CommitAsync(cancellationToken);
        }
        catch
        {
            await RollbackTransactionAsync(cancellationToken);
            throw;
        }
        finally
        {
            if (dbContextTransaction != null)
            {
                DisposeTransaction();
            }
        }
    }

    public async Task RetryOnExceptionAsync(Func<Task> func)
    {
        await Database.CreateExecutionStrategy().ExecuteAsync(func);
    }

    public async Task RollbackTransactionAsync(CancellationToken cancellationToken)
    {
        try
        {
            await dbContextTransaction?.RollbackAsync(cancellationToken);
        }
        finally
        {
            DisposeTransaction();
        }
    }

    private void DisposeTransaction()
    {
        try
        {
            if (dbContextTransaction != null)
            {
                dbContextTransaction.Dispose();
            }
        }
        catch (Exception ex)
        {

        }
    }
}

TransactionBehavior 类

public class TransactionBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : MediatR.IRequest<TResponse>
{
    private readonly ILogger<TransactionBehavior<TRequest, TResponse>> _logger;
    private readonly ISampleContext _sampleContext ;

    public TransactionBehavior(ILogger<TransactionBehavior<TRequest, TResponse>> logger, ISampleContext sampleContext )
    {
        _logger = logger;
        _sampleContext = sampleContext ;
    }
    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
    {
        TResponse response = default;

        try
        {
            await _sampleContext.RetryOnExceptionAsync(async () =>
            {
                _logger.LogInformation($"Begin Transaction : {typeof(TRequest).Name}");
                await _sampleContext .BeginTransactionAsync(cancellationToken);

                response = await next();

                await _sampleContext.CommitTransactionAsync(cancellationToken);
                _logger.LogInformation($"End transaction : {typeof(TRequest).Name}");
            });
        }
        catch (Exception ex)
        {
            _logger.LogInformation($"Rollback transaction executed {typeof(TRequest).Name}");
            await _sampleContext.RollbackTransactionAsync(cancellationToken);
            _logger.LogError(ex.Message, ex.StackTrace);

            throw;
        }

        return response;
    }
}

异常调用栈

at MySqlConnector.MySqlTransaction.d__27.MoveNext()
at MySqlConnector.MySqlTransaction.d__25.MoveNext()
at MySqlConnector.MySqlTransaction.Dispose(Boolean disposing)
at Microsoft.EntityFrameworkCore.Storage.RelationalTransaction.Dispose()
at Sample.Infrastructure.Data.SampleContext.DisposeTransaction() in D:\Projects\Sample\src\Infrastructure\Sample.Infrastructure\Data\SampleContext.cs:line

问题排查与修复

核心问题分析

  1. 事务实例未置空导致重复操作:DisposeTransaction方法仅释放事务,但未将dbContextTransaction字段置为null。当重试策略触发多次执行时,会复用已释放的事务实例,调用其方法或释放时触发空引用异常。
  2. 异步事务操作未等待完成:CommitTransactionAsync中调用dbContextTransaction?.CommitAsync(cancellationToken)时未加await,导致事务提交操作未完成就进入finally块执行释放,此时事务内部状态可能已失效,引发空引用。
  3. 重试场景下事务复用错误:BeginTransactionAsync使用??=运算符,仅在第一次调用时创建事务。但重试策略会重复执行委托,前一次事务已被释放,再次调用时会使用已失效的实例。

修复方案

1. 修复事务释放逻辑,释放后置空字段

修改DisposeTransaction方法:

private void DisposeTransaction()
{
    try
    {
        if (dbContextTransaction != null)
        {
            dbContextTransaction.Dispose();
            dbContextTransaction = null; // 释放后置空,避免复用已失效实例
        }
    }
    catch (Exception ex)
    {
        // 可添加日志记录异常
    }
}

2. 等待异步事务操作完成

修改CommitTransactionAsync中的提交调用:

public async Task CommitTransactionAsync(CancellationToken cancellationToken)
{
    try
    {
        await SaveChangesAsync(cancellationToken);
        await dbContextTransaction?.CommitAsync(cancellationToken); // 添加await等待提交完成
    }
    catch
    {
        await RollbackTransactionAsync(cancellationToken);
        throw;
    }
    finally
    {
        if (dbContextTransaction != null)
        {
            DisposeTransaction();
        }
    }
}

3. 调整事务创建逻辑,适配重试场景

修改BeginTransactionAsync,确保每次调用都创建新事务(或先清理旧事务):

public async Task BeginTransactionAsync(CancellationToken cancellationToken)
{
    // 若存在未释放的事务,先释放
    if (dbContextTransaction != null)
    {
        DisposeTransaction();
    }
    dbContextTransaction = await Database.BeginTransactionAsync(cancellationToken);
}

4. 优化重试与事务的配合

由于EF Core的执行策略(重试)会重复执行整个委托逻辑,每次重试都需要全新的事务实例,上述修改已确保每次BeginTransactionAsync都创建新事务,避免复用失效实例。

内容的提问来源于stack exchange,提问作者Erandika Sandaruwan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 08:38:49