- 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, достаточно тестировать методы чтения отдельно.
Заключение
Паттерн "Репозиторий" помогает отделить детали работы с данными от бизнес-логики. Однако важно, чтобы абстракция не протекала — то есть не зависела от конкретных технологий. Разделение интерфейсов на чтение и запись делает код более чистым, тестируемым и гибким.