Реализуем паттерн Unit of Work в ASP.NET Core
Привет, Хабр!
Сегодня разберём, как реализовать паттерн Unit of Work в ASP.NET Core. Вместо долгих теоретических рассуждений, посмотрим, зачем он вообще нужен, и как правильно его применить на практике.
Почему вообще нужен Unit of Work?
Ты наверняка сталкивался с ситуацией, когда несколько операций с базой данных нужно обернуть в одну транзакцию. Например, при создании пользователя нужно добавить его в несколько таблиц. А что если что‑то пошло не так? Одна из операций упала, а данные уже частично добавлены? Здесь и помогает Unit of Work. Он следит за тем, чтобы все изменения проходили через одну точку, и либо подтверждаются все сразу, либо откатываются.
Но почему именно Unit of Work, а не просто транзакции через DbContext? Ответ простой — паттерн позволяет работать с несколькими репозиториями одновременно.
Интерфейс IUnitOfWork
Начнём с основы — интерфейса IUnitOfWork, который будет управлять нашими транзакциями.
public interface IUnitOfWork : IDisposable
{
IRepository UserRepository { get; }
IRepository OrderRepository { get; }
void Commit();
void Rollback();
}
Пара заметок:
IDisposable нужен для корректного освобождения ресурсов. Это значит, что когда ты закончишь работу с транзакцией, Dispose автоматически закроет все соединения и освободит память. Не забываем вызывать метод!
Commit и Rollback — это методы, которые отвечают за подтверждение или откат транзакций.
Теперь у нас есть интерфейс, переходим к главному — DbContext.
Основа операции — DbContext
Как я уже говорил, Unit of Work сам по себе мало что может, если у него нет связи с базой данных. Здесь на помощь приходит DbContext, который отвечает за все операции с БД в ASP.NET Core.
public class AppDbContext : DbContext
{
public DbSet Users { get; set; }
public DbSet Orders { get; set; }
public AppDbContext(DbContextOptions options)
: base(options) { }
public void BeginTransaction()
{
Database.BeginTransaction();
}
public void CommitTransaction()
{
Database.CommitTransaction();
}
public void RollbackTransaction()
{
Database.RollbackTransaction();
}
}
Тут видим несколько методов:
BeginTransaction — начинает транзакцию. Это первый шаг, перед тем как выполнять изменения.
CommitTransaction — подтверждает все изменения.
RollbackTransaction — откатывает изменения, если что-то пошло не так.
Эти методы — основа работы паттерна Unit of Work, но ещё важнее то, как их правильно интегрировать в бизнес-логику.
Реализация Unit of Work
Теперь соберём наш Unit of Work в единый механизм.
public class UnitOfWork : IUnitOfWork
{
private readonly AppDbContext _context;
private IRepository _userRepository;
private IRepository _orderRepository;
public UnitOfWork(AppDbContext context)
{
_context = context;
}
public IRepository UserRepository
{
get { return _userRepository ??= new Repository(_context); }
}
public IRepository OrderRepository
{
get { return _orderRepository ??= new Repository(_context); }
}
public void Commit()
{
_context.SaveChanges();
_context.CommitTransaction();
}
public void Rollback()
{
_context.RollbackTransaction();
}
public void Dispose()
{
_context.Dispose();
}
Обрати внимание:
Ленивая инициализация репозиториев. Это значит, что мы создаём репозитории только тогда, когда они действительно нужны.
Commit вызывает метод SaveChanges, который сохраняет все изменения в базу, а затем подтверждает транзакцию. В случае ошибки — откат.
Когда Unit of Work — это не лучший выбор?
Unit of Work — отличный инструмент для управления транзакциями, но его не всегда стоит использовать. Например, если есть приложение с небольшими и простыми операциями, добавление лишнего уровня абстракции только усложнит код. В таких случаях лучше использовать дефолт транзакции через DbContext.
Помимо этого, если существует слишком много репозиториев и зависимостей, Unit of Work может стать лишь узким местом по производительности. Поэтому всегда оценивай, насколько оправдано его использование.
Репозитории
Unit of Work без репозиториев — как велосипед без колёс. Они управляют конкретными сущностями и отвечают за CRUD-операции. Пример репозитория:
public class Repository : IRepository where T : class
{
private readonly AppDbContext _context;
private readonly DbSet _dbSet;
public Repository(AppDbContext context)
{
_context = context;
_dbSet = context.Set();
}
public void Add(T entity)
{
_dbSet.Add(entity);
}
public void Update(T entity)
{
_dbSet.Update(entity);
}
public void Delete(T entity)
{
_dbSet.Remove(entity);
}
public IEnumerable GetAll()
{
return _dbSet.ToList();
}
public T GetById(int id)
{
return _dbSet.Find(id);
}
}
Этот репозиторий универсален и может работать с любыми сущностями. Все операции — через DbSet.
Как это выглядит на практике
Теперь посмотрим на реальный пример использования Unit of Work в контроллере:
public class UserController : Controller
{
private readonly IUnitOfWork _unitOfWork;
public UserController(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
[HttpPost]
public IActionResult CreateUser(UserViewModel model)
{
try
{
_unitOfWork.UserRepository.Add(new User { Name = model.Name });
_unitOfWork.Commit();
return Ok("User created successfully.");
}
catch (Exception ex)
{
_unitOfWork.Rollback();
return BadRequest($"Error: {ex.Message}");
}
}
}
Мы добавляем пользователя через UserRepository и фиксируем транзакцию через Commit. Если что-то пошло не так, транзакция откатывается.
Как это тестировать?
Тестирование транзакций — важная часть работы с Unit of Work. Для этого идеально подходит библиотека Moq:
[Test]
public void CreateUser_ShouldCommitTransaction_WhenUserIsValid()
{
var mockUnitOfWork = new Mock();
var controller = new UserController(mockUnitOfWork.Object);
var result = controller.CreateUser(new UserViewModel { Name = "Test User" });
mockUnitOfWork.Verify(u => u.Commit(), Times.Once);
}
Здесь проверяем, что метод Commit вызывается при успешном добавлении пользователя.
Заключение
Теперь ты знаешь, как реализовать и использовать паттерн Unit of Work в ASP.NET Core. Но помни: не всегда этот паттерн нужен, и его использование должно быть оправдано архитектурой проекта. Если у тебя возникли вопросы или есть чем поделиться — пиши в комментариях.
Пользуясь случаем, напоминаю про открытые уроки, которые скоро пройдут в рамках курса «C# ASP.NET Core разработчик»: