前言
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 = https://www.cnblogs.com/coredx/p/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 =https://www.cnblogs.com/coredx/p/ 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一下,
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/64876.html
標籤:C#
下一篇:關于“Failed to complete setup of assembly(hr = 0x80131040). Probing terminated”
