文章目錄:
1、簡單的表達式樹實現以及聲明方式
2、表達式樹條件拼接
3、表達式樹關系映射
4、表達式樹訪問者
簡單介紹表達式樹
相信大家使用EF框架的時候,對實體集延遲查詢對象IQueryable一定不陌生,對實體集操作的時候,參數要求傳遞一個Expression<TDelegate>的泛型類,泛型參數是一個委托Expression;然后Expression<TDelegate>又繼承自LambdaExpression抽象類,其父類是Expression抽象類,Expression抽象類就是我們所說的表達式目錄樹基類,如下圖所示。
1、簡單的表達式樹實現以及聲明方式
首先我們看以下一段代碼
//普通的Lambda表達式 Func<int,int,int> func = (x,y)=> x + y - 2; //表達式目錄樹的Lambda表達式聲明方式 Expression<Func<int, int, int>> expression = (x, y) => x + y - 2; //表達式目錄樹的拼接方式實現 ParameterExpression parameterx = Expression.Parameter(typeof(int), "x"); ParameterExpression parametery = Expression.Parameter(typeof(int), "y"); ConstantExpression constantExpression = Expression.Constant(2, typeof(int)); BinaryExpression binaryAdd = Expression.Add(parameterx, parametery); BinaryExpression binarySubtract = Expression.Subtract(binaryAdd, constantExpression); Expression<Func<int, int, int>> expressionMosaic = Expression.Lambda<Func<int, int, int>>(binarySubtract, new ParameterExpression[] { parameterx, parametery }); int ResultLambda = func(5, 2); int ResultExpression = expression.Compile()(5, 2); int ResultMosaic = expressionMosaic.Compile()(5, 2); Console.WriteLine($"func:{ResultLambda}"); Console.WriteLine($"expression:{ResultExpression}"); Console.WriteLine($"expressionMosaic:{ResultMosaic}");
上面這段代碼分別用普通的委托,表達式目錄樹的lambda實現方式,表達式目錄樹的拼接方式實現了兩個變量相加再減去一個常量。結果很顯然都是5如下圖:
上面兩段代碼相信就不用解釋了,讓我們來看一下目錄樹拼接這段代碼,
ParameterExpression parameterx = Expression.Parameter(typeof(int), "x");//聲明一個參數表達式,int類型,名字叫“x”
ConstantExpression constantExpression = Expression.Constant(2, typeof(int));//聲明一個常量表達式,int類型,值為2
BinaryExpression binaryAdd = Expression.Add(parameterx, parametery); //二進制運算符表達式相加 BinaryExpression binarySubtract = Expression.Subtract(binaryAdd, constantExpression);//二進制運算符表達式相減
//將表達式樹翻譯成lambda表達式,並將變量參數傳入 Expression<Func<int, int, int>> expressionMosaic = Expression.Lambda<Func<int, int, int>>(binarySubtract, new ParameterExpression[] { parameterx, parametery });
//編譯執行 int ResultMosaic = expressionMosaic.Compile()(5, 2);
讓我們再來看一個例子
//目錄樹的Lambda聲明方式 Expression<Func<Book, bool>> expressionLambda = x => x.Id.ToString().Equals("6"); //目錄樹的變量聲明拼接方式 ParameterExpression parameterExpression = Expression.Parameter(typeof(Book), "x"); //聲明一個參數表達式,Book類型,名字叫“x” Expression<Func<Book, bool>> expression = Expression.Lambda<Func<Book, bool>>( Expression.Call( //Expression.Call創建一個表示帶參數的方法調用 Expression.Call( Expression.Property(parameterExpression, typeof(Book).GetProperty("Id")), //反射拿到Id屬性 typeof(Int32).GetMethod("ToString", new Type[] { }), //反射拿到Int類型的Tostring方法 new Expression[0]), //這里是沒有參數的 typeof(string).GetMethod("Equals", new Type[] { typeof(string) }), //反射拿到String的Equals 方法 new Expression[] { Expression.Constant("6",typeof(string)) //反射拿到String的Equals 5 }) , new ParameterExpression[] //最后一個參數,代表傳入的book { parameterExpression }); bool a = expressionLambda.Compile()(new Book { Id = 4, Name = "C#高級編程", Price = 100 }); bool b = expressionLambda.Compile()(new Book { Id = 6, Name = "CLR Via C#", Price = 100 }); bool c = expression.Compile()(new Book { Id = 4, Name = "C#高級編程", Price = 100 }); bool d = expression.Compile()(new Book { Id = 6, Name = "CLR Via C#", Price = 100 }); Console.WriteLine(a); Console.WriteLine(b); Console.WriteLine(c); Console.WriteLine(d); Console.Read();
相信到這里,對表達式目錄樹拼接有一個基本的認識了,第一個例子這個過程有點像后綴表達式(逆波蘭式)的執行。5+2-2 從左往右遍歷入棧,遇到運算符出棧運算后入棧。
2、表達式樹條件拼接
平時業務中,經常要根據用戶的輸入參數進行數據過濾,下面兩種方法我們一起對比一下
//這里用List 然后AsQueryable轉下 偷個懶。。
List<Book> books = new List<Book> { new Book{Id = 1,Name ="C#高級編程第1版",Price=100}, new Book{Id = 2,Name ="C#高級編程第2版",Price=110}, new Book{Id = 3,Name ="C#高級編程第3版",Price=120}, new Book{Id = 4,Name ="C#高級編程第5版",Price=130}, new Book{Id = 5,Name ="C#高級編程第5版",Price=140}, new Book{Id = 6,Name ="C#高級編程第5版",Price=150}, new Book{Id = 6,Name ="C#高級編程第7版",Price=160}, }; //這里我們查詢三個條件 QueryDto query = new QueryDto { Id = 3, Name = "第5版", Price = 140 }; //我們一般用EF查詢時候 var entitys = books.AsQueryable(); if (query.Id.HasValue) entitys = entitys.Where(t => t.Id == query.Id.Value); if (!string.IsNullOrEmpty(query.Name)) entitys = entitys.Where(t => t.Name.Contains(query.Name)); if (query.Price.HasValue) entitys = entitys.Where(t => t.Price == query.Price.Value); //表達式樹拼接方式 Expression<Func<Book, bool>> expression = t => true; ParameterExpression parameterExpression = Expression.Parameter(typeof(Book), "x"); var ee = typeof(int).GetMethods(); if (query.Id.HasValue) { MemberExpression memberExpressionId = Expression.Property(parameterExpression, typeof(Book).GetProperty("Id")); MethodCallExpression method = Expression.Call(memberExpressionId , typeof(int).GetMethod("Equals", new Type[] { typeof(int) }) , new Expression[] { Expression.Constant(query.Id.Value, typeof(int)) }); expression = Expression.Lambda<Func<Book, bool>>(method, new ParameterExpression[] { parameterExpression }); } if (!string.IsNullOrEmpty(query.Name)) { MemberExpression memberExpressionName = Expression.Property(parameterExpression, typeof(Book).GetProperty("Name")); MethodCallExpression method = Expression.Call(memberExpressionName , typeof(string).GetMethod("Contains", new Type[] { typeof(string) }) , new Expression[] { Expression.Constant(query.Name, typeof(string)) }); expression = Expression.Lambda<Func<Book, bool>>(method, new ParameterExpression[] { parameterExpression }); } if (query.Price.HasValue) { MemberExpression memberExpressionId = Expression.Property(parameterExpression, typeof(Book).GetProperty("Price")); MethodCallExpression method = Expression.Call(memberExpressionId , typeof(double).GetMethod("Equals", new Type[] { typeof(double) }) , new Expression[] { Expression.Constant(query.Price.Value, typeof(double)) }); expression = Expression.Lambda<Func<Book, bool>>(method, new ParameterExpression[] { parameterExpression }); } var entity = books.AsQueryable().Where(expression);
大家可以對比下這兩種方式那種比較好。第二種雖然要多寫代碼,但是防止了數據暴露的風險。
這里借鑒 騰飛(Jesse)前輩的幾張圖,博客地址=》 http://www.cnblogs.com/jesse2013/p/expressiontree-part1.html
3、表達式樹關系映射
平時工作中,經常會有這樣的需求,就是數據庫實體映射為DTO,返回給前端。例如這里的Book是我們的數據庫實體類,BookCopy 是Dto類
public class Book { [Key] public int Id { get; set; } public string Name { get; set; } public double Price { get; set; } } public class BookCopy { public int Id { get; set; } public string Name { get; set; } public double Price { get; set; } }
最簡單粗暴的方法應該是這樣。
//寫死的屬性映射 Book book = new Book { Id = 1, Name = "C#高級編程", Price = 100.00 }; BookCopy bookCopy = new BookCopy { Id = book.Id, Name = book.Name, Price = book.Price };
//目錄樹的方式 Expression<Func<Book, BookCopy>> expression = b => new BookCopy { Id = b.Id, Name = b.Name, Price = b.Price };
但是這樣寫的話特別不靈活,還是寫死的。假如我們的Book類又添加一個屬性"Press",那又得改動代碼,所以我們可以用反射的放射+表達式樹參數拼接來實現
//參數拼接的方式 ParameterExpression parameterExpression = Expression.Parameter(typeof(Book), "book"); List<MemberBinding> memberbindingList = new List<MemberBinding>(); //表示綁定的類派生自的基類,這些綁定用於對新創建對象的成員進行初始化(vs的注解。太生澀了,我這樣的小白解釋不了,大家將就着看) foreach (var item in typeof(BookCopy).GetProperties()) //遍歷BookCopy的所有屬性 { MemberExpression property = Expression.Property(parameterExpression, typeof(Book).GetProperty(item.Name));//拿到Book的這個屬性 MemberBinding memberBinding = Expression.Bind(item, property); //初始化這個屬性 memberbindingList.Add(memberBinding); } foreach(var item in typeof(BookCopy).GetFields()) { MemberExpression filed = Expression.Field(parameterExpression, typeof(Book).GetField(item.Name));//拿到book的這個字段,這里book類沒有字段。。 MemberBinding memberBinding = Expression.Bind(item, filed); memberbindingList.Add(memberBinding); } MemberInitExpression memberInitExpression = Expression.MemberInit(Expression.New(typeof(BookCopy)), memberbindingList);//初始化創建新對象 Expression<Func<Book,BookCopy>> ExpressionRun = Expression.Lambda<Func<Book, BookCopy>>(memberInitExpression, new ParameterExpression[]{ parameterExpression }); var ExpressionbookCopy = ExpressionRun.Compile()(book);
結果如上圖所示,但是問題又來了,我們不可能只有一個類,也不可能只有一個Dto,那我們應該怎么實現呢? 對 ,可以用泛型來實現
public class ExpressionMapper<TIn,TOut> { public static Func<TIn, TOut> _FuncCatch = null; //我們這里利用靜態類的特性作為一個緩存,靜態類跟隨類的初始化創建,而且有CLR保證單例的 static ExpressionMapper() { ParameterExpression parameterExpression = Expression.Parameter(typeof(TIn), "entity"); List<MemberBinding> memberbindingList = new List<MemberBinding>(); foreach (var item in typeof(TOut).GetProperties()) { MemberExpression property = Expression.Property(parameterExpression, typeof(TIn).GetProperty(item.Name)); MemberBinding memberBinding = Expression.Bind(item, property); memberbindingList.Add(memberBinding); } foreach (var item in typeof(TOut).GetFields()) { MemberExpression filed = Expression.Field(parameterExpression, typeof(TIn).GetField(item.Name)); MemberBinding memberBinding = Expression.Bind(item, filed); memberbindingList.Add(memberBinding); } MemberInitExpression memberInitExpression = Expression.MemberInit(Expression.New(typeof(TOut)), memberbindingList); Expression<Func<TIn, TOut>> ExpressionRun = Expression.Lambda<Func<TIn, TOut>>(memberInitExpression, new ParameterExpression[]{ parameterExpression }); _FuncCatch = ExpressionRun.Compile(); //靜態構造函數,每一次只有Tin或者Tout不同的時候才會創建新的變量_FuncCatch } public static TOut Mapping(TIn t) //取出TOut,靜態變量是一個Func,參數是傳入的Tin 實例 { return _FuncCatch(t); }
讓我們來測試一波性能如何=》
4、表達式樹訪問者
這里先讓我們看下IQueryable對象,IQueryable有三個屬性,Provider屬性就是拿到當前解析的表達式樹,並將表達式樹交給Expression(個人理解,不對地方希望大家指正)。
C#給我們提供了一個抽象類,ExpressionVisitor,我們可以通過這個繼承抽象類,重寫這個抽象類的方法來訪問表達式樹中的各個節點,達到自己預定義的效果
public class MyOperationVisitor : ExpressionVisitor //定義自己的表達式樹訪問者類,繼承ExpressionVisitor抽象類 { public Expression Modify(Expression expression) //對外公開的方法 { return this.Visit(expression); } protected override Expression VisitBinary(BinaryExpression node) //假如這里是個二元運算,代碼運行我們重寫VisitBinary方法的邏輯 { if(node.NodeType == ExpressionType.Add) //假如這里是個加法運算我們給它改成一個減法 { Expression left = this.Visit(node.Left); Expression right = this.Visit(node.Right); return Expression.Subtract(left, right); } if(node.NodeType == ExpressionType.LessThan) //假如這里是個<運算我們給它改成> { Expression left = this.Visit(node.Left); Expression right = this.Visit(node.Right); return Expression.GreaterThan(left, right); } return base.VisitBinary(node); } protected override Expression VisitConstant(ConstantExpression node) //假如節點中存在常量,我們打印個hahahah { Console.WriteLine("hahahah"); return base.VisitConstant(node); } }
這里的 x*y+2目錄樹的訪問方式就像右圖我畫的那樣,二叉樹的中序遍歷。這里再借用騰飛前輩博客里面的一張圖
接下來,趁熱打鐵,讓我們來根據表達式樹的條件生成自定義SQL腳本===================華麗的分割線====================================================
public class MyConditionVisitor: ExpressionVisitor { private Queue<string> _queueCommand = new Queue<string>(); //這里我們用一個隊列來保存生成的腳本 public string Condition() //返回腳本 { string condition = string.Concat(_queueCommand.ToArray()); this._queueCommand.Clear(); return condition; } /// <summary> /// 處理二元表達式 /// </summary> /// <param name="node"></param> /// <returns></returns> protected override Expression VisitBinary(BinaryExpression node) { if (node == null) throw new ArgumentException("BinaryExpression"); this._queueCommand.Enqueue("("); base.Visit(node.Left); this._queueCommand.Enqueue($" {node.NodeType.ToSqlCommandString()} "); base.Visit(node.Right); this._queueCommand.Enqueue(")"); return node; } /// <summary> /// 訪問每一個成員 /// </summary> /// <param name="node"></param> /// <returns></returns> protected override Expression VisitMember(MemberExpression node) { if(node ==null) throw new ArgumentException("MemberExpression"); this._queueCommand.Enqueue($"[{node.Member.Name}]"); return node; } /// <summary> /// 常亮表達式 /// </summary> /// <param name="node"></param> /// <returns></returns> protected override Expression VisitConstant(ConstantExpression node) { if (node == null) throw new ArgumentNullException("ConstantExpression"); switch (node.Value.GetType().Name) { case "String": this._queueCommand.Enqueue($" ' {node.Value} '"); break; case "Boolean": this._queueCommand.Enqueue((bool)node.Value ? "1" : "0"); break; default: this._queueCommand.Enqueue(node.Value.ToString()); break; } return node; } /// <summary> /// 方法表達式 /// </summary> /// <param name="node"></param> /// <returns></returns> protected override Expression VisitMethodCall(MethodCallExpression node) { if (node == null) throw new ArgumentNullException("MethodCallExpression"); string format = string.Empty; switch (node.Method.Name) { case "StartsWith": format = "({0} LIKE {1}+'%')"; break; case "Contains": format = "({0} LIKE '%'+{1}+'%')"; break; case "EndsWith": format = "({0} LIKE '%'+{1})"; break; default: throw new NotSupportedException(node.NodeType + " is not supported!"); } this.Visit(node.Object); this.Visit(node.Arguments[0]); this._queueCommand.Enqueue(string.Format(format, node.Object, node.Arguments[0])); return node; } }
這里我們把二元運算符用擴展方法的形式標識,代碼如下
/// <summary> /// 使用一個擴展方法處理二元運算符 /// </summary> public static class CommandCreaterHelper { public static string ToSqlCommandString(this ExpressionType type) { switch (type) { case (ExpressionType.AndAlso): case (ExpressionType.And): return "AND"; case (ExpressionType.OrElse): case (ExpressionType.Or): return "OR"; case (ExpressionType.Not): return "NOT"; case (ExpressionType.NotEqual): return "<>"; case ExpressionType.GreaterThan: return ">"; case ExpressionType.GreaterThanOrEqual: return ">="; case ExpressionType.LessThan: return "<"; case ExpressionType.LessThanOrEqual: return "<="; case (ExpressionType.Equal): return "="; default: throw new Exception("不支持該方法"); } } }
讓我們來測試下、、
========================================最后聲明=======================================
博主菜鳥一只,歡迎看完文章的同學共同討論,拍磚;有前輩看到了也希望能慷慨指點,感激不盡。