How To Use IUnitOfWork: Single Responsibility Principle

Josiah T Mahachi
6 min readMar 7, 2023

--

How To Use IUnitOfWork: Single Responsibility Principle
Single Responsibility Principle Meme

Welcome back to the third part of our series where we have been building an API using Entity Framework with Code First. In the previous article, I said that it was not a good idea to inject the LibraryContext into a controller. In this article, we will talk about why it’s not a good idea, and offer an alternative solution using the Unit of Work pattern.

Introduction

As we have seen in our previous articles, the LibraryContext is the heart of our application. It connects to the database and is responsible for mapping our entities to the corresponding tables. However, injecting the LibraryContext directly into our controllers violates the Single Responsibility Principle. Our controller's responsibility is to handle incoming HTTP requests and return HTTP responses. It should not be responsible for managing the lifetime of the LibraryContext or the underlying database connection.

The problem with injecting the LibraryContext into our controllers is that it makes it hard to test our controllers in isolation. In a typical scenario, we will have multiple controllers that need to use the LibraryContext. Each controller will create its own instance of the LibraryContext, resulting in multiple instances of the same LibraryContext. This leads to code duplication and maintenance issues, which could result in subtle bugs that are hard to find.

So, what’s the alternative? The answer is to use the Unit of Work pattern. The Unit of Work pattern is a design pattern that helps us manage database transactions in a more efficient way. Instead of injecting the LibraryContext directly into our controllers, we inject an instance of the IUnitOfWork interface.

The IUnitOfWork interface defines a contract that represents a single transaction of work against the database. It has methods to begin, commit, and rollback a transaction. It also provides access to repositories, which are responsible for querying and updating the entities in the database.

By using the Unit of Work pattern, we can ensure that all our controllers share the same instance of the LibraryContext. This improves performance and reduces code duplication. It also makes it easy to test our controllers in isolation.

Let us jump into the code.

IUnitOfWork

Before we create the Unit of Work, I usually like to put all common code in a base class as we saw in previous articles. In this case a generic IRepositoryBase<T>. This will be responsible for making all our CRUD operations. Here is the interface and its concrete implementation:

    public interface IRepositoryBase<T> where T : ModelBase
{
Task<IEnumerable<T>> GetAllAsync();
Task<T?> GetByIdAsync(int id);
Task AddAsync(T entity);
void Update(T entity);
void Delete(T entity);
Task<bool> ExistsAsync(int id);
}

public class BaseRepository<T> : IRepositoryBase<T> where T : ModelBase
{
protected readonly LibraryContext _context;

public BaseRepository(LibraryContext context)
{
_context = context;
}

public virtual async Task<IEnumerable<T>> GetAllAsync()
{
return await _context.Set<T>().ToListAsync();
}

public virtual async Task<T?> GetByIdAsync(int id)
{
return await _context.Set<T>().FindAsync(id);
}

public virtual async Task AddAsync(T entity)
{
await _context.Set<T>().AddAsync(entity);
}

public virtual void Update(T entity)
{
_context.Entry(entity).State = EntityState.Modified;
}

public virtual void Delete(T entity)
{
_context.Set<T>().Remove(entity);
}

public virtual async Task<bool> ExistsAsync(int id)
{
return await _context.Set<T>().AnyAsync(x => x.Id == id);
}
}

Notice that none of these methods are actually committing data to the database. That is the work of IUnitOfWork. The _context is protected, meaning it can be used in derived classes.

We would then create our IBookRepository as:

    /// <summary>
/// The book repository
/// </summary>
public interface IBookRepository : IRepositoryBase<Book>
{
/// <summary>
/// Get books writen by a certain author
/// </summary>
/// <param name="authorId">Id of the authur</param>
/// <returns></returns>
Task<IEnumerable<Book>> GetBooksByAuthorId(int authorId);
}

/// <summary>
/// Repository for accessing and modifying Book entities.
/// </summary>
public class BookRepository : BaseRepository<Book>, IBookRepository
{
/// <summary>
/// Initializes a new instance of the <see cref="BookRepository"/> class.
/// </summary>
/// <param name="context">The DbContext to use for database access.</param>
public BookRepository(LibraryContext context) : base(context)
{
}

/// <summary>
/// Add a book
/// </summary>
/// <param name="entity">The book entity</param>
/// <returns>void</returns>
public override async Task AddAsync(Book entity)
{
// Do something here before calling base.AddAsync(entity)

await base.AddAsync(entity);
}

/// <summary>
/// Get books writen by a certain author
/// </summary>
/// <param name="authorId">Id of the authur</param>
/// <returns></returns>
public async Task<IEnumerable<Book>> GetBooksByAuthorId(int authorId)
{
return await _context.Books.Where(b => b.Authors.Any(x => x.Id == authorId)).ToListAsync();
}
}

Notice here that we do not need to have the CRUD methods in the BookRepository. Should we need to add custom logic before we commit to the database, we can simply override the method.

We can repeat the same process for Author and Publisher to create IAuthorRepository and IPublisherRepository.

Then our IUnitOfWork:

    public interface IUnitOfWork : IDisposable
{
IBookRepository Books { get; }
IAuthorRepository Authors { get; }
IPublisherRepository Publishers { get; }
Task<int> Complete();
}

public class UnitOfWork : IUnitOfWork
{
private readonly LibraryContext _context;
private IBookRepository _bookRepository;
private IAuthorRepository _authorRepository;
private IPublisherRepository _publisherRepository;

public UnitOfWork(LibraryContext context)
{
_context = context;
}

public IBookRepository Books => _bookRepository ??= new BookRepository(_context);

public IAuthorRepository Authors => _authorRepository ??= new AuthorRepository(_context);

public IPublisherRepository Publishers => _publisherRepository ??= new PublisherRepository(_context);

public async Task<int> Complete()
{
return await _context.SaveChangesAsync();
}

public void Dispose()
{
_context.Dispose();
}
}

There is, however, one small adjustment we can make to the IUnitOfWork to introduce lazy loading.

    public interface IUnitOfWork : IDisposable
{
TRepository GetRepository<TRepository, TEntity>() where TRepository : class, IRepositoryBase<TEntity> where TEntity : ModelBase;
Task SaveAsync();
}

public class UnitOfWork : IUnitOfWork
{
private readonly LibraryContext _context;
private Dictionary<Type, object> _repositories;

public UnitOfWork(LibraryContext context)
{
_context = context;
_repositories = new Dictionary<Type, object>();
}

public void Dispose()
{
_context.Dispose();
}

TRepository IUnitOfWork.GetRepository<TRepository, TEntity>()
{
var type = typeof(TEntity);

if (_repositories.ContainsKey(type))
{
return (TRepository)_repositories[type];
}
else
{
var repositoryType = typeof(TRepository);
var repositoryInstance = Activator.CreateInstance(repositoryType.MakeGenericType(typeof(TEntity)), _context);
_repositories.Add(type, repositoryInstance);
}

return (TRepository)_repositories[type];
}

public async Task SaveAsync()
{
await _context.SaveChangesAsync();
}
}

With this small change, we no longer need to remember to add a new repository whenever we create one. This also makes it easier for us to use the IUnitOfWork inside our controllers.

Using the IUnitOfWork inside our controllers

We now swap out LibraryContext for the IUnitOfWork in our controller as shown in the BookController:

using EightApp.Demo.EfCoreCodeFirst01.Interfaces;
using EightApp.Demo.EfCoreCodeFirst01.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Swashbuckle.AspNetCore.Annotations;

namespace EightApp.Demo.EfCoreCodeFirst01.Controllers
{
/// <summary>
/// Controller for accessing book endpoints
/// </summary>
[Route("api/[controller]")]
[ApiController]
public class BookController : ControllerBase
{
private readonly IUnitOfWork _unitOfWork;
private readonly IBookRepository _bookRepository;

/// <summary>
/// Constructor
/// </summary>
/// <param name="unitOfWork"></param>
public BookController(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
_bookRepository = unitOfWork.GetRepository<IBookRepository, Book>();
}

/// <summary>
/// Retrieves all books.
/// </summary>
[HttpGet]
[SwaggerOperation("GetAllBooks")]
[ProducesResponseType(typeof(IEnumerable<Book>), 200)]
public async Task<ActionResult<IEnumerable<Book>>> GetBooks()
{
var books = await _bookRepository.GetAllAsync();

return Ok(books);
}

/// <summary>
/// Retrieves a specific book by ID.
/// </summary>
/// <param name="id">The ID of the book to retrieve.</param>
[HttpGet("{id}")]
[SwaggerOperation("GetBookById")]
[ProducesResponseType(typeof(Book), 200)]
[ProducesResponseType(404)]
public async Task<ActionResult<Book>> GetBook(int id)
{
var book = await _bookRepository.GetByIdAsync(id);

if (book == null)
{
return NotFound();
}

return Ok(book);
}

/// <summary>
/// Adds a new book to the database.
/// </summary>
/// <param name="book">The book to add.</param>
[HttpPost]
[SwaggerOperation("AddBook")]
[ProducesResponseType(typeof(Book), 201)]
public async Task<ActionResult<Book>> PostBook(Book book)
{
await _bookRepository.AddAsync(book);
await _unitOfWork.SaveAsync();

return CreatedAtAction("GetBook", new { id = book.Id }, book);
}

/// <summary>
/// Updates an existing book in the database.
/// </summary>
/// <param name="id">The ID of the book to update.</param>
/// <param name="book">The updated book information.</param>
[HttpPut("{id}")]
[SwaggerOperation("UpdateBook")]
[ProducesResponseType(204)]
[ProducesResponseType(404)]
public async Task<IActionResult> PutBook(int id, Book book)
{
if (id != book.Id)
{
return BadRequest();
}

_bookRepository.Update(book);

try
{
await _unitOfWork.SaveAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!await BookExists(id))
{
return NotFound();
}
else
{
throw;
}
}

return NoContent();
}

/// <summary>
/// Deletes a book from the database.
/// </summary>
/// <param name="id">The ID of the book to delete.</param>
[HttpDelete("{id}")]
[SwaggerOperation("DeleteBook")]
[ProducesResponseType(204)]
[ProducesResponseType(404)]
public async Task<IActionResult> DeleteBook(int id)
{
var book = await _bookRepository.GetByIdAsync(id);

if (book == null)
{
return NotFound();
}

_bookRepository.Delete(book);

await _unitOfWork.SaveAsync();

return NoContent();
}

private async Task<bool> BookExists(int id)
{
return await _bookRepository.ExistsAsync(id);
}
}
}

The same can then be done for the other controllers. We should remember to register all the repositories that we have created in the Program.cs class. “All” is in bold because it is the subject of one of our upcoming articles.

builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
builder.Services.AddScoped<IAuthorRepository, AuthorRepository>();
builder.Services.AddScoped<IBookRepository, BookRepository>();
builder.Services.AddScoped<IPublisherRepository, PublisherRepository>();

In this article, we discuss how to use Reflection to register these repositories.

Wrapping up…

Injecting the LibraryContext directly into our controllers is not recommended. It violates the Single Responsibility Principle and makes it hard to test our controllers in isolation. Instead, we should use the Unit of Work pattern, which helps us manage database transactions in a more efficient way. This improves performance, reduces code duplication, and makes it easy to test our controllers in isolation.

--

--

Josiah T Mahachi

Full-stack Developer, C#, ASP.Net, Laravel, React Native, PHP, Azure Dev Ops, MSSQL, MySQL