前言 Linq 是 C# 中一個非常好用的集合處理庫,用好了能幫我們簡化大量又臭又長的嵌套迴圈,使處理邏輯清晰可見。EF 查詢主要也是依賴 Linq。但是 Linq 相對 sql 也存在一些缺點,最主要的就是動態構造查詢的難度。sql 只需要簡單進行字元串拼接,操作難度很低(當然出錯也相當容易),而 ...
前言
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 } <