65.9K
CodeProject 正在变化。 阅读更多。
Home

QueryMap:LINQ 表达式的自定义转换

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2012年4月23日

Ms-PL

9分钟阅读

viewsIcon

38056

downloadIcon

465

QueryMap 允许您将 LINQ 表达式预先转换为底层查询提供程序(如 LINQ to SQL)可以理解的形式。

引言

本文将向您展示如何在代码中定义表达式,并在内存和查询表达式中重用它们。该解决方案将适用于任何 LINQ 提供程序,包括 LINQ to SQL。

问题

当我第一次开始使用 LINQ to SQL 时,我喜欢 LINQ to SQL 设计器生成分部类的事实。这意味着我可以通过添加一个分部类文件来为我的实体添加自定义的“计算”属性,就像这样

public partial class OrderDetail {
    public decimal Subtotal {
        get { return Quantity * UnitPrice; }
    }
}

然而,当我想要在查询中使用这个属性时,我很快就遇到了一个问题

var bigOrders =
    from o in orderContext.OrderDetails
    where o.Subtotal > 1000
    select o;
    
System.NotSupportedException : The member 'Overboard.Linq.OrderDetail.Subtotal' 
                               has no supported translation to SQL.

LINQ to SQL 不知道如何将 Subtotal 属性转换为 SQL 查询。与 QuantityUnitPrice 列不同,它没有列映射,而且我们无法为其提供一个,因为它不直接对应表中的列。

对于这种情况,有几种可能的解决方法

  • 我们可以将 Subtotal 添加到数据库中(如果我们有修改数据库模式的权限,当然)作为
    • 表中的一列(需要保持更新)
    • 计算列(如果数据库引擎支持)
    • 视图中的一列
  • 除了修改数据库模式以绕过技术限制以及可能存储和更新重复数据之外,所有这些解决方法都有一个额外的缺点:它们不允许我们获取新创建的 OrderDetail 的小计,在我们持久化它之前。
  • 我们可以将 Subtotal 属性的定义插入到查询本身中,而不是在查询中使用它
    var bigOrders =
        from o in orderContext.OrderDetails
        where o.Quantity * o.UnitPrice > 1000
        select o;
    现在,小计的定义存在于两个不同的地方;我们不再 DRY。如果 Subtotal 必须更改(例如,为了处理折扣),我们将不得不更改使用该表达式的每个查询。

我想要另一个选择,但找了一段时间后,我发现许多其他人也有同样的问题,但没有解决方案。所以我决定自己制作一个。

设计解决方案

LINQ to SQL 是 IQueryable 的一个实现,它使用表达式树在 LINQ 标准查询运算符和底层查询实现之间进行转换。我意识到,通过构建自己的 IQueryable 实现,我可以预处理表达式树,并将 Subtotal 属性引用(LINQ to SQL 不理解)转换为其底层表达式(LINQ to SQL 理解)。然后,LINQ to SQL 可以接管并像往常一样运行查询。

为了完成这种预处理,我使用了 Matt Warren 关于 实现 IQueryable 的精彩系列博客文章,他后来将其打包到 IQ Toolkit 中。如果您进行任何高级 LINQ 工作,或者只是喜欢深入研究内部结构,这绝对值得一看。

您不需要知道表达式树如何工作即可使用此解决方案,但如果您想了解解决方案的工作原理,它会有所帮助。Charlie Calvert 在 他的博客上有一个很好的介绍,您可以通过搜索网络找到许多其他介绍。

在实施此解决方案时,我有三个主要目标

  • 易于使用。没有什么比没有人知道如何使其工作更快地扼杀一个巧妙的解决方案了。
  • 低可见性。我需要一些不会在代码中引入太多繁琐内容的东西,尤其是因为最可能使用它的情况最初相对简单。
  • 低影响。我希望在使用它时对调用代码或吞吐量影响很小,而在不使用时完全没有影响。

基础知识

首先,我们将介绍在代码中使用 QueryMap 的基本步骤,然后我们将研究如何使其使用起来更容易一些。

正如已经提到的,此解决方案将与表达式树一起工作。这意味着我们需要做的第一件事是将 Subtotal 重新定义为表达式树

private static readonly Expression<Func<OrderDetail, decimal>> 
_subtotalExpression = od => od.Quantity * od.UnitPrice;

但我们仍然需要能够像常规属性一样使用 Subtotal。我们如何使用表达式树做到这一点?我们利用所有生成的表达式树都可以编译成委托的事实,这些委托就像它作为普通方法编写一样执行表达式

private static readonly Func<OrderDetail, decimal> _subtotalLambda = _subtotalExpression.Compile();
public decimal Subtotal {
    get { return _subtotalLambda(this); }
}

现在我们可以像常规属性一样调用 Subtotal,并且我们还将其定义为表达式树。接下来,我们需要一种将属性与表达式树关联起来的方法。MapToExpressionAtrribute 告诉 QueryMap 从哪里获取 Subtotal 属性的定义

[MapToExpression("_subtotalExpression")]
public decimal Subtotal {
    get { return _subtotalLambda(this); }
}

最后一步是使用 AsMapped 扩展方法将预处理器连接到查询中

var orders =
    from o in context.OrderDetails.AsMapped()
    where o.Subtotal > 1000
    select o;

AsMappedIQueryable 的一个扩展方法,因此您需要导入 Overboard.Linq 命名空间。类似于 AsQueryableAsMapped 更改将用于处理查询的查询提供程序,在本例中为 MappedQueryProvider。原始查询提供程序,无论是 LINQ to SQL 还是其他任何东西,都被存储起来,以便在预处理器完成其工作后用于生成最终查询。

这就是使其工作所需的一切!还不错,但我们可以使其更容易一些。

清理

让我们从清理声明表达式树的方式开始。现在,我们必须分别定义表达式树和编译后的委托;我们可以使用 ExpressionMethod 类将它们组合成一个定义

private static readonly ExpressionMethod<Func<OrderDetail, decimal>> _subtotal =
    new ExpressionMethod<Func<OrderDetail, decimal>>(od => od.Quantity * od.UnitPrice);

ExpressionMethod 类自动执行将表达式树编译成委托的步骤,并在其 Invoke 属性中公开该委托。需要对关联的属性定义进行小幅更改

[MapToExpression("_subtotal")]
public decimal Subtotal {
    get { return _subtotal.Invoke(this); }
}

现在我们只有一个定义了,但它很长,有很多丑陋的尖括号。幸运的是,我们可以利用类型推断来创建一些帮助方法,这将使其不那么痛苦

private static readonly ExpressionMethod<Func<OrderDetail, decimal>> _subtotal =
    ExpressionMethod.Create((OrderDetail od) => od.Quantity * od.UnitPrice);

ExpressionMethod.Create 有 9 个重载,每个 FuncAction 委托的变体各一个。但是,其中一个与其他不同。每当我们使用一个带一个参数并返回一个值的表达式(当表达式表示一个属性时总是如此),Create 方法返回一个派生自 ExpressionMethod 的类的实例,它将其类型参数替换为 Func 委托。结果是 ExpressionMethod 声明的左侧稍微更简洁

private static readonly ExpressionMethod<OrderDetail, decimal> _subtotal =
    ExpressionMethod.Create((OrderDetail od) => od.Quantity * od.UnitPrice);

所以,我们从这个开始……

public decimal Subtotal {
    get { return Quantity * UnitPrice; }
}

var orders =
    from o in context.OrderDetails
    where o.Subtotal > 1000
    select o;

……我们得到了这个……

private static readonly ExpressionMethod<OrderDetail, decimal> _subtotal =
    ExpressionMethod.Create((OrderDetail od) => od.Quantity * od.UnitPrice);
[MapToExpression("_subtotal")]
public decimal Subtotal {
    get { return _subtotal.Invoke(this); }
}

var orders =
    from o in context.OrderDetails.AsMapped()
    where o.Subtotal > 1000
    select o;

一些额外的代码使我们能够定义一次计算属性,然后在常规代码和查询表达式中重用该定义。在 LINQ to SQL 中,此查询将生成以下 SQL。

手动映射

基于属性的映射在您向自己的类型添加自定义属性时非常有用。但是,如果您想将表达式映射到您无法控制的类型上的成员怎么办?您可以通过将成员和应替换它的表达式传递给 AsMapped 方法,为任何类型上的任何成员创建自己的映射

public decimal Total {
    get { return Subtotal; }
}

var totalProperty = typeof(Order).GetProperty("Total");
var orders = from o in context.Orders
    .AsMapped(totalProperty, x => x.Subtotal)
where o.Total > 10000
select o;    

您可以重复调用 AsMapped 来设置额外的映射。您还可以存储 AsMapped 调用的结果,它是 IMappedQuery<T> 的一个实例,然后在其上调用 SetMapping

您应该小心使用此方法;虽然它可能有用,但它也存在成员定义和替换表达式不同步的风险。

细节

术语

  • 映射成员:在查询处理期间需要替换的成员(例如,上面示例中的 Subtotal)。
  • 目标成员:由映射成员上的 MapToExpressionAttribute 标识的成员(例如,上面示例中的 _subtotal)。
  • 目标表达式:通过检索目标成员的值获得的表达式(或 ExpressionMethod),用于在查询处理期间替换映射成员(例如,上面示例中 _subtotal 的值)。

MapToExpressionAttribute 可以应用于任何实例或 static 字段、属性或方法(映射成员)。参数是同一类中目标字段、属性或方法的名称(目标成员),其返回类型为 LambdaExpressionExpressionMethod。该成员的值(目标表达式)将是替换映射成员的表达式。

目标表达式中的参数必须与映射成员的参数(如果有)匹配。如果映射成员是实例成员,则用于替换它的表达式必须将其第一个参数作为包含映射成员的类(类似于扩展方法的评估方式)。在 Subtotal 示例中就是这种情况;Subtotal 是一个实例属性,替换它的表达式的第一个(也是唯一的)参数是对 OrderDetail 的引用。

目标成员通常应该是 static 字段。如果可以在执行查询之前完全评估调用它的实例,则它可以是实例。它也可以是属性或方法而不是字段。我想不出这种灵活性是必要或可取的用例,但我想启用它,以防有人提出一个。通常,您应该坚持使用 static 字段作为目标表达式(例如,上面示例中的 _subtotal)。

让我们看看 QueryMap 如何使用 MapToExpressionAttribute 将表达式转换为其底层表示

  1. 当查询准备好处理时(例如,当它在 foreach 循环中用作源时),QueryMap 检查表达式树中引用的每个字段、属性和方法,以查找 MapToExpressionAttribute。这些是映射成员。
  2. 对于每个具有 MapToExpressionAttribute 的成员,QueryMap 找到该属性中命名的同一类中的成员。这是目标成员。目标成员必须是一个字段、一个非索引属性或一个没有参数的方法。如果找不到目标成员,则抛出异常。
  3. 目标成员被评估。其返回值必须是 LambdaExpressionExpression<TDelegate> 派生自它)或 ExpressionMethod。如果它不是这两种类型之一,则抛出异常。否则,返回值成为目标表达式。
  4. 目标表达式被替换到表达式树中,用于映射成员。如果映射成员是实例,则调用它的实例作为第一个参数传递给目标表达式。如果映射成员有参数,它们也会传递给目标表达式。如果目标表达式没有正确数量的正确类型参数,则抛出异常。
  5. 目标表达式使用相同的步骤递归评估。

最后一点很有趣,因为它可用于创建引用其他映射成员的映射成员。例如,我们可以在 Order 上定义一个 Subtotal

private static readonly ExpressionMethod<Order, decimal> _subtotal =
    ExpressionMethod.Create((Order o) => o.OrderDetails.Sum(od => od.Subtotal));
[MapToExpression("_subtotal")]
public decimal Subtotal {
    get { return _subtotal.Invoke(this); }
}

var orders = from o in context.Orders.AsMapped()
             select o.Subtotal;

当处理此查询时,QueryMap 将首先将 Order.Subtotal 的引用替换为其底层表达式,然后在该表达式中,它将把 OrderDetail.Subtotal 的引用替换为其底层表达式。

历史

  • 2012 年 4 月 22 日 - 初次发布
© . All rights reserved.