前言
Linq 是 C# 中一個非常好用的集合處理庫,用好了能幫我們簡化大量又臭又長的嵌套循環,使處理邏輯清晰可見。EF 查詢主要也是依賴 Linq。但是 Linq 相對 sql 也存在一些缺點,最主要的就是動態構造查詢的難度。sql 只需要簡單進行字符串拼接,操作難度很低(當然出錯也相當容易),而 Linq 表達式由於對強類型表達式樹的依賴,動態構造查詢表達式基本相當於手寫 AST(抽象語法樹),可以說難度暴增。
AST 已經進入編譯原理的領域,對計算機系統的了解程度需求比一般 crud 寫業務代碼高了幾個量級,也導致很多人覺得 EF 不好用,為了寫個動態查詢要學編譯原理這個代價還是挺高的。后來也有一些類似 DynamicLinq 的類庫能用表達式字符串寫動態查詢。
本着學習精神,研究了一段時間,寫了一個在我的想象力范圍內,可以動態構造任意復雜的 Where 表達式的輔助類。這個輔助類的過濾條件使用了 JqGrid 的高級查詢的數據結構,這是我第一個知道能生成復雜嵌套查詢,並且查詢數據使用 json 方便解析的 js 表格插件。可以無縫根據 JqGrid 的高級查詢生成 Where 表達式。
正文
實現
JqGrid 高級查詢數據結構定義,用來反序列化:
1 public class JqGridParameter 2 { 3 /// <summary> 4 /// 是否搜索,本來應該是bool,true 5 /// </summary> 6 public string _search { get; set; } 7 /// <summary> 8 /// 請求發送次數,方便服務器處理重復請求 9 /// </summary> 10 public long Nd { get; set; } 11 /// <summary> 12 /// 當頁數據條數 13 /// </summary> 14 public int Rows { get; set; } 15 /// <summary> 16 /// 頁碼 17 /// </summary> 18 public int Page { get; set; } 19 /// <summary> 20 /// 排序列,多列排序時為排序列名+空格+排序方式,多個列之間用逗號隔開。例:id asc,name desc 21 /// </summary> 22 public string Sidx { get; set; } 23 /// <summary> 24 /// 分離后的排序列 25 /// </summary> 26 public string[][] SIdx => Sidx.Split(", ").Select(s => s.Split(" ")).ToArray(); 27 /// <summary> 28 /// 排序方式:asc、desc 29 /// </summary> 30 public string Sord { get; set; } 31 /// <summary> 32 /// 高級搜索條件json 33 /// </summary> 34 public string Filters { get; set; } 35 36 /// <summary> 37 /// 序列化的高級搜索對象 38 /// </summary> 39 public JqGridSearchRuleGroup FilterObject => Filters.IsNullOrWhiteSpace() 40 ? new JqGridSearchRuleGroup { Rules = new[] { new JqGridSearchRule { Op = SearchOper, Data = SearchString, Field = SearchField } } } 41 : JsonSerializer.Deserialize<JqGridSearchRuleGroup>(Filters ?? string.Empty); 42 43 /// <summary> 44 /// 簡單搜索字段 45 /// </summary> 46 public string SearchField { get; set; } 47 /// <summary> 48 /// 簡單搜索關鍵字 49 /// </summary> 50 public string SearchString { get; set; } 51 /// <summary> 52 /// 簡單搜索操作 53 /// </summary> 54 public string SearchOper { get; set; } 55 56 } 57 58 /// <summary> 59 /// 高級搜索條件組 60 /// </summary> 61 public class JqGridSearchRuleGroup 62 { 63 /// <summary> 64 /// 條件組合方式:and、or 65 /// </summary> 66 public string GroupOp { get; set; } 67 /// <summary> 68 /// 搜索條件集合 69 /// </summary> 70 public JqGridSearchRule[] Rules { get; set; } 71 /// <summary> 72 /// 搜索條件組集合 73 /// </summary> 74 public JqGridSearchRuleGroup[] Groups { get; set; } 75 } 76 77 /// <summary> 78 /// 高級搜索條件 79 /// </summary> 80 public class JqGridSearchRule 81 { 82 /// <summary> 83 /// 搜索字段 84 /// </summary> 85 public string Field { get; set; } 86 /// <summary> 87 /// 搜索字段的大駝峰命名 88 /// </summary> 89 public string PascalField => Field?.Length > 0 ? Field.Substring(0, 1).ToUpper() + Field.Substring(1) : Field; 90 /// <summary> 91 /// 搜索操作 92 /// </summary> 93 public string Op { get; set; } 94 /// <summary> 95 /// 搜索關鍵字 96 /// </summary> 97 public string Data { get; set; } 98 }
Where 條件生成器,代碼有點多,有點復雜。不過注釋也很多,稍微耐心點應該不難看懂:
1 /// <summary> 2 /// JqGrid搜索表達式擴展 3 /// </summary> 4 public static class JqGridSearchExtensions 5 { 6 //前端的(不)屬於條件搜索需要傳遞一個json數組的字符串作為參數 7 //為了避免在搜索字符串的時候分隔符是搜索內容的一部分導致搜索關鍵字出錯 8 //無論定義什么分隔符都不能完全避免這種尷尬的情況,所以使用標准的json以絕后患 9 /// <summary> 10 /// 根據搜索條件構造where表達式,支持JqGrid高級搜索 11 /// </summary> 12 /// <typeparam name="T">搜索的對象類型</typeparam> 13 /// <param name="ruleGroup">JqGrid搜索條件組</param> 14 /// <param name="propertyMap">屬性映射,把搜索規則的名稱映射到屬性名稱,如果屬性是復雜類型,使用點號可以繼續訪問內部屬性</param> 15 /// <returns>where表達式</returns> 16 public static Expression<Func<T, bool>> BuildWhere<T>(JqGridSearchRuleGroup ruleGroup, IDictionary<string, string> propertyMap) 17 { 18 ParameterExpression parameter = Expression.Parameter(typeof(T), "searchObject"); 19 20 return Expression.Lambda<Func<T, bool>>(BuildGroupExpression<T>(ruleGroup, parameter, propertyMap), parameter); 21 } 22 23 /// <summary> 24 /// 構造搜索條件組的表達式(一個組中可能包含若干子條件組) 25 /// </summary> 26 /// <typeparam name="T">搜索的對象類型</typeparam> 27 /// <param name="group">條件組</param> 28 /// <param name="parameter">參數表達式</param> 29 /// <param name="propertyMap">屬性映射</param> 30 /// <returns>返回bool的條件組的表達式</returns> 31 private static Expression BuildGroupExpression<T>(JqGridSearchRuleGroup group, ParameterExpression parameter, IDictionary<string, string> propertyMap) 32 { 33 List<Expression> expressions = new List<Expression>(); 34 foreach (var rule in group.Rules ?? new JqGridSearchRule[0]) 35 { 36 expressions.Add(BuildRuleExpression<T>(rule, parameter, propertyMap)); 37 } 38 39 foreach (var subGroup in group.Groups ?? new JqGridSearchRuleGroup[0]) 40 { 41 expressions.Add(BuildGroupExpression<T>(subGroup, parameter, propertyMap)); 42 } 43 44 if (expressions.Count == 0) 45 { 46 throw new InvalidOperationException("構造where子句異常,生成了0個比較條件表達式。"); 47 } 48 49 if (expressions.Count == 1) 50 { 51 return expressions[0]; 52 } 53 54 var expression = expressions[0]; 55 switch (group.GroupOp) 56 { 57 case "AND": 58 foreach (var exp in expressions.Skip(1)) 59 { 60 expression = Expression.AndAlso(expression, exp); 61 } 62 break; 63 case "OR": 64 foreach (var exp in expressions.Skip(1)) 65 { 66 expression = Expression.OrElse(expression, exp); 67 } 68 break; 69 default: 70 throw new InvalidOperationException($"不支持創建{group.GroupOp}類型的邏輯運算表達式"); 71 } 72 73 return expression; 74 } 75 76 private static readonly string[] SpecialRuleOps = {"in", "ni", "nu", "nn"}; 77 78 /// <summary> 79 /// 構造條件表達式 80 /// </summary> 81 /// <typeparam name="T">搜索的對象類型</typeparam> 82 /// <param name="rule">條件</param> 83 /// <param name="parameter">參數</param> 84 /// <param name="propertyMap">屬性映射</param> 85 /// <returns>返回bool的條件表達式</returns> 86 private static Expression BuildRuleExpression<T>(JqGridSearchRule rule, ParameterExpression parameter, 87 IDictionary<string, string> propertyMap) 88 { 89 Expression l; 90 91 string[] names = null; 92 //如果實體屬性名稱和前端名稱不一致,或者屬性是一個自定義類型,需要繼續訪問其內部屬性,使用點號分隔 93 if (propertyMap?.ContainsKey(rule.Field) == true) 94 { 95 names = propertyMap[rule.Field].Split('.', StringSplitOptions.RemoveEmptyEntries); 96 l = Expression.Property(parameter, names[0]); 97 foreach (var name in names.Skip(1)) 98 { 99 l = Expression.Property(l, name); 100 } 101 } 102 else 103 { 104 l = Expression.Property(parameter, rule.PascalField); 105 } 106 107 Expression r = null; //值表達式 108 Expression e; //返回bool的各種比較表達式 109 110 //屬於和不屬於比較是多值比較,需要調用Contains方法,而不是調用比較操作符 111 //為空和不為空的右值為常量null,不需要構造 112 var specialRuleOps = SpecialRuleOps; 113 114 var isNullable = false; 115 var pt = typeof(T); 116 if(names != null) 117 { 118 foreach(var name in names) 119 { 120 pt = pt.GetProperty(name).PropertyType; 121 } 122 } 123 else 124 { 125 pt = pt.GetProperty(rule.PascalField).PropertyType; 126 } 127 128 //如果屬性類型是可空值類型,取出內部類型 129 if (pt.IsDerivedFrom(typeof(Nullable<>))) 130 { 131 isNullable = true; 132 pt = pt.GenericTypeArguments[0]; 133 } 134 135 //根據屬性類型創建要比較的常量值表達式(也就是r) 136 if (!specialRuleOps.Contains(rule.Op)) 137 { 138 switch (pt) 139 { 140 case Type ct when ct == typeof(bool): 141 r = BuildConstantExpression(rule, bool.Parse); 142 break; 143 144 #region 文字 145 146 case Type ct when ct == typeof(char): 147 r = BuildConstantExpression(rule, str => str[0]); 148 break; 149 case Type ct when ct == typeof(string): 150 r = BuildConstantExpression(rule, str => str); 151 break; 152 153 #endregion 154 155 #region 有符號整數 156 157 case Type ct when ct == typeof(sbyte): 158 r = BuildConstantExpression(rule, sbyte.Parse); 159 break; 160 case Type ct when ct == typeof(short): 161 r = BuildConstantExpression(rule, short.Parse); 162 break; 163 case Type ct when ct == typeof(int): 164 r = BuildConstantExpression(rule, int.Parse); 165 break; 166 case Type ct when ct == typeof(long): 167 r = BuildConstantExpression(rule, long.Parse); 168 break; 169 170 #endregion 171 172 #region 無符號整數 173 174 case Type ct when ct == typeof(byte): 175 r = BuildConstantExpression(rule, byte.Parse); 176 break; 177 case Type ct when ct == typeof(ushort): 178 r = BuildConstantExpression(rule, ushort.Parse); 179 break; 180 case Type ct when ct == typeof(uint): 181 r = BuildConstantExpression(rule, uint.Parse); 182 break; 183 case Type ct when ct == typeof(ulong): 184 r = BuildConstantExpression(rule, ulong.Parse); 185 break; 186 187 #endregion 188 189 #region 小數 190 191 case Type ct when ct == typeof(float): 192 r = BuildConstantExpression(rule, float.Parse); 193 break; 194 case Type ct when ct == typeof(double): 195 r = BuildConstantExpression(rule, double.Parse); 196 break; 197 case Type ct when ct == typeof(decimal): 198 r = BuildConstantExpression(rule, decimal.Parse); 199 break; 200 201 #endregion 202 203 #region 其它常用類型 204 205 case Type ct when ct == typeof(DateTime): 206 r = BuildConstantExpression(rule, DateTime.Parse); 207 break; 208 case Type ct when ct == typeof(DateTimeOffset): 209 r = BuildConstantExpression(rule, DateTimeOffset.Parse); 210 break; 211 case Type ct when ct == typeof(Guid): 212 r = BuildConstantExpression(rule, Guid.Parse); 213 break; 214 case Type ct when ct.IsEnum: 215 r = Expression.Constant(rule.Data.ToEnumObject(ct)); 216 break; 217 218 #endregion 219 220 default: 221 throw new InvalidOperationException($"不支持創建{pt.FullName}類型的數據表達式"); 222 } 223 } 224 225 if (r != null && pt.IsValueType && isNullable) 226 { 227 var gt = typeof(Nullable<>).MakeGenericType(pt); 228 r = Expression.Convert(r, gt); 229 } 230 231 switch (rule.Op) 232 { 233 case "eq": //等於 234 e = Expression.Equal(l, r); 235 break; 236 case "ne": //不等於 237 e = Expression.NotEqual(l, r); 238 break; 239 case "lt": //小於 240 e = Expression.LessThan(l, r); 241 break; 242 case "le": //小於等於 243 e = Expression.LessThanOrEqual(l, r); 244 break; 245 case "gt": //大於 246 e = Expression.GreaterThan(l, r); 247 break; 248 case "ge": //大於等於 249 e = Expression.GreaterThanOrEqual(l, r); 250 break; 251 case "bw": //開頭是(字符串) 252 if (pt == typeof(string)) 253 { 254 e = Expression.Call(l, pt.GetMethod(nameof(string.StartsWith), new[] {typeof(string)}), r); 255 } 256 else 257 { 258 throw new InvalidOperationException($"不支持創建{pt.FullName}類型的開始於表達式"); 259 } 260 261 break; 262 case "bn": //開頭不是(字符串) 263 if (pt == typeof(string)) 264 { 265 e = Expression.Not(Expression.Call(l, pt.GetMethod(nameof(string.StartsWith), new[] {typeof(string)}), r)); 266 } 267 else 268 { 269 throw new InvalidOperationException($"不支持創建{pt.FullName}類型的不開始於表達式"); 270 } 271 272 break; 273 case "ew": //結尾是(字符串) 274 if (pt == typeof(string)) 275 { 276 e = Expression.Call(l, pt.GetMethod(nameof(string.EndsWith), new[] {typeof(string)}), r); 277 } 278 else 279 { 280 throw new InvalidOperationException($"不支持創建{pt.FullName}類型的結束於表達式"); 281 } 282 283 break; 284 case "en": //結尾不是(字符串) 285 if (pt == typeof(string)) 286 { 287 e = Expression.Not(Expression.Call(l, pt.GetMethod(nameof(string.EndsWith), new[] {typeof(string)}), r)); 288 } 289 else 290 { 291 throw new InvalidOperationException($"不支持創建{pt.FullName}類型的不結束於表達式"); 292 } 293 294 break; 295 case "cn": //包含(字符串) 296 if (pt == typeof(string)) 297 { 298 e = Expression.Call(l, pt.GetMethod(nameof(string.Contains), new[] {typeof(string)}), r); 299 } 300 else 301 { 302 throw new InvalidOperationException($"不支持創建{pt.FullName}類型的包含表達式"); 303 } 304 305 break; 306 case "nc": //不包含(字符串) 307 if (pt == typeof(string)) 308 { 309 e = Expression.Not(Expression.Call(l, pt.GetMethod(nameof(string.Contains), new[] {typeof(string)}), r)); 310 } 311 else 312 { 313 throw new InvalidOperationException($"不支持創建{pt.FullName}類型的包含表達式"); 314 } 315 316 break; 317 case "in": //屬於(是候選值列表之一) 318 e = BuildContainsExpression(rule, l, pt); 319 break; 320 case "ni": //不屬於(不是候選值列表之一) 321 e = Expression.Not(BuildContainsExpression(rule, l, pt)); 322 break; 323 case "nu": //為空 324 r = Expression.Constant(null); 325 e = Expression.Equal(l, r); 326 break; 327 case "nn": //不為空 328 r = Expression.Constant(null); 329 e = Expression.Not(Expression.Equal(l, r)); 330 break; 331 case "bt": //區間 332 throw new NotImplementedException($"尚未實現創建{rule.Op}類型的比較表達式"); 333 default: 334 throw new InvalidOperationException($"不支持創建{rule.Op}類型的比較表達式"); 335 } 336 337 return e; 338 339 static Expression BuildConstantExpression<TValue>(JqGridSearchRule jRule, Func<string, TValue> valueConvertor) 340 { 341 var rv = valueConvertor(jRule.Data); 342 return Expression.Constant(rv); 343 } 344 } 345 346 /// <summary> 347 /// 構造Contains調用表達式 348 /// </summary> 349 /// <param name="rule">條件</param> 350 /// <param name="parameter">參數</param> 351 /// <param name="parameterType">參數類型</param> 352 /// <returns>Contains調用表達式</returns> 353 private static Expression BuildContainsExpression(JqGridSearchRule rule, Expression parameter, Type parameterType) 354 { 355 Expression e = null; 356 357 var genMethod = typeof(Queryable).GetMethods() 358 .Single(m => m.Name == nameof(Queryable.Contains) && m.GetParameters().Length == 2); 359 360 var jsonArray = JsonSerializer.Deserialize<string[]>(rule.Data); 361 362 switch (parameterType) 363 { 364 #region 文字 365 366 case Type ct when ct == typeof(char): 367 if (jsonArray.Any(o => o.Length != 1)) {throw new InvalidOperationException("字符型的候選列表中存在錯誤的候選項");} 368 e = CallContains(parameter, jsonArray, str => str[0], genMethod, ct); 369 break; 370 case Type ct when ct == typeof(string): 371 e = CallContains(parameter, jsonArray, str => str, genMethod, ct); 372 break; 373 374 #endregion 375 376 #region 有符號整數 377 378 case Type ct when ct == typeof(sbyte): 379 e = CallContains(parameter, jsonArray, sbyte.Parse, genMethod, ct); 380 break; 381 case Type ct when ct == typeof(short): 382 e = CallContains(parameter, jsonArray, short.Parse, genMethod, ct); 383 break; 384 case Type ct when ct == typeof(int): 385 e = CallContains(parameter, jsonArray, int.Parse, genMethod, ct); 386 break; 387 case Type ct when ct == typeof(long): 388 e = CallContains(parameter, jsonArray, long.Parse, genMethod, ct); 389 break; 390 391 #endregion 392 393 #region 無符號整數 394 395 case Type ct when ct == typeof(byte): 396 e = CallContains(parameter, jsonArray, byte.Parse, genMethod, ct); 397 break; 398 case Type ct when ct == typeof(ushort): 399 e = CallContains(parameter, jsonArray, ushort.Parse, genMethod, ct); 400 break; 401 case Type ct when ct == typeof(uint): 402 e = CallContains(parameter, jsonArray, uint.Parse, genMethod, ct); 403 break; 404 case Type ct when ct == typeof(ulong): 405 e = CallContains(parameter, jsonArray, ulong.Parse, genMethod, ct); 406 break; 407 408 #endregion 409 410 #region 小數 411 412 case Type ct when ct == typeof(float): 413 e = CallContains(parameter, jsonArray, float.Parse, genMethod, ct); 414 break; 415 case Type ct when ct == typeof(double): 416 e = CallContains(parameter, jsonArray, double.Parse, genMethod, ct); 417 break; 418 case Type ct when ct == typeof(decimal): 419 e = CallContains(parameter, jsonArray, decimal.Parse, genMethod, ct); 420 break; 421 422 #endregion 423 424 #region 其它常用類型 425 426 case Type ct when ct == typeof(DateTime): 427 e = CallContains(parameter, jsonArray, DateTime.Parse, genMethod, ct); 428 break; 429 case Type ct when ct == typeof(DateTimeOffset): 430 e = CallContains(parameter, jsonArray, DateTimeOffset.Parse, genMethod, ct); 431 break; 432 case Type ct when ct == typeof(Guid): 433 e = CallContains(parameter, jsonArray, Guid.Parse, genMethod, ct); 434 break; 435 case Type ct when ct.IsEnum: 436 e = CallContains(Expression.Convert(parameter, typeof(object)), jsonArray, enumString => enumString.ToEnumObject(ct), genMethod, ct); 437 break; 438 439 #endregion 440 } 441 442 return e; 443 444 static MethodCallExpression CallContains<T>(Expression pa, string[] jArray, Func<string, T> selector, MethodInfo genericMethod, Type type) 445 { 446 var data = jArray.Select(selector).ToArray().AsQueryable(); 447 var method = genericMethod.MakeGenericMethod(type); 448 449 return Expression.Call(null, method, new[] { Expression.Constant(data), pa }); 450 } 451 } 452 }
使用
此處是在 Razor Page 中使用,內部使用的其他輔助類和前端頁面代碼就不貼了,有興趣的可以在我的文章末尾找到 GitHub 項目鏈接:
1 public async Task<IActionResult> OnGetUserListAsync([FromQuery]JqGridParameter jqGridParameter) 2 { 3 var usersQuery = _userManager.Users.AsNoTracking(); 4 if (jqGridParameter._search == "true") 5 { 6 usersQuery = usersQuery.Where(BuildWhere<ApplicationUser>(jqGridParameter.FilterObject, null)); 7 } 8 9 var users = usersQuery.Include(u => u.UserRoles).ThenInclude(ur => ur.Role).OrderBy(u => u.InsertOrder) 10 .Skip((jqGridParameter.Page - 1) * jqGridParameter.Rows).Take(jqGridParameter.Rows).ToList(); 11 var userCount = usersQuery.Count(); 12 var pageCount = Ceiling((double) userCount / jqGridParameter.Rows); 13 return new JsonResult( 14 new 15 { 16 rows //數據集合 17 = users.Select(u => new 18 { 19 u.UserName, 20 u.Gender, 21 u.Email, 22 u.PhoneNumber, 23 u.EmailConfirmed, 24 u.PhoneNumberConfirmed, 25 u.CreationTime, 26 u.CreatorId, 27 u.Active, 28 u.LastModificationTime, 29 u.LastModifierId, 30 u.InsertOrder, 31 u.ConcurrencyStamp, 32 //以下為JqGrid中必須的字段 33 u.Id //記錄的唯一標識,可在插件中配置為其它字段,但是必須能作為記錄的唯一標識用,不能重復 34 }), 35 total = pageCount, //總頁數 36 page = jqGridParameter.Page, //當前頁碼 37 records = userCount //總記錄數 38 } 39 ); 40 }
啟動項目后訪問 /Identity/Manage/Users/Index 可以嘗試使用。
結語
通過這次實踐,深入了解了很多表達式樹的相關知識,表達式樹在編譯流程中還算是高級結構了,耐點心還是能看懂,IL 才是真的暈,比原生匯編也好不到哪里去。C# 確實很有意思,入門簡單,內部卻深邃無比,在小白和大神手上完全是兩種語言。Java 在 Java 8 時增加了 Stream 和 Lambda 表達式功能,一看就是在對標 Linq,不過那名字取的真是一言難盡,看代碼寫代碼感覺如鯁在喉,相當不爽。由於 Stream 體系缺少表達式樹,這種動態構造查詢表達式的功能從一開始就不可能支持。再加上 Java 沒有匿名類型,沒有對象初始化器,每次用 Stream 就難受的一批,中間過程的數據結構也要專門寫類,每個中間類還要獨占一個文件,簡直暈死。抄都抄不及格!
C# 引入 var 關鍵字核心是為匿名類型服務,畢竟是編譯器自動生成的類型,寫代碼的時候根本沒有名字,不用 var 用什么?簡化變量初始化代碼只是順帶的。結果 Java 又抄一半,還是最不打緊的一半,簡化變量初始化代碼。真不知道搞 Java 的那幫人在想些什么。
轉載請完整保留以下內容並在顯眼位置標注,未經授權刪除以下內容進行轉載盜用的,保留追究法律責任的權利!
本文地址:https://www.cnblogs.com/coredx/p/12423929.html
完整源代碼:Github
里面有各種小東西,這只是其中之一,不嫌棄的話可以Star一下。