「ほっ」と。キャンペーン
(.Net)LINQでラムダ式を動的に生成する
(.Net)LINQのクエリ構文とメソッド構文(ラムダ式)を使ってみた で LINQ で動的なクエリを生成する方法がわからんと言っていましたが、いろいろ参考サイトを巡回した結果、よーやく動的クエリをある程度なら処理できる仕組みを作ることができました。
任意のクラスのコレクションで、複数の条件を動的に処理するというものです。(現段階では論理演算子はネストできず、ANDかORしか選べないのでSQLほど複雑なクエリはできません。)

方法はラムダ式を動的に生成するというものです。C#3.0(.NET Framework 3.5)

まず、呼び出し元側のソースです。TestClassのコレクションをLINQで動的ラムダ式を使って検索します。(文字数の関係でハイライトはOFFです。)

public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}

private void button1_Click(object sender, EventArgs e)
{
LamdaUtility lamda = new LamdaUtility();

//テスト用のデータ生成
List<TestClass> dataSource = new List<TestClass>();
dataSource.Add(new TestClass() { Name = "Asou Kumiko", Id = 1, TrueFalse = false });
dataSource.Add(new TestClass() { Name = "asou kumiko", Id = 2, TrueFalse = true });
dataSource.Add(new TestClass() { Name = "Mikazuki Shizuka", Id = 3, TrueFalse = true });
dataSource.Add(new TestClass() { Name = "mikazuki shizuka", Id = 1, TrueFalse = true });
dataSource.Add(new TestClass() { Name = "Kiriyama", Id = 5, TrueFalse = false });
dataSource.Add(new TestClass() { Name = "JikouKeisatu", Id = 4, TrueFalse = false });
dataSource.Add(new TestClass() { Name = "", Id = 6, TrueFalse = false });
dataSource.Add(new TestClass() { Name = null, Id = 7, TrueFalse = false });

Console.WriteLine("動的ラムダ結果");
//条件クリア
lamda.ClearQuery();
//IDの条件設定 1,7 が対象
lamda.AddQuery(LamdaUtility.QueryTypeEnum.Equal, "Id", 1);
lamda.AddQuery(LamdaUtility.QueryTypeEnum.Equal, "Id", 7);
//Nameの条件設定 "zuka"を含むもの
lamda.AddQuery(LamdaUtility.QueryTypeEnum.StringContainsIgnoreCase, "Name", "zuka");


//論理条件
//lamda.AndOr = LamdaUtility.AndOrTypeEnum.AND;
lamda.AndOr = LamdaUtility.AndOrTypeEnum.OR;

//上記条件を普通にラムダ式使うと下記のようになる

//var list = from p in dataSource
// where p.Id == 1 || p.Id == 7 || p.Name.ToLower().Contains("zuka")
// select p;

// Expression Tree を使って条件を組み立てる
var list = dataSource.AsQueryable().
Where(GetPredicate(lamda));

/*下記のラムダ式クエリでしたい場合は、GetPredicateメソッドの戻り値をコメントアウトしてる方にすればよい
var list = from p in dataSource
where GetPredicate(lamda)(p)
select p;
*/

//フィルタを実行
List<TestClass> resList = list.ToList();

//抽出結果表示
resList.ForEach(p => Console.WriteLine(p.Name + " " + p.Id.ToString()));
}

/// <summary>条件ラムダ式を生成する</summary>
/// <returns></returns>
public static Expression<Func<TestClass, bool>> GetPredicate(LamdaUtility lmd)
{
// パラメータ生成
ParameterExpression param = Expression.Parameter(typeof(TestClass), "p");
//右辺を生成
Expression body = lmd.GetPredicate(param);
// パラメータと本体をくっつけて、実行コード生成
//return Expression.Lambda<Func<Person, bool>>(body, param).Compile(); //コンパイルしたものを返す場合はメソッド定義の"Expression"を消す必要あり
return Expression.Lambda<Func<TestClass, bool>>(body, param);
}

}

public class TestClass
{
public string Name { get; set; }
public int Id { get; set; }
public bool TrueFalse { get; set; }
}


そして下記がラムダ式の右辺(条件部分)を動的に作成するクラスです。このクラスをDLLにしとくと便利かもしれません。

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

namespace LamdaUtil
{
/// <summary>動的クエリのためのラムダ式を生成するコアクラス</summary>
public class LamdaUtility
{
/// <summary>列挙型:比較方法</summary>
public enum QueryTypeEnum
{
/// <summary>比較演算子(等しい) ==</summary>
Equal = 0,
/// <summary>比較演算子(等しくない) !=</summary>
NotEqual = 1,
/// <summary>String.Containsを使って文字列を含むか検索(大文字小文字無視)</summary>
StringContainsIgnoreCase = 6,
/// <summary>String.Containsを使って文字列を含むか検索(大文字小文字を判断)</summary>
StringContains = 7,
/// <summary>比較演算子(小なり) <</summary>
LessThan = 2,
/// <summary>比較演算子(大なり) ></summary>
GreaterThan = 3,
/// <summary>比較演算子(小なりイコール) <=</summary>
LessThanOrEqual = 4,
/// <summary>比較演算子(大なりイコール) >=</summary>
GreaterThanOrEqual = 5,

}

/// <summary>列挙型:複数条件の場合にANDかORか(設定しないとORになる。OR=0のため)</summary>
public enum AndOrTypeEnum
{
/// <summary>複数条件の場合、AND条件とする</summary>
AND = 1,
/// <summary>複数条件の場合、OR条件とする</summary>
OR = 0
}

/// <summary>インナークラス:クエリ条件を保存する</summary>
class LamdaUtilityQuery
{
/// <summary>比較方法</summary>
public LamdaUtility.QueryTypeEnum QueryType { get; set; }
/// <summary>比較対象の項目(フィールド)名</summary>
public string ItemName { get; set; }
/// <summary>比較条件</summary>
public object Keyword { get; set; }
}

/// <summary>String.Containsメソッド(文字列検索に利用)</summary>
private readonly MethodInfo Contains = typeof(string).GetMethod("Contains");

/// <summary>String.ToLowerメソッド(文字列検索に利用)</summary>
private readonly MethodInfo ToLower = typeof(string).GetMethod("ToLower", Type.EmptyTypes);


/// <summary>クエリ条件のリスト</summary>
private List<LamdaUtilityQuery> mQueryKeywod = new List<LamdaUtilityQuery>();

/// <summary>検索条件の論理演算子(設定しないとORになる)</summary>
public AndOrTypeEnum AndOr { get; set; }

/// <summary>条件を追加する</summary>
/// <param name="type">比較方法</param>
/// <param name="itemName">比較対象の項目名</param>
/// <param name="keyword">比較条件</param>
public void AddQuery(QueryTypeEnum type, string itemName, object keyword)
{
mQueryKeywod.Add(new LamdaUtilityQuery() { QueryType = type, ItemName = itemName, Keyword = keyword });
}

/// <summary>条件をすべてクリアする</summary>
public void ClearQuery()
{
mQueryKeywod.Clear();
}

/// <summary>設定されたクエリ条件からラムダ式(右辺のみ)を生成する</summary>
/// <param name="param">パラメータ(比較対象クラス)</param>
/// <returns>右辺のラムダ式</returns>
public Expression GetPredicate(ParameterExpression param)
{
//条件が設定されてない時
if (this.mQueryKeywod.Count <= 0)
{
//リスト内全てをヒットしたことにするための苦肉の策(もっといい方法はないか?)
Expression orExpr = Expression.Or(
Expression.Constant(true),
Expression.Constant(true)
);
return orExpr;
}

// 条件式木のリスト
var predList = new List<Expression>();
//検索条件クラスリストをループ
this.mQueryKeywod.ForEach(q =>
{
predList.Add(GetExpressionOperator(q.QueryType, param, q.ItemName, q.Keyword));
});

//論理演算子を設定
ExpressionType exType = ExpressionType.OrElse;
switch (this.AndOr)
{
case AndOrTypeEnum.AND:
exType = ExpressionType.AndAlso;
break;
case AndOrTypeEnum.OR:
exType = ExpressionType.OrElse;
break;
default:
break;
}

//右辺式の組み立て
var body = predList.Aggregate(
(l, r) => Expression.MakeBinary(exType, l,r));
return body;
}

/// <summary>比較方法によって条件式を設定</summary>
/// <param name="type">比較条件</param>
/// <param name="param">パラメータ</param>
/// <param name="itemName">項目名</param>
/// <param name="keyword">条件</param>
/// <returns>式</returns>
private Expression GetExpressionOperator(QueryTypeEnum type, ParameterExpression param, string itemName, object keyword)
{
Expression body = null;
//左辺は項目名
var left = Expression.PropertyOrField(param, itemName);
switch (type)
{
case QueryTypeEnum.Equal:
body = Expression.Equal(left, Expression.Constant(keyword));
break;
case QueryTypeEnum.NotEqual:
body = Expression.NotEqual(left, Expression.Constant(keyword));
break;
case QueryTypeEnum.GreaterThan:
body = Expression.GreaterThan(left, Expression.Constant(keyword));
break;
case QueryTypeEnum.GreaterThanOrEqual:
body = Expression.GreaterThanOrEqual(left, Expression.Constant(keyword));
break;
case QueryTypeEnum.LessThan:
body = Expression.LessThan(left, Expression.Constant(keyword));
break;
case QueryTypeEnum.LessThanOrEqual:
body = Expression.LessThanOrEqual(left, Expression.Constant(keyword));
break;
case QueryTypeEnum.StringContainsIgnoreCase:
var keywordValue = Expression.Constant(keyword, typeof(string));
body = Expression.Call(
Expression.Call(left, ToLower),
Contains,
Expression.Call(keywordValue, ToLower));
//左辺がNULLの場合、NULL例外が発生するので、左辺はNULLで無いフィルタを設定。
body = Expression.AndAlso(
Expression.NotEqual(left, Expression.Constant(null)),
body);
break;
case QueryTypeEnum.StringContains:
var keywordValueStringContains = Expression.Constant(keyword, typeof(string));
body = Expression.Call(
left,
Contains,
keywordValueStringContains);
//左辺がNULLの場合、NULL例外が発生するので、左辺はNULLで無いフィルタを設定。
body = Expression.AndAlso(
Expression.NotEqual(left, Expression.Constant(null)),
body);
break;
default:
break;
}
return body;
}
}

}


ほんとは、最終的な式ツリーを作成する GetPredicate() メソッドも LamdaUtility クラスに入れたかったのですが、式を生成する Expression.Lambda の引数に型をしていしないといけないんですよね。
この型(今回はTestClass)は動的に変わると思ったので、呼び出し元で書くようにしました。ここをもっと汎用化できるとうれしいのですが。。。


式ツリー理解してこれ書くのにかなり時間を費やしてしまいました。。。

参考:
more Dynamic LINQ - 当面C#と.NETな記録 一番の参考になりました。感謝です。
LINQ文で動的にWhere句を組み立てるには?[3.5、C#、VB] - @IT stringのコレクションを扱うとかいった比較的単純なコレクション処理に役立ちます。
MSDN:方法 : 式ツリーを使用して動的クエリをビルドする
LINQを活用した簡単XMLデータベース・アプリケーション - @IT
Expressionを使った動的なOR文の生成 - Programmable Life
式木 - C#にハマってみる日記 Expression の演算子が載っているので参考になります。
[PR]
by jehoshaphat | 2011-06-06 23:24 | .Net開発 | Trackback | Comments(2)
トラックバックURL : http://jehupc.exblog.jp/tb/14909775
トラックバックする(会員専用) [ヘルプ]
※このブログはトラックバック承認制を適用しています。 ブログの持ち主が承認するまでトラックバックは表示されません。
Commented by ss at 2012-05-08 21:49 x
vs2010、fw4.0 で 同じことをしようとすると エラー 145 'System.Linq.IQueryable<xxxxx.xxxx.~List>' に 'Where' の定義が含まれておらず、最も適している拡張メソッド オーバーロード 'System.Linq.Queryable.Where<TSource>(System.Linq.IQueryable<TSource>, System.Linq.Expressions.Expression<System.Func<TSource,int,bool>>)' には無効な引数がいくつか含まれています ~ とかエラーになってうまくいきません。もしよろしければ、対策を教えていただけると助かります。

ちなみにdatasourceの型はList<TestClass>とかにしないとエラーになるのでそんな感じです。

以上よろしくお願いします。
Commented by Jehoshaphat at 2012-05-12 12:27
ども。3流PGです。
一部のコードが欠落していたので、そのせいかもしれません。
再度上げ直したので、これでビルドしてみてください。



<< (.Net)特定のターミナルサ... (.Net)LINQのクエリ構... >>