Unit of Work Pattern and Its Role in Managing Transactions

Unit of Work Pattern and Its Role in Managing Transactions
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:

  1. The implementation of the Unit of Work pattern using Dapper.
  2. How Dapper simplifies interaction with the database while maintaining transaction management.
  3. The role of Unit of Work in ensuring atomicity across multiple database operations.
  4. 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:

  1. Clean Architecture: Introduction to the Project Structure
  2. Clean Architecture: Implementing AutoMapper for DTO Mapping and Audit Logging
  3. Clean Architecture: Validating Inputs with FluentValidation
  4. Clean Architecture: Dependency Injection Setup Across Layers
  5. Clean Architecture: Handling Authorization and Role-Based Access Control (RBAC)
  6. Clean Architecture: Implementing Activity Logging with Custom Attributes
  7. Clean Architecture: Unit of Work Pattern and Its Role in Managing Transactions (You are here)
  8. Clean Architecture: Using Dapper for Data Access and Repository Pattern
  9. Clean Architecture: Best Practices for Creating and Using DTOs in the API
  10. Clean Architecture: Error Handling and Exception Management in the API
  11. Clean Architecture: Dockerizing the .NET Core API and MS SQL Server
  12. 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

  1. December 3, 2024

    […] Clean Architecture: Unit of Work Pattern and Its Role in Managing Transactions […]

  2. December 16, 2024

    […] Clean Architecture: Unit of Work Pattern and Its Role in Managing Transactions […]

  3. December 16, 2024

    […] Clean Architecture: Unit of Work Pattern and Its Role in Managing Transactions […]

  4. December 16, 2024

    […] Clean Architecture: Unit of Work Pattern and Its Role in Managing Transactions […]

  5. December 16, 2024

    […] Clean Architecture: Unit of Work Pattern and Its Role in Managing Transactions […]

Leave a Reply