Entity Framework и DDD - загрузка необходимых связанных данных перед передачей сущности на бизнес-уровень
Допустим, у вас есть объект домена:
class ArgumentEntity
{
public int Id { get; set; }
public List<AnotherEntity> AnotherEntities { get; set; }
}
И у вас есть контроллер ASP.NET Web API для этого:
[HttpPost("{id}")]
public IActionResult DoSomethingWithArgumentEntity(int id)
{
ArgumentEntity entity = this.Repository.GetById(id);
this.DomainService.DoDomething(entity);
...
}
Он получает идентификатор объекта, загружает объект по идентификатору и выполняет на нем некоторую бизнес-логику с помощью службы домена.
Проблема: проблема здесь со связанными данными. ArgumentEntity имеет коллекцию AnotherEntities, которая будет загружена EF, только если вы явно попросите сделать это с помощью методов Include/Load. DomainService является частью бизнес-уровня и не должен ничего знать о постоянстве, связанных данных и других концепциях EF.
Сервисный метод DoDomething ожидает получения экземпляра ArgumentEntity с загруженной коллекцией AnotherEntities. Вы скажете - это просто, просто включите необходимые данные в Repository.GetById и загрузите весь объект с соответствующей коллекцией.
Теперь вернемся от упрощенного примера к реальности большого приложения:
ArgumentEntity гораздо сложнее. Он содержит несколько связанных коллекций, и связанные объекты также имеют связанные данные.
У вас есть несколько методов DomainService. Каждый метод требует загрузки различных комбинаций связанных данных.
Я мог бы представить возможные решения, но все они далеки от идеала:
Всегда загружать всю сущность -> но это неэффективно и часто невозможно.
Добавьте несколько методов репозитория: GetByIdOnlyHeader, GetByIdWithAnotherEntities, GetByIdFullData для загрузки определенных подмножеств данных в контроллер -> но контроллер узнает, какие данные загрузить и передать в каждый метод службы.
Добавьте несколько методов репозитория: GetByIdOnlyHeader, GetByIdWithAnotherEntities, GetByIdFullData для загрузки определенных подмножеств данных в каждом методе службы -> это неэффективно, SQL-запрос для каждого вызова метода службы. Что, если вы вызываете 10 сервисных методов для одного действия контроллера?
Каждый метод доменного метода вызывает метод репозитория для загрузки дополнительных необходимых данных (например: EnsureAnotherEntitiesLoaded) -> это ужасно, потому что моя бизнес-логика осознает концепцию EF связанных данных.
Вопрос: как бы вы решили проблему загрузки необходимых связанных данных для объекта перед передачей его на бизнес-уровень?
3 ответа
В вашем примере я вижу метод DoSomethingWithArgumentEntity
который, очевидно, принадлежит прикладному уровню. Этот метод имеет вызов Repository
который принадлежит к уровню доступа к данным. Я думаю, что эта ситуация не соответствует классической многоуровневой архитектуре - вам не следует вызывать DAL напрямую из уровня приложений.
Таким образом, ваш код может быть переписан другим способом:
[HttpPost("{id}")]
public IActionResult DoSomethingWithArgumentEntity(int id)
{
this.DomainService.DoDomething(id);
...
}
В DomainService
Реализация вы можете прочитать из репо все, что нужно для этой конкретной операции. Это позволяет избежать проблем на уровне приложений. В бизнес-уровне у вас будет больше свободы для реализации чтения: с несколькими методами репозитория считывает наполовину заполненную сущность, или с методами EnsureXXX, или чем-то еще. Знания о том, что вам нужно прочитать для работы, будут помещены в код операции, и вам больше не нужны эти знания на уровне приложений.
Каждый раз, когда возникает подобная ситуация, это сильный сигнал о том, что ваша сущность не разработана заранее. Как сказал Крзис, у сущности нет сплоченных частей. Другими словами, если вам часто нужны отдельные части сущности, вам следует разделить эту сущность.
Говоря в контексте DDD, кажется, что вы пропустили некоторые аспекты моделирования в вашем проекте, которые привели вас к этой проблеме. Сущность, о которой вы писали, выглядела не слишком сплоченной. Если для разных процессов (методов обслуживания) требуются разные связанные данные, кажется, что вы еще не нашли подходящих агрегатов. Подумайте о том, чтобы разделить вашу сущность на несколько совокупностей с высокой сплоченностью. Тогда всем процессам, связанным с конкретным Агрегатом, потребуются все или большинство данных, которые содержит этот Агрегат.
Поэтому я не знаю ответа на ваш вопрос, но если вы можете позволить себе сделать несколько шагов назад и провести рефакторинг своей модели, я думаю, что вы не столкнетесь с такими проблемами.
Хороший вопрос:)
Я бы сказал, что "связанные данные" сами по себе не являются строгой концепцией EF. Связанные данные - это правильная концепция с NHibernate, с Dapper, или даже если вы используете файлы для хранения.
Я согласен с другими пунктами в основном, хотя. Итак, вот что я обычно делаю: у меня есть один метод хранилища, в вашем случае GetById
, который имеет два параметра: идентификатор и params Expression<Func<T,object>>[]
, И затем, внутри хранилища я делаю включения. Таким образом, вы не будете зависеть от EF в своей бизнес-логике (выражения могут быть проанализированы вручную для другого типа среды хранения данных, если это необходимо), и каждый метод BLL может решить для себя, какие связанные данные им действительно нужны.
public async Task<ArgumentEntity> GetByIdAsync(int id, params Expression<Func<ArgumentEntity,object>>[] includes)
{
var baseQuery = ctx.ArgumentEntities; // ctx is a reference to your context
foreach (var inlcude in inlcudes)
{
baseQuery = baseQuery.Include(include);
}
return await baseQuery.SingleAsync(a=>a.Id==id);
}