Использование шаблона репозитория для активной загрузки объектов с помощью ThenIclude

мое приложение использует Entity Framework 7 и шаблон репозитория.

метод 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);
    }

использование заключается в следующем, чтобы получить продукт (чей 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.

2 ответов


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

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

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

я начал с исходного кода для EF Core EntityFrameworkQueryableExtensions.cs где Include и ThenInclude определены методы расширения. К сожалению, EF использует внутренний класс IncludableQueryable чтобы держать дерево предыдущие свойства для включения strongly type later включают. Однако реализация для этого не более чем 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. Реализовать в 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 чтобы разрешить цепочку методов. Внутри которого я называю EF 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 и это ребенок!--32-->. Каждая лямбда-функция строго типизирована и поддерживается intellisense для отображения свойств, доступных для каждой сущности.


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

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))