Unit of Work Pattern and Its Role in Managing Transactions
Introduction
In modern applications, the Unit of Work (UoW) pattern is a core architectural principle that allows developers to manage transactions across multiple database operations in a consistent way. In the Contact Management Application, we utilize the Unit of Work pattern along with Dapper for efficient data access. This ensures that all operations are committed or rolled back as a single transaction, maintaining the integrity of the data.
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: Introduction to the Project Structure
- Clean Architecture: Implementing AutoMapper for DTO Mapping and Audit Logging
- Clean Architecture: Validating Inputs with FluentValidation
- 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 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.
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.
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:
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:
GitHub Repository: Contact Management Application
Discover more from Nitin Singh
Subscribe to get the latest posts sent to your email.
5 Responses
[…] Clean Architecture: Unit of Work Pattern and Its Role in Managing Transactions […]
[…] Clean Architecture: Unit of Work Pattern and Its Role in Managing Transactions […]
[…] Clean Architecture: Unit of Work Pattern and Its Role in Managing Transactions […]
[…] Clean Architecture: Unit of Work Pattern and Its Role in Managing Transactions […]
[…] Clean Architecture: Unit of Work Pattern and Its Role in Managing Transactions […]