如何使用 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)
其中 A
、B
、C
、D
是 SearchTerm[0]
、SearchTerm[1]
、SearchTerm[2]
、SearchTerm[3]
和opB
、opC
、opD
是定义在SearchTerm[1].LogicalConnector
、SearchTerm[2].LogicalConnector
、SearchTerm[3].LogicalConnector
.
中的运算符
虽然放置括号很容易,但选择如何“描述”它们却很复杂,除非您显着更改 SearchTerm
集合(它不能是“线性”数组,但需要是树)。
P.S。我错了,你不需要 ExpressionVisitor
。当您尝试“合并”具有不同 ParameterExpression
的多个 LambdaExpression
时,您需要一个 ExpressionVisitor
。在这段代码中,我们可以对所有查询使用一个 var obj = ExpressionHelper.Parameter<TEntity>()
,因此合并条件没有问题。明确一点:如果你想将 x1 => x1.Foo == "Foo1"
与 x2 => x2.Foo == "Foo2"
“合并”,那么你需要一个 ExpressionVisitor
将 x2
替换为 x1
,否则你会得到像 x1 => x1.Foo == "Foo1" || x2.Foo == "Foo2"
这样的错误查询。在给出的代码中我们只有 x1
(即 var obj = ExpressionHelper.Parameter<TEntity>()
),所以没问题。
希望有人能指导和帮助我解决这个问题。我们有一个使用 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
中构建了 QUERYSELECT
[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)
其中 A
、B
、C
、D
是 SearchTerm[0]
、SearchTerm[1]
、SearchTerm[2]
、SearchTerm[3]
和opB
、opC
、opD
是定义在SearchTerm[1].LogicalConnector
、SearchTerm[2].LogicalConnector
、SearchTerm[3].LogicalConnector
.
虽然放置括号很容易,但选择如何“描述”它们却很复杂,除非您显着更改 SearchTerm
集合(它不能是“线性”数组,但需要是树)。
P.S。我错了,你不需要 ExpressionVisitor
。当您尝试“合并”具有不同 ParameterExpression
的多个 LambdaExpression
时,您需要一个 ExpressionVisitor
。在这段代码中,我们可以对所有查询使用一个 var obj = ExpressionHelper.Parameter<TEntity>()
,因此合并条件没有问题。明确一点:如果你想将 x1 => x1.Foo == "Foo1"
与 x2 => x2.Foo == "Foo2"
“合并”,那么你需要一个 ExpressionVisitor
将 x2
替换为 x1
,否则你会得到像 x1 => x1.Foo == "Foo1" || x2.Foo == "Foo2"
这样的错误查询。在给出的代码中我们只有 x1
(即 var obj = ExpressionHelper.Parameter<TEntity>()
),所以没问题。