- Published on
Почему плохо возвращать IQueryable в репозитории
- Authors
- Name
- Николай Маракасов
Разделение ответственности в паттерне "Репозиторий"
Репозиторий позволяет абстрагировать логику доступа к данным от доменной логики. Рассмотрим пример:
public interface IOrderRepository
{
IQueryable<Order> Get();
Task<int> Add(Order order, CancellationToken cancellationToken);
Task<int> Update(Order order, CancellationToken cancellationToken);
Task<int> Delete(Order order, CancellationToken cancellationToken);
}
Это стандартный набор операций, который требуется в большинстве приложений. Однако метод Get()
вызывает вопросы:
он возвращает IQueryable<Order>
, что даёт возможность выполнять запросы непосредственно к источнику данных.
Entity Framework Core предоставляет свою реализацию этого интерфейса, которой мы часто пользуемся при работе с DbContext
.
IQueryable
Проблема подхода с Использование IQueryable
на уровне репозитория приводит к следующим проблемам:
- Логика фильтрации и проекции "вытекает" наверх — в слой бизнес-правил или даже в контроллеры. Это ведёт к дублированию кода.
- Усложнённое тестирование — проверка таких методов требует заглушек для
IQueryable
, что неудобно. - Жёсткая привязка к ORM — при необходимости заменить
Entity Framework Core
наDapper
илиADO.NET
могут возникнуть сложности, так как они не поддерживаютIQueryable
.
Альтернативный вариант
Первым шагом заменим возвращаемый тип метода Get()
на IEnumerable<Order>
:
public interface IOrderRepository
{
IEnumerable<Order> Get();
Task<int> Add(Order order, CancellationToken cancellationToken);
Task<int> Update(Order order, CancellationToken cancellationToken);
Task<int> Delete(Order order, CancellationToken cancellationToken);
}
Теперь логика фильтрации переходит внутрь репозитория. Однако, если в приложении появится много различных способов выборки данных, интерфейс репозитория начнёт разрастаться.
Разделение на репозитории для чтения и записи
Чтобы избежать перегрузки интерфейса, разделим его на два:
IOrderRepository
— отвечает за операции записи и используется бизнес-логикой.IOrderReadRepository
— предназначен для операций чтения, используя проекции (DTO
) иIEnumerable
.
// Репозиторий для записи
public interface IOrderRepository
{
Task<Order> Get(string orderId);
Task<int> Add(Order order, CancellationToken cancellationToken);
Task<int> Update(Order order, CancellationToken cancellationToken);
Task<int> Delete(Order order, CancellationToken cancellationToken);
}
// Репозиторий для чтения
public interface IOrderReadRepository
{
Task<IEnumerable<OrderDto>> GetByDate(DateOnly date, CancellationToken ct);
// Другие методы выборки...
}
Преимущества такого подхода:
Чёткое разделение ответственности — репозиторий для записи используется бизнес-логикой, а репозиторий для чтения инкапсулирует логику выборки данных.
Гибкость в реализации — IOrderReadRepository
можно оптимизировать с помощью Dapper
, ADO.NET
или кеширования (Redis
).
Упрощённое тестирование — не нужно мокать IQueryable
, достаточно тестировать методы чтения отдельно.
Заключение
Паттерн "Репозиторий" помогает отделить детали работы с данными от бизнес-логики. Однако важно, чтобы абстракция не протекала — то есть не зависела от конкретных технологий. Разделение интерфейсов на чтение и запись делает код более чистым, тестируемым и гибким.