A .NET 10 library providing database-agnostic transaction abstractions. It enables multiple independent libraries to participate in a single shared database transaction without tight coupling to specific database implementations.
Transactional solves the problem of coordinating transactions across multiple libraries that need to work with the same database. Instead of each library managing its own transactions, they can participate in a shared transaction context that is controlled at the application level.
# Core abstractions (no external dependencies)
dotnet add package Transactional.Abstractions
# MongoDB implementation (requires MongoDB.Driver 3.x)
dotnet add package Transactional.MongoDB
# PostgreSQL implementation (requires Npgsql 8.x)
dotnet add package Transactional.PostgreSQL// Program.cs - Register services
builder.Services.AddMongoDbTransactionManager(sp =>
new MongoClient("mongodb://localhost:27017"));
// Usage
public class MyService
{
private readonly IMongoTransactionManager _transactionManager;
public MyService(IMongoTransactionManager transactionManager)
{
_transactionManager = transactionManager;
}
public async Task DoWorkAsync()
{
await using var context = await _transactionManager.BeginTransactionAsync();
// Pass context to repositories...
await context.CommitAsync();
}
}// Program.cs - Register services
builder.Services.AddPostgresTransactionManager("Host=localhost;Database=myapp;Username=postgres;Password=secret");
// Usage
public class MyService
{
private readonly IPostgresTransactionManager _transactionManager;
public MyService(IPostgresTransactionManager transactionManager)
{
_transactionManager = transactionManager;
}
public async Task DoWorkAsync()
{
await using var context = await _transactionManager.BeginTransactionAsync();
// Pass context to repositories...
await context.CommitAsync();
}
}| Package | Description | Dependencies |
|---|---|---|
Transactional.Abstractions |
Core interface (ITransactionContext) |
None |
Transactional.MongoDB |
MongoDB implementation wrapping IClientSessionHandle |
MongoDB.Driver 3.x |
Transactional.PostgreSQL |
PostgreSQL implementation wrapping NpgsqlTransaction |
Npgsql 8.x |
// Option 1: With existing IMongoClient registration
builder.Services.AddSingleton<IMongoClient>(new MongoClient("mongodb://localhost:27017"));
builder.Services.AddMongoDbTransactionManager();
// Option 2: With factory (registers both IMongoClient and transaction manager)
builder.Services.AddMongoDbTransactionManager(sp =>
new MongoClient(configuration.GetConnectionString("MongoDB")));// Option 1: With connection string (registers both NpgsqlDataSource and transaction manager)
builder.Services.AddPostgresTransactionManager("Host=localhost;Database=myapp");
// Option 2: With existing NpgsqlDataSource registration
builder.Services.AddSingleton(NpgsqlDataSource.Create("Host=localhost;Database=myapp"));
builder.Services.AddPostgresTransactionManager();
// Option 3: With factory (registers both NpgsqlDataSource and transaction manager)
builder.Services.AddPostgresTransactionManager(sp =>
{
var config = sp.GetRequiredService<IConfiguration>();
return NpgsqlDataSource.Create(config.GetConnectionString("PostgreSQL")!);
});Repositories should accept the base ITransactionContext interface, then cast to the database-specific type internally. This keeps your repository contracts database-agnostic while still allowing full access to native transaction features.
public class UserRepository
{
private readonly IMongoDatabase _database;
public UserRepository(IMongoDatabase database)
{
_database = database;
}
public async Task CreateUserAsync(User user, ITransactionContext? transactionContext = null)
{
var collection = _database.GetCollection<User>("users");
if (transactionContext != null)
{
if (transactionContext is not IMongoTransactionContext mongoContext)
{
throw new InvalidOperationException(
$"Expected {nameof(IMongoTransactionContext)} but received {transactionContext.GetType().Name}");
}
await collection.InsertOneAsync(mongoContext.Session, user);
}
else
{
await collection.InsertOneAsync(user);
}
}
}
public class OrderRepository
{
private readonly IMongoDatabase _database;
public OrderRepository(IMongoDatabase database)
{
_database = database;
}
public async Task CreateOrderAsync(Order order, ITransactionContext? transactionContext = null)
{
var collection = _database.GetCollection<Order>("orders");
if (transactionContext != null)
{
if (transactionContext is not IMongoTransactionContext mongoContext)
{
throw new InvalidOperationException(
$"Expected {nameof(IMongoTransactionContext)} but received {transactionContext.GetType().Name}");
}
await collection.InsertOneAsync(mongoContext.Session, order);
}
else
{
await collection.InsertOneAsync(order);
}
}
}The service layer controls transaction boundaries and passes the context to repositories:
public class OrderService
{
private readonly IMongoTransactionManager _transactionManager;
private readonly UserRepository _userRepo;
private readonly OrderRepository _orderRepo;
public OrderService(
IMongoTransactionManager transactionManager,
UserRepository userRepo,
OrderRepository orderRepo)
{
_transactionManager = transactionManager;
_userRepo = userRepo;
_orderRepo = orderRepo;
}
public async Task CreateOrderWithUserAsync(User user, Order order)
{
await using var context = await _transactionManager.BeginTransactionAsync();
try
{
await _userRepo.CreateUserAsync(user, context);
await _orderRepo.CreateOrderAsync(order, context);
await context.CommitAsync();
}
catch
{
await context.RollbackAsync();
throw;
}
}
}A helper method that wraps the transaction lifecycle:
public static class TransactionScope
{
public static async Task ExecuteAsync(
IMongoTransactionManager manager,
Func<ITransactionContext, Task> action)
{
await using var context = await manager.BeginTransactionAsync();
try
{
await action(context);
await context.CommitAsync();
}
catch
{
await context.RollbackAsync();
throw;
}
}
}
// Usage
await TransactionScope.ExecuteAsync(_transactionManager, async context =>
{
await _userRepo.CreateUserAsync(user, context);
await _orderRepo.CreateOrderAsync(order, context);
});public class UserRepository
{
private readonly NpgsqlDataSource _dataSource;
public UserRepository(NpgsqlDataSource dataSource)
{
_dataSource = dataSource;
}
public async Task CreateUserAsync(string name, ITransactionContext? transactionContext = null)
{
if (transactionContext != null)
{
if (transactionContext is not IPostgresTransactionContext pgContext)
{
throw new InvalidOperationException(
$"Expected {nameof(IPostgresTransactionContext)} but received {transactionContext.GetType().Name}");
}
await using var command = pgContext.Transaction.Connection!.CreateCommand();
command.Transaction = pgContext.Transaction;
command.CommandText = "INSERT INTO users (name) VALUES (@name)";
command.Parameters.AddWithValue("name", name);
await command.ExecuteNonQueryAsync();
}
else
{
await using var connection = await _dataSource.OpenConnectionAsync();
await using var command = connection.CreateCommand();
command.CommandText = "INSERT INTO users (name) VALUES (@name)";
command.Parameters.AddWithValue("name", name);
await command.ExecuteNonQueryAsync();
}
}
}-
Always use
await usingfor transactions - This ensures proper disposal even if exceptions occur. -
Pass
ITransactionContextto repositories - Use the base interface in method signatures, then cast internally to keep contracts database-agnostic. -
Commit explicitly before dispose - While dispose will rollback uncommitted transactions, it's clearer to commit explicitly.
-
Handle exceptions appropriately - Catch exceptions, rollback, and rethrow to ensure proper transaction cleanup.
-
Make transaction participation optional - Allow methods to work with or without a transaction context for flexibility.
-
Use DI extension methods - Prefer
AddMongoDbTransactionManager()andAddPostgresTransactionManager()over manual registration.
All public APIs include XML documentation. Enable documentation generation in your IDE or refer to the generated XML documentation files in the NuGet packages.
ITransactionContext- Represents an active transaction withCommitAsyncandRollbackAsync
IMongoTransactionContext- ExtendsITransactionContext, exposesSessionpropertyIMongoTransactionManager- Creates transactions viaBeginTransactionAsync
IPostgresTransactionContext- ExtendsITransactionContext, exposesTransactionpropertyIPostgresTransactionManager- Creates transactions viaBeginTransactionAsync
This project is licensed under the Apache 2.0 License - see the LICENSE file for details.