通常我們在做一些數據過濾的操作的時候,經常需要做一些判斷再進行是否要對其進行條件過濾。 普通做法 最原始的做法我們是先通過If()判斷是否需要進行數據過濾,然後再對數據源使用Where來過濾數據。 示例如下: if(!string.IsNullOrWhiteSpace(str)) { query = ...
通常我們在做一些數據過濾的操作的時候,經常需要做一些判斷再進行是否要對其進行條件過濾。
普通做法
最原始的做法我們是先通過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的拼接了。同時,在添加新條件的時候我們也無需修改其他業務代碼。