【问题标题】:Using expressions in the "Select" part of LINQ to entities queries在 LINQ 到实体查询的“选择”部分中使用表达式
【发布时间】:2014-07-02 16:59:26
【问题描述】:

我希望能够重用我的 LINQ to Entities 查询的“选择”部分。例如,我可以采取以下...

projectQuery.Select(p => new ProjectModel
    ProjectName = p.ProjectName,
    ProjectNumber = p.ProjectNumber);

并用表达式替换它...

projectQuery.Select(ProjectModel.FullSelector);

FullSelector 看起来像这样:

public static System.Linq.Expressions.Expression<Func<Project, ProjectModel>> FullSelector = project => new ProjectModel
{
    ProjectName = p.ProjectName,
    ProjectNumber = p.ProjectNumber
};

这很好用,发送到数据库的查询只选择 FullSelector 使用的字段。另外,每次我需要查询项目实体时,我都可以重用 FullSelector。

现在是棘手的部分。在执行包含导航属性的查询时,嵌套的选择器表达式不起作用。

public static System.Linq.Expressions.Expression<Func<Project, ProjectModel>> FullSelector = project => new ProjectModel
{
    ProjectName = p.ProjectName,
    ProjectNumber = p.ProjectNumber
    Addresses = p.Addresses.Select(AddressModel.FullSelector);
};

这不起作用。内部 Select 给出编译时错误“无法从用法推断类型参数。尝试显式指定类型参数。”

以下示例编译但在执行查询时会崩溃并显示“Internal .NET Framework Data Provider error 1025。”:

public static System.Linq.Expressions.Expression<Func<Project, ProjectModel>> FullSelector = project => new ProjectModel
{
    ProjectName = p.ProjectName,
    ProjectNumber = p.ProjectNumber
    Addresses = p.Addresses.Select(AddressModel.FullSelector.Compile());
};

下一个示例编译但抛出运行时错误“LINQ to Entities 无法识别方法 'EPIC.WebAPI.Models.AddressModel Invoke(EPIC.Domain.Entities.Address)' 方法,并且此方法无法转换为存储表达式。”

public static System.Linq.Expressions.Expression<Func<Project, ProjectModel>> FullSelector = project => new ProjectModel
{
    ProjectName = p.ProjectName,
    ProjectNumber = p.ProjectNumber
    Addresses = p.Addresses.Select(a => AddressModel.PartialSelector.Compile().Invoke(a));
};

有谁知道如何让内部选择工作?我明白为什么最后一个例子不起作用,但前两个接近工作了吗?

谢谢!

【问题讨论】:

    标签: c# linq linq-to-entities


    【解决方案1】:

    那么,首先,为什么您的代码不起作用。第一个sn-p:

    p.Addresses.Select(AddressModel.FullSelector);
    

    这不起作用,因为导航属性没有实现IQueryable,它们实现了ICollectionICollection 当然没有接受Expression 参数的Select 方法。

    第二个sn-p:

    p.Addresses.Select(AddressModel.FullSelector.Compile());
    

    这不起作用,因为正在编译 FullSelector。由于正在编译,查询提供程序无法查看方法体并将代码转换为 SQL 代码。

    第三个 sn-p 的问题与第二个完全相同。将其包装在 lambda 中并不会改变这一事实。


    那么,既然我们知道了为什么您的代码不起作用,那么现在该怎么办?

    这有点令人难以置信,我不是这种方法设计的忠实粉丝,但我们开始吧。我们将编写一个方法,该方法接受一个表示具有一个参数的函数的表达式,然后它会接受另一个接受一些不相关类型的函数,然后与我们的第一个参数中的委托具有相同类型的函数 ,然后返回一个不相关的类型。

    此方法的实现可以简单地将所有使用的委托参数实例替换为我们拥有的表达式,然后将其全部包装在一个新的 lambda 中:

    public static Expression<Func<T1, T2>> Use<T1, T2, T3, T4>(
        this Expression<Func<T3, T4>> expression,
        Expression<Func<T1, Func<T3, T4>, T2>> other)
    {
        return Expression.Lambda<Func<T1, T2>>(
            other.Body.Replace(other.Parameters[1], expression),
            other.Parameters[0]);
    }
    //another overload if there are two selectors
    public static Expression<Func<T1, T2>> Use<T1, T2, T3, T4, T5, T6>(
        this Expression<Func<T3, T4>> firstExpression,
        Expression<Func<T5, T6>> secondExpression,
        Expression<Func<T1, Func<T3, T4>, Func<T5, T6>, T2>> other)
    {
        return Expression.Lambda<Func<T1, T2>>(
            other.Body.Replace(other.Parameters[1], firstExpression)
                .Replace(other.Parameters[2], secondExpression),
            other.Parameters[0]);
    }
    

    这个想法有点令人难以置信,但代码实际上很短。它依靠这种方法将一个表达式的所有实例替换为另一个:

    public static Expression Replace(this Expression expression,
        Expression searchEx, Expression replaceEx)
    {
        return new ReplaceVisitor(searchEx, replaceEx).Visit(expression);
    }
    internal class ReplaceVisitor : ExpressionVisitor
    {
        private readonly Expression from, to;
        public ReplaceVisitor(Expression from, Expression to)
        {
            this.from = from;
            this.to = to;
        }
        public override Expression Visit(Expression node)
        {
            return node == from ? to : base.Visit(node);
        }
    }
    

    现在要调用它,我们可以在地址选择器上调用Use,然后编写一个方法来接受我们的普通参数以及地址选择器的委托:

    public static Expression<Func<Project, ProjectModel>> FullSelector =
        AddressModel.FullSelector.Use((Project project,
            Func<Address, AddressModel> selector) => new ProjectModel
            {
                ProjectName = project.ProjectName,
                ProjectNumber = project.ProjectNumber,
                Addresses = project.Addresses.Select(selector),
            });
    

    现在这将完全按照预期工作。

    【讨论】:

    • 我试过了,效果很好。谢谢。如果有多个导航属性,例如,如果在 Addresses 下面有一个名为 ProjectManagers 的属性,该怎么做?
    • @Bumper 您必须创建一个额外的Use 方法,该方法接受两个选择器并将它们都传递给最后一个。我在问题中编辑了这样一个超载。如果您需要为三、四等选择器创建一个,我希望您能看到这种模式。
    • 我的 Replace 方法未定义。它在哪里?
    • @Servy 对不起,我的错。我想我需要再来一杯咖啡,今天太多了。
    【解决方案2】:

    编辑: 我意识到我之前的回答并没有真正回答这个问题,所以我删除了它;然后我意识到你在问什么,并意识到解决问题的最简单方法很可能是用你的表情说:

    Addresses = p.Addresses.AsQueryable().Select(AddressModel.PartialSelector)
    

    其中 AddressModel.PartialSelector 本身就是一个表达式。

    通过使用AsQueryable() 方法将p.Addresses 转换为IQueryable,您允许Select() 方法使用接收表达式的版本,而不必编译它。

    我希望这会有所帮助。

    【讨论】:

    • 不,他不能。这不会创建Expression 对象,因此查询提供程序将无法检查代码以将其转换为查询。相反,这将简单地将查询的全部内容下拉到内存中的对象中,然后在那里执行转换。 避免这样做很重要。
    • 这实际上帮助了我的特殊情况,而其他解决方案都无法做到,所以它并非完全没有优点。
    猜你喜欢
    • 1970-01-01
    • 2022-01-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-10-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多