动态Linq 模糊查询 反射

488 阅读3分钟

微软文档

基于运行时状态进行查询 (C#)

问题描述

假设你有多个实体类型:

record Person(string LastName, string FirstName, DateTime DateOfBirth);
record Car(string Model, int Year);

对于这些实体类型中的任何一个,你都需要筛选并仅返回那些在其某个 string 字段内具有给定文本的实体。 对于 Person,你希望搜索 FirstName 和 LastName 属性:

string term = /* ... */;
var personsQry = new List<Person>()
    .AsQueryable()
    .Where(x => x.FirstName.Contains(term) || x.LastName.Contains(term));

但对于 Car,你希望仅搜索 Model 属性:

string term = /* ... */;
var carsQry = new List<Car>()
    .AsQueryable()
    .Where(x => x.Model.Contains(term));

尽管可以为 IQueryable<Person> 编写一个自定义函数,并为 IQueryable<Car> 编写另一个自定义函数,但以下函数会将此筛选添加到任何现有查询,而不考虑特定的元素类型如何。

微软示例

// using static System.Linq.Expressions.Expression;

IQueryable<T> TextFilter<T>(IQueryable<T> source, string term)
{
    if (string.IsNullOrEmpty(term)) { return source; }

    // T is a compile-time placeholder for the element type of the query.
    Type elementType = typeof(T);

    // Get all the string properties on this specific type.
    PropertyInfo[] stringProperties =
        elementType.GetProperties()
            .Where(x => x.PropertyType == typeof(string))
            .ToArray();
    if (!stringProperties.Any()) { return source; }

    // Get the right overload of String.Contains
    MethodInfo containsMethod = typeof(string).GetMethod("Contains", new[] { typeof(string) })!;

    // Create a parameter for the expression tree:
    // the 'x' in 'x => x.PropertyName.Contains("term")'
    // The type of this parameter is the query's element type
    ParameterExpression prm = Parameter(elementType);

    // Map each property to an expression tree node
    IEnumerable<Expression> expressions = stringProperties
        .Select(prp =>
            // For each property, we have to construct an expression tree node like x.PropertyName.Contains("term")
            Call(                  // .Contains(...) 
                Property(          // .PropertyName
                    prm,           // x 
                    prp
                ),
                containsMethod,
                Constant(term)     // "term" 
            )
        );

    // Combine all the resultant expression nodes using ||
    Expression body = expressions
        .Aggregate(
            (prev, current) => Or(prev, current)
        );

    // Wrap the expression body in a compile-time-typed lambda expression
    Expression<Func<T, bool>> lambda = Lambda<Func<T, bool>>(body, prm);

    // Because the lambda is compile-time-typed (albeit with a generic parameter), we can use it with the Where method
    return source.Where(lambda);
}

由于 TextFilter 函数采用并返回 IQueryable(而不仅仅是 IQueryable),因此你可以在文本筛选器后添加更多的编译时类型的查询元素。

var qry = TextFilter(
        new List<Person>().AsQueryable(), 
        "abcd"
    )
    .Where(x => x.DateOfBirth < new DateTime(2001, 1, 1));

var qry1 = TextFilter(
        new List<Car>().AsQueryable(), 
        "abcd"
    )
    .Where(x => x.Year == 2010);

利用反射 建立模糊查询通用工具类

public static IQueryable<T> TextFilter<T>(IQueryable<T> source, T param)
        {
            // 如果参数为空
            if (param == null) { return source; }
            // 获取对象参数
            Type elementType = typeof(T);
            // 在这个类型参数中获取所有的string类型
            // Get all the string properties on this specific type.
            PropertyInfo[] stringProperties =
                elementType.GetProperties()
                    .Where(x => x.PropertyType == typeof(string))
                    .ToArray();
            // 数组是否包含任何元素
            if (!stringProperties.Any()) { return source; }
            // Get the right overload of String.Contains
            // 创建 String 方法中的 Contains 重载
            MethodInfo containsMethod = typeof(string).GetMethod("Contains", new[] { typeof(string) });
            // Create a parameter for the expression tree:
            // the 'x' in 'x => x.PropertyName.Contains("term")'
            // The type of this parameter is the query's element type
            // 这是 Linq 表达式中的 x   the 'x' in 'x => x.PropertyName.Contains("term")'
            ParameterExpression prm = Expression.Parameter(elementType);

            List<Expression> list = new List<Expression>();
            // 遍历每个string类型
            foreach (PropertyInfo prp in stringProperties)
            {
                // 获取string类型的值
                String propertyValue = (String)prp.GetValue(param);
                // 如果是空,或者是null,不做处理
                if (String.IsNullOrEmpty(propertyValue))
                {
                    continue;
                }
                // 定义表达式
                var expression = Expression.Call(                  // .Contains(...) 
                    Expression.Property(                           // .PropertyName
                        prm,                                       // x 
                        prp
                    ),
                    containsMethod,
                    Expression.Constant(propertyValue)     // "term" 
                );
                list.Add(expression);
            }
            var expressions = list as IEnumerable<Expression>;
            // Combine all the resultant expression nodes using ||
            Expression body = expressions
                .Aggregate(
                    (prev, current) => Expression.And(prev, current)
                );

            // Wrap the expression body in a compile-time-typed lambda expression
            Expression<Func<T, bool>> lambda = Expression.Lambda<Func<T, bool>>(body, prm);

            // Because the lambda is compile-time-typed (albeit with a generic parameter), we can use it with the Where method
            return source.Where(lambda);
        }

调用

List<Person> src = new List<Person>() { new Person { LastName = "abcd", FirstName = "abcd" }, new Person { LastName = "abcd", FirstName = "a" }, new Person { LastName = "a", FirstName = "abcd" } };
            List<Car> src1 = new List<Car>() { new Car { Model = "abcd" }, new Car { Model = "a" }, new Car { Model = "c" } };
            var query1 = DynamicLinqUtils.TextFilter(
                src.AsQueryable(),
               new Person { LastName = "c", FirstName = "c" }
            )
           .Where(x => x.DateOfBirth < new DateTime(2001, 1, 1));

            var list1 = query1.ToList();
            var query2 = DynamicLinqUtils.TextFilter(
                src1.AsQueryable(),
               new Car { Model = "c" }
            );
            var list2 = query2.ToList();