3流プログラマのメモ書き

元開発職→社内SE→派遣で営業支援の三流プログラマのIT技術メモ書き。 このメモが忘れっぽい自分とググってきた技術者の役に立ってくれれば幸いです。(jehupc.exblog.jpから移転中)

(.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 dataSource = new List();

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 resList = list.ToList();

//抽出結果表示

resList.ForEach(p => Console.WriteLine(p.Name + " " + p.Id.ToString()));

}

///

条件ラムダ式を生成する

///

public static Expression> GetPredicate(LamdaUtility lmd)

{

// パラメータ生成

ParameterExpression param = Expression.Parameter(typeof(TestClass), "p");

//右辺を生成

Expression body = lmd.GetPredicate(param);

// パラメータと本体をくっつけて、実行コード生成

//return Expression.Lambda>(body, param).Compile(); //コンパイルしたものを返す場合はメソッド定義の"Expression"を消す必要あり

return Expression.Lambda>(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

{

///

動的クエリのためのラムダ式を生成するコアクラス

public class LamdaUtility

{

///

列挙型:比較方法

public enum QueryTypeEnum

{

///

比較演算子(等しい) ==

Equal = 0,

///

比較演算子(等しくない) !=

NotEqual = 1,

///

String.Containsを使って文字列を含むか検索(大文字小文字無視)

StringContainsIgnoreCase = 6,

///

String.Containsを使って文字列を含むか検索(大文字小文字を判断)

StringContains = 7,

///

比較演算子(小なり) <

LessThan = 2,

///

比較演算子(大なり) >

GreaterThan = 3,

///

比較演算子(小なりイコール) <=

LessThanOrEqual = 4,

///

比較演算子(大なりイコール) >=

GreaterThanOrEqual = 5,

}

///

列挙型:複数条件の場合にANDかORか(設定しないとORになる。OR=0のため)

public enum AndOrTypeEnum

{

///

複数条件の場合、AND条件とする

AND = 1,

///

複数条件の場合、OR条件とする

OR = 0

}

///

インナークラス:クエリ条件を保存する

class LamdaUtilityQuery

{

///

比較方法

public LamdaUtility.QueryTypeEnum QueryType { get; set; }

///

比較対象の項目(フィールド)名

public string ItemName { get; set; }

///

比較条件

public object Keyword { get; set; }

}

///

String.Containsメソッド(文字列検索に利用)

private readonly MethodInfo Contains = typeof(string).GetMethod("Contains");

///

String.ToLowerメソッド(文字列検索に利用)

private readonly MethodInfo ToLower = typeof(string).GetMethod("ToLower", Type.EmptyTypes);

///

クエリ条件のリスト

private List mQueryKeywod = new List();

///

検索条件の論理演算子(設定しないとORになる)

public AndOrTypeEnum AndOr { get; set; }

///

条件を追加する

/// 比較方法

/// 比較対象の項目名

/// 比較条件

public void AddQuery(QueryTypeEnum type, string itemName, object keyword)

{

mQueryKeywod.Add(new LamdaUtilityQuery() { QueryType = type, ItemName = itemName, Keyword = keyword });

}

///

条件をすべてクリアする

public void ClearQuery()

{

mQueryKeywod.Clear();

}

///

設定されたクエリ条件からラムダ式(右辺のみ)を生成する

/// パラメータ(比較対象クラス)

/// 右辺のラムダ式

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();

//検索条件クラスリストをループ

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;

}

///

比較方法によって条件式を設定

/// 比較条件

/// パラメータ

/// 項目名

/// 条件

///

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 の演算子が載っているので参考になります。