式ツリーを動的に生成する

 とあるシステムのプロトタイプを作成するなか、画面上のチェックボックスの状態に応じて対象データリストを絞り込む必要があった。その際に利用した動的にLinqのメソッド式を取得する方法を、備忘録として残しておこうと思う。

STEP
Linqで実現したいこと
    /// 生徒クラス
    class Student(int id, string name, string classification)
    {
        public int Id { get; } = id;
        public string Name { get; } = name;
        public string Classification { get; } = classification;
    }

例えばIDと名前、クラスを持つ生徒クラスがあると仮定して

          Student[] students =  { new Student(1, "鈴木", "A"),
                                  new Student(2, "山本", "B"),
                                  new Student(3, "田中", "C"),
                                  new Student(4, "山田", "A"),
                                  new Student(5, "大谷", "B"),
                                  new Student(6, "岡田", "B"),
                                  new Student(7, "岡田", "C")};

上記のように生徒情報が存在すると仮定する。

画面上でAクラスにチェックが入っている場合は、Aクラスの生徒のみを抽出。画面上でBクラスとCクラスにチェックが入っている場合は、BおよびCクラスの生徒を動的に抽出したい。

ついでにBクラスの岡田のみ、Cクラスの岡田のみ、BとCクラスの岡田みたいな柔軟な条件を作成したい。

STEP
フォームにチェックボックスを3つとボタンを配置

checkBox1~3を配置して、各Tagプロパティに”A”、”B”、”C”を設定する。チェックが入った場合にここからクラス名を取得するものとする。

STEP
コーディング

結果から書くと、コードは下記のようになる。

  /// フォーム
  public partial class Form1 : Form
  {
      public Form1()
      {
          InitializeComponent();
      }

   //抽出ボタン押下
      private void button1_Click(object sender, EventArgs e)
      {
          //生徒配列初期化、適当にデータを作成する
          //岡田はBとCクラスにいる
          Student[] students =  { new Student(1, "鈴木", "A"),
                                  new Student(2, "山本", "B"),
                                  new Student(3, "田中", "C"),
                                  new Student(4, "山田", "A"),
                                  new Student(5, "大谷", "B"),
                                  new Student(6, "岡田", "B"),
                                  new Student(7, "岡田", "C")};

          CheckBox[] checks = { checkBox1, checkBox2, checkBox3 };

          //チェックなしは何もしない
          if (checks.Where(x => x.Checked).Count() == 0) return;

          //[01]氏名を無指定として検索(クラスの選択のみ有効)
          var tree = GetExpressionTreeWhere(checks, "");
          List<Student> selectStudent = students.AsQueryable().Where(tree).ToList();
          Debug.WriteLine("氏名無指定 =========================================");
          foreach (var s in selectStudent.OrderBy(x=>x.Id))
          {
              Debug.WriteLine($"ID:{s.Id} 氏名:{s.Name} クラス:{s.Classification}");
          }

          //[02]氏名に岡田を指定して検索(氏名が岡田、かつクラス選択が条件になる)
      //本来なら画面から「岡田」を取得するところだけど省略
          tree = GetExpressionTreeWhere(checks, "岡田");
          selectStudent = students.AsQueryable().Where(tree).ToList();
          Debug.WriteLine("岡田指定 =========================================");
          foreach (var s in selectStudent.OrderBy(x => x.Id))
          {
              Debug.WriteLine($"ID:{s.Id} 氏名:{s.Name} クラス:{s.Classification}");
          }
      }

    //式ツリー作成関数
      private static Expression<Func<Student, bool>> GetExpressionTreeWhere(CheckBox[] checks, string name)
      {
          //[03]ラムダ式の左辺「x=>」にあたる部分
          ParameterExpression parameter = Expression.Parameter(typeof(Student), "x");
          ParameterExpression[] parameters = { parameter }; //今回のパラメータは1つ

          //氏名の指定があれば式生成
          Expression bodyName = null;
          if (name!="")
          { 
              var typeStudent = typeof(Student);
              var propName = typeStudent.GetProperty("Name");//Nameプロパティ
              var accessName = Expression.MakeMemberAccess(parameter, propName);
              bodyName = Expression.Equal(Expression.Constant(name), accessName);
          }

          //[04]クラスのラムダ式を生成
          Expression bodyClass = null;
          for (var i = 0; i < checks.Length; i++)
          {
              // クラスはOR演算子で連結
              if (checks[i].Checked)
              {
          //Tagプロパティにクラス名を設定しているので、それを使用する
                  bodyClass = GetClassExpression(checks[i].Tag.ToString(), (i == 0) ? null : bodyClass, parameter);
              }
          }

          //[05]氏名の指定があればクラス条件とANDで結合
          if (bodyName != null)
          {
              bodyClass = Expression.And(bodyName, bodyClass);
          }

          //作成したラムダ式から式ツリーを取得
          return Expression.Lambda<Func<Student, bool>>(bodyClass, parameters);
      }

   //クラス条件はチェックが入っているものを結合していく
      public static Expression GetClassExpression(string className, Expression currentBody, ParameterExpression parameter)
      {
          var typeStudent = typeof(Student);
          var propClass = typeStudent.GetProperty("Classification");//Classificationプロパティ
          var accessClass = Expression.MakeMemberAccess(parameter, propClass);
          var newBody = Expression.Equal(Expression.Constant(className), accessClass);
          if (currentBody != null)
          {
              //OR条件で結合
              return Expression.OrElse(currentBody, newBody);
          }
          return newBody;
      }
  }

BクラスとCクラスにチェックを入れた場合の結果は以下の通り。

STEP
説明

ある程度コメントに記載しているので、およそのところは理解してもらえるだろうと考えるので、念のため注意点を記載しておく。

番号説明
[01][02]GetExpressionTreeWhere関数の第二引数が、空の場合と指定している場合との違い。
[03]ラムダ式の左辺としてparameterオブジェクトを作成しているが、これは他の式を生成する際にも同じオブジェクトを使用しなければならない。そのためGetClassExpression関数に渡してその中で使用している。
[04]ループ内では選択されたクラス名条件をOR結合している。”Expression.OrElse”を使用し(AクラスorBクラス)、(AクラスorBクラスorCクラス)のように条件を生成する。
[05]氏名の指定があればクラス名条件と結合する。”Expression.And”を使用することで (氏名 and (AクラスorBクラス)) のように条件を生成している。

なお上記コードはできるだけ簡潔にするため、プロジェクトのプロパティでNull許容参照を無効化している。.NetCoreのバージョンにもよるだろうが、各所でNullチェックを入れないとビルドに失敗する場合があるので注意してもらいたい。(とりあえず「#nullable disable」の一文を入れておくのもいい)

  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

目次