使用Expression实现数据的任意字段过滤(1)

时间:2021-05-27 16:20:40

在项目常常要和数据表格打交道。 现在BS的通常做法都是前端用一个js的Grid控件, 然后通过ajax的方式从后台加载数据, 然后将数据和Grid绑定。 数据往往不是一页可以显示完的, 所以要加分页;然后就是根据关键字段做排序, 做筛选过滤。 作为后端人员, 要考虑的是如何优雅的实现分页、排序、筛选的功能。

本文先谈谈筛选。 因为分页、排序、筛选这3个动作, 一定是先处理筛选的——筛选后的结果再去排序, 然后再做分页 , 才有意义。

筛选首先要考虑如下两个问题:

1) 字段类型

2) 比较方式

以下面的模拟数据为例( 该数据为服务器的性能监控, 包括处理器、内存的监控结果和时间)。

ServerName

ProcessorMaxValue

ProcessorMinValue

ProcessorAvgValue

MemoryMaxValue

MemoryMinValue

MemoryAvgValue

DateTime

Server1

8

3

3.29

82.18

82.11

82.14

2016/10/1

Server1

10

3

3.29

82.23

82.12

82.17

2016/10/2

Server1

11

3

3.32

82.21

82.15

82.18

2016/10/3

Server1

10

3

3.29

82.21

82.10

82.16

2016/10/4

Server1

10

3

3.42

82.20

82.12

82.15

2016/10/5

Server2

10

3

3.40

82.20

82.12

82.16

2016/10/6

Server2

9

3

4.08

82.22

82.11

82.15

2016/10/7

Server2

10

3

3.69

82.20

82.12

82.16

2016/10/8

Server3

11

3

4.13

82.21

82.14

82.16

2016/10/9

Server3

11

3

4.03

82.20

82.15

82.17

2016/10/10

 

对于用户来讲, 可能会使用所有的字段来做过滤。比如 "ServerName like 'Server'", "ProcessorMaxValue>10 ", "DateTime < '2016/10/9'"。

 

小结下, 比较常见的字段类型有字符串、数值、日期,以及boolean值。为什么要强调字段类型, 因为一样的值在不同的字段类型要求下, 比较结果是不同的, 比如说数字11>2 , 但字符串”11”<”2”。

其次考虑比较方式, 比较常见的有“大于、大于等于、等于、小于等于、小于、不等于”, 其次还有 “in (…set)”; 字符串类型可能有”包含”, “开头匹配”, “结尾匹配”等。

 

如果需求比较固定,直接在代码中依次处理有限的若干字段的筛选完全不是事。可是实际的项目中,这种情况很少。 更多的是, 客户一会要加这条件, 一会要加那条件。 如果都老老实实的一个一个加, 代码就很容易臃肿,甚至失控了。

本文中推荐的是使用Expression方案。 由于Expression是 对集合进行操作, 所以不使用于自己拼SQL然后使用SQLCommand的场景。比较适用于:

1) 使用EntityFramework作为ORM框架的

2) 直接对全集合处理的

先感受下代码

  1 public class CriteriaCollectionHandler : ICollectionHandler
  2    {
  3        /* By Harvey Hu. @2016 */
  4  
  5        protected string PropertyName { get; set; }
  6  
  7        protected ComparerEnum Comparer { get; set; }
  8  
  9        protected object Target { get; set; }  // 
 10  
 11        public CriteriaCollectionHandler(string propertyName, object target, ComparerEnum comparer)
 12        {
 13            this.PropertyName = propertyName;
 14            this.Comparer = comparer;
 15            this.Target = target;
 16        }
 17  
 18        private IQueryable<T> Filter<T>(IQueryable<T> source, string propertyName, ComparerEnum comparer, object target)
 19        {
 20            var type = typeof(T);
 21            var property = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
 22  
 23  
 24  
 25            var parameter = Expression.Parameter(type, "p");
 26            Expression propertyAccess = Expression.MakeMemberAccess(parameter, property);
 27            if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
 28            {
 29                var getValueOrDefault = property.PropertyType.GetMethods().First(p => p.Name == "GetValueOrDefault");
 30                propertyAccess = Expression.Call(propertyAccess, getValueOrDefault);
 31            }
 32            var constExpression = Expression.Constant(ConvertTo(target, property.PropertyType)); // 转换为target的类型,以作比较
 33            Expression comparisionExpression;
 34            switch (comparer)
 35            {
 36                case ComparerEnum.Eq:
 37                    comparisionExpression = Expression.Equal(propertyAccess, constExpression);
 38                    break;
 39                case ComparerEnum.Ne:
 40                    comparisionExpression = Expression.NotEqual(propertyAccess, constExpression);
 41                    break;
 42                case ComparerEnum.Lt:
 43                    comparisionExpression = Expression.LessThan(propertyAccess, constExpression);
 44                    break;
 45                case ComparerEnum.Gt:
 46                    comparisionExpression = Expression.GreaterThan(propertyAccess, constExpression);
 47                    break;
 48                case ComparerEnum.Le:
 49                    comparisionExpression = Expression.LessThanOrEqual(propertyAccess, constExpression);
 50                    break;
 51                case ComparerEnum.Ge:
 52                    comparisionExpression = Expression.GreaterThanOrEqual(propertyAccess, constExpression);
 53                    break;
 54                case ComparerEnum.StringLike:
 55                    if (property.PropertyType != typeof(string))
 56                    {
 57                        throw new NotSupportedException("StringLike is only suitable for string type property!");
 58                    }
 59  
 60  
 61                    var stringContainsMethod = typeof(CriteriaCollectionHandler).GetMethod("StringContains");
 62  
 63                    comparisionExpression = Expression.Call(stringContainsMethod, propertyAccess, constExpression);
 64  
 65                    break;
 66                default:
 67                    comparisionExpression = Expression.Equal(propertyAccess, constExpression);
 68                    break;
 69            }
 70  
 71  
 72            var compareExp = Expression.Lambda(comparisionExpression, parameter);
 73            var typeArguments = new Type[] { type };
 74            var methodName = "Where"; //sortOrder == SortDirection.Ascending ? "OrderBy" : "OrderByDescending";
 75            var resultExp = Expression.Call(typeof(Queryable), methodName, typeArguments, source.Expression, Expression.Quote(compareExp));
 76  
 77            return source.Provider.CreateQuery<T>(resultExp);
 78        }
 79  
 80        public static bool StringContains(string value, string subValue)
 81        {
 82            if (value == null)
 83            {
 84                return false;
 85            }
 86  
 87            return value.Contains(subValue);
 88        }
 89  
 90  
 91        protected object ConvertTo(object convertibleValue, Type targetType)
 92        {
 93            if (null == convertibleValue)
 94            {
 95                return null;
 96            }
 97  
 98            if (!targetType.IsGenericType)
 99            {
100                return Convert.ChangeType(convertibleValue, targetType);
101            }
102            else
103            {
104                Type genericTypeDefinition = targetType.GetGenericTypeDefinition();
105                if (genericTypeDefinition == typeof(Nullable<>))
106                {
107                    var temp = Convert.ChangeType(convertibleValue, Nullable.GetUnderlyingType(targetType));
108                    var result = Activator.CreateInstance(targetType, temp);
109                    return result;
110                }
111            }
112            throw new InvalidCastException(string.Format("Invalid cast from type \"{0}\" to type \"{1}\".", convertibleValue.GetType().FullName, targetType.FullName));
113        }
114  
115  
116        public virtual ICollection<T> Execute<T>(ICollection<T> values)
117        {
118            var result = Filter(values.AsQueryable(), this.PropertyName, this.Comparer, this.Target).ToList();
119            return result;
120        }
121  
122    }

 

使用示例(伪码):

1 var criteria1 = New CriteriaCollectionHandler(“ServerName”, “server”, ComparerEnum.StringLike);  // serverName like 'server'”
2 var criteria2 = New CriteriaCollectionHandler(“ProcessorMaxValue”, 10, ComparerEnum.Gt);
3 var criteria3 = New CriteriaCollectionHandler(“Datetime”, Datetime.Parse("2016/12/9"), ComparerEnum.lt);
4 ICollection<T> result =  criteria3.Execute(
5                                             criteria2.Execute(
6                                                      criteria1.Execute(YourDataCollection)));

 

 

核心是Filter()方法 ——IQueryable<T> Filter<T>(IQueryable<T> source, string propertyName, ComparerEnum comparer, object target)。

ICollectionHandler是用来处理集合Collection的对象接口,前面提到的分页、排序和筛选处理, 都可以适用于这个接口。这个接口的Execute方法处理一个集合,并返回一个集合。筛选也是这个逻辑,所以适用这个接口。

在Filter()方法中, 通过Expression构建了一个Lamda表达式, 如p=>p.Property == target。这个表达式有几个问题需要注意下:

1) 如何取到p.Property? 通过类型反射获取。

2) 如何取到判断操作? 根据比较符comparer枚举。如果是常规比较, 则直接调用Expression的相关方法生成, 比如Expression.Equal(); 如果是特殊, 则通过Expression.Call调用自定义的方法生成, 比如StringLike

3) 比较值的类型用什么?获取p.Property类型,并将target强制转换为该类型;参考ConvertTo()方法。

4) 是否支持Nullable类型?支持。但这个是个比较坑的事情。因为Nullable<T>实际上不支持和T的直接比较,所以不能将target转换为Nullable<T>类型,只能是T类型,因此lamda表达式只能用p=>p.Property.GetValueOrDefault() == target 规格来处理 。所以在ConvertTo()方法中, 对nullable<T>类型也做了判断处理。

Lamda表达式构造好了, 就可以通过Linq的Where方法来实现筛选了。这同样适用Expression的Call方法构造出来。最后通过IQuaryable的IQueryProvider
的CreateQuery()方法完成调用。

 5) 是否支持其他比较操作? 通过适当的扩展,我想应该可以实现的。 比如StringLike就是我们自己扩展的比较方法, 当然这个不是EntityFramework提供的,所以不支持EF的Queryable。

 

代码实现分析到此暂告段落。 在实际使用中, 将每个条件都分别封装成CriteriaCollectionHandler对象, 然后依次调用即可完成”逻辑与”的操作。参考上面的实现示例。

如果要实现”逻辑或”怎么办?目前的考虑结果是将两个集合intersect()处理。如果各位有什么更好的办法, 欢迎回复讨论。

 

下一篇《使用Expression实现数据的任意字段过滤(2)》, 我将讨论下一些特殊字段的情况, 比如非Public的Property过滤。

 

注: 使用Expression过程也参考了博客园的其他朋友的文章。在此贡献出来, 也希望能帮助一些朋友。