C#表達式目錄樹系列之5 –動態創建查詢表達式

概要

在項目開發中,根據用戶的需求,一般來是,我們的查詢表達式是固定的,新的查詢需求都要通過代碼的修改來實現。而對於不確定的查詢條件,固定查詢表達式的方式顯然是行不通的。

針對固定查詢表達式存在的問題,我們提出基於表達式目錄樹的解決方案,該解決方案能幫助我們實時自動構建任何需要的查詢表達式,以應用對各種複雜的查詢場景。

通過定義IQueryable的擴展方法,來實現查詢表達式的實時構建,並且以保證構建的查詢表達式同IQueryable已支持的EF查詢或內存查詢的兼容性,完整代碼已經上傳CSDN,需要的讀者可以免費下載,如果下載失敗請留言告訴我郵箱。

設計及關鍵代碼實現

實現目標

基於既定需求,我們的實現目標如下:

  1. 支持的查詢字段類型包括數字,字符串,日期,枚舉和布爾:
    • 數字類型查詢支持大於,小於,等於,大於等於,小於等於的比較操作
    • 字符串類型查詢支持等於,包含,以指定字符開頭或結尾的比較操作
    • 日期類型查詢支持大於,小於,等於,大於等於,小於等於的比較操作
    • 枚舉和布爾類型查詢支持等於的比較操作
  2. 支持多個條件按照And或Or關係的組合查詢,And爲默認組合方式
  3. 支持數字,字符串,日期及和枚舉類型的否定查詢

設計實現

IQueryable的擴展方法定於如下:

  • public static IQueryable WhereEnhance (this IQueryable source, List conditions)
  • public static IQueryable WhereEnhance (this IQueryable source, ConditionTreeNode root)

針對不同複雜度的查詢,我們定義了兩個WhereEnhance的方法,兩個方法接收的參數類型不同。

  • 對於方法1,將查詢條件通過一個List傳入。每個查詢條件記錄和下一個查詢條件的邏輯關係And或者Or。因爲每個節點只能記錄和下一個節點的邏輯關係,對於複雜的嵌套邏輯關係無法支持,所以該方法只適用於查詢條件的邏輯關係比較簡單的查詢情景, WhereCondition類的定義請參見附錄。
  • 對於方法2,將查詢條件通過一個二叉樹的根節點傳入。二叉樹的非葉節點保存查詢條件之間的邏輯關係And或者Or。葉節點保存具體的查詢條件。這樣,複雜的嵌套邏輯關係完全可以通過二叉樹來表示,所以該方法適用於查詢條件的邏輯關係比較複雜的情景。ConditionTreeNode 定義詳見附錄。

關鍵代碼解釋

WhereEnhance方法(接收List參數)

本文通過定義一個IQueryable的擴展方法WhereEnhance來實現上述查詢表達式構建流程,該方法既可以用於內存數據的查詢,也可以和EF配合在一起查詢數據庫中的數據。具體代碼如下:

 public static IQueryable<T> WhereEnhance<T> (this IQueryable<T> source, List<WhereCondition> conditions) {
   
   
			ParameterExpression parameter = Expression.Parameter(typeof(T),"s");
			Expression conditionsExpression = CreateExpressions<T>(conditions, parameter);			
			LambdaExpression whereCondition = Expression.Lambda<Func<T,bool>>(conditionsExpression,new ParameterExpression[]{
   
   parameter});
			Console.WriteLine("Lambda Expression: " + whereCondition);
			MethodCallExpression whereCallExpression = Expression.Call(  
				typeof(Queryable),  
				"Where",  
				new Type[] {
   
    source.ElementType },  
				source.Expression,  
				whereCondition);  
			return source.Provider.CreateQuery<T>(whereCallExpression) as IQueryable<T>;    
		} 
private static Expression CreateExpressions<T>(List<WhereCondition> conditions, ParameterExpression parameter){
   
   		
			Expression conditionsExpression = Expression.And(Expression.Constant(true),Expression.Constant(true));
            ConditionRelation relation = ConditionRelation.And;
            for(var i = 0 ; i< conditions.Count; ++i){
   
   
                MemberExpression property = Expression.Property(parameter, conditions[i].PropertyName);		
				PropertyType propertyType = CheckFieldType(property);
			 	Expression whereCondition;
				if (propertyType == PropertyType.String){
   
   
					whereCondition = buildStringWhere<T>(conditions[i], parameter); 
				}else if (propertyType == PropertyType.Number
					 || propertyType == PropertyType.DateTime 
					 || propertyType == PropertyType.Enum
					 || propertyType == PropertyType.Bool){
   
   
					whereCondition = buildNumberOrDateTimeWhere<T>(conditions[i], parameter);
				}else{
   
   
					throw new ArgumentException($"We cannot support the property type {property.Type} in {typeof(T)}. ");
				}       
                if (i==0){
   
   
                    conditionsExpression = whereCondition;
                    relation = conditions[i].NextRelation;
                }else{
   
   
                    conditionsExpression = Combine<T>(
					    conditionsExpression,
						whereCondition,
                        relation
				    );
                    relation = conditions[i].NextRelation;
                }
            }
			return conditionsExpression;
		} 
  1. 通過在該方法中定義泛型參數,實現在不同的WepAPI中,所有業務相關類型的查詢支持。在實際應用中,建議增加泛型約束。
  2. 構建Lambda表達式左側的變量s,類型爲T。
  3. 調用方法CreateExpressions創建查詢表達式,其中傳入的查詢條件排列順序,即爲這些條件生成對應查詢表達式後,表達式進行And或Or組合時候的順序。
  4. 遍歷所有查詢條件,生成具體的查詢表達式。
    • 調用CheckFieldType方法,檢查屬性類型,該方法請見附錄。
    • 字符串類型的屬性,所有的比較全部通過字符串類的實例方法實現,所以定義函數buildStringWhere來實現字符串查詢表達式的構建。
    • 數字,日期,枚舉都支持通過操作符 < > 或=的比較,布爾類型也支持=操作符。因此將這些類型查詢表達式構建分爲一類,通過buildNumberOrDateTimeWhere方法構建查詢表達式。
    • 將生成的查詢表達式通過Combine方法組合在一起。Combine方法支持And和Or兩種組合方式,默認支持And方式。Combine方法定義詳見附錄。
  5. 將構建好的表達式和Lambda表達式組裝到一起,構建出Lambda表達式。
  6. 調用Where方法,執行Lambda表達式。

WhereEnhance方法(接收二叉樹根節點參數)

public static IQueryable<T> WhereEnhance<T> (this IQueryable<T> source, ConditionTreeNode root) {
   
   
			ParameterExpression parameter = Expression.Parameter(typeof(T),"s");
			Expression conditionsExpression = CreateExpressions<T>(root, parameter);	
			LambdaExpression whereCondition = Expression.Lambda<Func<T,bool>>(conditionsExpression,new ParameterExpression[]{
   
   parameter});
			Console.WriteLine("Lambda Expression: " + whereCondition);
			MethodCallExpression whereCallExpression = Expression.Call(  
				typeof(Queryable),  
				"Where",  
				new Type[] {
   
    source.ElementType },  
				source.Expression,  
				whereCondition
				);  
			return source.Provider.CreateQuery<T>(whereCallExpression) as IQueryable<T>;    
		}
private static Expression CreateExpressions<T>(ConditionTreeNode node, ParameterExpression parameter){
   
   	
			if (node.Relation != ConditionRelation.None){
   
   
				return Combine<T>(
					CreateExpressions<T>(node.LeftNode, parameter), 
					CreateExpressions<T>(node.RightNode, parameter),
					node.Relation
					);
			}
			else{
   
   
				MemberExpression property = Expression.Property(parameter, node.NodeValue.PropertyName);		
				PropertyType propertyType = CheckFieldType(property);
				Expression whereCondition;
				if (propertyType == PropertyType.String){
   
   
					whereCondition = buildStringWhere<T>(node.NodeValue, parameter); 
				}else if (propertyType == PropertyType.Number || propertyType == PropertyType.DateTime || propertyType == PropertyType.Enum){
   
   
					whereCondition = buildNumberOrDateTimeWhere<T>(node.NodeValue, parameter);
				}else{
   
   
					throw new ArgumentException($"We cannot support the property type {property.Type} in {typeof(T)}. ");
				}
				return whereCondition;
			}	
		}

  1. 通過在該方法中定義泛型參數,實現在不同的WepAPI中,所有業務相關類型的查詢支持。在實際應用中,建議增加泛型約束。
  2. 構建Lambda表達式左側的變量s,類型爲T。
  3. 調用方法CreateExpressions創建查詢表達式,傳入的查詢條件是一棵二叉樹根的節點。
    • 如果當前節點是非葉節點:
      • 遞歸構建當前節點的左子樹節點。
      • 遞歸構建當前節點的右子樹節點。
      • 將生成的查詢表達式通過Combine方法組合在一起。Combine方法支持And和Or兩種組合方式,默認支持And方式。Combine方法定義詳見附錄。
      • 返回構建好的查詢表達式。
    • 如果當前節點是葉節點:
      • 調用CheckFieldType方法,檢查屬性類型,該方法請見附錄。
      • 字符串類型的屬性,所有的比較全部通過字符串類的實例方法實現,所以定義函數buildStringWhere來實現字符串查詢表達式的構建。
      • 數字,日期,枚舉都支持通過操作符 < > 或=的比較,布爾類型也支持=操作符。因此將這些類型查詢表達式構建分爲一類,通過buildNumberOrDateTimeWhere方法構建查詢表達式。
      • 返回構建好的查詢表達式。
  4. 將構建好的表達式和Lambda表達式組裝到一起,構建出Lambda表達式。
  5. 調用Where方法,執行Lambda表達式。

buildStringWhere方法

buildStringWhere方法用於構建字符串類型的查詢表達式。構建目標:

s => s.Property.Method(keyword),其中:

  • s的類型是T,在運行時會傳入具體的類型。
  • Property是類中的某一個屬性,類型爲字符串類型。
  • Method是具體字符串比較方法,支持Equals,Contains,StartsWith和EndsWith,4個字符串比較方法。
  • keyword是查詢關鍵字。
private static Expression buildStringWhere<T>(WhereCondition condition, ParameterExpression parameter){
   
   
			Dictionary<string,string> methodsMap = new Dictionary<string,string>(){
   
   
				{
   
    EQ, "Equals"},
				{
   
   "contain", "Contains"},
				{
   
   "beginwith", "StartsWith"},
				{
   
   "endwith", "EndsWith"},
			};
			MemberExpression property = Expression.Property(parameter, condition.PropertyName);
			ConstantExpression rightValue = GetConstantExpression(condition.PropertyValue, property);
			ConstantExpression optionValue = Expression.Constant(StringComparison.CurrentCultureIgnoreCase);
			Expression methodCall = Expression.Call(
					property,
					typeof(string).GetMethod(methodsMap[condition.Action], 
					new Type[]{
   
    
						typeof(string),
						//typeof(StringComparison)
						}), // method
					new Expression[] // Work method's parameter
					{
   
   
						rightValue,
					//	optionValue
					}
				);

			if (condition.Reversed){
   
   
				methodCall = Expression.Not(methodCall);
			}
			return methodCall;
		}
  • 根據查詢條件查詢條件中的關鍵字,調用GetConstantNumberExpression方法,構建關鍵字的常量表達式。關鍵字類型與對應的類中屬性類型一致。該方法請見附錄。
  • 根據查詢條件,調用具體的字符串比較方法。對於忽略大小寫的查詢,只能支持內存查詢。如果需要在EFCore的查詢中使用,需要在數據庫中進行配置,因此暫時註釋掉了對應代碼。
  • 如果是否定比較,將原有查詢表達式,傳化爲否定查詢表達式。
  • 返回構建好的字符串查詢表達式。

buildNumberOrDateTimeWhere方法

buildNumberOrDateTimeWhere方法用於構建數字,枚舉和日期類型的查詢表達式。構建目標:
s=> s.Property [>|<|=|<=|>=] keywork 其中:

  • s的類型是T,在運行時會傳入具體的類型。
  • Property是類中的某一個屬性,類型數字,枚舉或日期類型。其中數字類型支持decimal,int, long,short,byte類型。
  • 比較操作符包含大於,小於,等於,小於等於,大於等於中的一個。
  • keyword是查詢關鍵字。
private static Expression buildNumberOrDateTimeWhere<T>(WhereCondition condition, ParameterExpression parameter){
   
   	
			MemberExpression propert = Expression.Property(parameter, condition.PropertyName);
			ConstantExpression rightValue = GetConstantExpression(condition.PropertyValue, property);
			Expression whereCondition;
			switch(condition.Action){
   
   
				case GT:
					whereCondition = Expression.GreaterThan(property, rightValue);
					break;
				case GE:
					whereCondition = Expression.GreaterThanOrEqual(property, rightValue);
					break;
				case LT:
					whereCondition = Expression.LessThan(property, rightValue);
					break;
				case LE:
					whereCondition = Expression.LessThanOrEqual(property, rightValue);
					break;
				default:
					whereCondition = Expression.Equal(property, rightValue);
					break;
			}			
			if (condition.Reversed){
   
   
				whereCondition = Expression.Not(whereCondition);
			}
			return whereCondition; 
		} 
  • 根據查詢條件查詢條件中的關鍵字,調用GetConstantExpression方法,構建關鍵字的常量表達式。關鍵字類型與對應的類中屬性類型一致。該方法請見附錄。
  • 根據查詢條件,選擇具體的比較操作符。
  • 如果是否定比較,將原有查詢表達式傳化爲否定查詢表達式。
  • 返回構建好的查詢表達式。

驗證及測試

本次開發的WhereEnhance方法,作爲IQueryable的擴展方法,可以支持內存查詢和配合EFCore,進行數據庫的查詢。本文以查詢銀行分行信息作爲查詢數據,進行查詢。

由於內存數據查詢和數據庫查詢使用的數據和測試結果完全相同,故本文只提供數據庫查詢的測試用例和結果。如果需要內存查詢的相關數據,請參看完整代碼。測試數據詳見附錄。

Branch類支持EFCore的完整代碼

  [Table ("t_branch")]
     public class Branch{
   
   
        [Key,DatabaseGenerated(DatabaseGeneratedOption.Identity), Column(Order=0)]
        public int Id {
   
   get;set;}
        [Required, Column(Order=1)]
        public string BranchName {
   
   get;set;}
        [Required, Column(Order=2)]
        public string BranchManager {
   
   get;set;}
        [Required, Column(Order=3)]
        public int BranchCode {
   
   get;set;}
        [Column (TypeName = "datetime", Order=4), Required]
        public DateTime BuildDate {
   
   get;set;}
        [Required, Column(Order=5)]
        public BranchStatus Status {
   
   get;set;} = BranchStatus.Open;
        [Required, Column(Order=6)]
        public bool HasATM {
   
   get;set;} =  true;
        [Timestamp, Column(Order=7)]
        public byte[] RowVersion {
   
    get; set; }
    }

數據庫建表語句和初始化數據請參看附錄。

測試用例

查詢分行代碼小於4的分行信息

 using(var context = new ExpressionTreeContext()){
   
   
            WhereCondition condition = new WhereCondition(){
   
   
                PropertyName = "BranchCode",
                PropertyValue = "4",
                Action = ActionType.lt.ToString(),
                Reversed = false
            };
            List<WhereCondition> conditions = new List<WhereCondition>(){
   
   
		        condition,
		    };
            var branches = await context.Branches
                .WhereEnhance(conditions)
                .AsNoTracking ()
                .ToListAsync ();
                foreach(var branch in branches){
   
   
                    System.Console.WriteLine(branch.BranchName);
                }
        }  

執行結果:
在這裏插入圖片描述

查詢分行名稱以“分行2”結尾的分行信息

 using(var context = new ExpressionTreeContext()){
   
   
            WhereCondition condition = new WhereCondition(){
   
   
                PropertyName = "BranchName",
                PropertyValue = "分行2",
                Action = ActionType.endwith.ToString(),
                Reversed = false
		    };
            List<WhereCondition> conditions = new List<WhereCondition>(){
   
   
		        condition,
		    };
            var branches = await context.Branches
                .WhereEnhance(conditions)
                .AsNoTracking ()
                .ToListAsync ();
                foreach(var branch in branches){
   
   
                    System.Console.WriteLine(branch.BranchName);
                }
        }

執行結果:

在這裏插入圖片描述

查詢成立日期晚於2016-10-04的分行信息

using(var context = new ExpressionTreeContext()){
   
   
            WhereCondition condition = new WhereCondition(){
   
   
                PropertyName = "BuildDate",
			    PropertyValue = "2016-10-04",
			    Action = ActionType.gt.ToString(),        
			    Reversed = false
		    };
            List<WhereCondition> conditions = new List<WhereCondition>(){
   
   
		        condition,
		    };
            var branches = await context.Branches
                .WhereEnhance(conditions)
                .AsNoTracking ()
                .ToListAsync ();
                foreach(var branch in branches){
   
   
                    System.Console.WriteLine(branch.BranchName);
                }
        }   

執行結果:
在這裏插入圖片描述

查詢狀態爲Open的分行信息

using(var context = new ExpressionTreeContext()){
   
   
            WhereCondition condition = new WhereCondition(){
   
   
                PropertyName = "Status",
                PropertyValue = "1",
                Action = ActionType.eq.ToString(),
                Reversed = false
		    };
            List<WhereCondition> conditions = new List<WhereCondition>(){
   
   
		        condition,
		    };
            var branches = await context.Branches
                .WhereEnhance(conditions)
                .AsNoTracking ()
                .ToListAsync ();
                foreach(var branch in branches){
   
   
                    System.Console.WriteLine(branch.BranchName);
                }
        }    

執行結果:
在這裏插入圖片描述

查詢沒有ATM機的分行信息

using(var context = new ExpressionTreeContext()){
   
   
            WhereCondition condition = new WhereCondition(){
   
   
                PropertyName = "HasATM",
                PropertyValue = "false",
                Action = ActionType.eq.ToString(),
                Reversed = false
		    };
            List<WhereCondition> conditions = new List<WhereCondition>(){
   
   
		        condition,
		    };
            var branches = await context.Branches
                .WhereEnhance(conditions)
                .AsNoTracking ()
                .ToListAsync ();
                foreach(var branch in branches){
   
   
                    System.Console.WriteLine(branch.BranchName);
                }
        }    

執行結果:
在這裏插入圖片描述

查詢類型不是Open的分行信息

  using(var context = new ExpressionTreeContext()){
   
   
            WhereCondition condition = new WhereCondition(){
   
   
                PropertyName = "Status",
                PropertyValue = "2",
                Action = ActionType.eq.ToString(),
                Reversed = true
		    };
            List<WhereCondition> conditions = new List<WhereCondition>(){
   
   
		        condition,
		    };
            var branches = await context.Branches
                .WhereEnhance(conditions)
                .AsNoTracking ()
                .ToListAsync ();
            foreach(var branch in branches){
   
   
                System.Console.WriteLine(branch.BranchName);
            }
        }    

執行結果:
在這裏插入圖片描述

查詢成立日期不晚於2016-10-04的分行信息

using(var context = new ExpressionTreeContext()){
   
   
            WhereCondition condition = new WhereCondition(){
   
   
                PropertyName = "BuildDate",
			    PropertyValue = "2016-10-04",
			    Action = ActionType.gt.ToString(),        
			    Reversed = true
		    };
            List<WhereCondition> conditions = new List<WhereCondition>(){
   
   
		        condition,
		    };
            var branches = await context.Branches
                .WhereEnhance(conditions)
                .AsNoTracking ()
                .ToListAsync ();
            foreach(var branch in branches){
   
   
                System.Console.WriteLine(branch.BranchName);
            }
        }   

執行結果:
在這裏插入圖片描述

查詢成立日期在2016-10-02到2016-10-06之間,不包含2016-10-02的分行信息

using(var context = new ExpressionTreeContext()){
   
   
            WhereCondition conditionStartDate = new WhereCondition(){
   
   
                PropertyName = "BuildDate",
                PropertyValue = "2016-10-02",
                Action = ActionType.gt.ToString(),
                Reversed = false
		    };
            WhereCondition conditionEndDate = new WhereCondition(){
   
   
                PropertyName = "BuildDate",
                PropertyValue = "2016-10-06",
                Action = ActionType.le.ToString(),
                Reversed = false
		    };
            List<WhereCondition> conditions = new List<WhereCondition>(){
   
   
		        conditionStartDate,
                conditionEndDate,
		    };
            var branches = await context.Branches
                .WhereEnhance(conditions)
                .AsNoTracking ()
                .ToListAsync ();
            foreach(var branch in branches){
   
   
                System.Console.WriteLine(branch.BranchName);
            }
        }   

執行結果:
在這裏插入圖片描述

查詢成立日期在2016-10-02到2016-10-07之間,不包含2016-10-02並且分行類型不是Open的分行信息

 using(var context = new ExpressionTreeContext()){
   
   
            WhereCondition condition = new WhereCondition(){
   
   
                PropertyName = "Status",
                PropertyValue = "1",
                Action = ActionType.eq.ToString(),
                Reversed = true
		    };
            WhereCondition conditionStartDate = new WhereCondition(){
   
   
                PropertyName = "BuildDate",
                PropertyValue = "2016-10-02",
                Action = ActionType.gt.ToString(),
                Reversed = false
		    };
            WhereCondition conditionEndDate = new WhereCondition(){
   
   
                PropertyName = "BuildDate",
                PropertyValue = "2016-10-07",
                Action = ActionType.le.ToString(),
                Reversed = false
		    };
            List<WhereCondition> conditions = new List<WhereCondition>(){
   
                 
		        conditionStartDate,
                conditionEndDate,
                condition,
		    };
            var branches = await context.Branches
                .WhereEnhance(conditions)
                .AsNoTracking ()
                .ToListAsync ();
            foreach(var branch in branches){
   
   
                System.Console.WriteLine(branch.BranchName);
            }
        }   

執行結果:
在這裏插入圖片描述

查詢成立日期早於2016-10-02或者晚於2016-10-09,並且分行經理是Tom的分行信息

using(var context = new ExpressionTreeContext()){
   
   
            WhereCondition condition = new WhereCondition(){
   
   
                PropertyName = "BranchManager",
                PropertyValue = "Tom",
                Action = ActionType.eq.ToString(),
                Reversed = false
		    };
            WhereCondition conditionStartDate = new WhereCondition(){
   
   
                PropertyName = "BuildDate",
                PropertyValue = "2016-10-02",
                Action = ActionType.lt.ToString(),
                NextRelation = ConditionRelation.Or,
                Reversed = false
		    };
            WhereCondition conditionEndDate = new WhereCondition(){
   
   
                PropertyName = "BuildDate",
                PropertyValue = "2016-10-04",
                Action = ActionType.gt.ToString(),
                Reversed = false
		    };
            List<WhereCondition> conditions = new List<WhereCondition>(){
   
                 
                conditionStartDate,
                conditionEndDate,
                condition,
		    };
            var branches = await context.Branches
                .WhereEnhance(conditions)
                .AsNoTracking ()
                .ToListAsync ();
            foreach(var branch in branches){
   
   
                System.Console.WriteLine(branch.BranchName);
            }
        }     

執行結果:
在這裏插入圖片描述

如果我們將執行順序改爲如下:

List<WhereCondition> conditions = new List<WhereCondition>(){
   
                 
		       condition,
               conditionStartDate,
               conditionEndDate,
		    };

執行結果如下:
在這裏插入圖片描述

根據測試結果可以看到,如果改變條件的順序,無論時生成的查詢表達式,還是EFCore生成的SQL都不是我們預期的。由於當前版本不支持括號,請將Or操作儘量放到查詢條件的最前面。

複雜條件查詢

查詢條件的Lambda表達式構建目標:

t => (([t].[BranchCode] > 1) AND ([t].[BranchCode] < 3)) OR ((([t].[BranchCode] < 9) AND ([t].[BranchCode] > 5))
AND ([t].[BranchManager] = N’Tom’))

using(var context = new ExpressionTreeContext()){
   
   
            ConditionTreeNode root = new ConditionTreeNode();
            root.Relation = ConditionRelation.Or;
            ConditionTreeNode p11 = new ConditionTreeNode();
            p11.Relation = ConditionRelation.And;
            root.LeftNode = p11; 
            ConditionTreeNode p12 = new ConditionTreeNode();
            p12.Relation = ConditionRelation.And;
            root.RightNode = p12;
            
            
            ConditionTreeNode p21 = new ConditionTreeNode();
            p11.LeftNode = p21;
            p21.NodeValue = new WhereCondition(){
   
   
                PropertyName = "BranchCode",
                Action = "gt",
                PropertyValue = "1"
            };
            ConditionTreeNode p22 = new ConditionTreeNode();
            p11.RightNode = p22;
            p22.NodeValue = new WhereCondition(){
   
   
                PropertyName = "BranchCode",
                Action = "lt",
                PropertyValue = "3"
            };
            ConditionTreeNode p23 = new ConditionTreeNode();
            p23.Relation = ConditionRelation.And;

            ConditionTreeNode p24 = new ConditionTreeNode();
            p24.NodeValue = new WhereCondition(){
   
   
                PropertyName = "BranchManager",
                Action = "eq",
                PropertyValue = "Tom",
                Reversed = false
            };
            p12.LeftNode = p23;
            p12.RightNode = p24; 
            ConditionTreeNode p31 = new ConditionTreeNode();
            p31.NodeValue = new WhereCondition(){
   
   
                PropertyName = "BranchCode",
                Action = "lt",
                PropertyValue = "9"
            };
            ConditionTreeNode p32 = new ConditionTreeNode();
            p32.NodeValue = new WhereCondition(){
   
   
                PropertyName = "BranchCode",
                Action = "gt",
                PropertyValue = "5"
            };
            p23.LeftNode = p31;
            p23.RightNode = p32;
            string json = JsonConvert.SerializeObject(root);
            root = JsonConvert.DeserializeObject<ConditionTreeNode>(json); 

            var branches = await context.Branches
                .WhereEnhance(root)
                .AsNoTracking ()
                .ToListAsync ();
            foreach(var branch in branches){
   
   
                System.Console.WriteLine(branch.BranchName);
            }
        }    

執行結果如下:
在這裏插入圖片描述

表達式目錄樹系列全部文章

附錄

查詢條件相關的類

public class WhereCondition{
   
   
        public string PropertyName {
   
   get;set;}
        public string Action {
   
   get;set;}
        public bool Reversed  {
   
   get;set;} = false;
        public string PropertyValue {
   
   get;set;}
        public ConditionRelation NextRelation {
   
   get;set;} = ConditionRelation.And;
    }
    public enum ConditionRelation{
   
   
        And= 1,
        Or,
        None
    }
    public class ConditionTreeNode{
   
   
        public ConditionTreeNode LeftNode {
   
   get;set;}
        public ConditionTreeNode RightNode {
   
   get;set;}
        public ConditionRelation Relation {
   
   get;set;} = ConditionRelation.None;
        public WhereCondition NodeValue {
   
   get;set;}
    }

WhereCondition類具體內容包括:

  • 查詢字段名稱
  • 查詢條件,例如eq,gt等,具體請參見代碼。
  • 查詢關鍵字
  • Reversed字段表示是否是否定查詢,即不等於,不大於,不包含等,默認是false
  • ConditionRelation是當前查詢條件和下一個條件的關係,默認是And,可以設置爲Or

ConditionTreeNode類具體內容包括:

  • 二叉樹的左右節點
  • 如果是非葉節點,則Relation記錄其左右節點的邏輯關係And或者Or。如果是葉節點,Relation爲None。
  • 如果是非葉節點,NodeValue爲空。如果是葉節點,NodeValue是WhereCondition類的對象。

分行類代碼

public class Branch {
   
   
	public string BranchName {
   
   get;set;}
	public int BranchCode {
   
   get;set;}
	public DateTime BuildDate {
   
   get;set;}
	public BranchStatus Status {
   
   get;set;} = BranchStatus.Open;
	public string BranchManager {
   
   get;set;}
}
public enum BranchType {
   
   
	Open = 1,
	Closed,
	Maintance
}

判斷屬性類型的代碼

public enum PropertyType {
   
   
	String = 1,
	Number,
	DateTime,
	Enum,
	Bool,
	Unsupported
}
private static PropertyType CheckFieldType(MemberExpression member){
   
   
			string typeName = member.Type.ToString();
			string baseTypeName = member.Type.BaseType.ToString();
			string[] supportedNumbertypes = new string []{
   
   DECIMAL, BYTE, SHORT , INT, LONG};	
			if (baseTypeName == ENUM){
   
   
				return PropertyType.Enum;
			}		
			if (typeName == STRING){
   
   
				return PropertyType.String;
			}else if (supportedNumbertypes.Contains(typeName)){
   
   
				return PropertyType.Number;
			}else if (typeName == DATETIME){
   
   
				return PropertyType.DateTime;
			} else if (typeName == BOOL){
   
   
				return PropertyType.Bool;
			}
			return PropertyType.Unsupported;
		}

創建常量表達式的代碼

private static ConstantExpression GetConstantExpression(string val, MemberExpression member){
   
   
			ConstantExpression constExpression;
			string typeName = member.Type.ToString();
			string baseTypeName = member.Type.BaseType.ToString();
			switch(typeName){
   
   
				case STRING:
					constExpression = Expression.Constant(val, member.Type);
					break;
				case DECIMAL:
					constExpression = Expression.Constant(Convert.ToDecimal(val), member.Type);
					break;
				case SHORT:
					constExpression = Expression.Constant(Convert.ToInt16(val), member.Type);
					break;
				case INT:
					constExpression = Expression.Constant(Convert.ToInt32(val), member.Type);
					break;
				case LONG:
					constExpression = Expression.Constant(Convert.ToInt64(val), member.Type);
					break;
				case DATETIME:
					constExpression = Expression.Constant(DateTime.Parse(val), member.Type);
					break;
				case BYTE:
                    constExpression = Expression.Constant(Convert.ToByte(val), member.Type);
                    break;
				case BOOL:
                    constExpression = Expression.Constant(Convert.ToBoolean(val), member.Type);
                    break;                    
				default:
					if (baseTypeName == ENUM){
   
   
						int data = Int32.Parse(val.ToString());
                		String name = Enum.GetName(member.Type, data);
						constExpression = Expression.Constant(Enum.Parse(member.Type, name, false), member.Type);
					}else{
   
   
						constExpression = Expression.Constant(Convert.ToInt64(val), member.Type);
					}
					break;
			}
			return constExpression;
		}		

    }

注意枚舉類型需要通過基類來判斷。

查詢表達式組合代碼

private static Expression Combine<T>
        (
            Expression first, 
            Expression second,
            ConditionRelation relation = ConditionRelation.And
            )
        {
   
   
            if ( relation == ConditionRelation.And){
   
   
                return Expression.AndAlso(first, second);
            }else{
   
   
                return Expression.OrElse(first, second); 
            }
        }		

測試數據

數據庫建表語句:

USE [Bank]
GO

/****** Object:  Table [dbo].[t_branch]    Script Date: 2021/2/1 16:38:49 ******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[t_branch](
	[Id] [int] IDENTITY(1,1) NOT NULL,
	[BranchName] [nvarchar](max) NOT NULL,
	[BranchCode] [int] NOT NULL,
	[BuildDate] [datetime] NOT NULL,
	[RowVersion] [timestamp] NULL,
	[BranchManager] [nvarchar](max) NOT NULL,
	[HasATM] [bit] NOT NULL,
	[Status] [int] NOT NULL,
 CONSTRAINT [PK_t_branch] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

GO

ALTER TABLE [dbo].[t_branch] ADD  DEFAULT (N'') FOR [BranchManager]
GO

ALTER TABLE [dbo].[t_branch] ADD  DEFAULT (CONVERT([bit],(0))) FOR [HasATM]
GO

ALTER TABLE [dbo].[t_branch] ADD  DEFAULT ((0)) FOR [Status]
GO

數據初始化語句:

 insert into [dbo].[t_branch] ([BranchName], [BranchCode], [BuildDate],[BranchManager], [HasATM], [Status] ) values
  (N'天津分行1',1, '2016-10-01', N'Tom',1,1),
  (N'天津分行2',2, '2016-10-02', N'Tom',1,1),
  (N'天津分行3',3, '2016-10-03', N'Tom',1,1),
  (N'天津分行4',4, '2016-10-04', N'Tom',1,1),
  (N'天津分行5',5, '2016-10-05', N'Tom',1,1),
  (N'天津分行6',6, '2016-10-06', N'Tom',0,1),
  (N'天津分行7',7, '2016-10-07', N'Jack',1,2),
  (N'天津分行8',8, '2016-10-08', N'Jack',1,2),
  (N'天津分行9',9, '2016-10-09', N'Mary',1,3)

展望

本次開發的WhereEnhance方法,讓我們可以藉助表達式目錄樹相關的類和方法,根據需要動態構建查詢表達式,便於業務系統的擴展。但還存在一些不足,未來可以加強的功能包括:

  1. 當前對於查詢條件的組合順序上,僅支持按照標號,順序組合,未來可以支持通過增加括號,按照括號的優先級進行組合。
  2. 現在的查詢表達式的構建支持數字,字符串,枚舉和日期4種常用數據類型,未來會支持更多類型,例如各種類型對應的可空類型等。

表達式目錄樹系列全部文章

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