動的LINQ述語の構築手法

キーワード検索のような柔軟なフィルタリングを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経由で導入可能である。

タグ: LINQ Expression Tree PredicateBuilder Entity Framework LINQKit

7月9日 17:43 投稿