还在拼冗长的WhereIf吗?100行代码解放这个操作
通常我们在做一些数据过滤的操作的时候,经常需要做一些判断再进行是否要对其进行条件过滤。
普通做法
最原始的做法我们是先通过If()判断是否需要进行数据过滤,然后再对数据源使用Where来过滤数据。
示例如下:
if(!string.IsNullOrWhiteSpace(str))
{
query = query.Where(a => a == str);
}
封装WhereIf做法
进阶一些的就把普通做法的代码封装成一个扩展方法,WhereIf指代一个名称,也可以有其他名称,本质是一样的。
示例如下:
public static IQueryable<T> WhereIf<T>([NotNull] this IQueryable<T> query, bool condition, Expression<Func<T, int, bool>> predicate)
{
return condition
? query.Where(predicate)
: query;
}
使用方式:
query.WhereIf(!string.IsNullOrWhiteSpace(str), a => a == str);
封装WhereIf做法相比普通做法,已经可以减少我们代码的很多If块了,看起来也优雅一些。
但是如果查询条件增多的话,我们依旧需要写很多WhereIf,就会有这种现象:
query
.WhereIf(!string.IsNullOrWhiteSpace(str), a => a == str)
.WhereIf(!string.IsNullOrWhiteSpace(str), a => a == str)
.WhereIf(!string.IsNullOrWhiteSpace(str), a => a == str)
.WhereIf(!string.IsNullOrWhiteSpace(str), a => a == str)
.WhereIf(!string.IsNullOrWhiteSpace(str), a => a == str)
.WhereIf(!string.IsNullOrWhiteSpace(str), a => a == str)
.WhereIf(!string.IsNullOrWhiteSpace(str), a => a == str)
.WhereIf(!string.IsNullOrWhiteSpace(str), a => a == str)
.WhereIf(!string.IsNullOrWhiteSpace(str), a => a == str)
.WhereIf(!string.IsNullOrWhiteSpace(str), a => a == str);
条件一但增多很多的话,这样一来代码看起来就又不够优雅了~
这时候就想,如果只用一个Where传进去一个对象,自动解析条件进行数据过滤,是不是就很棒呢~
WhereObj做法
想法来了,那就动手实现一下。
首先我们需要考虑如何对对象的属性进行标记来获取我们作为条件过滤的对应属性。那就得加一个Attribute,这里实现一个CompareAttribute,用于对对象的属性进行标记。
[AttributeUsage(AttributeTargets.Property)]
public class CompareAttribute : Attribute
{
public CompareAttribute(CompareType compareType)
{
CompareType = compareType;
}
public CompareAttribute(CompareType compareType, string compareProperty) : this(compareType)
{
CompareProperty = compareProperty;
}
public CompareType CompareType { get; set; }
public CompareSite CompareSite { get; set; } = CompareSite.LEFT;
public string? CompareProperty { get; set; }
}
public enum CompareType
{
Equal,
NotEqual,
GreaterThan,
GreaterThanOrEqual,
LessThan,
LessThanOrEqual,
Contains,
StartsWith,
EndsWith,
IsNull,
IsNotNull
}
public enum CompareSite
{
RIGHT,
LEFT
}
这里CompareType表示要进行比较的操作,很简单,一目了然。
CompareSite则表示在进行比较的时候比较的数据处于比较符左边还是右边,在CompareAttribute给与默认值在左边,表示比较的源数据处于左边。比如Contains操作,有时候是判断源字符串是否包含子字符串,此时应该是sourceStr.Contains(str),有时候是判断源字符串是否在某个集合字符串中则是ListString.Contains(sourceStr)。
CompareProperty则表示比较的属性名称,空的话则直接使用对象名称,如果有值则优先使用。
Attribute搞定了,接下来则实现我们的WhereObj
这里由于需要动态的拼接表达式,这里使用了DynamicExpresso.Core库来进行动态表达式生成。
先上代码:
namespace System.Linq;
public static class WhereExtensions
{
public static IQueryable<T> WhereObj<T>(this IQueryable<T> queryable, object parameterObject)
{
var interpreter = new Interpreter();
interpreter = interpreter.SetVariable("o", parameterObject);
var properties = parameterObject.GetType().GetProperties().Where(p => p.CustomAttributes.Any(a=>a.AttributeType == typeof(CompareAttribute)));
var whereExpression = new StringBuilder();
foreach (var property in properties)
{
if(property.GetValue(parameterObject) == null)
{
continue;
}
var compareAttribute = property.GetCustomAttribute<CompareAttribute>();
var propertyName = compareAttribute!.CompareProperty ?? property.Name;
if (typeof(T).GetProperty(propertyName) == null)
{
continue;
}
if (whereExpression.Length > 0)
{
whereExpression.Append(" && ");
}
whereExpression.Append(BuildCompareExpression(propertyName, property, compareAttribute.CompareType, compareAttribute.CompareSite));
}
if(whereExpression.Length > 0)
{
return queryable.Where(interpreter.ParseAsExpression<Func<T, bool>>(whereExpression.ToString(), "q"));
}
return queryable;
}
public static IEnumerable<T> WhereObj<T>(this IEnumerable<T> enumerable, object parameterObject)
{
var interpreter = new Interpreter();
interpreter = interpreter.SetVariable("o", parameterObject);
var properties = parameterObject.GetType().GetProperties().Where(p => p.CustomAttributes.Any(a=>a.AttributeType == typeof(CompareAttribute)));
var whereExpression = new StringBuilder();
foreach (var property in properties)
{
if(property.GetValue(parameterObject) == null)
{
continue;
}
var compareAttribute = property.GetCustomAttribute<CompareAttribute>();
var propertyName = compareAttribute!.CompareProperty ?? property.Name;
if (typeof(T).GetProperty(propertyName) == null)
{
continue;
}
if (whereExpression.Length > 0)
{
whereExpression.Append(" && ");
}
whereExpression.Append(BuildCompareExpression(propertyName, property, compareAttribute.CompareType, compareAttribute.CompareSite));
}
if(whereExpression.Length > 0)
{
return enumerable.Where(interpreter.ParseAsExpression<Func<T, bool>>(whereExpression.ToString(), "q").Compile());
}
return enumerable;
}
private static string BuildCompareExpression(string propertyName, PropertyInfo propertyInfo, CompareType compareType, CompareSite compareSite)
{
var source = $"q.{propertyName}";
var target = $"o.{propertyInfo.Name}";
return compareType switch
{
CompareType.Equal => compareSite == CompareSite.LEFT ? $"{source} == {target}" : $"{target} == {source}",
CompareType.NotEqual => compareSite == CompareSite.LEFT ? $"{source} != {target}" : $"{target} != {source}",
CompareType.GreaterThan => compareSite == CompareSite.LEFT ? $"{source} < {target}" : $"{target} > {source}",
CompareType.GreaterThanOrEqual => compareSite == CompareSite.LEFT ? $"{source} <= {target}" : $"{target} >= {source}",
CompareType.LessThan => compareSite == CompareSite.LEFT ? $"{source} > {target}" : $"{target} < {source}",
CompareType.LessThanOrEqual => compareSite == CompareSite.LEFT ? $"{source} >= {target}" : $"{target} <= {source}",
CompareType.Contains => compareSite == CompareSite.LEFT ? $"{source}.Contains({target})" : $"{target}.Contains({source})",
CompareType.StartsWith => compareSite == CompareSite.LEFT ? $"{source}.StartsWith({target})" : $"{target}.StartsWith({source})",
CompareType.EndsWith => compareSite == CompareSite.LEFT ? $"{source}.EndsWith({target})" : $"{target}.EndsWith({source})",
CompareType.IsNull => $"{source} == null",
CompareType.IsNotNull => $"{source} != null",
_ => throw new NotSupportedException()
};
}
}
代码对IEnumerable和IQueryable都进行了扩展,总共行数100行。
在WhereObj中,我们传入一个parameterObject,然后获取对象的所有加了CompareAttribute的属性。
然后进行循环拼接条件。在循环中我们先判断属性是否有值,有值才会添加表达式。所以建议条件属性都为可空类型。
if(property.GetValue(parameterObject) == null)
{
continue;
}
然后获取属性的CompareAttribute, 先指定条件属性名称,在判断属性是否在源对象存在,如果不存在则不处理。
if (typeof(T).GetProperty(propertyName) == null)
{
continue;
}
最后就是根据CompareType来动态生成拼接的表达式了。
BuildCompareExpression方法根据CompareType和CompareSite动态拼接表达式字符串,然后使用Interpreter.ParseAsExpression<Func<T, bool>>转换成我们的表达式类型。就完成啦。
测试效果
搞一个Customer类和CustomerFilter,再搞一个数据。
namespace Test
{
public class Customer
{
public string Name { get; set; }
public int Age { get; set; }
public char Gender { get; set; }
}
public class CustomerFilter
{
[Compare(CompareType.StartsWith)]
public string? Name { get; set; }
[Compare(CompareType.Contains, "Name", CompareSite = CompareSite.RIGHT)]
public List<string>? Names { get; set; }
[Compare(CompareType.GreaterThan)]
public int? Age { get; set; }
[Compare(CompareType.Equal)]
public char? Gender { get; set; }
}
public class T
{
public static IEnumerable<Customer> customers = (new List<Customer> {
new Customer() { Name = "David", Age = 31, Gender = 'M' },
new Customer() { Name = "Mary", Age = 29, Gender = 'F' },
new Customer() { Name = "Jack", Age = 2, Gender = 'M' },
new Customer() { Name = "Marta", Age = 1, Gender = 'F' },
new Customer() { Name = "Moses", Age = 120, Gender = 'M' },
}).AsEnumerable();
}
}
测试代码
T.customers.WhereObj(new CustomerFilter()
{
//Name = "M",
Names = ["Mary", "Jack"],
//Age = 20,
//Gender = 'M'
})
.ToList().ForEach(c => Console.WriteLine(c.Name));
可以看到正常执行。
这样我们在应对条件很多的数据过滤的时候,就可以只用一个WhereObj就可以代替很多个WhereIf的拼接了。同时,在添加新条件的时候我们也无需修改其他业务代码。