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

.net-core c# entity-framework-core lambda repository-pattern

Question

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?

1
14
9/22/2017 10:58:56 PM

Accepted Answer

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));
31
7/18/2018 5:13:13 AM

Popular Answer

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"
            }
      }
} 


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow