C#表達式樹Expression動態創建表達式

上一篇中說到瞭 Expression 的一些概念性東西,其實也是為瞭這一篇做知識準備。為瞭實現 EFCore 的多條件、連表查詢,簡化查詢代碼編寫,也就有瞭這篇文章。

在一些管理後臺中,對數據進行多條件查詢是一件很普遍的事情,比如在用戶列表需要實現可以對 “用戶名”、”手機號”、”賬戶是否凍結” 等等一系列的條件查詢,常見的處理方式就是通過一系列 if…else… 來對條件進行拼接。這會導致查詢接口實現起來堆疊瞭一堆看起來有用但實際很繁瑣的代碼。所以根據前後端請求報文協商,我們就可以按照一定的格式來動態創建表達式樹。

創建 QueryEntity 類

QueryEntity 是前端向 API 傳遞的參數列表,通過這個類,服務端可以知道前端需要查詢哪個字段,使用什麼方法(Equals、Contains)過濾。

/// <summary>
/// 查詢實體
/// </summary>
public class QueryEntity
{
    /// <summary>
    /// 字段名稱
    /// </summary>
    public string Key { get; set; }

    /// <summary>
    /// 值
    /// </summary>
    public string Value { get; set; }

    /// <summary>
    /// 操作方法,對應OperatorEnum枚舉類
    /// </summary>
    public string Operator { get; set; }

    /// <summary>
    /// 邏輯運算符,隻支持AND、OR
    /// </summary>
    public string LogicalOperator { get; set; }
}

創建 OperatorEnum 類

OperatorEnum 這是一個操作方法的枚舉類,規定瞭 API 允許的查詢方法,比如 Equals、Contains 等等。

/// <summary>
/// 操作方法枚舉
/// </summary>
public enum OperatorEnum
{
    /// <summary>
    /// 等於
    /// </summary>
    Equals,

    /// <summary>
    /// 不等於
    /// </summary>
    NotEqual,

    /// <summary>
    /// 包含
    /// </summary>
    Contains,

    /// <summary>
    /// 由什麼開始
    /// </summary>
    StartsWith,

    /// <summary>
    /// 由什麼結束
    /// </summary>
    EndsWith,

    /// <summary>
    /// 大於
    /// </summary>
    Greater,

    /// <summary>
    /// 大於等於
    /// </summary>
    GreaterEqual,

    /// <summary>
    /// 小於
    /// </summary>
    Less,

    /// <summary>
    /// 小於等於
    /// </summary>
    LessEqual,
}

創建 ExpressionExtension 類

ExpressionExtension 類實現瞭表達式樹的動態創建,將前端傳入的多條件查詢轉換成表達式,用於 EFCore 的查詢。

/// <summary>
/// 表達式擴展
/// </summary>
/// <typeparam name="T">泛型</typeparam>
public static class ExpressionExtension<T> where T : class, new()
{
    /// <summary>
    /// 表達式動態拼接
    /// </summary>
    public static Expression<Func<T, bool>> ExpressionSplice(List<QueryEntity> entities)
    {
        if (entities.Count < 1)
        {
            return ex => true;
        }
        var expression_first = CreateExpressionDelegate(entities[0]);
        foreach (var entity in entities.Skip(1))
        {
            var expression = CreateExpressionDelegate(entity);
            InvocationExpression invocation = Expression.Invoke(expression_first, expression.Parameters.Cast<Expression>());
            BinaryExpression binary;
            // 邏輯運算符判斷
            if (entity.LogicalOperator.ToUpper().Equals("OR"))
            {
                binary = Expression.Or(expression.Body, invocation);
            }
            else
            {
                binary = Expression.And(expression.Body, invocation);
            }
            expression_first = Expression.Lambda<Func<T, bool>>(binary, expression.Parameters);
        }
        return expression_first;
    }

    /// <summary>
    /// 創建 Expression<TDelegate>
    /// </summary>
    private static Expression<Func<T, bool>> CreateExpressionDelegate(QueryEntity entity)
    {
        ParameterExpression param = Expression.Parameter(typeof(T));

        Expression key = param;
        var entityKey = entity.Key.Trim();
        // 包含'.',說明是父表的字段
        if (entityKey.Contains('.'))
        {
            var tableNameAndField = entityKey.Split('.');
            key = Expression.Property(key, tableNameAndField[0].ToString());
            key = Expression.Property(key, tableNameAndField[1].ToString());
        }
        else
        {
            key = Expression.Property(key, entityKey);
        }

        Expression value = Expression.Constant(ParseType(entity));
        Expression body = CreateExpression(key, value, entity.Operator);
        var lambda = Expression.Lambda<Func<T, bool>>(body, param);
        return lambda;
    }

    /// <summary>
    /// 屬性類型轉換
    /// </summary>
    /// <param name="entity">查詢實體</param>
    /// <returns></returns>
    private static object ParseType(QueryEntity entity)
    {
            try
            {
                PropertyInfo property;
                // 包含'.',說明是子類的字段
                if (entity.Key.Contains('.'))
                {
                    var tableNameAndField = entity.Key.Split('.');

                    property = typeof(T).GetProperty(tableNameAndField[0], BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
                    property = property.PropertyType.GetProperty(tableNameAndField[1], BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
                }
                else
                {
                    property = typeof(T).GetProperty(entity.Key, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
                }

                return Convert.ChangeType(entity.Value, property.PropertyType);
            }
            catch (Exception)
            {
                throw new ArgumentException("字段類型轉換失敗:字段名錯誤或值類型不正確");
            }
    }

    /// <summary>
    /// 創建 Expression
    /// </summary>
    private static Expression CreateExpression(Expression left, Expression value, string entityOperator)
    {
        if (!Enum.TryParse(entityOperator, true, out OperatorEnum operatorEnum))
        {
            throw new ArgumentException("操作方法不存在,請檢查operator的值");
        }

        return operatorEnum switch
        {
                OperatorEnum.Equals => Expression.Equal(left, Expression.Convert(value, left.Type)),
                OperatorEnum.NotEqual => Expression.NotEqual(left, Expression.Convert(value, left.Type)),
                OperatorEnum.Contains => Expression.Call(left, typeof(string).GetMethod("Contains", new Type[] { typeof(string) }), value),
                OperatorEnum.StartsWith => Expression.Call(left, typeof(string).GetMethod("StartsWith", new Type[] { typeof(string) }), value),
                OperatorEnum.EndsWith => Expression.Call(left, typeof(string).GetMethod("EndsWith", new Type[] { typeof(string) }), value),
                OperatorEnum.Greater => Expression.GreaterThan(left, Expression.Convert(value, left.Type)),
                OperatorEnum.GreaterEqual => Expression.GreaterThanOrEqual(left, Expression.Convert(value, left.Type)),
                OperatorEnum.Less => Expression.LessThan(left, Expression.Convert(value, left.Type)),
                OperatorEnum.LessEqual => Expression.LessThanOrEqual(left, Expression.Convert(value, left.Type)),
                _ => Expression.Equal(left, Expression.Convert(value, left.Type)),
        };
    }
}

使用示例

例如有以下兩個實體類,Address 是 User 的子類

public class User
{
    public int Id { get; set; }

    public string Name { get; set; } = string.Empty;

    public int Age { get; set; }

    public DateTime CreateTime { get; set; }

    public Address Address { get; set; }

}

public class Address
{
    public string Province { get; set; }

    public string City { get; set; }
}

單條件查詢

查詢用戶表中名稱(name) 包含 “chen” :

List<QueryEntity> list = new List<QueryEntity>
{
    new QueryEntity
    {
        Key = "name",
        Value = "chen",
        Operator = "Contains"
    }
};
var expression = ExpressionExtension<User>.ExpressionSplice(list);
// expression = Param_0 => Param_0.Name.Contains("chen")

查詢用戶表中年齡(age) 大於等於 18:

List<QueryEntity> list = new List<QueryEntity>
{
    new QueryEntity
    {
        Key = "age",
        Value = "18",
        Operator = "GreaterEqual"
    }
};
var expression = ExpressionExtension<User>.ExpressionSplice(list);
// expression = Param_0 => Param_0.Name.GreaterThanOrEqual(18)

多條件查詢

查詢用戶表中名稱(name) 包含 “chen” 並且年齡(age) 大於等於 18:

List<QueryEntity> list = new List<QueryEntity>
{
    new QueryEntity
    {
        Key = "name",
        Value = "chen",
        Operator = "Contains"
    },
    new QueryEntity
    {
        Key = "age",
        Value = "18",
        Operator = "GreaterEqual",
        // 註意:這裡得填入 "AND",代表兩個條件是並且的關系,如果需要查詢名稱包含 "chen" 或者 年齡大於等於18,則填入 "OR"
        "logicalOperator": "AND"
    }
};
var expression = ExpressionExtension<User>.ExpressionSplice(list);
// expression = Param_0 => ((Param_0.Status >= Convert(1, Int32)) And Invoke(Param_1 => Param_1.OpenId.Contains("9JJdFTVt6oimCgdbW61sk"), Param_0))

多表查詢

查詢用戶表中名稱(name) 包含 “chen” 並且 地址(address)在廣東省:

List<QueryEntity> list = new List<QueryEntity>
{
    new QueryEntity
    {
        Key = "name",
        Value = "chen",
        Operator = "Contains"
    },
    new QueryEntity
    {
        Key = "address.Province",
        Value = "廣東省",
        Operator = "Equals",
        // 註意:這裡得填入 "AND",代表兩個條件是並且的關系,如果需要查詢名稱包含 "chen" 或者 年齡大於等於18,則填入 "OR"
        "logicalOperator": "AND"
    }
};

var expression = ExpressionExtension<BookingRecord>.ExpressionSplice(list);
// expression = {Param_0 => ((Param_0.Address.Province == Convert("廣東省", String)) And Invoke(Param_1 => Param_1.Name.Contains("chen"), Param_0))}

到此這篇關於C#表達式樹Expression動態創建表達式的文章就介紹到這瞭。希望對大傢的學習有所幫助,也希望大傢多多支持WalkonNet。

推薦閱讀: