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
- Transaction Management: Ensures that a group of operations are treated as a single transaction.
- Minimized Database Access: Reduces the number of database calls by batching multiple operations.
- Consistency: Maintains data integrity by ensuring that all operations succeed or fail as a unit.
- 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
- Create a new project
dotnet new console -n UnitOfWorkExample
cd UnitOfWorkExample
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
- 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; }
}
- 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.
- 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);
}
- 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
- Unit of Work Interface
public interface IUnitOfWork : IDisposable
{
IRepository<Author> Authors { get; }
IRepository<Book> Books { get; }
Task<int> CompleteAsync();
}
- 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.
- 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();
}
- 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.