.NET深入實戰系列--EF到底怎麼寫過濾條件

    本文唯一訪問地址:http://www.cnblogs.com/yubaolee/p/DynamicLinq.html

    對於系統開發來說,按不同字段進行過濾查詢是一種常見的需求。在EF中通常的做法是:

        /// <summary>
        /// 只是簡單舉例,只用了兩個過濾條件
        /// </summary>
        IEnumerable<UserInfo> Search(string username = "", string usertype = "")
        {
            var query = _context.UserInfoes.AsQueryable();
            if (!string.IsNullOrEmpty(username))
                query = query.Where(u => u.UserName == username);
            if (!string.IsNullOrEmpty(usertype))
                query = query.Where(u => u.UserType == usertype);

            return query.ToList();
        }

 

    這時如果我有一個新的需求,比如查詢用戶名中必須包含不定個數關鍵字的用戶。那我們可以用參數數組做類似下面的升級

private IEnumerable<UserInfo> Search(params string[] keys)
        {
            var query = _context.UserInfoes.AsQueryable();

            foreach (var key in keys)
            {
                query = query.Where(u => u.UserName.Contains(key));
            }

            return query.ToList();
        }

 

    上面的代碼都是能夠良好運行的,這時如果需求變成了:查詢用戶名中至少包含一個關鍵字的用戶,那我們該如何處理?很明顯要用到Or運算,但怎麼處理纔是最合理的?普通的查詢已經不能很好的解決該問題。於是Joe Albahari大神在他的一篇博文中使用PredicateBuilder輕鬆地解決了該問題:

IQueryable<UserInfo> Search(params string[] keys)
        {
            var predicate = PredicateBuilder.False<UserInfo>();

            foreach (string keyword in keys)
            {
                predicate = predicate.Or(p => p.UserName.Contains(keyword));
            }
            return _context.UserInfoes.Where(predicate);
        }

 

    至於PredicateBuilder的實現可以去他的博文中查看或者直接在nuget中查找添加LINQKit引用。PredicateBuilder很好的解決的動態生成Lambda問題,支持And/Or等主流運算。但它仍沒能解決一個問題:如果查詢條件中的屬性(即數據庫中的字段)也是不確定的,這樣該如何處理?

    這時Scott大神站出來了。在他的博客Dynamic LINQ (Part 1: Using the LINQ Dynamic Query Library)中,他把EF整成了拼接SQL的方式來實現這個需求。如下:

  private IQueryable<UserInfo> Search(string key, string value)
        {
            return _context.UserInfoes.Where("@0 ='@1'", key, value);
        }

    這樣我們就不怕無盡變更的需求,想怎麼調用都可以:

 var users = Search("UserNmae", "yubaolee");  //過濾用戶名
var users2 = Search("UserType", "administrator"); //過濾用戶類型

    你也可以使用Key-Value之類的組合成更強大的查詢函數。然,世界上的事情總不是那麼美好的。你會在實踐中用着用着發現,丫竟然不支持 like,用着用着發現,丫竟然不支持guid等等。

    唉!我去,我等向來拿來就用之流,竟然會碰到這種鳥事。還是自己動手吧!

    好吧,下面纔是博文主要內容,如果少年你沒看到下面,嘖嘖!着實有些可惜…

 

    分析一下我們的需求:

  1. 可以根據用戶提交過來的字符串還得到他想查找的屬性(或數據庫字段);
  2. 可以把傳過來的操作轉成一個比較型的lambda表達式;
  3. 得把該表達式傳到EF查詢的Where中;

    根據上面的需求,我們可以借鑑一下PredicateBuilder的實現方式,用表達式樹來生成動態lambda,然後傳到ef的過濾條件中。如下:

 public class Filter
    {
        public string Key { get; set; } //過濾的關鍵字
        public string Value { get; set; } //過濾的值
        public string Contract { get; set; }// 過濾的約束 比如:'<' '<=' '>' '>=' 'like'等
    }

    public static class DynamicLinq
    {
        /// <summary>
        /// 創建lambda中的參數,即c=>c.xxx==xx 中的c
        /// </summary>
        public static ParameterExpression CreateLambdaParam<T>(string name)
        {
            return Expression.Parameter(typeof(T), name);
        }

        /// <summary>
        /// 創建linq表達示的body部分,即c=>c.xxx==xx 中的c.xxx==xx
        /// </summary>
        public static Expression GenerateBody<T>(this ParameterExpression param, Filter filterObj)
        {
            PropertyInfo property = typeof(T).GetProperty(filterObj.Key);

            //組裝左邊
            Expression left = Expression.Property(param, property);
            //組裝右邊
            Expression right = null;

            //todo: 下面根據需要,擴展自己的類型
            if (property.PropertyType == typeof(int))
            {
                right = Expression.Constant(int.Parse(filterObj.Value));
            }
            else if (property.PropertyType == typeof(DateTime))
            {
                right = Expression.Constant(DateTime.Parse(filterObj.Value));
            }
            else if (property.PropertyType == typeof(string))
            {
                right = Expression.Constant((filterObj.Value));
            }
            else if (property.PropertyType == typeof(decimal))
            {
                right = Expression.Constant(decimal.Parse(filterObj.Value));
            }
            else if (property.PropertyType == typeof(Guid))
            {
                right = Expression.Constant(Guid.Parse(filterObj.Value));
            }
            else if (property.PropertyType == typeof(bool))
            {
                right = Expression.Constant(filterObj.Value.Equals("1"));
            }
            else
            {
                throw new Exception("暫不能解析該Key的類型");
            }

            //todo: 下面根據需要擴展自己的比較
            Expression filter = Expression.Equal(left, right);
            switch (filterObj.Contract)
            {
                case "<=":
                    filter = Expression.LessThanOrEqual(left, right);
                    break;

                case "<":
                    filter = Expression.LessThan(left, right);
                    break;

                case ">":
                    filter = Expression.GreaterThan(left, right);
                    break;

                case ">=":
                    filter = Expression.GreaterThanOrEqual(left, right);
                    break;

                case "like":
                    filter = Expression.Call(left, typeof(string).GetMethod("Contains", new[] { typeof(string) }),
                                 Expression.Constant(filterObj.Value));
                    break;
            }

            return filter;
        }

        /// <summary>
        /// 創建完整的lambda,即c=>c.xxx==xx
        /// </summary>
        public static LambdaExpression GenerateLambda(this ParameterExpression param, Expression body)
        {
            return Expression.Lambda(body, param);
        }

        /// <summary>
        /// 創建完整的lambda,爲了兼容EF中的where語句
        /// </summary>
        public static Expression<Func<T, bool>> GenerateTypeLambda<T>(this ParameterExpression param, Expression body)
        {
            return (Expression<Func<T, bool>>)(param.GenerateLambda(body));
        }

        public static Expression AndAlso(this Expression expression, Expression expressionRight)
        {
            return Expression.AndAlso(expression, expressionRight);
        }

        public static Expression Or(this Expression expression, Expression expressionRight)
        {
            return Expression.Or(expression, expressionRight);
        }

        public static Expression And(this Expression expression, Expression expressionRight)
        {
            return Expression.And(expression, expressionRight);
        }
    }

 

    來看看我們客戶端的調用:

//模擬過濾對象
            var filters = new Filter[]
            {
                new Filter {Key = "UserName", Value = "yubaolee", Contract = "like"},
                new Filter {Key = "UserType", Value = "administrator", Contract = "="}
            };

            var param = DynamicLinq.CreateLambdaParam<UserInfo>("c");
            Expression body = Expression.Constant(true); //初始默認一個true
            foreach (var filter in filters)
            {
                body = body.AndAlso(param.GenerateBody<UserInfo>(filter)); //這裏可以根據需要自由組合
            }
            var lambda = param.GenerateTypeLambda<UserInfo>(body);  //最終組成lambda
          
            var users = _context.UserInfoes.Where(lambda);  //得到最終結果
            Console.Read();

 

    這時我們可以自由組合,但客戶端代碼量看起來好像不少。我們來優化封裝一下:

public static class DynamicExtention 
    {
        public static IQueryable<T> Where<T>(this IQueryable<T> query, Filter[] filters)
        {
            var param = DynamicLinq.CreateLambdaParam<T>("c");
            Expression body = Expression.Constant(true); //初始默認一個true
            foreach (var filter in filters)
            {
                body = body.AndAlso(param.GenerateBody<T>(filter)); //這裏可以根據需要自由組合
            }
            var lambda = param.GenerateTypeLambda<T>(body); //最終組成lambda
            return query.Where(lambda);
        }
    }

 

    最後看看我們客戶端的調用:

 //模擬過濾對象
            var filters = new Filter[]
            {
                new Filter {Key = "UserName", Value = "yubaolee", Contract = "like"},
                new Filter {Key = "UserType", Value = "administrator", Contract = "="}
            };

            var users = _context.UserInfoes.Where(filters);  //得到最終結果
            Console.Read();

 

    代碼如此的乾淨整潔。而且因爲擴展的Where語句是基於泛型的,所以無論你的EF集合是哪種DbSet,都可以直接拿來使用。如果再把過濾類Filter功能深化,擴展成樹狀結構,那麼可以實現種組合查詢。哪怕是聯表查詢也不在話下。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章