Pull to refresh

Comments 14

Но ведь если вы используете EF, то фреймворк уже заботливо предоставил вам имплементацию таких паттернов как Repository и UnitOfWork. В виде DbSet и DbContext соответственно. Городить поверх них абстракции делающие тоже самое - раздувать кодовую базу проекта, создавая дополнительные точки отказа и поле для ошибок в имплементации.

Для простых проектов может и не нужно. Но с определённого момента написание юнит тестов под логику завязанную на EF может обернутся сущим адом. Интерфейсы IRepository<T> и IUnitOfWork изолируют вашу бизнес логику от инфраструктуры.

С подходом, когда мы оборачиваем EF в абстракцию и мокаем ее, есть две проблемы. Во-первых, появляется огромное количество бойлерплейта, который утомительно поддерживать. Во-вторых, ощутимая доля настоящей логики оказывается замокана, а следовательно не покрыта тестами.

На моем опыте, самым удобным вариантом оказалось подсунуть EF вместо настоящего провайдера БД провайдер от SQLite InMemory. Они заявляют не 100%, но довольно высокую совместимость с PostgreSQL, как в виде LINQ-запросов, так и в виде голых SQL-запросов. Мелкое оставшееся уже нужно покрывать интеграционными тестами, если это критично.

В UoW и репозиторий часто прячут логику проверки или дополнения данных, инвалидации кеша или часто используемые запросы. Бизнес логика при этом становится проще. Как вы и сами написали, для тестирования всей логики целиком, есть интеграционные тесты.

Да в том-то и дело, что не проще. Я могу понять, когда работа с базой ведется через ADO или Dapper - тогда действительно имеет смысл оборачивать ее в DAL и выставлять наружу осмысленные методы, чтобы в бизнес-логике не нужно было конструировать SQL-запросы. Правда, в таком случае со временем в этом DAL появляется миллион методов с тысячей параметров в каждом, и каждая конкретная комбинация используется всего в одном месте, но это вроде как все еще меньшее из зол. Но если мы говорим про LINQ и ORM вида Linq2DB/EF, то это уже абстракция, причем максимально гибкая. Зачем ее оборачивать в еще одну абстракцию?

Ну а что касается интеграционных тестов - они по своей природе медленные, зачастую на несколько порядков. Поэтому чем больше мест получается протестировать без них, тем лучше.

Если оба ваших контекста работают на основе одного и того же DbConnection, то не было смысла разделять их на два (тогда DbSet = IRepository, а DbContext = IUnitOfWork). А если на разных, то вам потребуются распределенные транзакции, которые дотнет поддерживает только на Windows.

Итого - бойлерплейта написали, какую задачу решили непонятно.

Если приложение использует несколько DbContext, то транзакция и вместе с ней UnitOfWork должны быть на более высоком уровне, чем DbContext = IUnitOfWork.

Так я про это и говорю - в тех случаях, когда в приложении действительно нужно иметь два отдельных контекста, предлагаемый вариант не решает главную проблему отсутствия единой транзакции.

Автор статьи не изобрел EF - он просто его использует. Но то ли он забыл написать об этом, то ли считает, что EF - это встроенная функция ASP.NET Core (на самом деле, нет).

Рекомендую добавить ещё абстракцию на транзакции ITransaction, Тогда вы сможете использовать несколько Unit of Work в одной логике:

void DoBusiness()
{
   using ITransaction coreTransaction = _coreUnitOfWork.BeginTransaction();
   using ITransaction auditTransaction = _auditUnitOfWork.BeginTransaction();

   try
   {
      _coreUnitOfWork.UserRepository.Add(new User());
      _coreUnitOfWork.SaveChanges();

      _auditUnitOfWork.AuditRepository.Add(new UserAdded());
      _auditUnitOfWork.SaveChanges();

      _coreTransaction.Commit();
      _auditTransaction.Commit();
   }
   catch
   {
      _coreTransaction.Rollback();
      _auditTransaction.Rollback();
   }
}

Не идеально, но в большинстве случаев достаточно.

В моём представлении транзакция должна быть одна. Если так сделать невозможно, то строится набор алгоритмов по обновлению и откату обновления данных, которые сейчас любят называть Saga.

Если вызов _auditTransaction.Commit() упадет, что будете делать?

Не идеально, но в большинстве случаев достаточно.

Ну, в большинстве случаев можно вообще без транзакций - обычно всё работает и так. Транзакции - они для меньшинства случаев.

И для этоого самого меньшинства ваше решение неудачное: если одна транзакция зафиксирована, а фиксация второй транзакции вызовет сбой, то даннные останутся в несогласованном состоянии. Решать эту проблему можно по разному: использовать менеджер транзакций (например> был в свое время такой Microsoft Transaction Coordinator для Windows), самостоятельно использовать двухфазовую фиксацию (2 - phase commit), если источники данных ее поддерживают, а если нет - реализовывать сагу.

Sign up to leave a comment.