LINQ文で動的にWhere句を組み立てるには?[3.5、C#、VB].NET TIPS

» 2010年03月11日 05時00分 公開
[一色政彦デジタルアドバンテージ]

この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。

「.NET TIPS」のインデックス

連載目次

 .NET Framework 3.5(=Visual Studio 2008)以降では、LINQ(Language INtegrated Query)機能がC#言語やVB言語に導入されている。LINQを使うと、SQL文ライクな構文のプログラム・コードを記述することで、オブジェクト配列やXML、データベースなどに対するクエリ(=データ取得)を効率的に行える。従来のように、SQL文を文字列で記述してクエリする場合と比べて、コードがかなり短くなる。

●LINQの問題と解決方法

 しかしその手軽さの半面、欠点もある。一番大きな問題は、(簡単には)動的にクエリを組み立てられないことだ。

 例えばキーワード検索で、そのキーワードが1つなのか10個なのか事前に決まっていない場合などではLINQは使いにくい。従来の文字列のSQL文であれば、文字列を連結しながら動的にWhere句を組み立てればよかったが、LINQの場合、ソース・ファイルにLINQ文がハード・コーディングされるため、実行時に動的にそれを変更することができない。

 この問題で悩む開発者は多く、実際にさまざまな解決方法が提示されている。

 動的にLINQ文を組み立てるには、通常のクエリ構文(from/select/whereキーワードなど)のLINQでハード・コーディングするのではなく、メソッド構文(Select/Whereメソッドなど)のLINQ(=メソッド・ベースのLINQ)を利用する必要がある。例えばWhereメソッドを使うことで、その引数に動的な値(=式ツリー)を指定できるようになる。メソッド・ベースのLINQで動的に式ツリーを組み立てる解決方法を2つほど紹介しよう。

 1つ目が、こちらのブログ記事で紹介されている「DynamicLINQ」という拡張メソッドを実装したクラスを利用して、(例えば)Whereメソッドの引数を文字列として記述する方法だ。文字列なので、動的に式を組み立てられるというわけだ。

 2つ目が、「XMLを扱えるLINQ ―LINQ to XML― の基礎を学ぼう」の「○ラムダ式を動的に組み上げる方法」で紹介されている、式ツリーをそのままプログラム・コードで動的に作成する方法だ。

 1つ目のDynamicLINQを筆者が試したところ、文字列が解析されてから式ツリーへの変換処理が行われるため、(解析後にどのメソッドが呼ばれるかが簡単に予想できないので)デバッグしにくい。これだと、例えば何らかのエラーが発生したときに、そのデバッグに余計に時間が掛かってしまう可能性がある。また現時点では、DynamicLINQはサンプルという扱いであり、不足する機能が出てきたら自分で拡張しなければならない。DynamicLINQを拡張していくのは、以下で紹介する手法よりもハードルが高く、あまりお勧めできない。

 それに対し、2つ目の手法はストレートに動的LINQを実現できる。メソッド・ベースのLINQコードのラムダ式部分、例えば左辺のパラメータ、右辺の式、さらに式と式を条件AND(=論理積)演算子(&&/AndAlso)で結合した式、というような感じでコーディングすればよい。ラムダ式の内容をストレートにコーディングするので、デバッグもしやすい。もちろんさまざまな意見や好みはあるだろうが、筆者はこの2番目の手法をお勧めする。

 そこで以下では、2番目の方法を説明する。

●ラムダ式を動的に組み立てて式ツリーを取得する方法(Whereメソッドの場合)

 ここでは、プログラム・コードでWhereメソッドの引数として指定する式ツリー型オブジェクト、具体的にはExpression<TDelegate>オブジェクト(System.Linq.Expressions名前空間)を動的に組み立てる方法を説明する。

 まず、静的なラムダ式でハード・コーディングしたメソッド・ベースのLINQコードの例を示そう。

using System;
using System.Linq;

static class Program
{
  static void Main()
  {
    // クエリ対象となるデータソース
    string[] dataSource =
      { "apple", "orange", "peach", "melon", "grape" };

    // 検索キーワードを3つ取得
    Console.WriteLine("指定した文字を含む文字列を検索します。");
    ConsoleKeyInfo info = Console.ReadKey();
    string character = info.KeyChar.ToString();
    Console.WriteLine("かlかoを含むものを検索。");
    string[] keywords = { character, "l", "o" };

    // メソッド・ベースのLINQ文(静的なラムダ式を利用)
    var query = dataSource.AsQueryable().
      Where(item =>
        item.ToLower().Contains(keywords[0].ToLower()) ||
        item.ToLower().Contains(keywords[1].ToLower()) ||
        item.ToLower().Contains(keywords[2].ToLower())).
      OrderByDescending(item => item);

    // 検索された用語を出力
    foreach (string term in query)
    {
      Console.WriteLine(term);
    }
    // 出力例(「m」を入力した場合):
    // 指定した文字を含む文字列を検索します。
    // mかlかoを含むものを検索。
    // orange
    // melon
    // apple

    // 出力を確認するために実行を止める
    Console.ReadKey();
  }
}

Sub Main()

  ' クエリ対象となるデータソース
  Dim dataSource As String() = _
    {"apple", "orange", "peach", "melon", "grape"}

  ' 検索キーワードを3つ取得
  Console.WriteLine("指定した文字を含む文字列を検索します。")
  Dim info As ConsoleKeyInfo = Console.ReadKey()
  Dim character As String = info.KeyChar.ToString()
  Console.WriteLine("かlかoを含むものを検索。")
  Dim keywords As String() = {character, "l", "o"}

  ' メソッド・ベースのLINQ文(静的なラムダ式を利用)
  Dim query = dataSource.AsQueryable(). _
    Where(Function(item) _
      item.ToLower().Contains(keywords(0).ToLower()) OrElse _
      item.ToLower().Contains(keywords(1).ToLower()) OrElse _
      item.ToLower().Contains(keywords(2).ToLower())). _
    OrderByDescending(Function(item) item)

  ' 検索された用語を出力
  For Each term As String In query
    Console.WriteLine(term)
  Next

  ' 出力例(「m」を入力した場合):
  ' 指定した文字を含む文字列を検索します。
  ' mかlかoを含むものを検索。
  ' orange
  ' melon
  ' apple

  ' 出力を確認するために実行を止める
  Console.ReadKey()

End Sub

メソッド・ベースのLINQ文(静的なラムダ式を利用)(上:C#、下:VB)

 上記のコードにはコメントを多く入れたので、コード内容の説明は割愛する。

 太字になっている「メソッド・ベースのLINQ文(静的なラムダ式を利用)」というコメントの下にあるWhereメソッドとOrderByDescendingメソッドの引数はラムダ式になっている(参考:「C#ラムダ式 基礎文法最速マスター」)。Whereメソッドの引数を見ると、3つのキーワードが条件OR(=論理和)演算子(||/OrElse)で連結されている。ハード・コーディングされているため、常に3つのキーワードのどれかが含まれるかという検索しか行えない。

 このLINQコードを、何個のキーワードでも自由に検索できるようにしてみよう。今回はWhereメソッドの引数を次のように書き換える。

……省略……
// メソッド・ベースのLINQ文(動的な式ツリー型を利用)
var query = dataSource.AsQueryable().
  Where(GetExpressionTreeWhere(keywords)).
  OrderByDescending(item => item);
……省略……

……省略……
' メソッド・ベースのLINQ文(動的な式ツリー型を利用)
Dim query = dataSource.AsQueryable(). _
  Where(GetExpressionTreeWhere(keywords)). _
  OrderByDescending(Function(item) item)
……省略……

メソッド・ベースのLINQ文(動的な式ツリー型を利用)(上:C#、下:VB)

 GetExpressionTreeWhereメソッドは独自に作成したメソッドで、引数として文字列配列(キーワード群)を受け取り、戻り値として式ツリー型オブジェクト(=Expression<TDelegate>オブジェクト)を返す。具体的なコードは次のとおり。

private static Expression<Func<string, bool>> GetExpressionTreeWhere(string[] keywords)
{
  // 引数が適切でない場合には、例外を発行する
  int length = keywords.Length;
  if (length == 0)
  {
    throw new ArgumentException("キーワード指定なし。");
  }

  // ラムダ式における左辺のパラメータの名前
  // (「item => ……」の部分に対応)
  const string paramName = "item";

  // ラムダ式の左辺を構成するパラメータ項目の1つを作成
  ParameterExpression parameter =
    LambdaUtil.GetStringParameterExpression(paramName);

  // ラムダ式の左辺であるパラメータ(全項目)を配列にまとめる
  // (※ただし今回はパラメータ項目は1つしかない)
  ParameterExpression[] parameters =
    LambdaUtil.GetParameterExpressions(parameter);

  // ラムダ式の右辺である式を組み立てる
  Expression body = null;
  for (int i = 0; i < length; i++)
  {
    // 「item.ToLower().Contains(keywords[0].ToLower())」
    // という式を作成して、2つ目以降のキーワードは
    // 条件OR演算子(||)で連結する
    body = LambdaUtil.GetContainsExpression(
      parameter, keywords[i], (i == 0) ? null : body);
  }

  // 最後に、動的に作成したラムダ式から式ツリーを取得する
  return LambdaUtil.GetLambdaExpressionWhere(parameters, body);
}

Private Function GetExpressionTreeWhere(ByVal keywords As String()) As Expression(Of Func(Of String, Boolean))

  ' 引数が適切でない場合には、例外を発行する
  Dim length As Integer = keywords.Length
  If length = 0 Then
    Throw New ArgumentException("キーワード指定なし。")
  End If

  ' ラムダ式における左辺のパラメータの名前
  ' (「Function(item) ……」の部分に対応)
  Const paramName As String = "item"

  ' ラムダ式の左辺を構成するパラメータ項目の1つを作成
  Dim parameter As ParameterExpression = _
    LambdaUtil.GetStringParameterExpression(paramName)

  ' ラムダ式の左辺であるパラメータ(全項目)を配列にまとめる
  ' (※ただし今回はパラメータ項目は1つしかない)
  Dim parameters As ParameterExpression() = _
    LambdaUtil.GetParameterExpressions(parameter)

  ' ラムダ式の右辺である式を組み立てる
  Dim body As Expression = Nothing

  For i As Integer = 0 To length - 1
    ' 「item.ToLower().Contains(keywords[0].ToLower())」
    ' という式を作成して、2つ目以降のキーワードは
    ' 条件OR演算子(OrElse)で連結する
    body = LambdaUtil.GetContainsExpression( _
      parameter, keywords(i), IIf(i = 0, Nothing, body))
  Next


  ' 最後に、動的に作成したラムダ式から式ツリーを取得する
  Return LambdaUtil.GetLambdaExpressionWhere(parameters, body)

End Function

Whereメソッドの引数に指定する式ツリーを動的に作成するメソッド(上:C#、下:VB)
LambdaUtil.GetParameterExpressionsメソッドは、「,」で区切って、可変数の引数を受け取る(上記の例では1つの引数を受け取っている)。

 こちらも、コード内容についてはコメントを読んでいただきたい。

 このメソッドでは、Whereメソッドのラムダ式の部分を動的に作成して、最後にそれを式ツリー型オブジェクトとして取得している。

 上記のコードで使われているLambdaUtilクラスは、筆者が独自に作成したクラスである。具体的な内容は次のようになっている。

using System;
using System.Linq.Expressions;
using System.Reflection;

public static class LambdaUtil
{
  /// <summary>
  /// String.ToLowerメソッド。
  /// </summary>
  private static readonly MethodInfo ToLower =
    typeof(string).GetMethod("ToLower", Type.EmptyTypes);
 
  /// <summary>
  /// String.Containsメソッド。
  /// </summary>
  private static readonly MethodInfo Contains =
    typeof(string).GetMethod("Contains");

  /// <summary>
  /// ラムダ式におけるパラメータを作成する。
  /// </summary>
  /// <param name="paramName">パラメータ名</param>
  /// <returns>パラメータ式</returns>
  public static ParameterExpression GetStringParameterExpression(string paramName)
  {
    return Expression.Parameter(typeof(string), paramName);
  }

  /// <summary>
  /// ラムダ式におけるパラメータ式の配列を作成する。
  /// </summary>
  /// <param name="parameters">複数のパラメータ式</param>
  /// <returns>パラメータ式の配列</returns>
  public static ParameterExpression[] GetParameterExpressions(params ParameterExpression[] parameters)
  {
    return parameters;
  }

  /// <summary>
  /// 「x.Contains("keyword")」を条件OR演算子(||)で連結しながら式を作成する。
  /// </summary>
  /// <param name="parameter">ラムダ式の左にあるパラメータ</param>
  /// <param name="keyword">検索キーワード</param>
  /// <param name="curBody">現在の「x.Contains("keyword")」の表現文。初回はnullを指定。2回目以降は前回の戻り値を指定。</param>
  /// <returns>「x.Contains("keyword")」を||演算子で連結した式</returns>
  public static Expression GetContainsExpression(Expression parameter, string keyword, Expression curBody)
  {
    var keywordValue = Expression.Constant(keyword, typeof(string));
    var newBody = Expression.Call(
      Expression.Call(parameter, ToLower),
      Contains,
      Expression.Call(keywordValue, ToLower));
    if (curBody != null)
    {
      return Expression.OrElse(curBody, newBody);
    }
    return newBody;
  }
 
  /// <summary>
  /// 動的に作成したラムダ式から式ツリー型オブジェクトを取得する。
  /// </summary>
  /// <param name="parameters">パラメータ式の配列(=ラムダ式の左辺)</param>
  /// <param name="body">式/文(=ラムダ式の右辺)</param>
  /// <returns>式ツリー型オブジェクト</returns>
  public static Expression<Func<string, bool>> GetLambdaExpressionWhere(ParameterExpression[] parameters, Expression body)
  {
    return Expression.Lambda<Func<string, bool>>(body, parameters);
  }
}

Imports System.Linq.Expressions
Imports System.Reflection

Public Class LambdaUtil

  ''' <summary>
  ''' String.ToLowerメソッド。
  ''' </summary>
  Private Shared ReadOnly ToLower As MethodInfo = _
    GetType(String).GetMethod("ToLower", Type.EmptyTypes)

  ''' <summary>
  ''' String.Containsメソッド。
  ''' </summary>
  Private Shared ReadOnly Contains As MethodInfo = _
    GetType(String).GetMethod("Contains")

  ''' <summary>
  ''' ラムダ式におけるパラメータを作成する。
  ''' </summary>
  ''' <param name="paramName">パラメータ名</param>
  ''' <returns>パラメータ式</returns>
  Public Shared Function GetStringParameterExpression(ByVal paramName As String) As ParameterExpression

    Return Expression.Parameter(GetType(String), paramName)

  End Function

  ''' <summary>
  ''' ラムダ式におけるパラメータ式の配列を作成する。
  ''' </summary>
  ''' <param name="parameters">複数のパラメータ式</param>
  ''' <returns>パラメータ式の配列</returns>
  Public Shared Function GetParameterExpressions(ByVal ParamArray parameters As ParameterExpression()) As ParameterExpression()

    Return parameters

  End Function

  ''' <summary>
  ''' 「x.Contains("keyword")」を条件OR演算子(OrElse)で連結しながら式を作成する。
  ''' </summary>
  ''' <param name="parameter">ラムダ式の左にあるパラメータ</param>
  ''' <param name="keyword">検索キーワード</param>
  ''' <param name="curBody">現在の「x.Contains("keyword")」の表現文。初回はnullを指定。2回目以降は前回の戻り値を指定。</param>
  ''' <returns>「x.Contains("keyword")」を||演算子で連結した式</returns>
  Public Shared Function GetContainsExpression(ByVal parameter As Expression, ByVal keyword As String, ByVal curBody As Expression) As Expression

    Dim keywordValue = Expression.Constant(keyword, GetType(String))
    Dim newBody = Expression.Call( _
      Expression.Call(parameter, ToLower), _
      Contains, _
      Expression.Call(keywordValue, ToLower))

    If Not curBody Is Nothing Then
      Return Expression.OrElse(curBody, newBody)
    End If

    Return newBody

  End Function

  ''' <summary>
  ''' 動的に作成したラムダ式から式ツリー型オブジェクトを取得する。
  ''' </summary>
  ''' <param name="parameters">パラメータ式の配列(=ラムダ式の左辺)</param>
  ''' <param name="body">式/文(=ラムダ式の右辺)</param>
  ''' <returns>式ツリー型オブジェクト</returns>
  Public Shared Function GetLambdaExpressionWhere(ByVal parameters As ParameterExpression(), ByVal body As Expression) As Expression(Of Func(Of String, Boolean))

    Return Expression.Lambda(Of Func(Of String, Boolean))(body, parameters)

  End Function

End Class

式ツリーを動的に作成するヘルパーであるLambdaUtilクラス(上:C#、下:VB)

 コードの内容はコメントを参考にしてほしい。

 LambdaUtilクラス内で使われているExpressionクラス(System.Linq.Expressions名前空間)の各メソッドの目的を簡単に示す。動的に式ツリーを組み立てるには、Expressionクラスのメソッドを活用する(今回使っているのは、ほんの一部である)。

 以上で完成だ。

 テストしやすいように、Visual Studio 2008プロジェクト全体を、下記のリンクからダウンロードできるようにした。

 それでは、サンプル・プログラムの「keywords」変数(文字列配列)の部分に指定しているキーワードを1つにしたり、2つにしたりしてみてほしい。LINQコードを書き換えなくてもクエリが実行できることが確認できる。

利用可能バージョン:.NET Framework 3.5以降
カテゴリ:クラス・ライブラリ 処理対象:LINQ
使用ライブラリ:Expressionクラス(System.Linq.Expressions名前空間)
使用ライブラリ:Expression<TDelegate>オブジェクト(System.Linq.Expressions名前空間)

「.NET TIPS」のインデックス

.NET TIPS

Copyright© Digital Advantage Corp. All Rights Reserved.

スポンサーからのお知らせPR

注目のテーマ

4AI by @IT - AIを作り、動かし、守り、生かす
Microsoft & Windows最前線2025
AI for エンジニアリング
ローコード/ノーコード セントラル by @IT - ITエンジニアがビジネスの中心で活躍する組織へ
Cloud Native Central by @IT - スケーラブルな能力を組織に
システム開発ノウハウ 【発注ナビ】PR
あなたにおすすめの記事PR

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。