🌊

【LINQ】List型の任意プロパティ(列)を複数の任意文字列検索

2021/07/07に公開

悩み

LINQはList型などのEnumableな型へのアクセスを非常に楽に書けるようになるので
結構強力なツールです。
ただ、条件式内でターゲットとするプロパティ(テーブルだと列)も固定され、
任意のプロパティをターゲットにできません。
また、検索条件もソースコードに記述した数だけしか検索できない為、
数が変化するような複数文字列の検索もできません。
これによって、SQLで言うところのFROM句やWHERE句を動的に変えることができないのです。
なんだか汎用性には欠けているように感じます。(個人の感想)

    var searchWord = "AnyString"
    var list = new List<Hoge>();
    //Whereメソッドで検索
    var query = list.Where(h => h.OneProp.Contains(searchWord));
    //↑検索はHoge.OnePropに対してしか検索しか行えない
    // また、そのプロパティはsearchWordを含むかどうかしか検索できず、  
    // 複数語句の検索はできない。
    // やるにしてもforeachなどを活用するにしかなく、冗長になってしまう。  

なんとか、プロパティ部分を動的に変化させることができないものでしょうか。
もしできたとしたらその都度処理を書く必要も無いのですが。。。

できた処理

        /// <summary>
        /// 動的要素検索ラムダ式の定義
        /// (row => row.col.Contains(word1) || row.col.Contains(word2) || ...という式や
        /// (row => row.col.Contains(word1) && row.col.Contains(word2) && ...という式を動的に作成
        /// </summary>
        /// <typeparam name="T">検索対象のエンティティクラス</typeparam>
        /// <param name="columnName">要素名、列名など</param>
        /// <param name="searchWord">検索語句</param>
        /// <param name="isOr">Or検索かどうか(FalseでAnd検索)</param>
        /// <returns>検索ラムダ</returns>
        public Expression<Func<T, bool>> DynamicContains<T>(string columnName, string[] searchWord, bool isOr)
            where T : class
        {
            try
            {
                //[1]引数の準備
                var type = typeof(T);                               //クラスの特定
                var property = type.GetProperty(columnName);        //クラスのプロパティ一覧取得
                var parameter = Expression.Parameter(type, "p");    //あるパラメータ"p"の定義
                Expression body;                                    //ラムダ式用変数定義
                var nodes = new List<Expression>();                 //各ノード処理格納用

                //[2] p.Nameの形式を作成
                var propertyAccess = Expression.MakeMemberAccess(parameter, property);

                //[3]Containsメソッドの定義
                MethodInfo Contains = typeof(string).GetMethod("Contains", new[] { typeof(string) });

                //[4]各要素のメソッド定義
                foreach (var w in searchWord)
                {
                    nodes.Add(Expression.Call(propertyAccess, Contains, Expression.Constant(w)));
                }

                //[5]各要素同士の短絡評価
                if (isOr)
                {
                    body = nodes.Aggregate((l, r) => Expression.MakeBinary(ExpressionType.OrElse, l, r));
                }
                else
                {
                    body = nodes.Aggregate((l, r) => Expression.MakeBinary(ExpressionType.AndAlso, l, r));
                }

                //[6] 戻る
                return Expression.Lambda<Func<T, bool>>(body, parameter);
            }
            catch
            {
                throw;
            }
        }

        /// <summary>
        /// こんなクラスがあったとする
        /// </summary>
        public class Idol
        {
            /// <summary>
            /// 名前
            /// </summary>
            public string Name { get; set; }
            /// <summary>
            /// 性格
            /// </summary>
            public string Personality { get; set; }
            /// <summary>
            /// 年齢?
            /// </summary>
            public int Age { get { return 17; } }
        }

        /// <summary>
        /// 呼び出し例
        /// </summary>
        public main()
        {
            //[1] 例えばこんなListがあったとします(値はテキトーに設定)
            var idol_dic = new List<Idol>(){
                new Idol() { Name = "Shiburin", Personality = "Cool and Childish(Japanese CHUNI)" }, // 1.
                new Idol() { Name = "Arin", Personality = "Cute and Gullible(Japanese Tyoroi)" },    // 2.
                new Idol() { Name = "Furin", Personality = "Cute and Childish(Japanese CHUNI)" },    // 3.
            };

            //[2] 検索を行ないます
            //[2-1] クールで中二な性格のアイドルを検索
            //      生成されるラムダ式 → p => p.Personality.Contains("Cool") && p.Personality.Contains("Childish")
            //      結果:1.が取得できる
            var CoolAndChildish = idol_dic.Where(DynamicContains<T>("Personality", new string[] {"Cool", "Childish"}, ture));
            //[2-2] キュートでちょろい性格のアイドルを検索
            //      生成されるラムダ式 → p => p.Personality.Contains("Cute") && p.Personality.Contains("Gullible")
            //      結果:3が取得できる
            var CuteAndGullible = idol_dic.Where(DynamicContains<T>("Personality", new string[] {"Cute", "Gullible"}, ture));
            //[2-3] 名前に"u"と"rin"が含まれるアイドルを検索
            //      生成されるラムダ式 → p => p.Name.Contains("u") && p.Name.Contains("rin")
            //      結果:1.と3.が取得できる
            var UAndRin = idol_dic.Where(DynamicContains<T>("Name", new string[] {"u", "rin"}, ture));

            //(以下略)
        }

改善点

任意テーブルで且つ、複数カラムに対して操作できるように実装したい。。。
あと、処理速度的にこれって遅いですよね?(未検証)

参考

Expression クラス
式木(Expression Trees)
【C#】Expression Treeでラムダ式を動的生成する
急に動的コード生成に興味がわいたので

Discussion