more Dynamic LINQ

今日、東京に来たらしい ScottGu さんの Dynamic LINQ に対抗してみました。動的に LINQ クエリーを組み立てる例はあちこちでたくさんの人がそれぞれ違う方法でやってますが(^^;、末席に加わります。

動的といっても Where に渡す条件を式木で組み立てて渡すだけのサンプルです。必要だったので式木の練習がてら書いてみました。きっと Where 条件組み立てって一番使うよね?式木のドキュメントはもっと充実させて欲しいところ…

まずはデータ。いつぞやのもの。

var persons = new[] {
  new Person { Name = "Wataru Abe", Gender = Gender.Male, IsActive = true },
  new Person { Name = "Masao Sueda", Gender = Gender.Male, IsActive = true },
  new Person { Name = "Sae Nakarai", Gender = Gender.Female, IsActive = true },
  new Person { Name = "Shiori Yamamoto", Gender = Gender.Female, IsActive = true },
  new Person { Name = "Satoshi Hatakeyama", Gender = Gender.Male, IsActive = false },
};

このデータにこんなクエリーを投げたいとします。

var list1 = from p in persons
            where p.Gender == Gender.Male && p.IsActive
            select p.Name;
list1.ToList().ForEach( Console.WriteLine );

このクエリーの where の条件「p.Gender == Gender.Male && p.IsActive」を式木を使って動的に組み立ててみます。

static Func<Person, bool> GetPredicate()
{
  // パラメータ
  var param = Expression.Parameter( typeof( Person ), "p" );

  // 条件式木のリスト
  var predList = new List<Expression>();

  // 条件(1) p.Gender == Gender.Male 組み立て
  var left = Expression.PropertyOrField( param, "Gender" );
  var right = Expression.Constant( Gender.Male );
  predList.Add( Expression.Equal( left, right ) );

  // 条件(2) p.IsActive 組み立て
  predList.Add( Expression.PropertyOrField( param, "IsActive" ) );

  // p.Gender == Gender.Male && p.IsActive 組み立て
  var body = predList.Aggregate(
    ( l, r ) => Expression.MakeBinary( ExpressionType.AndAlso, l, r ) );

  // パラメータと本体をくっつけて、実行コード生成
  return Expression.Lambda<Func<Person, bool>>( body, param ).Compile();
}

簡単なサンプルにしたかったので固定値で組み立ててます。必ず同じ条件式のデリゲートができあがるだけですが、それでも十分ややこしいので。ここがわかれば変数に応じて組み立てることはできるハズ。
条件(1)の BinaryExpression、条件(2)の MemberExpression を作ってリストに入れていき、最後に AndAlso でリスト中の式をくっつけてます。コメントと変数名でいろいろわかると思いますが、詳細は書ききれないのでMSDNライブラリをどうぞ。
式とか木とか二項演算子とか、いかにも情報処理って感じですねw 細々と面倒ですが、ブロックで遊んでるみたいなもんです。珍しく Aggregate が使えたのがうれしかったり。

あとは組み立てた条件を使ってクエリーを投げるだけです。

var list2 = from p in persons
            where GetPredicate()( p )
            select p.Name;
list2.ToList().ForEach( Console.WriteLine );

GetPredicate() はデリゲートが帰ってくることに注意。LINQ to SQL などで、Expression が欲しければ GetPredicate() の最後の Compile() せずに Expression> を返せば OK。

最後に全文。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

class Program
{
  static void Main()
  {
    var persons = new[] {
     new Person { Name = "Wataru Abe", Gender = Gender.Male, IsActive = true },
     new Person { Name = "Masao Sueda", Gender = Gender.Male, IsActive = true },
     new Person { Name = "Sae Nakarai", Gender = Gender.Female, IsActive = true },
     new Person { Name = "Shiori Yamamoto", Gender = Gender.Female, IsActive = true },
     new Person { Name = "Satoshi Hatakeyama", Gender = Gender.Male, IsActive = false },
    };

    var list1 = from p in persons
                where p.Gender == Gender.Male && p.IsActive
                select p.Name;
    list1.ToList().ForEach( Console.WriteLine );
    Console.WriteLine();

    // Expression Tree を使って条件を組み立てるサンプル
    var list2 = from p in persons
                where GetPredicate()( p )
                select p.Name;
    list2.ToList().ForEach( Console.WriteLine );

    Console.ReadKey();
  }

  static Func<Person, bool> GetPredicate()
  {
    // Expression Tree を使って条件を組み立てるサンプル

    // パラメータ
    var param = Expression.Parameter( typeof( Person ), "p" );

    // 条件式木のリスト
    var predList = new List<Expression>();

    // 条件(1) p.Gender == Gender.Male 組み立て
    var left = Expression.PropertyOrField( param, "Gender" );
    var right = Expression.Constant( Gender.Male );
    predList.Add( Expression.Equal( left, right ) );

    // 条件(2) p.IsActive 組み立て
    predList.Add( Expression.PropertyOrField( param, "IsActive" ) );

    // p.Gender == Gender.Male && p.IsActive 組み立て
    var body = predList.Aggregate(
      ( l, r ) => Expression.MakeBinary( ExpressionType.AndAlso, l, r ) );

    // パラメータと本体をくっつけて、実行コード生成
    return Expression.Lambda<Func<Person, bool>>( body, param ).Compile();
  }
}

public enum Gender { Male, Female }

sealed class Person
{
  public string Name { get; set; }
  public bool IsActive { get; set; }
  public Gender Gender { get; set; }
}

Aggregate のところは、こんなヘルパー関数を用意してしまうのもいいかも。

public static class ExpressionExt
{
  public static Expression MakeBinary(
    this IEnumerable<Expression> exprs, ExpressionType binaryType )
  {
    if ( exprs == null )
      throw new ArgumentNullException( "exprs" );

    return exprs.Aggregate(
      ( l, r ) => Expression.MakeBinary( binaryType, l, r ) );
  }
}