使用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
问题排查与修复
核心问题分析
- 事务实例未置空导致重复操作:
DisposeTransaction方法仅释放事务,但未将dbContextTransaction字段置为null。当重试策略触发多次执行时,会复用已释放的事务实例,调用其方法或释放时触发空引用异常。 - 异步事务操作未等待完成:
CommitTransactionAsync中调用dbContextTransaction?.CommitAsync(cancellationToken)时未加await,导致事务提交操作未完成就进入finally块执行释放,此时事务内部状态可能已失效,引发空引用。 - 重试场景下事务复用错误:
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
相关产品推荐
相关产品推荐

