初探表达式目录树
文章目录:
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("不支持该方法"); } } }
让我们来测试下、、
========================================最后声明=======================================
博主菜鸟一只,欢迎看完文章的同学共同讨论,拍砖;有前辈看到了也希望能慷慨指点,感激不尽。