EF Core 动态过滤器
EF Core dynamic filter
我一直在为使用表达式树的 EF Core 查询使用动态过滤器 class,一切看起来都很好,过滤器正在工作,我可以传递一个过滤器集合并且它有效,但是当我看SQL 语句,它正在查询整个 table 并对结果集合应用过滤器,这是我的 class...
public static class QueryExpressionBuilder
{
private static readonly MethodInfo ContainsMethod = typeof(string).GetMethod("Contains", new[] { typeof(string) });
private static readonly MethodInfo StartsWithMethod = typeof(string).GetMethod("StartsWith", new[] { typeof(string) });
private static readonly MethodInfo EndsWithMethod = typeof(string).GetMethod("EndsWith", new[] { typeof(string) });
#region DynamicWhere
/// <summary>Where expression generator.</summary>
/// <typeparam name="T"></typeparam>
/// <param name="filters">The filters.</param>
/// <returns></returns>
public static Expression<Func<T, bool>> GetExpression<T>(IList<Filter> filters)
{
if (filters.Count == 0)
return null;
ParameterExpression param = Expression.Parameter(typeof(T), "t");
Expression exp = null;
if (filters.Count == 1)
exp = GetExpression(param, filters[0]);
else if (filters.Count == 2)
exp = GetExpression<T>(param, filters[0], filters[1]);
else
{
while (filters.Count > 0)
{
var f1 = filters[0];
var f2 = filters[1];
if (exp == null)
exp = GetExpression<T>(param, filters[0], filters[1]);
else
exp = Expression.AndAlso(exp, GetExpression<T>(param, filters[0], filters[1]));
filters.Remove(f1);
filters.Remove(f2);
if (filters.Count == 1)
{
exp = Expression.AndAlso(exp, GetExpression(param, filters[0]));
filters.RemoveAt(0);
}
}
}
return Expression.Lambda<Func<T, bool>>(exp, param);
}
/// <summary>Comparision operator expression generator.</summary>
/// <param name="param">The parameter.</param>
/// <param name="filter">The filter.</param>
/// <returns></returns>
private static Expression GetExpression(ParameterExpression param, Filter filter)
{
MemberExpression member = Expression.Property(param, filter.PropertyName);
var type = member.Type;
ConstantExpression constant;
switch (type.Name)
{
case "Int32":
constant = Expression.Constant(Convert.ToInt32(filter.Value));
break;
case "String":
default:
constant = Expression.Constant(filter.Value);
break;
}
// ConstantExpression constant = Expression.Constant(filter.Value);
switch (filter.Operation)
{
case Op.Equals:
return Expression.Equal(member, constant);
case Op.GreaterThan:
return Expression.GreaterThan(member, constant);
case Op.GreaterThanOrEqual:
return Expression.GreaterThanOrEqual(member, constant);
case Op.LessThan:
return Expression.LessThan(member, constant);
case Op.LessThanOrEqual:
return Expression.LessThanOrEqual(member, constant);
case Op.Contains:
return Expression.Call(member, ContainsMethod, constant);
case Op.StartsWith:
return Expression.Call(member, StartsWithMethod, constant);
case Op.EndsWith:
return Expression.Call(member, EndsWithMethod, constant);
}
return null;
}
/// <summary>And logic connector expression generator.</summary>
/// <typeparam name="T"></typeparam>
/// <param name="param">The parameter.</param>
/// <param name="filter1">The filter1.</param>
/// <param name="filter2">The filter2.</param>
/// <returns></returns>
private static BinaryExpression GetExpression<T>(ParameterExpression param, Filter filter1, Filter filter2)
{
var bin1 = GetExpression(param, filter1);
var bin2 = GetExpression(param, filter2);
return Expression.AndAlso(bin1, bin2);
}
#endregion
}
}
为了调用这个 class 我做了这样的事情 :
var whereDeleg = QueryExpressionBuilder.GetExpression<Tax>(filters).Compile();
var myList = _dbContext.MyEntity.Where(whereDeleg).ToList();
我传递的过滤器参数是这个 class 的集合:
public class Filter
{
public string PropertyName { get; set; }
public Op Operation { get; set; }
public object Value { get; set; }
}
如有任何帮助,我将不胜感激。
主要问题不是 class,而是您使用它的方式:
var whereDeleg = QueryExpressionBuilder.GetExpression<Tax>(filters).Compile();
var myList = _dbContext.MyEntity.Where(whereDeleg).ToList();
您正在从您的方法中获取 Expression<Func<T, bool>>
,但随后 Complie()
调用将其转换为 Func<T, bool>
。所以虽然 _dbContext.MyEntity
是 IQueryable<T>
,但没有 IQueryable<T>
扩展方法 Where
取 Func<T, bool>
(他们都取 Expression<Func<T, bool>>
)。但是由于 IQueryable<T>
继承(因此是一个)IEnumerable<T>
,编译器找到并使用 Where
扩展方法 IEnumerable<T>
(定义在 Enumerable
class).
这使得 Where
(以及所有后续方法,如果有的话)在 Where
之前执行并具体化查询之后执行客户端(在您的情况下 - 整个 table) .
IQueryable<T>
和 IEnumerable<T>
之间的区别由 Returning IEnumerable<T> vs. IQueryable<T> 涵盖。您需要做的就是确保始终调用 IQueryable<T>
扩展方法,而不是使用 Expression<Func<...>>
而不是 Func<...>
.
综上所述,您应该直接使用您的方法结果而不调用 Compile
:
var predicate = QueryExpressionBuilder.GetExpression<Tax>(filters);
var myList = _dbContext.MyEntity.Where(predicate).ToList();
或者只是
var myList = _dbContext.MyEntity.Where(QueryExpressionBuilder.GetExpression<Tax>(filters)).ToList();
或者更好的是,将以下自定义扩展方法添加到 QueryExpressionBuilder
class:
public static IQueryable<T> Where<T>(this IQueryable<T> source, IList<Filter> filters)
{
var predicate = GetExpression<T>(filters);
return predicate != null ? source.Where(predicate) : source;
}
能够简单地使用(并尽量减少出错的机会):
var myList = _dbContext.MyEntity.Where(filters).ToList();
旁注:主要的表达式生成器方法实现过于复杂,并且还会破坏传递的输入 filters
列表。可以简化如下(没有上述缺陷):
public static Expression<Func<T, bool>> GetExpression<T>(IEnumerable<Filter> filters)
{
var param = Expression.Parameter(typeof(T), "t");
var body = filters
.Select(filter => GetExpression(param, filter))
.DefaultIfEmpty()
.Aggregate(Expression.AndAlso);
return body != null ? Expression.Lambda<Func<T, bool>>(body, param) : null;
}
已更新以包含更多数据类型:
private static Expression GetExpression(ParameterExpression param, Filter filter)
{
// Defaults
FilterOperation filterOp = FilterOperation.Equals;
ConstantExpression constant = Expression.Constant(filter.Value);
MemberExpression member = Expression.Property(param, filter.PropertyName);
var type = member.Type;
switch (type.FullName)
{
case "System.Guid":
Guid outGuid;
if (Utilities.IsGuid(filter.Value.ToString(), out outGuid))
constant = Expression.Constant(outGuid);
break;
case "System.DateTime":
if (Utilities.IsDate(filter.Value.ToString()))
constant = Expression.Constant(Convert.ToDateTime(filter.Value));
break;
case "System.Single":
if (Utilities.IsNumber(filter.Value.ToString()))
constant = Expression.Constant(Convert.ToSingle(filter.Value));
break;
case "System.Int16":
if (Utilities.IsNumber(filter.Value.ToString()))
constant = Expression.Constant(Convert.ToInt16(filter.Value));
break;
case "System.Int32":
if (Utilities.IsNumber(filter.Value.ToString()))
constant = Expression.Constant(Convert.ToInt32(filter.Value));
break;
case "System.Double":
if (Utilities.IsNumber(filter.Value.ToString()))
constant = Expression.Constant(Convert.ToDouble(filter.Value));
break;
case "System.Decimal":
if (Utilities.IsNumber(filter.Value.ToString()))
constant = Expression.Constant(Convert.ToDecimal(filter.Value));
break;
case "System.Boolean":
if (Utilities.isBoolean(filter.Value.ToString().ToLower()))
{
constant = Expression.Constant(Convert.ToBoolean(filter.Value));
filterOp = FilterOperation.BooleanEquals;
}
break;
case "System.String":
constant = Expression.Constant(filter.Value);
filterOp = FilterOperation.Contains;
break;
}
switch (filterOp)
{
case FilterOperation.Equals:
return Expression.Equal(member, constant);
case FilterOperation.BooleanEquals:
var valueExpression = Expression.Convert(constant, typeof(bool));
return Expression.Equal(member, valueExpression);
case FilterOperation.GreaterThan:
return Expression.GreaterThan(member, constant);
case FilterOperation.GreaterThanOrEqual:
return Expression.GreaterThanOrEqual(member, constant);
case FilterOperation.LessThan:
return Expression.LessThan(member, constant);
case FilterOperation.LessThanOrEqual:
return Expression.LessThanOrEqual(member, constant);
case FilterOperation.Contains:
return Expression.Call(member, ContainsMethod, constant);
case FilterOperation.StartsWith:
return Expression.Call(member, StartsWithMethod, constant);
case FilterOperation.EndsWith:
return Expression.Call(member, EndsWithMethod, constant);
}
return null;
}
public enum FilterOperation
{
Equals,
BooleanEquals,
GreaterThan,
GreaterThanOrEqual,
LessThan,
LessThanOrEqual,
Contains,
StartsWith,
EndsWith
}
验证是可选的,但如果您需要:
public static bool IsDate(object obj)
{
if (obj == null) return false;
try
{
string strDate = obj.ToString();
DateTime dt = DateTime.Parse(strDate);
if (dt != DateTime.MinValue && dt != DateTime.MaxValue)
return true;
return false;
}
catch
{
return false;
}
}
public static bool IsNumber(string str)
{
if (string.IsNullOrEmpty(str)) return false;
bool isNum;
double retNum;
isNum = Double.TryParse(str, System.Globalization.NumberStyles.Any, System.Globalization.NumberFormatInfo.InvariantInfo, out retNum);
return isNum;
}
public static bool IsGuid(string str, out Guid theGuid)
{
if (string.IsNullOrEmpty(str))
{
theGuid = Guid.Empty;
return false;
}
return Guid.TryParse(str, out theGuid);
}
public static bool isBoolean(string str)
{
if (bool.TryParse(str, out _))
{
return true;
}
return false;
}
我一直在为使用表达式树的 EF Core 查询使用动态过滤器 class,一切看起来都很好,过滤器正在工作,我可以传递一个过滤器集合并且它有效,但是当我看SQL 语句,它正在查询整个 table 并对结果集合应用过滤器,这是我的 class...
public static class QueryExpressionBuilder
{
private static readonly MethodInfo ContainsMethod = typeof(string).GetMethod("Contains", new[] { typeof(string) });
private static readonly MethodInfo StartsWithMethod = typeof(string).GetMethod("StartsWith", new[] { typeof(string) });
private static readonly MethodInfo EndsWithMethod = typeof(string).GetMethod("EndsWith", new[] { typeof(string) });
#region DynamicWhere
/// <summary>Where expression generator.</summary>
/// <typeparam name="T"></typeparam>
/// <param name="filters">The filters.</param>
/// <returns></returns>
public static Expression<Func<T, bool>> GetExpression<T>(IList<Filter> filters)
{
if (filters.Count == 0)
return null;
ParameterExpression param = Expression.Parameter(typeof(T), "t");
Expression exp = null;
if (filters.Count == 1)
exp = GetExpression(param, filters[0]);
else if (filters.Count == 2)
exp = GetExpression<T>(param, filters[0], filters[1]);
else
{
while (filters.Count > 0)
{
var f1 = filters[0];
var f2 = filters[1];
if (exp == null)
exp = GetExpression<T>(param, filters[0], filters[1]);
else
exp = Expression.AndAlso(exp, GetExpression<T>(param, filters[0], filters[1]));
filters.Remove(f1);
filters.Remove(f2);
if (filters.Count == 1)
{
exp = Expression.AndAlso(exp, GetExpression(param, filters[0]));
filters.RemoveAt(0);
}
}
}
return Expression.Lambda<Func<T, bool>>(exp, param);
}
/// <summary>Comparision operator expression generator.</summary>
/// <param name="param">The parameter.</param>
/// <param name="filter">The filter.</param>
/// <returns></returns>
private static Expression GetExpression(ParameterExpression param, Filter filter)
{
MemberExpression member = Expression.Property(param, filter.PropertyName);
var type = member.Type;
ConstantExpression constant;
switch (type.Name)
{
case "Int32":
constant = Expression.Constant(Convert.ToInt32(filter.Value));
break;
case "String":
default:
constant = Expression.Constant(filter.Value);
break;
}
// ConstantExpression constant = Expression.Constant(filter.Value);
switch (filter.Operation)
{
case Op.Equals:
return Expression.Equal(member, constant);
case Op.GreaterThan:
return Expression.GreaterThan(member, constant);
case Op.GreaterThanOrEqual:
return Expression.GreaterThanOrEqual(member, constant);
case Op.LessThan:
return Expression.LessThan(member, constant);
case Op.LessThanOrEqual:
return Expression.LessThanOrEqual(member, constant);
case Op.Contains:
return Expression.Call(member, ContainsMethod, constant);
case Op.StartsWith:
return Expression.Call(member, StartsWithMethod, constant);
case Op.EndsWith:
return Expression.Call(member, EndsWithMethod, constant);
}
return null;
}
/// <summary>And logic connector expression generator.</summary>
/// <typeparam name="T"></typeparam>
/// <param name="param">The parameter.</param>
/// <param name="filter1">The filter1.</param>
/// <param name="filter2">The filter2.</param>
/// <returns></returns>
private static BinaryExpression GetExpression<T>(ParameterExpression param, Filter filter1, Filter filter2)
{
var bin1 = GetExpression(param, filter1);
var bin2 = GetExpression(param, filter2);
return Expression.AndAlso(bin1, bin2);
}
#endregion
}
}
为了调用这个 class 我做了这样的事情 :
var whereDeleg = QueryExpressionBuilder.GetExpression<Tax>(filters).Compile();
var myList = _dbContext.MyEntity.Where(whereDeleg).ToList();
我传递的过滤器参数是这个 class 的集合:
public class Filter
{
public string PropertyName { get; set; }
public Op Operation { get; set; }
public object Value { get; set; }
}
如有任何帮助,我将不胜感激。
主要问题不是 class,而是您使用它的方式:
var whereDeleg = QueryExpressionBuilder.GetExpression<Tax>(filters).Compile();
var myList = _dbContext.MyEntity.Where(whereDeleg).ToList();
您正在从您的方法中获取 Expression<Func<T, bool>>
,但随后 Complie()
调用将其转换为 Func<T, bool>
。所以虽然 _dbContext.MyEntity
是 IQueryable<T>
,但没有 IQueryable<T>
扩展方法 Where
取 Func<T, bool>
(他们都取 Expression<Func<T, bool>>
)。但是由于 IQueryable<T>
继承(因此是一个)IEnumerable<T>
,编译器找到并使用 Where
扩展方法 IEnumerable<T>
(定义在 Enumerable
class).
这使得 Where
(以及所有后续方法,如果有的话)在 Where
之前执行并具体化查询之后执行客户端(在您的情况下 - 整个 table) .
IQueryable<T>
和 IEnumerable<T>
之间的区别由 Returning IEnumerable<T> vs. IQueryable<T> 涵盖。您需要做的就是确保始终调用 IQueryable<T>
扩展方法,而不是使用 Expression<Func<...>>
而不是 Func<...>
.
综上所述,您应该直接使用您的方法结果而不调用 Compile
:
var predicate = QueryExpressionBuilder.GetExpression<Tax>(filters);
var myList = _dbContext.MyEntity.Where(predicate).ToList();
或者只是
var myList = _dbContext.MyEntity.Where(QueryExpressionBuilder.GetExpression<Tax>(filters)).ToList();
或者更好的是,将以下自定义扩展方法添加到 QueryExpressionBuilder
class:
public static IQueryable<T> Where<T>(this IQueryable<T> source, IList<Filter> filters)
{
var predicate = GetExpression<T>(filters);
return predicate != null ? source.Where(predicate) : source;
}
能够简单地使用(并尽量减少出错的机会):
var myList = _dbContext.MyEntity.Where(filters).ToList();
旁注:主要的表达式生成器方法实现过于复杂,并且还会破坏传递的输入 filters
列表。可以简化如下(没有上述缺陷):
public static Expression<Func<T, bool>> GetExpression<T>(IEnumerable<Filter> filters)
{
var param = Expression.Parameter(typeof(T), "t");
var body = filters
.Select(filter => GetExpression(param, filter))
.DefaultIfEmpty()
.Aggregate(Expression.AndAlso);
return body != null ? Expression.Lambda<Func<T, bool>>(body, param) : null;
}
已更新以包含更多数据类型:
private static Expression GetExpression(ParameterExpression param, Filter filter)
{
// Defaults
FilterOperation filterOp = FilterOperation.Equals;
ConstantExpression constant = Expression.Constant(filter.Value);
MemberExpression member = Expression.Property(param, filter.PropertyName);
var type = member.Type;
switch (type.FullName)
{
case "System.Guid":
Guid outGuid;
if (Utilities.IsGuid(filter.Value.ToString(), out outGuid))
constant = Expression.Constant(outGuid);
break;
case "System.DateTime":
if (Utilities.IsDate(filter.Value.ToString()))
constant = Expression.Constant(Convert.ToDateTime(filter.Value));
break;
case "System.Single":
if (Utilities.IsNumber(filter.Value.ToString()))
constant = Expression.Constant(Convert.ToSingle(filter.Value));
break;
case "System.Int16":
if (Utilities.IsNumber(filter.Value.ToString()))
constant = Expression.Constant(Convert.ToInt16(filter.Value));
break;
case "System.Int32":
if (Utilities.IsNumber(filter.Value.ToString()))
constant = Expression.Constant(Convert.ToInt32(filter.Value));
break;
case "System.Double":
if (Utilities.IsNumber(filter.Value.ToString()))
constant = Expression.Constant(Convert.ToDouble(filter.Value));
break;
case "System.Decimal":
if (Utilities.IsNumber(filter.Value.ToString()))
constant = Expression.Constant(Convert.ToDecimal(filter.Value));
break;
case "System.Boolean":
if (Utilities.isBoolean(filter.Value.ToString().ToLower()))
{
constant = Expression.Constant(Convert.ToBoolean(filter.Value));
filterOp = FilterOperation.BooleanEquals;
}
break;
case "System.String":
constant = Expression.Constant(filter.Value);
filterOp = FilterOperation.Contains;
break;
}
switch (filterOp)
{
case FilterOperation.Equals:
return Expression.Equal(member, constant);
case FilterOperation.BooleanEquals:
var valueExpression = Expression.Convert(constant, typeof(bool));
return Expression.Equal(member, valueExpression);
case FilterOperation.GreaterThan:
return Expression.GreaterThan(member, constant);
case FilterOperation.GreaterThanOrEqual:
return Expression.GreaterThanOrEqual(member, constant);
case FilterOperation.LessThan:
return Expression.LessThan(member, constant);
case FilterOperation.LessThanOrEqual:
return Expression.LessThanOrEqual(member, constant);
case FilterOperation.Contains:
return Expression.Call(member, ContainsMethod, constant);
case FilterOperation.StartsWith:
return Expression.Call(member, StartsWithMethod, constant);
case FilterOperation.EndsWith:
return Expression.Call(member, EndsWithMethod, constant);
}
return null;
}
public enum FilterOperation
{
Equals,
BooleanEquals,
GreaterThan,
GreaterThanOrEqual,
LessThan,
LessThanOrEqual,
Contains,
StartsWith,
EndsWith
}
验证是可选的,但如果您需要:
public static bool IsDate(object obj)
{
if (obj == null) return false;
try
{
string strDate = obj.ToString();
DateTime dt = DateTime.Parse(strDate);
if (dt != DateTime.MinValue && dt != DateTime.MaxValue)
return true;
return false;
}
catch
{
return false;
}
}
public static bool IsNumber(string str)
{
if (string.IsNullOrEmpty(str)) return false;
bool isNum;
double retNum;
isNum = Double.TryParse(str, System.Globalization.NumberStyles.Any, System.Globalization.NumberFormatInfo.InvariantInfo, out retNum);
return isNum;
}
public static bool IsGuid(string str, out Guid theGuid)
{
if (string.IsNullOrEmpty(str))
{
theGuid = Guid.Empty;
return false;
}
return Guid.TryParse(str, out theGuid);
}
public static bool isBoolean(string str)
{
if (bool.TryParse(str, out _))
{
return true;
}
return false;
}