Unit of Work Pattern and Its Role in Managing Transactions
Master transaction management in Clean Architecture with the Unit of Work pattern. This in-depth guide explains how to implement atomic operations, maintain data consistency, and integrate with Dapper for robust database transactions in .NET applications.
Introduction
The Unit of Work pattern is a critical component in maintaining data consistency within applications that perform multiple database operations as part of a single logical transaction. In Clean Architecture, this pattern works alongside the Repository pattern to manage database transactions effectively, ensuring that changes are saved atomically and consistently. This article explores how the Unit of Work pattern is implemented in the Contact Management Application to coordinate related operations and maintain data integrity.
In this article, we will cover:
The implementation of the Unit of Work pattern using Dapper.
How Dapper simplifies interaction with the database while maintaining transaction management.
The role of Unit of Work in ensuring atomicity across multiple database operations.
Code examples to demonstrate how transactions are managed in the Contact Management Application
Part of the Series
This article is part of a series on building and understanding the Contact Management Application, a sample project showcasing clean architecture principles. Below are the other articles in the series:
Clean Architecture: Implementing AutoMapper for DTO Mapping and Audit Logging
Clean Architecture: Dependency Injection Setup Across Layers
Clean Architecture: Handling Authorization and Role-Based Access Control (RBAC)
Clean Architecture: Implementing Activity Logging with Custom Attributes
Clean Architecture: Unit of Work Pattern and Its Role in Managing Transactions (You are here)
Clean Architecture: Using Dapper for Data Access and Repository Pattern
Clean Architecture: Best Practices for Creating and Using DTOs in the API
Clean Architecture: Error Handling and Exception Management in the API
Clean Architecture: Dockerizing the .NET Core API, Angular and MS SQL Server
Clean Architecture: Seeding Initial Data Using Docker Compose and SQL Scripts
1. What is the Unit of Work Pattern?
The Unit of Work pattern is a software design principle that ensures a group of operations on the database is treated as a single unit. In simpler terms, it ensures that all operations within a transaction either succeed or fail together. This is especially important when multiple repositories are involved, and you need to guarantee consistency across various entities.
In the Contact Management Application, it is used to manage transactions when working with the database through Dapper.
2. Unit of Work Implementation with Dapper
Dapper is a micro ORM (Object-Relational Mapper) that provides fast and efficient data access with SQL. To integrate Dapper with the Unit of Work pattern, we encapsulate database operations and ensure they run within a transaction.
2.1 Defining the Unit of Work Interface
The IUnitOfWork interface defines methods for starting, committing, and rolling back transactions, ensuring atomicity for database operations.
1
2
3
4
5
6
7
8
9
10
11
12
using System.Data;
namespace Contact.Application.Interfaces
{
public interface IUnitOfWork : IDisposable
{
IDbTransaction BeginTransaction();
Task CommitAsync();
Task RollbackAsync();
}
}
This interface ensures:
BeginTransaction() starts a new database transaction.
CommitAsync() commits all operations as a single unit of work.
RollbackAsync() rolls back the transaction in case of an error.
2.2 Implementing the Unit of Work with Dapper
The UnitOfWork class uses Dapper to manage the database connection and handle transactions.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
using Contact.Application.Interfaces;
using Contact.Infrastructure.Persistence.Helper;
using System.Data;
namespace Contact.Infrastructure.Persistence
{
public class UnitOfWork : IUnitOfWork
{
private readonly IDapperHelper _dapperHelper;
private IDbTransaction _transaction;
private IDbConnection _connection;
public UnitOfWork(IDapperHelper dapperHelper)
{
_dapperHelper = dapperHelper;
_connection = _dapperHelper.GetConnection();
}
public IDbTransaction BeginTransaction()
{
if (_connection.State == ConnectionState.Closed)
_connection.Open();
_transaction = _connection.BeginTransaction();
return _transaction;
}
public async Task CommitAsync()
{
try
{
_transaction?.Commit();
await Task.CompletedTask;
}
finally
{
Dispose();
}
}
public async Task RollbackAsync()
{
try
{
_transaction?.Rollback();
await Task.CompletedTask;
}
finally
{
Dispose();
}
}
public void Dispose()
{
_transaction?.Dispose();
if (_connection?.State == ConnectionState.Open)
{
_connection.Close();
}
_connection?.Dispose();
}
}
}
In this implementation:
BeginTransaction() opens a new transaction if the connection is closed.
CommitAsync() commits the transaction to persist changes.
RollbackAsync() rolls back the transaction in case of failure.
The Dispose() method ensures proper cleanup of the database connection and transaction.
3. Using the Unit of Work in Services
The Unit of Work is integrated with services to ensure that all operations within a service method run in a single transaction.
3.1 Example: Generic Service Using Unit of Work
Here is an example of how this pattern is used in a GenericService to ensure that multiple operations are handled as a single transaction:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
using AutoMapper;
using Contact.Application.Interfaces;
using Contact.Domain.Entities;
using Contact.Domain.Interfaces;
namespace Contact.Application.Services;
public class GenericService<TEntity, TResponse, TCreate, TUpdate>
: IGenericService<TEntity, TResponse, TCreate, TUpdate>
where TEntity : BaseEntity
where TResponse : class
where TCreate : class
where TUpdate : class
{
private readonly IGenericRepository<TEntity> _repository;
private readonly IMapper _mapper;
private readonly IUnitOfWork _unitOfWork;
public GenericService(IGenericRepository<TEntity> repository, IMapper mapper, IUnitOfWork unitOfWork)
{
_repository = repository;
_mapper = mapper;
_unitOfWork = unitOfWork;
}
public async Task<TResponse> Add(TCreate createDto)
{
using var transaction = _unitOfWork.BeginTransaction();
try
{
var entity = _mapper.Map<TEntity>(createDto);
var createdEntity = await _repository.Add(entity, transaction);
await _unitOfWork.CommitAsync();
return _mapper.Map<TResponse>(createdEntity);
}
catch
{
await _unitOfWork.RollbackAsync();
throw;
}
}
public async Task<TResponse> Update(TUpdate updateDto)
{
var entity = _mapper.Map<TEntity>(updateDto);
var updatedEntity = await _repository.Update(entity);
return _mapper.Map<TResponse>(updatedEntity);
}
public async Task<bool> Delete(Guid id)
{
return await _repository.Delete(id);
}
public async Task<TResponse> FindByID(Guid id)
{
var entity = await _repository.FindByID(id);
return _mapper.Map<TResponse>(entity);
}
public async Task<IEnumerable<TResponse>> FindAll()
{
var entities = await _repository.FindAll();
return _mapper.Map<IEnumerable<TResponse>>(entities);
}
}
In this example:
Add() method starts a transaction, adds a new entity, and commits the transaction if successful.
If an exception is thrown, the transaction is rolled back to ensure consistency.
Unit of Work ensures that multiple operations (like repository actions) are performed within the same transaction.
4. Benefits of Using the Unit of Work with Dapper
4.1 Transaction Management
The Unit of Work pattern allows you to group multiple database operations into a single transaction, ensuring atomicity. This is especially critical in complex operations that involve multiple repositories.
4.2 Improved Performance with Dapper
Dapper provides an efficient way to interact with the database, and when combined with Unit of Work, it minimizes the overhead of managing multiple database connections.
4.3 Centralized Transaction Handling
The Unit of Work centralizes the transaction management, making the code easier to maintain. All transaction logic is handled in one place, rather than being spread across repositories or services.
4.4 Consistency and Integrity
By using this pattern, the application ensures that all database changes within a service are consistent and that no partial changes are committed. This is critical for maintaining the integrity of the database.
Conclusion
In this article, we explored how the Unit of Work pattern, when combined with Dapper, plays a crucial role in managing transactions and ensuring data consistency in the Contact Management Application. By encapsulating multiple operations within a single transaction, the Unit of Work ensures that all database changes are committed or rolled back together.
In the next article, we will dive into how Dapper is used for efficient data access, working alongside the Repository Pattern to manage interactions with the database.
For more details, check out the full project on GitHub: