上篇文章中,对表达式树的基础概念,基本用法进行了介绍,现在来进一步学习,使用表达式树创建动态查询。
一、应用场景
列表排序是最常见的功能之一,在列表字段很多的情况下,用户要求可以按任意列正序或倒序,甚至多个列排序,后台服务应该如何实现呢?
举例Person类,有Name,Age,Gender,Ht,Wt等等属性列,此时用户要按【Gender正序,Age正序,Wt倒序】排序,前端传过来的排序参数应该是类似这样{"ordering":"Gender,Age,Wt DESC"},后台应该如何实现排序呢?
二、解决方案
这时候我们就需要对IQueryable<TSource>增加一个静态扩展了,比如我们定义为这样:
public static IOrderedQueryable<TSource> MyOrderBy<TSource>(this IQueryable<TSource> source, string ordering);
调用的时候就可以类似这样:
IOrderedQueryable<Person> personQ = _personRepository.MyOrderBy(input.ordering);
是不是很完美?!现在的问题就是怎样通过表达式树技术来实现这个MyOrderBy扩展方法,实现代码如下(看注释):
1 public static class MyQueryable 2 { 3 public static IOrderedQueryable<TSource> MyOrderBy<TSource>(this IQueryable<TSource> source, string ordering) 4 { 5 Expression expression = source.Expression; 6 var orders = ordering.Split(',')//分解为多个排序字段 7 .Select(x => x.Trim())//去除前后空格 8 .Select(x => new Regex("[\\s]+").Replace(x, "-"))//中间的任意个空格替换为- 9 .ToList(); 10 string sortingMethod = "OrderBy"; 11 for (int i = 0; i < orders.Count; i++) 12 { 13 var orderArray = orders[i].Split('-'); 14 if (i == 0) 15 { 16 sortingMethod = "OrderBy"; 17 if (orderArray.Length == 2 && (orderArray[1].ToUpper() == "DESC" || orderArray[1].ToUpper() == "DESCENDING")) 18 { 19 sortingMethod = "OrderByDescending"; 20 } 21 } 22 else 23 { 24 sortingMethod = "ThenBy"; 25 if (orderArray.Length == 2 && (orderArray[1].ToUpper() == "DESC" || orderArray[1].ToUpper() == "DESCENDING")) 26 { 27 sortingMethod = "ThenByDescending"; 28 } 29 } 30 //泛型参数 31 ParameterExpression param = Expression.Parameter(typeof(TSource), "x"); 32 //排序字段属性 33 PropertyInfo pInfo = typeof(TSource).GetProperty(orderArray[0]); 34 //类型参数数组 35 Type[] typeArgs = new Type[] { typeof(TSource), pInfo.PropertyType }; 36 //获取排序字段,即Func<TSource, TKey> keySelector 37 LambdaExpression keySelector = Expression.Lambda(Expression.Property(param, orderArray[0]), param); 38 //方法调用表达式 39 expression = Expression.Call(typeof(Queryable), sortingMethod, typeArgs, 40 expression, keySelector); 41 } 42 //通过表达式树创建动态可执行查询 43 var query = (IOrderedQueryable<TSource>)source.Provider.CreateQuery<TSource>(expression); 44 return query; 45 } 46 public static IQueryable<TSource> MyPage<TSource>(this IQueryable<TSource> source, int pageNumber, int pageSize) 47 { 48 return source.Skip(pageNumber * pageSize).Take(pageSize); 49 } 50 public static IQueryable<TSource> MySortAndPage<TSource>(this IQueryable<TSource> source, string ordering, int pageNumber, int pageSize) 51 { 52 return source.MyOrderBy(ordering).MyPage(pageNumber, pageSize); 53 } 54 }
既然,我们是为了练习通过表达式树创建动态可执行查询,不妨再来一种写法来做对比,代码如下(看注释):
1 public static IOrderedQueryable<TSource> MyOrderBy<TSource>(this IQueryable<TSource> source, string ordering) 2 { 3 Object query = source; 4 var orders = ordering.Split(',')//分解为多个排序字段 5 .Select(x => x.Trim())//去除前后空格 6 .Select(x => new Regex("[\\s]+").Replace(x, "-"))//中间的任意个空格替换为- 7 .ToList(); 8 string sortingMethod = "OrderBy"; 9 for (int i = 0; i < orders.Count; i++) 10 { 11 var orderArray = orders[i].Split('-'); 12 if (i == 0) 13 { 14 sortingMethod = "OrderBy"; 15 if (orderArray.Length == 2 && (orderArray[1].ToUpper() == "DESC" || orderArray[1].ToUpper() == "DESCENDING")) 16 { 17 sortingMethod = "OrderByDescending"; 18 } 19 } 20 else 21 { 22 sortingMethod = "ThenBy"; 23 if (orderArray.Length == 2 && (orderArray[1].ToUpper() == "DESC" || orderArray[1].ToUpper() == "DESCENDING")) 24 { 25 sortingMethod = "ThenByDescending"; 26 } 27 } 28 //泛型类型 29 Type type = typeof(TSource); 30 //排序字段属性 31 PropertyInfo pInfo = type.GetProperty(orderArray[0]); 32 //泛型参数 33 ParameterExpression param = Expression.Parameter(type, "x"); 34 //构建Func<,>泛型类型,见博文http://www.cnblogs.com/jiujiduilie/p/8654576.html 35 Type delegateType = typeof(Func<,>).MakeGenericType(typeof(TSource), pInfo.PropertyType); 36 //获取排序字段,即x=>x.(orderArray[0]) 37 LambdaExpression keySelector = Expression.Lambda(delegateType, Expression.Property(param, pInfo), param); 38 //反射方法调用 39 query = (typeof(Queryable).GetMethods() 40 .Single(x => String.Equals(x.Name, sortingMethod, StringComparison.Ordinal) 41 && x.IsGenericMethodDefinition 42 && x.GetGenericArguments().Length == 2 43 && x.GetParameters().Length == 2) 44 .MakeGenericMethod(typeof(TSource), pInfo.PropertyType) 45 .Invoke(null, new Object[] { query, keySelector })); 46 } 47 return (IOrderedQueryable<TSource>)query; 48 }
表达式树结合反射,可以写出通用而强大的功能代码。一下学习了两种排序动态查询方法,相信你对表达式树的用法更加熟悉了^_^
然鹅,你表示这个已经有现成的实现了,是的,没错!我正要说的第三种实现,就是来自微软官方的Linq扩展:
1 // D:\...\packages\System.Linq.Dynamic.Core.1.0.6.13\lib\net45\System.Linq.Dynamic.Core.dll 2 namespace System.Linq.Dynamic.Core 3 { 4 public static class DynamicQueryableExtensions 5 { 6 //... 7 public static IOrderedQueryable<TSource> OrderBy<TSource>(this IQueryable<TSource> source, string ordering, params object[] args); 8 public static IOrderedQueryable OrderBy(this IQueryable source, string ordering, params object[] args); 9 //... 10 } 11 }
其内部实现与上述两种均不同,实现原理大概是将原表达式树翻译为SQL语句的基础上直接附加排序字符串参数,有兴趣的童鞋可以自行反编译查看实现代码,就不在这里贴图了。
官方实现固然可以直接拿来用,然鹅,哪有自己的实现灵活可变通?!而且最主要还是为了学习表达式树,扩展开发思路嘛^_^
三、举一反三
上面我们举栗了排序的动态查询,那么过滤的动态查询呢?也是一样的道理。各种动态查询,都是一个道理,相信你已经掌握了。
贴出动态过滤查询的代码,仅供参考:
1 public static IQueryable<TSource> MyWhere<TSource>(this IQueryable<TSource> source, Dictionary<string, string> predicates, bool andOr) 2 { 3 var type = typeof(TSource); 4 //构造常量True或False 5 Expression expression = Expression.Constant(andOr); 6 //构造参数,类似于x=>...中的x 7 ParameterExpression param = Expression.Parameter(type, "x"); 8 //遍历所有过滤字段,生成查询条件 9 foreach (string key in predicates.Keys) 10 { 11 //访问过滤字段属性信息 12 var pInfo = type.GetProperty(key); 13 //生成类似于“x.Name==常量”的表达式 14 Expression expressionTmp = Expression.Equal( 15 //x.Name 16 Expression.Property(param, pInfo), 17 //将value字符串值转换为字段相应类型,才可以进行比较哦 18 Expression.Constant(Convert.ChangeType(predicates[key], pInfo.PropertyType))); 19 /*错误写法示例,因为IQueryable(linq-to-sql)里面是不允许使用ToString()的,如果是IEnumerable则随意 20 Expression expressionTmp = Expression.Equal( 21 Expression.Call(Expression.Property(param, pInfo), type.GetMethod("ToString")), 22 Expression.Constant(predicates[key]));*/ 23 //合并查询条件 24 if (andOr) 25 expression = Expression.And(expression, expressionTmp); 26 else 27 expression = Expression.Or(expression, expressionTmp); 28 } 29 //生成Lambda表达式树 30 Expression<Func<TSource, bool>> lambdaExpression = Expression.Lambda<Func<TSource, bool>>(expression, param); 31 return source.Where(lambdaExpression); 32 }
测试调用代码如下:
1 Dictionary<string, string> predicates = new Dictionary<string, string>(); 2 predicates.Add("Name", "SONG"); 3 predicates.Add("Gender", "男"); 4 predicates.Add("Age", "30"); 5 //表达式相当于x => (((True And (x.Name == "SONG")) And (x.Gender == "男")) And (x.Age == 20)) 6 IQueryable<Person> personQ = _personRepository.MyWhere(predicates,true); 7 //表达式相当于x => (((False Or (x.Name == "SONG")) Or (x.Gender == "男")) Or (x.Age == 20)) 8 IQueryable<Person> personQ = _personRepository.MyWhere(predicates,false);
可以愉快的做动态过滤查询了。
最后再来一个扩展学习,上面合并查询条件的两行代码看起来似乎有点不合习惯?!
1 expression = Expression.And(expression, expressionTmp); 2 expression = Expression.Or(expression, expressionTmp);
你或许更习惯于这样编码:
1 expression = expression.MyAnd(expressionTmp); 2 expression = expression.MyOr(expressionTmp);
聪明如你,可以增加两个扩展来实现:
1 public static Expression MyOr(this Expression exprleft, Expression exprright) 2 { 3 return Expression.OrElse(exprleft, exprright); 4 } 5 public static Expression MyAnd(this Expression exprleft, Expression exprright) 6 { 7 return Expression.AndAlso(exprleft, exprright); 8 }
然鹅,如果是我们常用的LambdaExpression<Func<TSource, bool>>就不可以这样简单的处理了,需要处理参数。
空谈误国,放码过来^_^
1 public static Expression<Func<TSource, bool>> MyOrElse<TSource>(this Expression<Func<TSource, bool>> exprleft, Expression<Func<TSource, bool>> exprright) 2 { 3 var invokedExpr = Expression.Invoke(exprright, exprleft.Parameters.Cast<Expression>()); 4 return Expression.Lambda<Func<TSource, bool>>(Expression.OrElse(exprleft.Body, invokedExpr), exprleft.Parameters); 5 } 6 public static Expression<Func<TSource, bool>> MyAndAlso<TSource>(this Expression<Func<TSource, bool>> exprleft, Expression<Func<TSource, bool>> exprright) 7 { 8 var invokedExpr = Expression.Invoke(exprright, exprleft.Parameters.Cast<Expression>()); 9 return Expression.Lambda<Func<TSource, bool>>(Expression.AndAlso(exprleft.Body, invokedExpr), exprleft.Parameters); 10 }
至此,关于使用表达式树创建动态查询就介绍完了。后续再来继续探讨下如何通过翻译表达式树数据结构为特定的查询语言,实现对特定数据源的查询,也即LINQ-to-SQL的实现原理。