還在拼冗長的WhereIf嗎?100行程式碼解放這個操作

China Soft發表於2024-06-11

普通做法#

最原始的做法我們是先透過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的拼接了。同時,在新增新條件的時候我們也無需修改其他業務程式碼。

相關文章