Привет, Хабр!
Сегодня разберём, как реализовать паттерн 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 разработчик»:
-
16 октября: .NET Aspire: очередная прорывная технология в мире dotnet или не очень? Запись по ссылке
-
24 октября: Деплой ASP.NET приложений в Kubernetes. Запись по ссылке
ссылка на оригинал статьи https://habr.com/ru/articles/850124/
Добавить комментарий