Sagas
.NET implementation of the UnitOfWork pattern using IDbTransaction and Sagas for orchestrating cross-domain transactions.
Install / Use
/learn @orangeloop/SagasREADME
OrangeLoop.Sagas
[!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.
Installation
.Net Core CLI
dotnet add package OrangeLoop.Sagas
Package Manager Console
Install-Package OrangeLoop.Sagas
How to get started with IUnitOfWork (SQL Server)
Add connection string
// appsettings.json
{
"ConnectionStrings": {
"MyConnection": "Data Source=...",
},
...
}
Register IUnitOfWork Service
// Startup.cs or Program.cs
services.AddSqlServerUnitOfWork("MyConnection", IsolationLevel.ReadUncommitted);
Inject IConnectionFactory and IUnitOfWork in repository
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.
Example
// 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] >
IConnectionFactoryis 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.
IUnitOfWork.ExecuteAsync (Implicit)
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(...);
});
}
}
IUnitOfWork.ExecuteAsync (Explicit)
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
successorfailurewhen using the explicit option can lead to open database transactions. I will address this in a future update.
How to get started with ISaga<T>
[!NOTE] Documentation pending. Sample usage available in
SagaTests.cs
