You can add a transaction parameter to the end of methods that you want to run in a transaction and give it a default value of null. Thus, if you don't want to run the method in an existing transaction then leave off the end parameter or explicitly pass null.
Inside these methods you can check the parameter for null to determine whether to create a new transaction or else use one passed in. This logic can be pushed to a base class.
This keeps your methods purer than when using a context based solution.
void Update(int itemId, string text, IDbTransaction trans = null) =>
RunInTransaction(ref trans, () =>
{
trans.Connection.Update("...");
});
void RunInTransaction(ref IDbTransaction transaction, Action f)
{
if (transaction == null)
{
using (var conn = DatabaseConnectionFactory.Create())
{
conn.Open();
using (transaction = conn.BeginTransaction())
{
f();
transaction.Commit();
}
}
}
else
{
f();
}
}
Update(1, "Hello World!");
Update(1, "Hello World!", transaction);
Then you can have a transaction runner for your service layer...
public class TransactionRunner : ITransactionRunner
{
readonly IDatabaseConnectionFactory databaseConnectionFactory;
public TransactionRunner(IDatabaseConnectionFactory databaseConnectionFactory) =>
this.databaseConnectionFactory = databaseConnectionFactory;
public void RunInTransaction(Action<IDbTransaction> f)
{
using (var conn = databaseConnectionFactory.Create())
{
conn.Open();
using (var transaction = conn.BeginTransaction())
{
f(transaction);
transaction.Commit();
}
}
}
public async Task RunInTransactionAsync(Func<IDbTransaction, Task> f)
{
using (var conn = databaseConnectionFactory.Create())
{
conn.Open();
using (var transaction = conn.BeginTransaction())
{
await f(transaction);
transaction.Commit();
}
}
}
}
And a service method might look like this...
void MyServiceMethod(int itemId, string text1, string text2) =>
transactionRunner.RunInTransaction(trans =>
{
repos.UpdateSomething(itemId, text1, trans);
repos.UpdateSomethingElse(itemId, text2, trans);
});
Which is easy to mock for unit testing...
public class MockTransactionRunner : ITransactionRunner
{
public void RunInTransaction(Action<IDbTransaction> f) => f(null);
public Task RunInTransactionAsync(Func<IDbTransaction, Task> f) => f(null);
}