EF Core navigation properties disappear when ordering by lambda

c# entity-framework-core


I have Item entity, which has one to many relation to ItemVariant. I try to order Items by Price of ItemVariant, but ItemVariants navigation property (like any other navigation property) is empty. Interesting that it's not empty before entering ordering lambda. It only works if I do ToListAsync before ordering function.

// entities I use
public class Item
    public int Id { get; set; }
    public string Title { get; set; }

    public ICollection<ItemVariant> ItemVariants { get; set; } = new List<ItemVariant>();

public class ItemVariant
    public int Id { get; set; }
    public int ItemId { get; set; }

    public Item Item { get; set; }

/// <summary>
/// Contains full information for executing a request on database
/// </summary>
/// <typeparam name="T"></typeparam>
public class Specification<T> where T : class
    public Expression<Func<T, bool>> Criteria { get; }
    public List<Expression<Func<T, object>>> Includes { get; } = new List<Expression<Func<T, object>>>();
    public List<Func<T, IComparable>> OrderByValues { get; set; } = new List<Func<T, IComparable>>();
    public bool OrderByDesc { get; set; } = false;

    public int Take { get; protected set; }
    public int Skip { get; protected set; }
    public int Page => Skip / Take + 1;
    public virtual string Description { get; set; }

// retrieves entities according to specification passed
public static async Task<IEnumerable<TEntity>> EnumerateAsync<TEntity, TService>(this DbContext context, IAppLogger<TService> logger, Specification<TEntity> listSpec) where TEntity: class
    if (listSpec == null)
        throw new ArgumentNullException(nameof(listSpec));
        var entities = context.GetQueryBySpecWithIncludes(listSpec);
        var ordered = ApplyOrdering(entities, listSpec);
        var paged = await ApplySkipAndTake(ordered, listSpec).ToListAsync();
        return paged;
    catch (Exception readException)
        throw readException.LogAndGetDbException(logger, $"Function: {nameof(EnumerateAsync)}, {nameof(listSpec)}: {listSpec}");

// applies Includes and Where to IQueryable. note that Include happens before OrderBy.
public static IQueryable<T> GetQueryBySpecWithIncludes<T>(this DbContext context, Specification<T> spec) where T: class
    // fetch a Queryable that includes all expression-based includes
    var queryableResultWithIncludes = spec.Includes
            (current, include) => current.Include(include));
    var result = queryableResultWithIncludes;
    var filteredResult = result.Where(spec.Criteria);
    return filteredResult;

// paging
public static IQueryable<T> ApplySkipAndTake<T>(IQueryable<T> entities, Specification<T> spec) where T : class
    var result = entities;
    result = result.Skip(spec.Skip);
    return spec.Take > 0 ? result.Take(spec.Take) : result;

// orders IQueryable according to Lambdas in OrderByValues
public static IQueryable<T> ApplyOrdering<T>(IQueryable<T> entities, Specification<T> spec) where T : class
    // according to debugger all nav properties are loded at this point
    var result = entities;
    if (spec.OrderByValues.Count > 0)
        var firstField = spec.OrderByValues.First();
        // but disappear when go into ordering lamda
        var orderedResult = spec.OrderByDesc ? result.OrderByDescending(i => firstField(i)) : result.OrderBy(i => firstField(i));
        foreach (var field in spec.OrderByValues.Skip(1))
            orderedResult = spec.OrderByDesc ? orderedResult.ThenByDescending(i => field(i)) : orderedResult.ThenBy(i => field(i));
        result = orderedResult;
    return result;

this is part of my controller code applying ordering. it's called before EnumerateAsync

protected override void ApplyOrdering(Specification<Item> spec)
    spec.AddInclude(i => i.ItemVariants);
    spec.OrderByValues.Add(i =>
        // empty if ToListAsync() not called before
        if (i.ItemVariants.Any())
            return (from v in i.ItemVariants select v.Price).Min();
        return 0;

Calling ToListAsync before paging is not optimal, because it means loading much more entities than needed due to not applied paging yet (paging results depend on ordering too). Maybe there is some configuration to make nav properties load when needed?

Update: tried to use .UseLazyLoadingProxies(), but at ItemVariants.Any(), I get an exception, and I don't use AsNoTracking().

Error generated for warning 'Microsoft.EntityFrameworkCore.Infrastructure.DetachedLazyLoadingWarning: An attempt was made to lazy-load navigation property 'ItemVariants' on detached entity of type 'ItemProxy'. Lazy-loading is not supported for detached entities or entities that are loaded with 'AsNoTracking()'.'. This exception can be suppressed or logged by passing event ID 'CoreEventId.DetachedLazyLoadingWarning' to the 'ConfigureWarnings' method in 'DbContext.OnConfiguring' or 'AddDbContext'.'

11/16/2018 10:08:41 PM

Accepted Answer

The root cause of the issue is the usage of a delegate (Func<T, IComparable>) for ordering instead of Expression<Func<...>>.

EF6 would simply throw NotSupportedException at runtime, but EF Core will switch to client evaluation.

Apart from the introduced inefficiencies, client evaluation currently doesn't play well with navigation properties - looks like it is applied before eager loading / navigation property fixup, that's why the navigation property is null.

Even if the EF Core implementation is fixed to "work" in some future release, in general you should avoid client evaluation whenever possible. Which means the ordering part of the specification pattern implementation you are using has to be adjusted to work with expressions, in order to be able to produce something like this

.OrderBy(i => i.ItemVariants.Max(v => (decimal?)v.Price))

which should be translatable to SQL, hence evaluated server side and no issues with navigation properties.

11/18/2018 4:00:26 PM

Related Questions


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