The Unit of Work Pattern in C# for Modern Applications

Introduction

As software applications grow in complexity, maintaining clean and manageable code becomes increasingly important. The Unit of Work pattern is a widely used design pattern that helps manage database transactions and promotes a clean separation of concerns. In this article, we will explore the Unit of Work pattern, its benefits, and how to implement it in C# for modern applications.

What is the Unit of Work Pattern?

The Unit of Work pattern is a design pattern that helps manage the transactional work of multiple operations. It acts as a coordinator for a set of actions that need to be performed as a single unit. This ensures that all operations within the unit either complete successfully or roll back together, maintaining the integrity of the database.

Benefits of the Unit of Work Pattern

  1. Transaction Management: Ensures that a group of operations are treated as a single transaction.
  2. Minimized Database Access: Reduces the number of database calls by batching multiple operations.
  3. Consistency: Maintains data integrity by ensuring that all operations succeed or fail as a unit.
  4. Separation of Concerns: Decouples business logic from data access logic.

Implementing the Unit of Work Pattern in C#

To demonstrate the implementation of the Unit of Work pattern in C#, we'll use Entity Framework Core, a popular ORM for .NET applications. We'll create a sample application that manages a list of books and authors.

Step 1. Setting Up the Project

  1. Create a new project
    dotnet new console -n UnitOfWorkExample
    cd UnitOfWorkExample
    dotnet add package Microsoft.EntityFrameworkCore
    dotnet add package Microsoft.EntityFrameworkCore.SqlServer
    
  2. Define the models
    public class Author
    {
        public int AuthorId { get; set; }
        public string Name { get; set; }
        public ICollection<Book> Books { get; set; }
    }
    public class Book
    {
        public int BookId { get; set; }
        public string Title { get; set; }
        public int AuthorId { get; set; }
        public Author Author { get; set; }
    }
    
  3. Create the DbContext
    public class LibraryContext : DbContext
    {
        public DbSet<Author> Authors { get; set; }
        public DbSet<Book> Books { get; set; }
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer("YourConnectionStringHere");
        }
    }
    

Step 2. Creating Repositories

Repositories help abstract the data access logic. We will create generic repositories for basic CRUD operations.

  1. Generic Repository Interface
    public interface IRepository<T> where T : class
    {
        Task<IEnumerable<T>> GetAllAsync();
        Task<T> GetByIdAsync(int id);
        Task AddAsync(T entity);
        void Update(T entity);
        void Delete(T entity);
    }
    
  2. Generic Repository Implementation
    public class Repository<T> : IRepository<T> where T : class
    {
        private readonly LibraryContext _context;
        private readonly DbSet<T> _dbSet;
        public Repository(LibraryContext context)
        {
            _context = context;
            _dbSet = _context.Set<T>();
        }
        public async Task<IEnumerable<T>> GetAllAsync() => await _dbSet.ToListAsync();
        public async Task<T> GetByIdAsync(int id) => await _dbSet.FindAsync(id);
        public async Task AddAsync(T entity) => await _dbSet.AddAsync(entity);
        public void Update(T entity) => _dbSet.Update(entity);
        public void Delete(T entity) => _dbSet.Remove(entity);
    }
    

Step 3. Implementing the Unit of Work

  1. Unit of Work Interface
    public interface IUnitOfWork : IDisposable
    {
        IRepository<Author> Authors { get; }
        IRepository<Book> Books { get; }
        Task<int> CompleteAsync();
    }
    
  2. Unit of Work Implementation
    public class UnitOfWork : IUnitOfWork
    {
        private readonly LibraryContext _context;
        private IRepository<Author> _authors;
        private IRepository<Book> _books;
        public UnitOfWork(LibraryContext context)
        {
            _context = context;
        }
        public IRepository<Author> Authors => _authors ??= new Repository<Author>(_context);
        public IRepository<Book> Books => _books ??= new Repository<Book>(_context);
        public async Task<int> CompleteAsync() => await _context.SaveChangesAsync();
        public void Dispose() => _context. Dispose();
    }
    

Step 4. Using the Unit of Work

Finally, let's create a simple service that uses the Unit of Work to manage transactions.

  1. Book Service
    public class BookService
    {
        private readonly IUnitOfWork _unitOfWork;
        public BookService(IUnitOfWork unitOfWork)
        {
            _unitOfWork = unitOfWork;
        }
        public async Task AddBookAsync(string title, int authorId)
        {
            var book = new Book { Title = title, AuthorId = authorId };
            await _unitOfWork.Books.AddAsync(book);
            await _unitOfWork.CompleteAsync();
        }
        public async Task<IEnumerable<Book>> GetBooksAsync() => await _unitOfWork.Books.GetAllAsync();
    }
    
  2. Main Method
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var options = new DbContextOptionsBuilder<LibraryContext>()
                .UseSqlServer("YourConnectionStringHere")
                .Options;
            using var context = new LibraryContext(options);
            var unitOfWork = new UnitOfWork(context);
            var bookService = new BookService(unitOfWork);
            await bookService.AddBookAsync("C# in Depth", 1);
            var books = await bookService.GetBooksAsync();
            foreach (var book in books)
            {
                Console.WriteLine(book.Title);
            }
        }
    }
    

Conclusion

The Unit of Work pattern is a valuable tool for managing database transactions cleanly and efficiently. Coordinating multiple operations within a single transaction ensures data integrity and reduces database access. Implementing this pattern in C# with Entity Framework Core helps in creating maintainable and scalable applications.


Similar Articles