【问题标题】:EF Core navigation properties disappear when ordering by lambda按 lambda 排序时,EF Core 导航属性消失
【发布时间】:2018-11-19 00:00:26
【问题描述】:

我有 Item 实体,它与 ItemVariant 有一对多的关系。我尝试按 ItemVariant 的价格订购商品,但 ItemVariants 导航属性(与任何其他导航属性一样)为空。有趣的是,在输入订购 lambda 之前它不是空的。仅当我在订购功能之前执行 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;
}

这是我应用排序的控制器代码的一部分。它在 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;
    });
}

在分页之前调用ToListAsync 不是最优的,因为这意味着由于尚未应用分页而加载的实体比需要的多得多(分页结果也取决于排序)。也许有一些配置可以在需要时加载导航属性?

更新:尝试使用.UseLazyLoadingProxies(),但在ItemVariants.Any(),出现异常,我没有使用AsNoTracking()

为警告“Microsoft.EntityFrameworkCore.Infrastructure.DetachedLazyLoadingWarning”生成错误:尝试在“ItemProxy”类型的分离实体上延迟加载导航属性“ItemVariants”。分离实体或使用“AsNoTracking()”加载的实体不支持延迟加载。通过将事件 ID 'CoreEventId.DetachedLazyLoadingWarning' 传递给 'DbContext.OnConfiguring' 或 'AddDbContext' 中的 'ConfigureWarnings' 方法,可以抑制或记录此异常。

【问题讨论】:

  • Func&lt;T, IComparable&gt; 看起来不是个好选择。排序胸围是用表达式完成的,以便可翻译成 SQL。它现在的方式正在导致客户评估和所有相关问题。
  • @IvanStoev 请将此作为答案发布,此评论方向正确,我会接受,谢谢。

标签: c# entity-framework-core


【解决方案1】:

问题的根本原因是使用委托 (Func&lt;T, IComparable&gt;) 代替Expression&lt;Func&lt;...&gt;&gt; 进行排序。

EF6 会在运行时简单地抛出 NotSupportedException,但 EF Core 将切换到 client evaluation

除了引入的低效率之外,客户端评估目前不能很好地与导航属性配合使用 - 看起来它在急切加载/导航属性修复之前应用,这就是导航属性为 null 的原因.

即使 EF Core 实现在将来的某个版本中固定为“工作”,通常您也应该尽可能避免客户端评估。这意味着您正在使用的规范模式实现的排序部分必须调整以使用表达式,以便能够产生类似的东西

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

应该可以翻译成 SQL,因此评估服务器端并且导航属性没有问题。

【讨论】:

  • 同样用更简单的术语来说,应该检查 OrderBy 重载 - 正确的应该返回 IOrderedQueryable,它很容易与来自 linq 的那个混淆
猜你喜欢
  • 1970-01-01
  • 2020-05-23
  • 2018-10-22
  • 2018-10-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-04
  • 1970-01-01
相关资源
最近更新 更多