简体   繁体   English

按lambda订购时,EF Core导航属性消失

[英]EF Core navigation properties disappear when ordering by lambda

I have Item entity, which has one to many relation to ItemVariant. 我有Item实体,它与ItemVariant有一对多的关系。 I try to order Items by Price of ItemVariant, but ItemVariants navigation property (like any other navigation property) is empty. 我尝试按ItemVariant的价格订购商品,但ItemVariants导航属性(与任何其他导航属性一样)为空。 Interesting that it's not empty before entering ordering lambda. 有趣的是,在输入lambda之前它不是空的。 It only works if I do ToListAsync before ordering function. 它仅在订购函数之前执行ToListAsync时有效。

// 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));
    try
    {
        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
        .Aggregate(context.Set<T>().AsQueryable(),
            (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 在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). 在分页之前调用ToListAsync并不是最佳选择,因为由于尚未应用分页,这意味着加载的实体比所需的要多得多(分页的结果也取决于顺序)。 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() . 更新 :尝试使用.UseLazyLoadingProxies() ,但是在ItemVariants.Any() ,出现异常,并且不使用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'. 产生警告“ Microsoft.EntityFrameworkCore.Infrastructure.DetachedLazyLoadingWarning”错误:尝试在类型为“ ItemProxy”的分离实体上延迟加载导航属性“ ItemVariants”。 Lazy-loading is not supported for detached entities or entities that are loaded with 'AsNoTracking()'.'. 分离的实体或使用'AsNoTracking()'加载的实体不支持延迟加载。 This exception can be suppressed or logged by passing event ID 'CoreEventId.DetachedLazyLoadingWarning' to the 'ConfigureWarnings' method in 'DbContext.OnConfiguring' or 'AddDbContext'.' 可以通过将事件ID“ CoreEventId.DetachedLazyLoadingWarning”传递到“ DbContext.OnConfiguring”或“ AddDbContext”中的“ ConfigureWarnings”方法来抑制或记录该异常。

The root cause of the issue is the usage of a delegate ( Func<T, IComparable> ) for ordering instead of Expression<Func<...>> . 该问题的根本原因是使用委托Func<T, IComparable> )进行排序,而不是Expression<Func<...>>

EF6 would simply throw NotSupportedException at runtime, but EF Core will switch to client evaluation . EF6只会在运行时抛出NotSupportedException ,但是EF Core将切换到客户端评估

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 . 除了引入的效率低下之外,客户端评估目前还不能很好地与导航属性配合使用-似乎是急于加载/导航属性修复之前应用了它,这就是为什么导航属性为null的原因。

Even if the EF Core implementation is fixed to "work" in some future release, in general you should avoid client evaluation whenever possible. 即使在将来的某些发行版中将EF Core实现固定为“有效”,通常也应尽可能避免进行客户评估。 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. 应该可以转换为SQL,因此可以评估服务器端,并且导航属性没有问题。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM