Следует ли отслеживать возвращаемые элементы при использовании шаблона репозитория с EF? - PullRequest
1 голос
/ 22 октября 2019

Игнорируя пока аргументы о том, должен ли шаблон репозитория использоваться с EF, я хотел бы спросить, должен ли EF возвращать отслеживаемые объекты. Возьмем, к примеру, следующий код, метод которого Get() возвращает отслеживаемую сущность.

public virtual async Task<TSqlTable> Get(int id)
{
    var result = await _dbContext.Set<TSqlTable>().Where(set => set.Id == id).SingleOrDefaultAsync();
    return result;
}

public async Task<TSqlTable> Update(TSqlTable item)
{
    _dbContext.Set<TSqlTable>().Update(item);
    await _dbContext.SaveChangesAsync();
    return item;
}

Это означает, что obj2 будет сохранено в следующем коде, даже если я вызываю update для obj1:

var obj1 = await repo.Get(1);
var obj2 = await repo.Get(2);
obj2.MyProp = "changed";
await repo.Update(obj1);

Имеет ли смысл добавлять AsNoTracking() к методу Get(), чтобы ничего за пределами репо не отслеживалось?

public virtual async Task<TSqlTable> Get(int id)
{
    var result = await _dbContext.Set<TSqlTable>().Where(set => set.Id == id).AsNoTracking().SingleOrDefaultAsync();
    return result;
}

public async Task<TSqlTable> Update(TSqlTable item)
{
    _dbContext.Set<TSqlTable>().Update(item);
    await _dbContext.SaveChangesAsync();
    return item;
}

Ответы [ 2 ]

1 голос
/ 23 октября 2019

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

Например, если у меня есть CreateOrderController для обслуживания экрана создания заказа, я хочубыть в состоянии создавать заказы, строки заказа и предоставлять списки продуктов, а также ассоциированных клиентов. С универсальными репозиториями:

public CreateOrderController(IUnitOfWorkFactory unitOfWorkFactory, 
   IOrderRepository orderRepository,  
   IOrderLineRepository orderLineRepository, 
   IProductRepository productRepository, 
   ICustomerRepository customerRepository)
{  /* ... */ }

Где каждый репозиторий является Repository<T>, и я пытаюсь использовать универсальные методы. Неизменно у меня будут методы, специфичные для Ордена или другой сущности, которые будут отличаться от общего общего вида. Я также собираюсь пойти на компромисс по возвращению деталей, которые мне не нужны. Например, если это был ManageOrderController, и я хочу знать, содержит ли какая-либо из строк заказа продукт: Либо у меня есть определенный метод в OrderLineRepository:

bool HasOrderLineWithProduct(int orderId, int productId);

... и мой репозиторий усеянметоды, подобные этому для каждого сценария, или мой код контроллера усеян неэффективными «общими» методами, такими как:

var orderLines = _orderLineRepository.GetOrderLinesForOrder(orderId);
var hasProduct = orderLines.Any(x => x.ProductId == productId);

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

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

Для приведенного выше примерадля CreateOrderController у меня будет нечто, похожее на:

public CreateOrderController(IUnitOfWorkFactory unitOfWorkFactory,,
     ICreateOrderRepository createOrderRepository)
{  /* ... */ }

... где репозиторий обслуживает этот контроллер. Проще настроить тестовые макеты. Там, где мне нужно найти клиента или ссылки на продукты, репозиторий может обслуживать такие:

IQueryable<Customer> GetCustomerById(int customerId);
IQueryable<Product> GetAllProducts();

Почему IQueryable, когда я ожидаю только одного клиента? Потому что это дает моему потребляющему коду полный контроль над тем, как этот код потребляется. Я мог бы хотеть единственную сущность, или я мог бы хотеть сделать проверку существования (.Any()) или выбрать только подмножество деталей о них. То же самое касается получения продуктов. Скорее всего, я хочу заполнить простой список моделей представлений с идентификатором продукта и именем для создания пользовательского интерфейса для выбора. Реализация репозитория применяет правила базового уровня, такие как проверки подлинности / авторизации, и такие вещи, как проверки IsActive, чтобы возвращать только активные данные в сценариях мягкого удаления.

В приведенном выше случае, когда у меня есть контроллер (и хранилище), который я хочу проверить на наличие OrderLines для данного продукта:

var hasProduct = manageOrderRepository.GetOrderById(orderId).SelectMany(o => o.OrderLines).Any(ol => ol.Product.ProductId == productId);

Возвращая IQueryable, код потребления может использовать Linq для выполнения всего, что ему нужно. GetOrderById может быть смоделирован для целей тестирования, чтобы вернуть объект-заглушку с соответствующими деталями для теста, пустого списка и т. Д. Нам не нужны десятки методов на основе сценариев или выполнение дорогостоящих запросов. В хранилище нет сложных аргументов дерева выражений для динамической фильтрации, сортировки или разбиения на страницы. Linq и IQueryable уже предоставляют это. Он не «пропускает» EF-измы больше, чем сложные выражения, поскольку любые выражения и тому подобное, передаваемые в хранилище, все равно должны соответствовать правилам EF. Результатом является возможность использовать строго эффективные запросы.

Хранилище также служит фабрикой для новых сущностей. Когда я собираюсь создать новый ордер, я не оставляю это на усмотрение моего контроллера / службы:

var newOrder = new Order();
// populate order lines.
context.Orders.Add(newOrder);
context.SaveChanges();

, вместо этого мой репозиторий предоставляет метод CreateOrder:

Order CreateOrder(string OrderNumber, Customer customer, IEnumerable<Product> products);

Это может быть CustomerId и набор ProductIds вместо этого, в зависимости от того, как я структурирую систему ... В любом случае хранилище проверяет наличие необходимой информации, затем создает сущность заказа, связывает эти необходимые детали и добавляет ее в DbSet. и возвращает его. Все, что не обнуляется, потребуется в фабричном методе. Это гарантирует, что любой созданный объект всегда находится в минимально допустимом допустимом состоянии. Любые дополнительные данные могут быть установлены в возвращаемом заказе до того, как контроллер / служба вызовет SaveChanges для единицы работы. Фабрику можно рассматривать как отдельный концерн и отдельный класс, но я возлагаю эту ответственность на хранилище, поскольку у него есть все права доступа, необходимые для выполнения этой обязанности.

Существуют компромиссы с любым подходом к хранилищамнравится. Некоторые люди скажут «что насчет СУХОГО ?! (не повторяйте себя)», учитывая, что несколько репозиториев могут и будут запрашивать одни и те же объекты. Это абсолютно верно, однако я лично считаю, что KISS (Keep It Simple Stupid) превосходит DRY, и что использование хранилища для обслуживания контроллера или службы лучше удовлетворяет SRP. (Принцип единой ответственности) Когда вы разделяете репозитории на каждую сущность для удовлетворения DRY, у вашего репозитория теперь есть более чем одна причина для изменения. Чтобы попытаться повторно использовать код, эти методы теперь вызываются различными областями, которые могут и часто имеют разные потребности. DRY - это шаг оптимизации, и он должен применяться к «идентичному», а не просто «похожему» коду и задачам. Когда я перефакторинг CreateOrderRepository или ManageOrderRepository, это только для этой цели. Мне не нужно беспокоиться о возможных побочных эффектах от оптимизаций, которые могут повлиять на другие области или сломать другие тесты. Как правило, любое дублирование между репозиториями происходит при запросах типа чтения, что довольно просто. Другим компромиссом для небольшого дублирования не является необходимость объявлять дополнительные зависимости везде. (требует проверки и т. д. при тестировании) Будут объединены элементы кода, но это будет оптимизация на более позднем этапе, когда я знаю, что функциональность стабильна и идентична.

Надеемся, что некоторыепища хоть для использования репозиториев с EF.

1 голос
/ 22 октября 2019

Как и многие другие проблемы с переносом EF DbContext в дополнительный слой репозитория, вам необходимо решить, кто решит. Не Repository должен определять, отслеживаются ли объекты, а код, который использует Repository. Таким образом, ваш репозиторий второго уровня должен иметь некоторый метод, чтобы вызывающий код объявлял свое намерение для отслеживания сущностей.

Это одна из многих причин, почему хорошо спроектированный репозиторий должен представлять сущности как IQueryable<TEntity> вместоIEnumerable<TEntity> или TEntity.

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

...