Реализуем паттерн 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 разработчик»:


ссылка на оригинал статьи https://habr.com/ru/articles/850124/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *