如何使用 Expression 使用 Where 和 OR 构建动态查询

How to build dynamic query with Where and OR using Expression

希望有人能指导和帮助我解决这个问题。我们有一个使用 ExpressionHelper class 的继承项目。基本上,这个表达式助手将 return 一个 IQueryable 构建基于用户提供的搜索词的动态查询。

例如,我在下面的代码中传递了 2 个搜索词。

    IQueryable<UserEntity> modifiedQuery = _uow.UserRepository.GetAll();;

    var searchTerms = new List<SearchTerm>
    {
        new SearchTerm { Name = "FirstName", Operator = "eq", Value = "Bob" },
        new SearchTerm { Name = "FirstName", Operator = "eq", Value = "John" }
    };

    foreach (var searchTerm in searchTerms)
    {
        var propertyInfo = ExpressionHelper
            .GetPropertyInfo<TEntity>(searchTerm.EntityName ?? searchTerm.Name);

        var obj = ExpressionHelper.Parameter<TEntity>();

        var left = ExpressionHelper.GetPropertyExpression(obj, propertyInfo);
        var right = searchTerm.ExpressionProvider.GetValue(searchTerm.Value);
        var comparisonExpression = searchTerm.ExpressionProvider
            .GetComparison(left, searchTerm.Operator, right);

        // x => x.Property == "Value"
        var lambdaExpression = ExpressionHelper
            .GetLambda<TEntity, bool>(obj, comparisonExpression);

        // query = query.Where...
        modifiedQuery = ExpressionHelper.CallWhere(modifiedQuery, lambdaExpression);
    }

使用上面的代码并使用下面的 ExpressionHelper class,当我使用 SQLProfiler 检查时,这会生成下面的 SQL 查询。请注意查询中的 AND。我其实什么是OR.

在 SQL Profiler

中构建了 QUERY
SELECT 
    [Extent1].[FirstName] AS [FirstName], 
    FROM [dbo].[tblUser] AS [Extent1]
WHERE ([Extent1].[Conatact1] = N'Bob') AND ([Extent1].[Contact2] = N'John')

ExpressionHelper.cs

public static class ExpressionHelper
{
    private static readonly MethodInfo LambdaMethod = typeof(Expression)
        .GetMethods()
        .First(x => x.Name == "Lambda" && x.ContainsGenericParameters && x.GetParameters().Length == 2);

    private static MethodInfo[] QueryableMethods = typeof(Queryable)
        .GetMethods()
        .ToArray();

    private static MethodInfo GetLambdaFuncBuilder(Type source, Type dest)
    {
        var predicateType = typeof(Func<,>).MakeGenericType(source, dest);
        return LambdaMethod.MakeGenericMethod(predicateType);
    }

    public static PropertyInfo GetPropertyInfo<T>(string name)
        => typeof(T).GetProperties()
        .Single(p => p.Name == name);

    public static ParameterExpression Parameter<T>()
        => Expression.Parameter(typeof(T));

    public static MemberExpression GetPropertyExpression(ParameterExpression obj, PropertyInfo property)
        => Expression.Property(obj, property);

    public static LambdaExpression GetLambda<TSource, TDest>(ParameterExpression obj, Expression arg)
        => GetLambda(typeof(TSource), typeof(TDest), obj, arg);

    public static LambdaExpression GetLambda(Type source, Type dest, ParameterExpression obj, Expression arg)
    {
        var lambdaBuilder = GetLambdaFuncBuilder(source, dest);
        return (LambdaExpression)lambdaBuilder.Invoke(null, new object[] { arg, new[] { obj } });
    }

    public static IQueryable<T> CallWhere<T>(IQueryable<T> query, LambdaExpression predicate)
    {
        var whereMethodBuilder = QueryableMethods
            .First(x => x.Name == "Where" && x.GetParameters().Length == 2)
            .MakeGenericMethod(new[] { typeof(T) });

        return (IQueryable<T>)whereMethodBuilder
            .Invoke(null, new object[] { query, predicate });
    }

    public static IQueryable<TEntity> CallOrderByOrThenBy<TEntity>(
        IQueryable<TEntity> modifiedQuery,
        bool useThenBy,
        bool descending,
        Type propertyType,
        LambdaExpression keySelector)
    {
        var methodName = "OrderBy";
        if (useThenBy) methodName = "ThenBy";
        if (descending) methodName += "Descending";

        var method = QueryableMethods
            .First(x => x.Name == methodName && x.GetParameters().Length == 2)
            .MakeGenericMethod(new[] { typeof(TEntity), propertyType });

        return (IQueryable<TEntity>)method.Invoke(null, new object[] { modifiedQuery, keySelector });
    }
}

我很难理解查询是如何创建的以及如何将其更改为已创建查询中的 OR

希望有人能指导我并指出正确的方向。谢谢!

SearchTerm 添加一个新的 属性(此处为 C# 6.0 语法):

    // This is quite wrong. We should have an enum here, but Operator is 
    // done as a string, so I'm maintaining the "style"
    // Supported LogicalConnector: and, or
    public string LogicalConnector { get; set; } = "and";
}

然后:

private static IQueryable<TEntity> BuildQuery<TEntity>(IQueryable<TEntity> modifiedQuery, List<SearchTerm> searchTerms)
{
    Expression comparisonExpressions = null;

    var obj = ExpressionHelper.Parameter<TEntity>();

    foreach (var searchTerm in searchTerms)
    {
        var propertyInfo = ExpressionHelper
            .GetPropertyInfo<TEntity>(searchTerm.EntityName ?? searchTerm.Name);

        var left = ExpressionHelper.GetPropertyExpression(obj, propertyInfo);
        var right = searchTerm.ExpressionProvider.GetValue(searchTerm.Value);
        var comparisonExpression = searchTerm.ExpressionProvider.GetComparison(left, searchTerm.Operator, right);

        if (comparisonExpressions == null)
        {
            comparisonExpressions = comparisonExpression;
        }
        else if (searchTerm.LogicalConnector == "and")
        {
            comparisonExpressions = Expression.AndAlso(comparisonExpressions, comparisonExpression);
        }
        else if (searchTerm.LogicalConnector == "or")
        {
            comparisonExpressions = Expression.OrElse(comparisonExpressions, comparisonExpression);
        }
        else
        {
            throw new NotSupportedException(searchTerm.LogicalConnector);
        }
    }

    if (comparisonExpressions != null)
    {
        // x => x.Property == "Value"
        var lambdaExpression = ExpressionHelper.GetLambda<TEntity, bool>(obj, comparisonExpressions);
        // query = query.Where...
        modifiedQuery = ExpressionHelper.CallWhere(modifiedQuery, lambdaExpression);
    }

    return modifiedQuery;
}

像这样使用它:

var searchTerms = new List<SearchTerm>
{
    new SearchTerm { Name = "PrimaryContact", Operator = "eq", Value = "Bob" },
    new SearchTerm { Name = "SecondaryContact", Operator = "eq", Value = "Bob" },
    new SearchTerm { Name = "PrimaryContact", Operator = "eq", Value = "John", LogicalConnector = "or", }
};

IQueryable<UserEntity> query = BuildQuery<UserEntity>(modifiedQuery, searchTerms);

请注意,此代码中无法显式设置括号,将隐式设置为:

(((A opB b) opC C) opD D)

其中 ABCDSearchTerm[0]SearchTerm[1]SearchTerm[2]SearchTerm[3]opBopCopD是定义在SearchTerm[1].LogicalConnectorSearchTerm[2].LogicalConnectorSearchTerm[3].LogicalConnector.

中的运算符

虽然放置括号很容易,但选择如何“描述”它们却很复杂,除非您显着更改 SearchTerm 集合(它不能是“线性”数组,但需要是树)。

P.S。我错了,你不需要 ExpressionVisitor。当您尝试“合并”具有不同 ParameterExpression 的多个 LambdaExpression 时,您需要一个 ExpressionVisitor。在这段代码中,我们可以对所有查询使用一个 var obj = ExpressionHelper.Parameter<TEntity>(),因此合并条件没有问题。明确一点:如果你想将 x1 => x1.Foo == "Foo1"x2 => x2.Foo == "Foo2"“合并”,那么你需要一个 ExpressionVisitorx2 替换为 x1,否则你会得到像 x1 => x1.Foo == "Foo1" || x2.Foo == "Foo2" 这样的错误查询。在给出的代码中我们只有 x1(即 var obj = ExpressionHelper.Parameter<TEntity>()),所以没问题。