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 и загрузите весь объект с соответствующей коллекцией.

Теперь вернемся от упрощенного примера к реальности большого приложения:

  1. ArgumentEntity гораздо сложнее. Он содержит несколько связанных коллекций, и связанные объекты также имеют связанные данные.

  2. У вас есть несколько методов DomainService. Каждый метод требует загрузки различных комбинаций связанных данных.

Я мог бы представить возможные решения, но все они далеки от идеала:

  1. Всегда загружать всю сущность -> но это неэффективно и часто невозможно.

  2. Добавьте несколько методов репозитория: GetByIdOnlyHeader, GetByIdWithAnotherEntities, GetByIdFullData для загрузки определенных подмножеств данных в контроллер -> но контроллер узнает, какие данные загрузить и передать в каждый метод службы.

  3. Добавьте несколько методов репозитория: GetByIdOnlyHeader, GetByIdWithAnotherEntities, GetByIdFullData для загрузки определенных подмножеств данных в каждом методе службы -> это неэффективно, SQL-запрос для каждого вызова метода службы. Что, если вы вызываете 10 сервисных методов для одного действия контроллера?

  4. Каждый метод доменного метода вызывает метод репозитория для загрузки дополнительных необходимых данных (например: 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); 
}
Другие вопросы по тегам