How to write Repository method for .ThenInclude in EF Core 2

redwards510 picture redwards510 · Sep 23, 2017 · Viewed 13.1k times · Source

I'm trying to write a repository method for Entity Framework Core 2.0 that can handle returning child collections of properties using .ThenInclude, but I'm having trouble with the second expression. Here is a working method for .Include, which will return child properties (you supply a list of lambdas) of your entity.

public T GetSingle(Expression<Func<T, bool>> predicate, params Expression<Func<T, object>>[] includeProperties)
{
    IQueryable<T> query = _context.Set<T>();
    foreach (var includeProperty in includeProperties)
    {
        query = query.Include(includeProperty);
    } 

    return query.Where(predicate).FirstOrDefault();
}

Now here is my attempt at writing a method that will take a Tuple of two Expressions and feed those into a .Include(a => a.someChild).ThenInclude(b => b.aChildOfSomeChild) chain. This isn't a perfect solution because it only handles one child of a child, but it's a start.

public T GetSingle(Expression<Func<T, bool>> predicate, params Tuple<Expression<Func<T, object>>, Expression<Func<T, object>>>[] includeProperties)
{
    IQueryable<T> query = _context.Set<T>();
    foreach (var includeProperty in includeProperties)
    {
         query = query.Include(includeProperty.Item1).ThenInclude(includeProperty.Item2);              
    }

    return query.Where(predicate).FirstOrDefault();
}

Intellisense returns an error saying "The type cannot be inferred from the usage, try specifying the type explicitly". I have a feeling it's because the expression in Item2 needs to be classified as somehow related to Item1, because it needs to know about the child relationship it has.

Any ideas or better techniques for writing a method like this?

Answer

redwards510 picture redwards510 · Oct 31, 2017

I found this repository method online and it does exactly what I wanted. Yared's answer was good, but not all the way there.

/// <summary>
/// Gets the first or default entity based on a predicate, orderby delegate and include delegate. This method default no-tracking query.
/// </summary>
/// <param name="selector">The selector for projection.</param>
/// <param name="predicate">A function to test each element for a condition.</param>
/// <param name="orderBy">A function to order elements.</param>
/// <param name="include">A function to include navigation properties</param>
/// <param name="disableTracking"><c>True</c> to disable changing tracking; otherwise, <c>false</c>. Default to <c>true</c>.</param>
/// <returns>An <see cref="IPagedList{TEntity}"/> that contains elements that satisfy the condition specified by <paramref name="predicate"/>.</returns>
/// <remarks>This method default no-tracking query.</remarks>
public TResult GetFirstOrDefault<TResult>(Expression<Func<TEntity, TResult>> selector,
                                          Expression<Func<TEntity, bool>> predicate = null,
                                          Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
                                          Func<IQueryable<TEntity>, IIncludableQueryable<TEntity, object>> include = null,
                                          bool disableTracking = true)
{
    IQueryable<TEntity> query = _dbSet;
    if (disableTracking)
    {
        query = query.AsNoTracking();
    }

    if (include != null)
    {
        query = include(query);
    }

    if (predicate != null)
    {
        query = query.Where(predicate);
    }

    if (orderBy != null)
    {
        return orderBy(query).Select(selector).FirstOrDefault();
    }
    else
    {
        return query.Select(selector).FirstOrDefault();
    }
}

Usage:

var affiliate = await affiliateRepository.GetFirstOrDefaultAsync(
    predicate: b => b.Id == id,
    include: source => source
        .Include(a => a.Branches)
        .ThenInclude(a => a.Emails)
        .Include(a => a.Branches)
        .ThenInclude(a => a.Phones));