分享動態拼接Expression表達式組件及原理


前言

LINQ大家都知道,用起來也還不錯,但有一個問題,當你用Linq進行搜索的時候,你是這樣寫的

  1. var query = from user in db.Set<User>()
  2.                         where user.Username == "xxxx"
  3.                         select user;

OK,看起來很好,不過····如果你要進行動態搜索的話··呵呵!其實方法還是挺多,只不過繞大彎

動態搜索是什么?順便介紹下,假如你做了一個表格頁面,有用戶名、注冊時間、等級三列,你希望實現動態組合搜索,只有當用戶指定了要以用戶名搜索的時候才把用戶名這一列加入搜索條件,傳統的sql是這么干的

  1. string sql="select * from user ";
  2. if(userName!="")
  3. {
  4.     sql+=" Where Username="+userName
  5. }

聲明:只做示例,能不能運行不重要!(上面的代碼一般情況下必須是有問題的

那你用linq能不能這么干呢?呃····向下面這樣

  1. var query = from user in db.Set<User>()
  2.                         select user;
  3.             var userName = string.Empty;
  4.             if(!string.IsNullOrWhiteSpace(userName))
  5.             {
  6.                 query = query.Where(x => x.Username == userName);
  7.             }

對,是可以這樣,但在實際項目中一般是不會直接返回IQueryable接口的,也就沒辦法這么做。

所以,動態拼接linq就應運而生了,而linq的實質是表達式樹,大家可以看IQueryable的Where擴展方法的簽名,是以Expression開頭的。

組件介紹及使用

封裝的最初目的,也是給自己用。我是懶人,所以我會把組件搞得越簡單越好,下面是一個完整的使用示例

  1. TestDataContext db = new TestDataContext();
  2.            var builder = new ExpressionBuilder<User>();//實例化組件,User是什么下面說
  3.            var filters = new List<SqlFilter>();
  4.            filters.Add(SqlFilter.Create("Id", Operation.Equal, 1)); //添加User的Id屬性值等於1的搜索條件
  5.            filters.Add(SqlFilter.Create("LastLoginDate", Operation.GreaterThan, DateTime.Now));//添加User的LastLoginDate屬性值大於現在的搜索條件
  6.            filters.Add(SqlFilter.Create("Username", Operation.Like, "aaaa"));//添加User的Username屬性值like "aaaaa"的搜索條件
  7.            filters.Add(SqlFilter.Create("Id", Operation.In, new int[] { 1, 2, 3 }));//添加User的Id屬性值在1、2、3之中的搜索條件,當Operation為In的時候,最后一個參數必須為集合
  8.            filters.Add(SqlFilter.Create("Password", Operation.NotEqual, "1"));
  9.            filters.Add(SqlFilter.Create("Status", Operation.In, new int[] { 1 }));
  10.            var where = builder.Build(filters, new Dictionary<string, string>());//根據上面的條件,拼接出表達式樹
  11.            var results = db.Set<User>().Where(where).ToList();//地球人都知道

上面的代碼拼接出來表達式樹是這樣的

  1. var ids=new int[]{1,2,3};
  2.             var status=new int[]{1};
  3.             db.Set<User>().Where(x => x.Id == 1 && x.LastLoginDate > DateTime.Now && ids.Contains(x.Id) && x.Username.Contains("aaaa") && x.Password != "1" && status.Contains(x.Status));

 

注釋已經說得比較清楚了,但第2行和第10行需要特別解釋一下,第10行的Build方法原型如下

  1. public Expression<Func<TParameter, bool>> Build(IList<SqlFilter> filters, Dictionary<string, string> filterNameMap)

返回值是一個Expression<Func<TParameter,bool>>類型的,其中有一個泛型參數,這個參數就是第2行實例化的時候傳的User,在使用的時候,必須保證添加的每一個搜索條件的屬性在User類里面有。

為什么要這樣設計呢?這個感覺一下子說不清楚,等下講原理的時候說

另外還可以看到有個filterNameMap的參數,這個主要是用來進行屬性名的轉換的,一般用於外鍵。簡單說一下我當時的設計意圖。

例如,有一個列表,展示的是權限系統中的角色信息,有角色名、描述、擁有的功能三列,其中第三列內容是來自功能表中的,其他是來自角色表的。

一般情況下可能會將第三列的屬性名設置為Privileges,然后類型是string型,但如果說用戶要按角色擁有的功能進行搜索,你不可能按字符串過濾吧?一般是按照功能Id也就是PrivilegeId過濾。

因為我用的是ExtJs(沒用過這個的直接跳過這一段吧),所以問題來了,extjs傳給我的參數名是Privileges,值是一個集合,因為我的Model類屬性名是這個,但我后台用於過濾的真正屬性是PrivilegeId,所以我需要將Privileges映射到PrivilegeId,告訴ExpressionBuilder,如果遇到了Privileges屬性名的搜索條件,就將屬性名換成PrivilegeId進行拼接

為了方便理解,下面是SqlFilter的源碼,很簡單

  1. public class SqlFilter
  2.     {
  3.         public static SqlFilter Create(string propertyName, Operation operation, object value)
  4.         {
  5.             return new SqlFilter()
  6.             {
  7.                 Name = propertyName,
  8.                 Operation = operation,
  9.                 Value = value
  10.             };
  11.         }
  12.  
  13.         /// <summary>
  14.         /// 字段名
  15.         /// </summary>
  16.         public string Name { get; set; }
  17.  
  18.         /// <summary>
  19.         /// 搜索操作,大於小於等於
  20.         /// </summary>
  21.         public Xinchen.DbUtils.Operation Operation { get; set; }
  22.  
  23.         /// <summary>
  24.         /// 搜索參數值
  25.         /// </summary>
  26.         public object Value { get; set; }
  27.     }

下面是Operation的

  1. public enum Operation
  2.     {
  3.         GreaterThan,
  4.         LessThan,
  5.         GreaterThanOrEqual,
  6.         LessThanOrEqual,
  7.         NotEqual,
  8.         Equal,
  9.         Like,
  10.         In
  11.     }

組件原理

還真沒把握能說清楚···

其實就是拼接表達式樹的原理

  1. //假如我們要拼接x=>x.Id==1,假如x的類型為User
  2.             var parameterExp = Expression.Parameter(typeof(User), "x");
  3.             //結果是這樣:x=>,x是變量名
  4.             var propertyExp = Expression.Property(parameterExp, "Id");
  5.             //結果是這樣:x=>x.Id,這句是為了構建訪問屬性的表達式
  6.             //上面這句第一個參數是你要取屬性的對象表達式。我們要拼的表達式是x=>x.Id==1,==1這塊先不管,其實就是x=>x.Id,那么其實我們就是對x進行取屬性值,而x是parameterExp,所以第一個參數是parameterExp,第二個參數好說,就是屬性名
  7.             var constExp = Expression.Constant(1);
  8.             //結果是··沒有結果,構建一個常量表達式,值為1(LINQ的世界,一切皆表達式樹)
  9.             //馬上就是關鍵的一步了
  10.             var body = Expression.Equal(propertyExp, constExp);
  11.             //結果是:x=>x.Id==1,這個··還需要解釋么,很簡單,不是么。創建一個相等的表達式,然后傳入左邊和右邊的表達式
  12.             //當然到這兒還不能用,還需要繼續
  13.             var lambda = Expression.Lambda<Func<User, bool>>(body, parameterExp);
  14.             //這句和第二句是我學的時候最難理解的兩個地方。這句是將我們的成果封裝成能用的,第一個參數就是我們的成果,第二個參數是實現這個成果所需要的參數,那當然是parameterExp,然后泛型參數Func<User,bool>就是我們想把這個表達式封裝成什么樣的東西,此時,lambda的類型就是Expression<Fun<User,bool>>,這個時候就能用了

這是一個相當簡單的表達式樹,注釋寫得很清楚,下面來個復雜的

  1. //假如我們要拼接x=>x.Username.Contains("aaa"),假如x的類型為User
  2.             var parameterExp = Expression.Parameter(typeof(User), "x");
  3.             var propertyExp = Expression.Property(parameterExp, "Username");
  4.             //上面兩句不再介紹
  5.             var containsMethod = typeof(string).GetMethod("Contains", new Type[] { typeof(string) });
  6.             //因為我們要拼接的表達式中調用了string類型的Username的Contains方法,所以反射獲取string類型的Contains方法
  7.             var constExp = Expression.Constant("aaa");
  8.             //不再解釋
  9.             var containsExp = Expression.Call(propertyExp, containsMethod, constExp);
  10.             //結果是:x=>x.Username.Contains("aaa"),第一個參數,是要調用哪個實例的方法,這里是propertyExp,第二個是調用哪個方法,第三個是參數,理解了上一個示例,這個應該不難理解
  11.             var lambda = Expression.Lambda<Func<User, bool>>(containsExp, parameterExp);
  12.             //不再解釋

可以看到,第一句都是取了User的類型,所以我在設計ExpressionBuilder的使用了泛型,以供傳入這個參數


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM