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!
Рекомендую добавить ещё абстракцию на транзакции 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), если источники данных ее поддерживают, а если нет - реализовывать сагу.
Реализуем паттерн Unit of Work в ASP.NET Core