Совет, который я даю в отношении шаблона репозитория, заключается в том, что репозитории должны возвращать IQueryable<TEntity>
, а не IEnumerable<TEntity>
.
Целью репозитория является:
- Упрощение тестирования кода.
- Централизация общих бизнес-правил.
цель репозитория должна не быть:
- Абстрактная EF вне вашего проекта.
- Скрыть знание вашего домена. (Entities)
Если вы вводите репозиторий, чтобы скрыть тот факт, что решение зависит от EF, или скрыть домен, тогда вы жертвуете большей частью того, что EF может принести на стол для управления взаимодействие с вашими данными, или вы вносите много ненужных сложностей в свое решение, чтобы попытаться сохранить эту возможность. (фильтрация, сортировка, разбиение на страницы, выборочная загрузка и т. д. c.)
Вместо этого, используя IQueryable
и рассматривая EF как первоклассного гражданина в своем домене, вы можете использовать EF для создания гибких и быстрые запросы для получения нужных вам данных.
Для данной службы, где вы хотите "вернуть DTO с OrderId, OrderDetails, CustomerId, CustomerName."
Шаг 1: Необработанный пример, без репозитория. ..
Сервисный код:
public OrderDto GetOrderById(int orderId)
{
using (var context = new AppDbContext())
{
var order = context.Orders
.Select(x => new OrderDto
{
OrderId = x.OrderId,
OrderDetails = x.OrderDetails,
CustomerId = x.Customer.CustomerId,
CustomerName = x.Customer.Name
}).Single(x => x.OrderId == orderId);
return order;
}
}
Этот код может прекрасно работать, но он связан с DbContext, поэтому его сложно выполнить модульным тестированием. У нас может быть дополнительная бизнес-логика c, чтобы учесть, что ее придется применять практически ко всем запросам, например, если у заказов есть состояние «IsActive» (мягкое удаление) или база данных обслуживает несколько клиентов (мультитенант). В наших контроллерах будет много запросов, что повлечет за собой необходимость во многих вещах, таких как .Where(x => x.IsActive)
, включенных повсюду.
С шаблоном Repository (IQueryable
), единица работы:
public OrderDto GetOrderById(int orderId)
{
using (var context = ContextScopeFactory.CreateReadOnly())
{
var order = OrderRepository.GetOrders()
.Select(x => new OrderDto
{
OrderId = x.OrderId,
OrderDetails = x.OrderDetails,
CustomerId = x.Customer.CustomerId,
CustomerName = x.Customer.Name
}).Single(x => x.OrderId == orderId);
return order;
}
}
Теперь при номинальной стоимости в приведенном выше коде контроллера это не сильно отличается от первого пример, но есть несколько битов, которые делают этот тестируемым и могут помочь в управлении такими вещами, как общие критерии.
Код репозитория:
public class OrderRepository : IOrderRepository
{
private readonly IAmbientContextScopeLocator _contextScopeLocator = null;
public OrderRepository(IAmbientContextScopeLocator contextScopeLocator)
{
_contextScopeLocator = contextScopeLocator ?? throw new ArgumentNullException("contextScopeLocator");
}
private AppDbContext Context => return _contextScopeLocator.Get<AppDbContext>();
IQueryable<Order> IOrderRepository.GetOrders()
{
return Context.Orders.Where(x => x.IsActive);
}
}
В этом примере используется единица DhContextScope от Mehdime для единицы работать, но может быть адаптирован к другим или внедренному DbContext, если это время жизни ограничено запросом. Это также демонстрирует случай с очень распространенными критериями фильтра («IsActive»), который мы могли бы хотеть централизовать по всем запросам.
В приведенном выше примере мы используем репозиторий для возврата заказов в виде IQueryable
. Метод репозитория полностью поддается моделированию, когда вызов DbContextScopeFactory.CreateReadOnly
может быть заглушен, а вызов репозитория может быть смоделирован для возврата любых данных, которые вы хотите, используя, например, List<Order>().AsQueryable()
. Возвращая IQueryable
, вызывающий код полностью контролирует, как будут использоваться данные. Обратите внимание, что нет необходимости беспокоиться о том, чтобы загружать данные о клиенте / пользователе. Запрос не будет выполнен, пока вы не выполните вызов Single
(или ToList
et c.), Что приведет к очень эффективным запросам. Сам класс репозитория очень прост, так как нетрудно сказать ему, какие записи и связанные данные включать. Мы можем настроить наш запрос, добавив сортировку, нумерацию страниц (Skip
/ Take
) или получить Count
или просто проверить, существуют ли какие-либо данные (Any
) без добавления функций и т. Д. c. в хранилище или с накладными расходами на загрузку данных только для простой проверки.
Наиболее распространенные возражения, которые я слышу о возвращении хранилищ IQueryable
:
" Утечка. Вызывающие абоненты должны знать об EF и структуре сущностей. "Да, вызывающие абоненты должны знать об ограничениях EF и структуре сущностей. Однако многие альтернативные подходы, такие как внедрение деревьев выражений для управления фильтрацией, сортировкой и активной загрузкой, требуют одинакового знания ограничений EF и структуры сущностей. Например, введение выражения для выполнения фильтрации по-прежнему не может включать в себя детали, которые EF не может выполнить. Полное абстрагирование EF приведет к лоту аналогичных, но ограниченных методов в репозитории и / или к потере производительности и возможностей, которые дает EF. Если вы внедрите EF в свой проект, он будет работать намного лучше, если ему доверяют как первоклассному гражданину в рамках проекта.
" Как сопровождающий слой домена, я могу оптимизировать код, когда репозитории отвечают за критерии. "Я объяснил это преждевременной оптимизацией. В репозиториях можно применять фильтрацию на уровне ядра, такую как активное состояние или владение, оставляя желаемые запросы и поиск вплоть до кода реализации. Это правда, что вы не можете предсказать или контролировать, как эти итоговые запросы будут выглядеть по отношению к вашему источнику данных, но оптимизация запросов - это то, что лучше всего делать при рассмотрении использования данных в реальном мире. Запросы, которые генерирует EF, отражают необходимые данные, которые могут быть уточнены, и основу для того, какие индексы будут наиболее эффективными. Альтернативой является попытка предсказать, какие запросы будут использоваться, и предоставление этих ограниченных выборок сервисам для использования с намерением запросить дополнительные уточненные «варианты». Это часто приводит к тому, что сервисы, выполняющие менее эффективные запросы, чаще получают свои данные, когда возникает большая проблема с введением новых запросов в репозитории.