9

Мое приложение использует Entity Framework 7 и шаблон репозитория.Использование шаблона репозитория для загружаемых объектов с использованием ThenIclude

Метод GetByID на хранилище поддерживает жадную загрузку дочерних сущностей:

public virtual TEntity GetById(int id, params Expression<Func<TEntity, object>>[] paths) 
    { 
     var result = this.Set.Include(paths.First()); 
     foreach (var path in paths.Skip(1)) 
     { 
      result = result.Include(path); 
     } 
     return result.FirstOrDefault(e => e.Id == id); 
    } 

Использование выглядит следующим образом, чтобы получить продукт (чей идентификатор 2) вместе с заказами и частей, связанных с этим продуктом:

productRepository.GetById(2, p => p.Orders, p => p.Parts); 

Я хочу улучшить этот метод, чтобы поддерживать надежную загрузку объектов, вложенных глубже одного уровня. Например, предположим, что у Order есть своя коллекция LineItem.

До EF7 я считаю, что следующее было бы можно также извлечь LineItems, связанные с каждым заказом:

productRepository.GetById(2, p => p.Orders.Select(o => o.LineItems), p => p.Parts); 

Однако это, похоже, не поддерживается в EF7. Вместо этого есть новый метод ThenInclude, который извлекает дополнительные уровни вложенных сущностей:

https://github.com/aspnet/EntityFramework/wiki/Design-Meeting-Notes:-January-8,-2015

Я не уверен в том, как обновить свой репозиторий для поддержки извлечения множественных уровней нетерпеливых нагруженных лиц с использованием ThenInclude.

ответ

5

Это немного старый вопрос, но так как это Безразлично У меня есть принятый ответ. Я думал, что смогу опубликовать свое решение.

Я использую EF Core и хотел сделать именно это, получить доступ к загрузке из-за пределов моего класса репозитория, чтобы я мог указать свойства навигации для загрузки каждый раз, когда я вызываю метод репозитория. Поскольку у меня большое количество таблиц и данных, мне не нужен стандартный набор загружаемых объектов, так как некоторым моим запросам нужен только родительский объект, а некоторые нужны всему дереву.

Моя текущая реализация поддерживает только IQueryable метода (то есть. FirstOrDefault, Where, в основном стандартные функции лямбды), но я уверен, что вы могли бы использовать его, чтобы пройти через ваши конкретные методы хранилища.

Я начал с исходного кода для EF Core EntityFrameworkQueryableExtensions.cs, где определены методы расширения Include и ThenInclude. К сожалению, EF использует внутренний класс IncludableQueryable, чтобы удерживать дерево предыдущих свойств, чтобы включить сильный тип позже. Однако реализация для этого не более чем IQueryable с дополнительным родовым типом для предыдущего объекта.

Я создал свою версию я назвал IncludableJoin, который принимает IIncludableQueryable в качестве параметра конструктора и сохраняет его в закрытом поле для последующего доступа:

public interface IIncludableJoin<out TEntity, out TProperty> : IQueryable<TEntity> 
{ 
} 

public class IncludableJoin<TEntity, TPreviousProperty> : IIncludableJoin<TEntity, TPreviousProperty> 
{ 
    private readonly IIncludableQueryable<TEntity, TPreviousProperty> _query; 

    public IncludableJoin(IIncludableQueryable<TEntity, TPreviousProperty> query) 
    { 
     _query = query; 
    } 

    IEnumerator IEnumerable.GetEnumerator() 
    { 
     return GetEnumerator(); 
    } 

    public IEnumerator<TEntity> GetEnumerator() 
    { 
     return _query.GetEnumerator(); 
    } 

    public Expression Expression => _query.Expression; 
    public Type ElementType => _query.ElementType; 
    public IQueryProvider Provider => _query.Provider; 

    internal IIncludableQueryable<TEntity, TPreviousProperty> GetQuery() 
    { 
     return _query; 
    } 
} 

Примечания внутреннего GetQuery метода. Это будет важно позже.

Далее в моем родовом IRepository интерфейсе, я определил начальную точку для нетерпеливога загрузки:

public interface IRepository<TEntity> where TEntity : class 
{ 
    IIncludableJoin<TEntity, TProperty> Join<TProperty>(Expression<Func<TEntity, TProperty>> navigationProperty); 
    ... 
} 

TEntity универсального типа является интерфейсом моего EF объекта. Implmentation метода Join в моем родовом хранилище, как так:

public abstract class SecureRepository<TInterface, TEntity> : IRepository<TInterface> 
    where TEntity : class, new() 
    where TInterface : class 
{ 
    protected DbSet<TEntity> DbSet; 
    protected SecureRepository(DataContext dataContext) 
    { 
     DbSet = dataContext.Set<TEntity>(); 
    } 

    public virtual IIncludableJoin<TInterface, TProperty> Join<TProperty>(Expression<Func<TInterface, TProperty>> navigationProperty) 
    { 
     return ((IQueryable<TInterface>)DbSet).Join(navigationProperty); 
    } 
    ... 
} 

Теперь для той части, которая фактически позволяет несколько Include и ThenInclude. У меня есть несколько методов расширения, которые принимают и возвращают, и IIncludableJoin, чтобы обеспечить цепочку методов. Внутри который я называю ЭФ Include и ThenInclude методы на DbSet:

public static class RepositoryExtensions 
{ 
    public static IIncludableJoin<TEntity, TProperty> Join<TEntity, TProperty>(
     this IQueryable<TEntity> query, 
     Expression<Func<TEntity, TProperty>> propToExpand) 
     where TEntity : class 
    { 
     return new IncludableJoin<TEntity, TProperty>(query.Include(propToExpand)); 
    } 

    public static IIncludableJoin<TEntity, TProperty> ThenJoin<TEntity, TPreviousProperty, TProperty>(
     this IIncludableJoin<TEntity, TPreviousProperty> query, 
     Expression<Func<TPreviousProperty, TProperty>> propToExpand) 
     where TEntity : class 
    { 
     IIncludableQueryable<TEntity, TPreviousProperty> queryable = ((IncludableJoin<TEntity, TPreviousProperty>)query).GetQuery(); 
     return new IncludableJoin<TEntity, TProperty>(queryable.ThenInclude(propToExpand)); 
    } 

    public static IIncludableJoin<TEntity, TProperty> ThenJoin<TEntity, TPreviousProperty, TProperty>(
     this IIncludableJoin<TEntity, IEnumerable<TPreviousProperty>> query, 
     Expression<Func<TPreviousProperty, TProperty>> propToExpand) 
     where TEntity : class 
    { 
     var queryable = ((IncludableJoin<TEntity, IEnumerable<TPreviousProperty>>)query).GetQuery(); 
     var include = queryable.ThenInclude(propToExpand); 
     return new IncludableJoin<TEntity, TProperty>(include); 
    } 
} 

В этих методах я получаю внутреннее IIncludableQueryable свойство, используя вышеупомянутый GetQuery метод, вызывая соответствующую Include или ThenInclude метод, а затем возвращает новый IncludableJoin объект для поддержки цепочки методов.

И все. Использование это примерно так:

IAccount account = _accountRepository.Join(x=>x.Subscription).Join(x=>x.Addresses).ThenJoin(x=>x.Address).FirstOrDefault(x => x.UserId == userId); 

выше будет загружать базовый Account объект, это один-к-одному ребенку Subscription, это список Addresses ребенок один-ко-многим, и это ребенок Address. Каждая функция лямбда вдоль пути строго типизирована и поддерживается intellisense для отображения свойств, доступных для каждого объекта.

+0

Будет ли недостаток производительности для присоединения сначала, а затем выбор (через FirstOrDefault() по сравнению с первым выбором? – phhbr

+1

@phhbr Я так не думаю? Не думаю, что это имеет значение, потому что когда вы сидите в «IQueryable», запрос не выполняется до тех пор, пока вы не назовете «.ToList» или подобное. Таким образом, порядок операций, вероятно, не имеет значения, но я его не тестировал. – Steve

6

Вы можете изменить его на что-то вроде этого:

public virtual TEntity GetById<TEntity>(int id, Func<IQueryable<TEntity>, IQueryable<TEntity>> func) 
{ 
    DbSet<TEntity> result = this.Set<TEntity>(); 

    IQueryable<TEntity> resultWithEagerLoading = func(result); 

    return resultWithEagerLoading.FirstOrDefault(e => e.Id == id); 
} 


И вы можете использовать его как это:

productRepository.GetById(2, x => x.Include(p => p.Orders) 
            .ThenInclude(o => o.LineItems) 
            .Include(p => p.Parts)) 
+0

Спасибо, что нашли время ответить. Проблема с вашим предлагаемым решением заключается в том, что он выдает вызовы IQueryable/Entity Framework вне репозитория. Я предпочитаю не делать этого. – aw1975

+2

Я понимаю ваше беспокойство. Один из способов сделать это «неуверенность в постоянстве» - создать некоторые объекты в вашем слое данных, чтобы инкапсулировать ваши данные извлечения (EF Includes), а затем заставить их реализовать некоторые интерфейсы. Затем вы можете передать этот интерфейс вашему методу репозитория и создать репозиторий для решения реализованного объекта, а затем вызвать его внутри этого объекта. Это небольшая работа по настройке, но именно так я реализую активную загрузку в своих проектах. –