如何在Entity Framework Core中传递具有多个级别的lambda 'include'?

9

我有一个存储库,其中包含“包括”(include)的lambda表达式。

public TEntity FirstOrDefault(Expression<Func<TEntity, bool>> predicate, params Expression<Func<TEntity, object>>[] includePaths)
    {
        return Context.Set<TEntity>().Includes(includePaths).FirstOrDefault(predicate);
    }

在之前的EF版本中,我将其用于服务层,例如:

var plan = _unitOfWork.PlanRepository
            .FirstOrDefault(
                p => p.Id == id, 
                include => include.PlanSolutions.Select(ps => ps.Solution)
            );

在这里,“PlanSolutions”是一个集合,“Solution”是从“PlanSolution”嵌套的属性。

但是,现在这段代码出错了:

InvalidOperationException: 属性表达式 'include => {from PlanSolutions ps in [include].PlanSolutions select [ps].Solution}' 不是有效的。表达式应该代表属性访问:“t => t.MyProperty”。有关包含相关数据的更多信息,请参见 http://go.microsoft.com/fwlink/?LinkID=746393

现在似乎我不能使用“Select”方法来获取多个级别的包含,但我也不能使用Microsoft建议的“ThenInclude”方法,因为查询本身位于服务无法访问的存储库内。 有没有办法解决它?


脑海中浮现的一个选项是在您的FirstOrDefault实现中将所有的Select重写为ThenInclude - Evk
@Evk 这将是很好的。如何在 'ThenInclude' 上替换 'Select'?'Select' 只是一个 'ICollection',而 'ThenInclude' 是一个 'IIncludableQueryable'。 - Igor
1
重写为ThenInclude比较复杂,但你可以分析传递的表达式并将其转换为路径字符串(例如“PlanSolutions.Solution”),然后调用 Include("path string") - Evk
一段时间后,我找到了一个完美的解决方案来解决这个问题。 - Igor
3个回答

13

Entity Framework core为了提供更易理解的API而牺牲了参数设置的便利性。实际上,在EF6中,将多层Include表达式传递给方法要容易得多。在ef-core中几乎不可能。

但是,仍然存在接受属性路径作为字符串的Include方法,因此,如果我们可以将旧式的多级Include表达式转换为路径,就可以将路径馈入这个基于字符串的Include中。

幸运的是,在EF6中恰好发生了这种情况。由于EF6是开源的,我不必重新发明轮子,而是可以轻松借用他们的代码来实现我们想要的效果。结果是一个扩展方法AsPath,它返回一个lambda表达式作为属性路径。您可以在方法内部使用它将includes参数转换为一系列字符串,以添加Include。例如,表达式...

 include => include.PlanSolutions.Select(ps => ps.Solution)

...将被转换为PlanSolutions.Solution

如上所述:核心部分的源代码归功于 EF6。唯一的主要修改是,在我方法中,两个最常尝试的不支持特性:在Include中进行筛选和排序会引发异常。(在 ef-core 中仍不支持)。

public static class ExpressionExtensions
{
    public static string AsPath(this LambdaExpression expression)
    {
        if (expression == null) return null;

        var exp = expression.Body;
        string path;
        TryParsePath(exp, out path);
        return path;
    }

    // This method is a slight modification of EF6 source code
    private static bool TryParsePath(Expression expression, out string path)
    {
        path = null;
        var withoutConvert = RemoveConvert(expression);
        var memberExpression = withoutConvert as MemberExpression;
        var callExpression = withoutConvert as MethodCallExpression;

        if (memberExpression != null)
        {
            var thisPart = memberExpression.Member.Name;
            string parentPart;
            if (!TryParsePath(memberExpression.Expression, out parentPart))
            {
                return false;
            }
            path = parentPart == null ? thisPart : (parentPart + "." + thisPart);
        }
        else if (callExpression != null)
        {
            if (callExpression.Method.Name == "Select"
                && callExpression.Arguments.Count == 2)
            {
                string parentPart;
                if (!TryParsePath(callExpression.Arguments[0], out parentPart))
                {
                    return false;
                }
                if (parentPart != null)
                {
                    var subExpression = callExpression.Arguments[1] as LambdaExpression;
                    if (subExpression != null)
                    {
                        string thisPart;
                        if (!TryParsePath(subExpression.Body, out thisPart))
                        {
                            return false;
                        }
                        if (thisPart != null)
                        {
                            path = parentPart + "." + thisPart;
                            return true;
                        }
                    }
                }
            }
            else if (callExpression.Method.Name == "Where")
            {
                throw new NotSupportedException("Filtering an Include expression is not supported");
            }
            else if (callExpression.Method.Name == "OrderBy" || callExpression.Method.Name == "OrderByDescending")
            {
                throw new NotSupportedException("Ordering an Include expression is not supported");
            }
            return false;
        }

        return true;
    }

    // Removes boxing
    private static Expression RemoveConvert(Expression expression)
    {
        while (expression.NodeType == ExpressionType.Convert
               || expression.NodeType == ExpressionType.ConvertChecked)
        {
            expression = ((UnaryExpression)expression).Operand;
        }

        return expression;
    }
}

1
EF-core 5 现在支持过滤和排序。 - Gert Arnold
你能否编写包含筛选和排序的代码? - Tariq Hajeer
@TariqHajeer 不,这种字符串方法不允许过滤/排序“Include”。它没有相应的字符串语法。 - Gert Arnold
谢谢,那我该如何通过过滤和排序来传递 include? - Tariq Hajeer
在您的方式中,我可以只包含一个路径,但当我有一个集合,然后另一个集合时,我无法传递它,那么如何包含多个级别的集合? - Tariq Hajeer

8

接受的答案有些过时。在更新版本的Entity Framework Core中,您应该能够像这里描述的那样使用ThenInclude方法来包含多个级别的相关数据。

本文示例将变成:

var plan = _unitOfWork.PlanRepository
            .Include(x => x.PlanSolutions)
            .ThenInclude(x => x.Solution)
            .FirstOrDefault(p => p.Id == id);

1
我可以确认这个有效。 我感激不尽你的回答! - jGroot
4
这仍然无法让我们使用Lambda表达式,因此已接受的答案仍然有效。 - Daniël Tulp
4
我的回答哪些地方已经“过时”了呢?同时,你只是讲了如何使用Include-ThenInclude,但这并没有回答问题。 - Gert Arnold

5
    public TEntity FirstOrDefault(Expression<Func<TEntity, bool>> predicate, params Expression<Func<TEntity, object>>[] includePaths)
        {
DbSet = Context.Set<TEntity>();
    var query = includePaths.Aggregate(DbSet, (current, item) => EvaluateInclude(current, item));
             return query.Where(predicate).FirstOrDefault();
        }

    private IQueryable<T> EvaluateInclude(IQueryable<T> current, Expression<Func<T, object>> item)
                {
                    if (item.Body is MethodCallExpression)
                    {
                        var arguments = ((MethodCallExpression)item.Body).Arguments;
                        if (arguments.Count > 1)
                        {
                            var navigationPath = string.Empty;
                            for (var i = 0; i < arguments.Count; i++)
                            {
                                var arg = arguments[i];
                                var path = arg.ToString().Substring(arg.ToString().IndexOf('.') + 1);

                                navigationPath += (i > 0 ? "." : string.Empty) + path;
                            }
                            return current.Include(navigationPath);
                        }
                    }

                    return current.Include(item);
                }

2
这个完美地运作了。这就是我想要的全部。谢谢你的分享。 - LOKI
1
这个可行,谢谢。我只需要更改Aggregate函数调用的第一个参数为DbSet.AsQueryable() - Dipendu Paul
你会怎么称呼这个?比如我有一个实体,它有一个集合,然后这个实体又有另一个集合? - Zaki

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接