linq(lambd) 追加表达式 linqkit

发布时间 2023-04-22 14:20:22作者: WantRemake

动态组合表达式谓词

假设您要编写实现 SQL 的 LINQ to SQL 或实体框架查询 关键字样式搜索。换句话说,返回其行的查询 描述包含给定集合的部分或全部 的关键字。

我们可以按以下步骤进行:

IQueryable<Product> SearchProducts (params string[] keywords)
{
  IQueryable<Product> query = dataContext.Products;

  foreach (string keyword in keywords)
    query = query.Where (p => p.Description.Contains (keyword));

  return query;
}

目前为止,一切都好。但这只处理您想要的情况 以匹配所有指定的关键字。假设 相反,我们想要描述包含任何提供的关键字的产品。我们以前的链接方法 其中 运算符完全没用!我们可以改为连锁联盟运营商,但是 这将是低效的。理想的方法是动态构造一个 执行基于 OR 的 lambda 表达式树 谓语。

在所有会驱使您手动构建的事情中 表达式树,对动态谓词的需求是最常见的 典型的业务应用程序。幸运的是,可以编写一组 简单且可重用的扩展方法从根本上简化了此任务。这是 我们的谓词生成器类的角色。

使用 PredicateBuilder

下面介绍如何使用 PredicateBuilder 解决前面的示例:

IQueryable<Product> SearchProducts (params string[] keywords)
{
  var predicate = PredicateBuilder.False<Product>();

  foreach (string keyword in keywords)
    predicate = predicate.Or (p => p.Description.Contains (keyword));

  return dataContext.Products.Where (predicate);
}

如果使用实体框架进行查询,请将最后一行更改为:

return objectContext.Products.AsExpandable().Where (predicate);

AsExpandable 方法是 LINQKit 的一部分(见下文)。

试验 PredicateBuilder 的最简单方法是使用 LINQPad。 LINQPad 允许您针对数据库或本地集合即时测试 LINQ 查询,并直接支持 PredicateBuilder(按 F4 并选中“包含谓词生成器”)。

PredicateBuilder 源代码

这是完整的来源:

using System;
using System.Linq;
using System.Linq.Expressions;
using System.Collections.Generic;
 
public static class PredicateBuilder
{
  public static Expression<Func<T, bool>> True<T> ()  { return f => true;  }
  public static Expression<Func<T, bool>> False<T> () { return f => false; }
 
  public static Expression<Func<T, bool>> Or<T> (this Expression<Func<T, bool>> expr1,
                                                      Expression<Func<T, bool>> expr2)
  {
    var invokedExpr = Expression.Invoke (expr2, expr1.Parameters.Cast<Expression> ());
    return Expression.Lambda<Func<T, bool>>
          (Expression.OrElse (expr1.Body, invokedExpr), expr1.Parameters);
  }
 
  public static Expression<Func<T, bool>> And<T> (this Expression<Func<T, bool>> expr1,
                                                       Expression<Func<T, bool>> expr2)
  {
    var invokedExpr = Expression.Invoke (expr2, expr1.Parameters.Cast<Expression> ());
    return Expression.Lambda<Func<T, bool>>
          (Expression.AndAlso (expr1.Body, invokedExpr), expr1.Parameters);
  }
}

PredicateBuilder 也作为 LINQKit 的一部分提供,LINQKit 是 LINQ to SQL 和实体框架的生产力工具包。

如果使用 LINQ to SQL,则可以单独使用 PredicateBuilder 源代码。

如果你是 使用实体框架,您将需要完整的 LINQKit - 用于扩展功能。 您可以引用 LINQKit.dll也可以将 LINQKit 的源代码复制到应用程序中。

工作原理

True 和 False 方法没有什么特别之处:它们只是创建表达式<Func<T,bool>>的方便快捷方式,该表达式最初计算结果为 对或错。所以以下内容:

var predicate = PredicateBuilder.True <Product> ();

只是一个快捷方式:

Expression<Func<Product, bool>> predicate = c => true;

当您通过重复堆叠和/或条件来构建谓词时, 将起点设置为 true 或 false (分别)很有用。如果没有关键字,我们的搜索产品方法仍然有效 提供。

有趣的工作发生在 And 和 Or 方法中。我们首先用第一个表达式调用第二个表达式 表达式的参数。调用表达式调用 另一个使用给定表达式作为参数的 lambda 表达式。我们可以 从第一个表达式的主体和第二个表达式的调用版本创建条件表达式。最后一步是 将其包装在新的 lambda 表达式中。

实体框架的查询处理管道无法 处理调用表达式,这就是需要对查询中的第一个对象调用 AsExpandable 的原因。通过调用 AsExpandable,您可以 激活 LINQKit 的表达式访问者类,用于替换调用 具有实体框架可以理解的更简单构造的表达式。

更多示例

编写数据访问层的一个有用模式是创建一个 可重用的谓词库。然后,您的查询主要由选择和排序子句组成, 筛选逻辑已扩展到您的库。下面是一个简单的示例:

public partial class Product
{
  public static Expression<Func<Product, bool>> IsSelling()
  {
    return p => !p.Discontinued && p.LastSale > DateTime.Now.AddDays (-30);
  }
}

我们可以通过添加一个使用 PredicateBuilder 的方法来扩展它:

public partial class Product
{
  public static Expression<Func<Product, bool>> ContainsInDescription (
                                                params string[] keywords)
  {
    var predicate = PredicateBuilder.False<Product>();
    foreach (string keyword in keywords)
      predicate = predicate.Or (p => p.Description.Contains (keyword));

    return predicate;
  }
}

这提供了简单性和可重用性的完美平衡, 以及将业务逻辑与表达式管道逻辑分离。检索 描述中包含“黑莓”或“iPhone”的所有产品,以及 正在销售的诺基亚和爱立信,你会这样做:

var newKids  = Product.ContainsInDescription ("BlackBerry", "iPhone");

var classics = Product.ContainsInDescription ("Nokia", "Ericsson")
                      .And (Product.IsSelling());
var query =
  from p in Data.Products.Where (newKids.Or (classics))
  select p;

粗体中的 And 和 Or 方法解析为 PredicateBuilder 中的扩展方法。
表达式谓词可以通过以下方式执行 SQL 子查询的等效项: 引用关联属性。所以,如果产品有 一个名为“购买”的子实体集,我们可以优化我们的 IsSelling 方法,以仅返回那些 已售出最低单位数量,如下所示:

public static Expression<Func<Product, bool>> IsSelling (int minPurchases)
{
  return prod =>
    !prod.Discontinued &&
     prod.Purchases.Where (purch => purch.Date > DateTime.Now.AddDays(-30))
                    .Count() >= minPurchases;
}

嵌套谓词

请考虑以下谓词:

p => p.Price > 100 &&
     p.Price < 1000 &&
     (p.Description.Contains ("foo") || p.Description.Contains ("far"))

假设我们想动态构建它。问题是, 我们如何处理最后一行中两个表达式周围的括号?

答案是先构建括号表达式,然后 然后在外部表达式中使用它,如下所示:

var inner = PredicateBuilder.False<Product>();
inner = inner.Or (p => p.Description.Contains ("foo"));
inner = inner.Or (p => p.Description.Contains ("far"));

var outer = PredicateBuilder.True<Product>();
outer = outer.And (p => p.Price > 100);
outer = outer.And (p => p.Price < 1000);
outer = outer.And (inner);

请注意,对于内部表达式,我们从 谓词生成器。False(因为我们使用的是 Or 运算符)。跟 然而,外部表达式,我们从 PredicateBuilder 开始。True(因为我们使用的是 And 运算符)。

泛型谓词

假设数据库中的每个表都有 ValidFrom 和 ValidTo 列,如下所示:

create table PriceList
(
   ID int not null primary key,
   Name nvarchar(50) not null,
   ValidFrom datetime,
   ValidTo datetime
)

检索自 DateTime.Now 起有效的行(最 常见情况),您将这样做:

from p in PriceLists
where (p.ValidFrom == null || p.ValidFrom <= DateTime.Now) &&
      (p.ValidTo   == null || p.ValidTo   >= DateTime.Now)
select p.Name

当然,粗体字的逻辑很可能会在 多个查询!没问题:让我们在 PriceList 类中定义一个返回可重用表达式的方法:

public static Expression<Func<PriceList, bool>> IsCurrent()
{
   return p => (p.ValidFrom == null || p.ValidFrom <= DateTime.Now) &&
               (p.ValidTo   == null || p.ValidTo   >= DateTime.Now);
}

好的:我们的查询现在简单多了:

var currentPriceLists = db.PriceLists.Where (PriceList.IsCurrent());

使用PredicateBuilder的And and Or 方法,我们 可以轻松引入其他条件:

var currentPriceLists = db.PriceLists.Where (
                          PriceList.IsCurrent().And (p => p.Name.StartsWith ("A")));

但是,所有其他同时具有 ValidFrom 和 ValidTo 列的表呢?我们不想重复我们的 IsCurrent 方法 每张桌子!幸运的是,我们可以推广我们的 IsCurrent 方法 泛 型。

第一步是定义接口:

public interface IValidFromTo
{
   DateTime? ValidFrom { get; }
   DateTime? ValidTo   { get; }
}

现在我们可以定义一个通用的 IsCurrent 方法,使用 该接口作为约束:

public static Expression<Func<TEntity, bool>> IsCurrent<TEntity>()
   where TEntity : IValidFromTo
{
   return e => (e.ValidFrom == null || e.ValidFrom <= DateTime.Now) &&
               (e.ValidTo   == null || e.ValidTo   >= DateTime.Now);
}

最后一步是在每个类中实现此接口 支持 ValidFrom 和 ValidTo。如果您使用的是 Visual Studio 或 像 SqlMetal 这样的工具来生成你的实体类,在未生成的一半中执行此操作 分部类:

public partial class PriceList : IValidFromTo { }
public partial class Product   : IValidFromTo { }

在 LINQPad 中使用 PredicateBuilder

使用 LINQPad,您可以编写和测试查询 比Visual Studio的构建/运行/调试周期快得多。要使用 PredicateBuilder in LINQPad with LINQ to SQL:

  • 按 F4 并选中“包含谓词生成器”

若要在 LINQPad 中使用 PredicateBuilder 和实体框架,请执行以下操作:

  • 按 F4 并添加对 LinqKit 的引用.dll

 

转自:C# 简而言之 - PredicateBuilder (albahari.com)