キーワード検索のような柔軟なフィルタリングをLINQクエリで実現する際、条件を実行時に組み立てる必要がよく生じる。例えば、複数のキーワードのうち「いずれか」に一致するレコードを取得したい場合、単純なWhereチェーンでは対応できない。
このようなケースでは、式ツリー(Expression Tree)を動的に構築し、OR条件やAND条件をプログラムで合成するのが効果的である。PredicateBuilderはそのための再利用可能なユーティリティクラスとして広く使われている。
PredicateBuilderの基本的な使い方
以下の例は、商品説明に指定されたキーワードのいずれかを含む製品を検索するメソッドである:
IQueryable<Product> SearchProducts(params string[] terms)
{
var condition = PredicateBuilder.False<Product>();
foreach (string term in terms)
{
string keyword = term; // キャプチャ問題を回避
condition = condition.Or(p => p.Description.Contains(keyword));
}
return dbContext.Products.AsExpandable().Where(condition);
}
Entity Frameworkを使用する場合は、AsExpandable()を呼び出す必要がある。これはLINQKitライブラリの機能であり、式ツリー内のInvoke式をEFが解釈可能な形式に変換する。
PredicateBuilderの実装
以下はPredicateBuilderの最小限の実装例である:
using System;
using System.Linq.Expressions;
public static class PredicateBuilder
{
public static Expression<Func<T, bool>> True<T>() => _ => true;
public static Expression<Func<T, bool>> False<T>() => _ => false;
public static Expression<Func<T, bool>> Or<T>(
this Expression<Func<T, bool>> left,
Expression<Func<T, bool>> right)
{
var body = Expression.OrElse(left.Body, Expression.Invoke(right, left.Parameters));
return Expression.Lambda<Func<T, bool>>(body, left.Parameters);
}
public static Expression<Func<T, bool>> And<T>(
this Expression<Func<T, bool>> left,
Expression<Func<T, bool>> right)
{
var body = Expression.AndAlso(left.Body, Expression.Invoke(right, left.Parameters));
return Expression.Lambda<Func<T, bool>>(body, left.Parameters);
}
}
入れ子になった条件の構築
複雑な条件、例えば (A OR B) AND C のような式を動的に構築するには、内側のOR条件をまず別個に作成し、それを外側のAND条件に組み込む:
var orClause = PredicateBuilder.False<Product>();
orClause = orClause.Or(p => p.Description.Contains("foo"));
orClause = orClause.Or(p => p.Description.Contains("bar"));
var andClause = PredicateBuilder.True<Product>();
andClause = andClause.And(p => p.Price > 100);
andClause = andClause.And(p => p.Price < 1000);
andClause = andClause.And(orClause);
ジェネリックな述語の定義
複数のエンティティに共通する有効期間(ValidFrom / ValidTo)を持つ場合、インターフェースを介して汎用的な述語を定義できる:
public interface ITimeBound
{
DateTime? ValidFrom { get; }
DateTime? ValidTo { get; }
}
public static class TimeBoundPredicates
{
public static Expression<Func<T, bool>> IsActive<T>()
where T : ITimeBound
{
return e => (e.ValidFrom == null || e.ValidFrom <= DateTime.Now) &&
(e.ValidTo == null || e.ValidTo >= DateTime.Now);
}
}
各エンティティクラスでこのインターフェースを実装すれば、共通ロジックを再利用可能になる:
public partial class PriceList : ITimeBound { }
public partial class Product : ITimeBound { }
使用例:
var activeItems = dbContext.Products
.AsExpandable()
.Where(TimeBoundPredicates.IsActive<Product>()
.And(p => p.Name.StartsWith("A")));
注意点
Entity Frameworkでは、式ツリー内のExpression.Invokeが直接サポートされていないため、必ずAsExpandable()を適用する必要がある。これはLINQKitが提供する拡張メソッドであり、NuGet経由で導入可能である。