在LINQ to entities查询的"Select"部分中使用表达式

10

我希望能够重用我的LINQ to Entities查询中的"select"部分。例如,我可以采取以下措施...

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使用的字段。此外,每次需要查询Project实体时,可以重复使用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会给出编译时错误 "The type arguments cannot be inferred from the usage. Try specifying the type arguments explicitly."

以下示例编译通过,但在执行查询时崩溃,显示 "Internal .NET Framework 数据提供程序错误 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));
};

有人知道如何使内部选择器工作吗?我理解最后一个示例为什么不起作用,但前两个示例是否接近实现目标呢?

谢谢!

2个回答

6

首先,为什么你的代码不起作用。第一段代码:

p.Addresses.Select(AddressModel.FullSelector);

这不起作用,因为导航属性没有实现IQueryable接口,而是实现了ICollection接口。ICollection自然没有一个接受Expression参数的Select方法。

第二段代码:

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

这段代码无法正常工作,因为 FullSelector 正在被编译。由于正在编译,查询提供程序无法查看方法体并将代码翻译成 SQL 代码。

第三个片段与第二个完全相同。用 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
1
@Bumper 你需要创建一个额外的 Use 方法,该方法接受两个选择器并将它们都传递给最后一个选择器。我已经在问题中编辑了这样的重载。如果你需要为三个、四个等选择器创建一个方法,希望你能看到模式。 - Servy
我的代码出现了“Replace”方法未定义的错误。它在哪里? - Luke Vo
@Servy 对不起,我的错。看来我需要再喝一杯咖啡,今天太累了。 - Luke Vo

0

编辑:

我意识到我之前的回答实际上并没有回答你的问题,所以我将其删除了;然后我意识到你在问什么,并且认为解决这个问题最简单的方法可能是在你的表达式中加入以下内容:

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

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

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

希望这能帮到您。


不行,他做不到。这样做不会创建一个“Expression”对象,因此查询提供程序将无法检查代码以将其转换为查询。相反,这将仅将查询的整个内容拉入内存对象中,然后在那里执行转换。重要的是避免这样做。 - Servy
这实际上帮助了我的特定情况,其他解决方案都无法解决,因此它并非毫无价值。 - Eraph

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