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?
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();
}
}
});
}