24

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?

redwards510
  • 1,802
  • 1
  • 17
  • 23
  • 1
    This has been asked several times since it was de facto a standard of specifying the desired includes to repository methods using EF6. It would be interesting the hear some EFC team member what was the reason behind the decision to change the pattern to `Include` / `ThenInclude` which apparently cannot be represented this way, and more importantly, what's the EFC replacement. – Ivan Stoev Sep 23 '17 at 16:15
  • @IvanStoev Did you ever figure out a replacement for this in EFC? – DavidG Oct 18 '18 at 13:52
  • 2
    @DavidG Based on the source code of their own provided extension [Microsoft.EntityFrameworkCore.UnitOfWork](https://learn.microsoft.com/en-us/ef/core/extensions/#microsoftentityframeworkcoreunitofwork), the replacement in their vision is `Func, IIncludableQueryable> include = null` parameter. – Ivan Stoev Oct 18 '18 at 14:08
  • @IvanStoev Hmm, was afraid of something like that, doesn't fit my use case unfortunately. Many thanks for responding. (PS That extension is not made by MS, I think they just like it enough to link to it) – DavidG Oct 18 '18 at 14:16
  • @DavidG I guess you are right. I was assuming "they" based on *Microsoft* in the name :) But now I see it's the same as the self answer by the OP. Anyway, if you want to keep the old syntax, the source code of EF6 is available and can relatively easy be translated to `Include` / `ThenInclude` syntax. There are even such posts here on SO, just don't remember the exact ones. – Ivan Stoev Oct 18 '18 at 14:24

4 Answers4

55

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));
JHobern
  • 866
  • 1
  • 13
  • 20
redwards510
  • 1,802
  • 1
  • 17
  • 23
  • 22
    While this works, it exposes an Entity Framework class, which might not be desirable. – SebastianR Feb 13 '18 at 15:36
  • 1
    Sometimes its worth knowing when to keep things simple and readable. SOLID and over-abstracting can certainly be taken too far. – Victorio Berra Aug 02 '18 at 17:19
  • Also, id be open to some alternatives since there are not many answers to this question. – Victorio Berra Oct 29 '18 at 23:06
  • really nice method, where did you find it? I think I found it here: https://github.com/Arch/UnitOfWork/blob/master/src/Microsoft.EntityFrameworkCore.UnitOfWork/IRepository.cs – Daniël Tulp Dec 11 '18 at 19:36
  • @SebastianR but not if you make it a generic abstract class, from which descendant Repositories inherit, where you make each child repository expose IEnumerable instead, right? – WiseDev Aug 31 '20 at 08:37
  • How to use orderBy? – Maroof Nov 27 '21 at 07:32
6

I had the same issue since EF Core doesn't support lazy loading but i tried to get workaround in the following way:

First create an attribute class to mark our desired navigation properties from other properties of a given class.

[AttributeUsage(AttributeTargets.Property, Inherited = false)]
public class NavigationPropertyAttribute : Attribute
{
    public NavigationPropertyAttribute()
    {
    }
}

Extension methods to filter out navigation properties and apply Include/ThenInclude using string based Eager loading.

public static class DbContextHelper
{

    public static Func<IQueryable<T>, IQueryable<T>> GetNavigations<T>() where T : BaseEntity
    {
        var type = typeof(T);
        var navigationProperties = new List<string>();

        //get navigation properties
        GetNavigationProperties(type, type, string.Empty, navigationProperties);

        Func<IQueryable<T>, IQueryable<T>> includes = ( query => {
                    return  navigationProperties.Aggregate(query, (current, inc) => current.Include(inc));   
            });

        return includes;
    }

    private static void GetNavigationProperties(Type baseType, Type type, string parentPropertyName, IList<string> accumulator)
    {
        //get navigation properties
        var properties = type.GetProperties();
        var navigationPropertyInfoList = properties.Where(prop => prop.IsDefined(typeof(NavigationPropertyAttribute)));

        foreach (PropertyInfo prop in navigationPropertyInfoList)
        {
            var propertyType = prop.PropertyType;
            var elementType = propertyType.GetTypeInfo().IsGenericType ? propertyType.GetGenericArguments()[0] : propertyType;

            //Prepare navigation property in {parentPropertyName}.{propertyName} format and push into accumulator
            var properyName = string.Format("{0}{1}{2}", parentPropertyName, string.IsNullOrEmpty(parentPropertyName) ? string.Empty : ".", prop.Name);
            accumulator.Add(properyName);

            //Skip recursion of propert has JsonIgnore attribute or current property type is the same as baseType
            var isJsonIgnored = prop.IsDefined(typeof(JsonIgnoreAttribute));
            if(!isJsonIgnored && elementType != baseType){
                GetNavigationProperties(baseType, elementType, properyName, accumulator);
            }
        }
    }
}

Sample POCO classes implementing NavigationPropertyAttribute

public class A : BaseEntity{
  public string Prop{ get; set; }
}

public class B : BaseEntity{
   [NavigationProperty]
   public virtual A A{ get; set; }
}

public class C : BaseEntity{
   [NavigationProperty]
   public virtual B B{ get; set; }
}

Usage in Repository

public async Task<T> GetAsync(Expression<Func<T, bool>> predicate)
{
    Func<IQueryable<T>, IQueryable<T>> includes = DbContextHelper.GetNavigations<T>();
    IQueryable<T> query = _context.Set<T>();
    if (includes != null)
    {
        query = includes(query);
    }

    var entity = await query.FirstOrDefaultAsync(predicate);
    return entity;
}

Json result for sample class C would be:

{
  "B" : {
        "A" : {
              "Prop" : "SOME_VALUE"
            }
      }
} 
JHobern
  • 866
  • 1
  • 13
  • 20
Yared
  • 2,206
  • 1
  • 21
  • 30
4

Back in EF6 we could write something like this:

query.Include(t => t.Navigation1, t => t.Navigation2.Select(x => x.Child1));

And it was perfect and simple. We could expose it in an repository without dragging references from the EF assembly to other projects.

This was removed from EF Core, but since EF6 is open-source, the method that transforms the lambda expressions in paths can easily be extracted to use in EF Core so you can get the exact same behavior.

Here's the complete extension method.

/// <summary>
///     Provides extension methods to the <see cref="Expression" /> class.
/// </summary>
public static class ExpressionExtensions
{
    /// <summary>
    ///     Converts the property accessor lambda expression to a textual representation of it's path. <br />
    ///     The textual representation consists of the properties that the expression access flattened and separated by a dot character (".").
    /// </summary>
    /// <param name="expression">The property selector expression.</param>
    /// <returns>The extracted textual representation of the expression's path.</returns>
    public static string AsPath(this LambdaExpression expression)
    {
        if (expression == null)
            return null;

        TryParsePath(expression.Body, out var path);

        return path;
    }

    /// <summary>
    ///     Recursively parses an expression tree representing a property accessor to extract a textual representation of it's path. <br />
    ///     The textual representation consists of the properties accessed by the expression tree flattened and separated by a dot character (".").
    /// </summary>
    /// <param name="expression">The expression tree to parse.</param>
    /// <param name="path">The extracted textual representation of the expression's path.</param>
    /// <returns>True if the parse operation succeeds; otherwise, false.</returns>
    private static bool TryParsePath(Expression expression, out string path)
    {
        var noConvertExp = RemoveConvertOperations(expression);
        path = null;

        switch (noConvertExp)
        {
            case MemberExpression memberExpression:
            {
                var currentPart = memberExpression.Member.Name;

                if (!TryParsePath(memberExpression.Expression, out var parentPart))
                    return false;

                path = string.IsNullOrEmpty(parentPart) ? currentPart : string.Concat(parentPart, ".", currentPart);
                break;
            }

            case MethodCallExpression callExpression:
                switch (callExpression.Method.Name)
                {
                    case nameof(Queryable.Select) when callExpression.Arguments.Count == 2:
                    {
                        if (!TryParsePath(callExpression.Arguments[0], out var parentPart))
                            return false;

                        if (string.IsNullOrEmpty(parentPart))
                            return false;

                        if (!(callExpression.Arguments[1] is LambdaExpression subExpression))
                            return false;

                        if (!TryParsePath(subExpression.Body, out var currentPart))
                            return false;

                        if (string.IsNullOrEmpty(parentPart))
                            return false;

                        path = string.Concat(parentPart, ".", currentPart);
                        return true;
                    }

                    case nameof(Queryable.Where):
                        throw new NotSupportedException("Filtering an Include expression is not supported");
                    case nameof(Queryable.OrderBy):
                    case nameof(Queryable.OrderByDescending):
                        throw new NotSupportedException("Ordering an Include expression is not supported");
                    default:
                        return false;
                }
        }

        return true;
    }

    /// <summary>
    ///     Removes all casts or conversion operations from the nodes of the provided <see cref="Expression" />.
    ///     Used to prevent type boxing when manipulating expression trees.
    /// </summary>
    /// <param name="expression">The expression to remove the conversion operations.</param>
    /// <returns>The expression without conversion or cast operations.</returns>
    private static Expression RemoveConvertOperations(Expression expression)
    {
        while (expression.NodeType == ExpressionType.Convert || expression.NodeType == ExpressionType.ConvertChecked)
            expression = ((UnaryExpression)expression).Operand;

        return expression;
    }
}

Then you can use it like this (put it in an QueryableExtensions class or something like that):

 /// <summary>
 ///     Specifies related entities to include in the query result.
 /// </summary>
 /// <typeparam name="T">The type of entity being queried.</typeparam>
 /// <param name="source">The source <see cref="IQueryable{T}" /> on which to call Include.</param>
 /// <param name="paths">The lambda expressions representing the paths to include.</param>
 /// <returns>A new <see cref="IQueryable{T}" /> with the defined query path.</returns>
 internal static IQueryable<T> Include<T>(this IQueryable<T> source, params Expression<Func<T, object>>[] paths)
 {
     if (paths != null)
         source = paths.Aggregate(source, (current, include) => current.Include(include.AsPath()));

     return source;
 }

And then in your repository you call it normally like you would do in EF6:

query.Include(t => t.Navigation1, t => t.Navigation2.Select(x => x.Child1));

References:

How to pass lambda 'include' with multiple levels in Entity Framework Core?

https://github.com/aspnet/EntityFramework6

MurariAlex
  • 321
  • 1
  • 3
  • 20
0

When I need .ThenInclude I add my dbcontext class as dependency injection and write my query directly from dbcontext reference. I don't know if it is good or bad practice.

samedya
  • 1
  • 1
  • Adding DbContext as dependency and using it directly in code without burying it in a Repository/UoW layer is perfectly alright (even preferred). But it's not an answer to the question. On Stack Overflow we try to answer questions and only to offer alternatives if absolutely necessary (e.g. when the question can't be answered otherwise). – Gert Arnold Oct 07 '22 at 07:18
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Oct 10 '22 at 18:34