Search code examples
c#entity-framework-coretransactionscope

How to EnableRetryOnFailure inside Unit of Work TransactionScope?


Say I have the following UnitOfWork which can be injected into handlers and used to perform db operations within TransactionScope:

internal sealed class UnitOfWork : IUnitOfWork
{
    private readonly TransactionScope _transactionScope;

    public UnitOfWork(IOptions<UnitOfWorkOptions> options)
    {
        EnsureArg.IsNotNull(options, nameof(options));

        _transactionScope = new TransactionScope(
            TransactionScopeOption.RequiresNew,
            new TransactionOptions
            {
                IsolationLevel = IsolationLevel.ReadCommitted,
                Timeout = TransactionManager.DefaultTimeout,
            },
            TransactionScopeAsyncFlowOption.Enabled);
    }

    Task CompleteAsync(CancellationToken cancellationToken)
    {
        _transactionScope.Complete();
        Dispose(true);
        return Task.CompletedTask;
    }

    void IDisposable.Dispose()
        => Dispose(true);

    private void Dispose(bool disposing)
    {
        _transactionScope.Dispose();
    }
}

In my handlers:

public MyHandler(IUnitOfWork unitOfWork, IRepository repository)
{
    _unitOfWork = unitOfWork;
    _repository = repository;
}

public async Task HandleAsync(CancellationToken cancellationToken)
{
    var entity = await _repository.GetItem(cancellationToken);
    await _unitOfWork.CompleteAsync(cancellationToken);
}

After enabling ef core's connection resilience with EnableRetryOnFailure I get this annoying error whenever any transactions occur within my unit of work:

System.InvalidOperationException: 'The configured execution strategy 'SqlServerRetryingExecutionStrategy' does not support user-initiated transactions. Use the execution strategy returned by 'DbContext.Database.CreateExecutionStrategy()' to execute all the operations in the transaction as a retriable unit.'

So I injected my DbContext and tried passing my db code into CompleteAsync to wrap it around an execution strategy, but the same error still persists:

Task CompleteAsync(Func<Task> action, CancellationToken cancellationToken)
{
    var strategy = _dbContext.Database.CreateExecutionStrategy();
    return strategy.ExecuteAsync(
        async () =>
        {
            await action();
            _transactionScope.Complete();
            Dispose(true); 
            return Task.CompletedTask;
        });
}

Handler:

public async Task HandleAsync(CancellationToken cancellationToken)
{
    var task = new Func<Task>(async () =>
    {
        var entity = await _repository.GetItem(cancellationToken);
    });
    await _unitOfWork.CompleteAsync(task, cancellationToken);
}

I also tried simply wrapping the _transactionScope.Complete(); in the execution strategy but unsurprisingly it did not help

Does anyone know how to achieve this?


Solution

  • I got it working like this

    Task CompleteAsync(Func<Task> action, CancellationToken cancellationToken)
    {
        var strategy = _dbContext.Database.CreateExecutionStrategy();
        return strategy.ExecuteAsync(
            async () =>
            {
                using (var transaction = new TransactionScope())
                {
                    await action();
                    transaction.Complete();
                    Dispose(true); 
                    return Task.CompletedTask;
                }
            });
    }
    

    So instead of initialisting TransactionScope in the constructor, it gets created at the last minute, and is wrapped in the execution strategy

    They do actually do this in the docs:

    using (var context1 = new BloggingContext())
    {
        context1.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/visualstudio" });
    
        var strategy = context1.Database.CreateExecutionStrategy();
    
        strategy.Execute(
            () =>
            {
                using (var context2 = new BloggingContext())
                {
                    using (var transaction = new TransactionScope())
                    {
                        context2.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
                        context2.SaveChanges();
    
                        context1.SaveChanges();
    
                        transaction.Complete();
                    }
                }
            });
    }