Entity Framework なんかを使っていて、データを検索したいとき。
検索フォームからユーザーに検索条件を入力してもらって、それで OR 検索をしたいとなると、ちょっと面倒です。
もちろん、何も入力しなかった項目ではフィルタリングを行いません。
AND 検索だけなら、Where メソッドをチェーンさせて行けばいいのですが、OR 検索が絡むと、そう簡単にはいきません。
こういう場合は Expression Tree を頑張って組み立てなければいけないわけですが、やり方はこのへんを見て頂いてですね…
LINQ文で動的にWhere句を組み立てるには?[3.5、C#、VB] - @IT
Expressionを使った動的なOR文の生成 - Architect Life
いや、いやいやいや。わけがわからないし面倒くさい。そんなのやってらんない。
もうちょっと簡単に、こんな感じでやりたいわけですよ。*1
var expressions = new List<Expression<Func<Item, bool>>>(); if (searchCriteria.Keywords.Any()) { // 検索条件にキーワードが指定されていた場合 expressions.Add(x => searchCriteria.Keywords.Contains(x.Keyword)); } if (searchCriteria.Categories.Any()) { // 検索条件にカテゴリーが指定されていた場合 expressions.Add(x => searchCriteria.Categories.Contains(x.Category)); } // 複数の条件で OR 検索したい var results = data.Where(expressions.OrElse());
Keyword とか Category といったプロパティの参照や、Contains メソッドの呼び出しまで動的に組み立てるのではなく*2、そのあたりはラムダ式で作っておいて、条件の指定の有無に応じて OR 条件を組み立てるところ*3だけ動的にできれば十分です。
Expression.OrElse
先程のサンプルでは、Expression を List に入れて個数を可変にしていましたが、ここからは簡略化のために 2 つだけとします。
以下のような例で、expression1 と expression2 を合成して、単一の Expression を作れればよいわけです。
Expression<Func<Item, bool>> expression1 = x => searchCriteria.Keywords.Contains(x.Keyword)); Expression<Func<Item, bool>> expression2 = x => searchCriteria.Categories.Contains(x.Category));
↑これから、↓これを作りたい。
Expression<Func<Item, bool>> expression = x =>
searchCriteria.Keywords.Contains(x.Keyword) ||
searchCriteria.Categories.Contains(x.Category);
C# でいう || 演算子に対応する Expression を作るのは、Expression.OrElse メソッドです。
というわけで何も考えずにこう書くと、コンパイルが通りません。
var expression = Expression.OrElse(expression1, expression2); var results = data.Where(expression);
OrElse は BinaryExpression オブジェクトを返しますが、Where は Expression<Func<Item, bool>> オブジェクトを要求するので、型が合いません。
また、2 行目を削除するとコンパイルはできますが、実行時エラーになります。
ハンドルされていない例外: System.InvalidOperationException: バイナリ演算子 OrElse が型 'System.Func`2[Item,System.Boolean]' と 'System.Func`2[Item,System.Boolean]' に対して定義されていません。
なぜこうなるかと言うと、先のコードは、C# でハード コーディングすれば、以下のようなものに相当するからです。
これがコンパイルできないのは当然ですよね。
Expression<Func<Item, bool>> expression =
x => searchCriteria.Keywords.Contains(x.Keyword) ||
x => searchCriteria.Categories.Contains(x.Category);
再掲しますと、実現したいのは、こうです。ちょっと違いますね。
Expression<Func<Item, bool>> expression = x =>
searchCriteria.Keywords.Contains(x.Keyword) ||
searchCriteria.Categories.Contains(x.Category);
ラムダ式は、引数部と本体部から成ります。
|| 演算子を OrElse メソッドに読み替えれば、OrElse メソッドには、本体部のみを渡せばよいのがわかるでしょうか。
ラムダ式の本体部は LambdaExpression.Body プロパティで取れますから、以下のように呼び出すことができます。これなら実行時エラーにはなりません。
var expression = Expression.OrElse(expression1.Body, expression2.Body);
ただし、これで得られるのは、C# でいうと、この部分(本体部)だけです。
searchCriteria.Keywords.Contains(x.Keyword) || searchCriteria.Categories.Contains(x.Category);
引数部がないので、これだけではラムダ式として成り立たず、Where メソッドに渡すことができません。
Expression.Lambda
引数部と本体部からラムダ式を生成するのは、Expression.Lambda メソッドです。
引数部に相当するのは ParameterExpression オブジェクトで、これは Expression.Parameter メソッドで生成することができます。
というわけで、適当に引数部を生成してくっつけてやりましょう。
var expressionBody = Expression.OrElse(expression1.Body, expression2.Body); var parameter = Expression.Parameter(typeof(Item), "x"); var lambdaExpression = Expression.Lambda<Func<Item, bool>>(expressionBody, parameter); var results = data.Where(lambdaExpression);
コンパイルはできますね。
しかし…
ハンドルされていない例外: System.InvalidOperationException: 型 'Item' の変数 'x' がスコープ '' から参照されましたが、これは定義されていません
ダメでしたー。
なぜダメか。
組み合わせたい 2 つのラムダ式は、こう書き換えてもよいはずです。
Expression<Func<Item, bool>> expression1 = x1 => searchCriteria.Keywords.Contains(x1.Keyword)); Expression<Func<Item, bool>> expression2 = x2 => searchCriteria.Categories.Contains(x2.Category));
これに対して、先ほどのコードで生成されたものを、C# で書き下せば、こうなります。
Expression<Func<Item, bool>> expression = x =>
searchCriteria.Keywords.Contains(x1.Keyword) ||
searchCriteria.Categories.Contains(x2.Category);
そりゃエラーになりますよね。
繰り返しになりますが、ラムダ式は引数部と本体部から成ります。
そして、本体部は引数部への参照を持っています。
この参照元と参照先は同じ名前であるだけではダメで、同じ ParameterExpression インスタンスでなければならないのです。
さて、そうなると、ラムダ式の本体部に分け入って、その中にある ParameterExpression オブジェクトを、用意したもので置き換えてやる必要があります。
どうすればいいでしょうか。
ExpressionVisitor
そんなときのためのクラスが、.NET Framework にはちゃんと用意されています。
それが ExpressionVisitor クラス です。
このクラスの派生クラスを作って、Visit メソッドに Expression を渡してやると、その中の個々の構成要素(パラメーターとかプロパティ参照とかメソッド呼び出しとか…)に応じて、適切なメソッドを呼び出してくれます。
オーバーライドしたメソッド中では、引数に渡されてきたオリジナルの構成要素を元に、新しい構成要素を表す Expression を返すことで、式を作り替えることができます。
今回はパラメーターを置き換えたいので、VisitParameter メソッドをオーバーライドしてやればいいわけです。
このサンプルは文末をご覧ください。