Warning
Version 2 of this package is a complete refactor and is NOT backwards compatible. This version targets net8.0 and has a much easier interface to work with than version 1.
dotnet add package OrangeLoop.Sagas
Install-Package OrangeLoop.Sagas
// appsettings.json
{
"ConnectionStrings": {
"MyConnection": "Data Source=...",
},
...
}
// Startup.cs or Program.cs
services.AddSqlServerUnitOfWork("MyConnection", IsolationLevel.ReadUncommitted);
Database queries will need a reference to the current IDbTransaction
, which can be accessed via the CurrentTransaction
property of IUnitOfWork
. Libraries such as Dapper or RepoDB have a transaction
parameter for this purpose.
// ICustomersRepository.cs
public interface ICustomersRepository
{
Task<Customer> Create(Customer customer);
Task<Customer> Delete(Customer customer);
Task<Customer> FindById(long id);
}
// CustomersRepository.cs
public class CustomersRepository(IConnectionFactory connectionFactory, IUnitOfWork unitOfWork) : ICustomersRepository
{
public async Task<Customer> FindById(long id)
{
var conn = connectionFactory.Get();
var result = await conn.QueryFirstOrDefaultAsync<Customer>("...", transaction: unitOfWork.CurrentTransaction);
}
}
[!NOTE] >
IConnectionFactory
is registered as a Scoped service and implementsIDisposable
. When the scope is disposed (e.g. after an ASP.NET Request) the underlying database connection is closed and properly disposed.
When using the implicit option, if no exceptions are thrown, the transaction is committed. If an unhandled exception is thrown, the transaction will be rolled back.
// CustomersService.cs
public class CustomersService(IUnitOfWork unitOfWork, ICustomersRepository repo) : ICustomersService
{
public async Task SomeMethod()
{
await unitOfWork.ExecuteAsync(async () =>
{
await repo.Create(...);
await repo.Create(...);
await repo.Delete(...);
});
}
}
Usually the implicit option is best, but if you want to handle exceptions within the ExecuteAsync method, then using the explicit option provides that flexibility. Alternatively you can use the implict option and simply rethrow the exception.
// CustomersService.cs
public class CustomersService(IUnitOfWork unitOfWork, ICustomersRepository repo) : ICustomersService
{
public async Task SomeMethod()
{
await unitOfWork.ExecuteAsync(async (success, failure) =>
{
try
{
await repo.Create(...);
await repo.Create(...);
await repo.Delete(...);
await success();
}
catch(Exception e)
{
// Custom exception handling
await failure(e);
}
});
}
}
Warning
Failure to call success
or failure
when using the explicit option can lead
to open database transactions. I will address this in a future update.
Note
Documentation pending. Sample usage available in SagaTests.cs