C# 构建动态查询表达式(含查询、排序、分页)
/// <summary>/// 动态查询创建者/// </summary>public static class DynamicQueryBuilder{/// <summary>/// 构建查询/// </summary>/// <typeparam name="T"></typeparam>/// <param name="source"></param>/// <param name="parameters"></param>/// <param name="allowedFilterFields"></param>/// <returns></returns>public static IQueryable<T> BuildQuery<T>(this IQueryable<T> source,DynamicQueryParams parameters,List<string>? allowedFilterFields = null){var query = source;// 动态过滤query = ApplyFilters(query, JsonConvert.DeserializeObject<Dictionary<string, string>>(parameters.Filters ?? ""), allowedFilterFields);// 动态排序query = ApplySorting(query, parameters.SortField, parameters.SortDirection);// 分页return ApplyPagination(query, parameters.PageIndex, parameters.PageSize);}/// <summary>/// 应用过滤器/// </summary>/// <typeparam name="T"></typeparam>/// <param name="source"></param>/// <param name="filters"></param>/// <param name="allowedFields"></param>/// <returns></returns>private static IQueryable<T> ApplyFilters<T>(IQueryable<T> source,Dictionary<string, string> filters,List<string>? allowedFields){if (filters == null || !filters.Any())return source;var parameter = Expression.Parameter(typeof(T), "x");Expression? combinedExpression = null;foreach (var filter in filters){var fieldName = filter.Key;var searchValue = filter.Value;// 安全验证:检查字段是否允许查询if (allowedFields != null && !allowedFields.Contains(fieldName))continue;// 获取属性表达式var property = GetNestedPropertyExpression(parameter, fieldName);if (property == null) continue;// 解析操作符和值var (operatorType, cleanValue) = ParseOperator(searchValue);// 构建表达式var condition = BuildConditionExpression(property,cleanValue,operatorType,parameter);if (condition == null) continue;combinedExpression = combinedExpression == null? condition: Expression.AndAlso(combinedExpression, condition);}return combinedExpression == null? source: source.Where(Expression.Lambda<Func<T, bool>>(combinedExpression, parameter));}/// <summary>/// 解析操作器(高版本可以用这个)/// </summary>/// <param name="value"></param>/// <returns></returns>//private static (string Operator, string Value) ParseOperator(string value)//{// if (value.StartsWith(">=")) return (">=", value[2..]);// if (value.StartsWith("<=")) return ("<=", value[2..]);// if (value.StartsWith(">")) return (">", value[1..]);// if (value.StartsWith("<")) return ("<", value[1..]);// if (value.StartsWith("=")) return ("=", value[2..]);// if (value.StartsWith("!=")) return ("!=", value[2..]);// return ("", value);//}/// <summary>/// 解析操作器/// </summary>/// <param name="value"></param>/// <returns></returns>private static (string Operator, string Value) ParseOperator(string value){// 空值处理if (string.IsNullOrWhiteSpace(value))return (string.Empty, value);// 支持语义化字符串操作符(优先检查)if (value.StartsWith("contains:") && value.Length > 9){return ("contains", value.Substring(9));}if (value.StartsWith("startswith:") && value.Length > 11){return ("startswith", value.Substring(11));}if (value.StartsWith("endswith:") && value.Length > 9){return ("endswith", value.Substring(9));}// 比较操作符(按长度降序检查)if (value.StartsWith(">=") && value.Length > 2){return (">=", value.Substring(2).Trim());}if (value.StartsWith("<=") && value.Length > 2){return ("<=", value.Substring(2).Trim());}if (value.StartsWith("!=") && value.Length > 2){return ("!=", value.Substring(2).Trim());}if (value.StartsWith(">") && value.Length > 1){return (">", value.Substring(1).Trim());}if (value.StartsWith("<") && value.Length > 1){return ("<", value.Substring(1).Trim());}if (value.StartsWith("=") && value.Length > 1){return ("=", value.Substring(1).Trim());}// 默认返回原始值return (string.Empty, value.Trim());}/// <summary>/// 构建条件表达式/// </summary>/// <param name="property"></param>/// <param name="stringValue"></param>/// <param name="operatorType"></param>/// <param name="parameter"></param>/// <returns></returns>private static Expression? BuildConditionExpression(Expression property,string stringValue,string operatorType,ParameterExpression parameter){try{// 处理可空类型Type targetType = property.Type;bool isNullable = false;if (Nullable.GetUnderlyingType(targetType) is Type underlyingType){targetType = underlyingType;isNullable = true;}// 特殊类型处理if (targetType == typeof(Guid)){return HandleGuidCondition(property, stringValue, operatorType, isNullable, parameter);}else if (targetType.IsEnum){return HandleEnumCondition(property, stringValue, operatorType, targetType, isNullable);}// 转换值到目标类型object? value = Convert.ChangeType(stringValue, targetType);// 处理空值检查Expression nullSafeProperty = property;if (isNullable || !property.Type.IsValueType){var nullCheck = Expression.NotEqual(property,Expression.Constant(null, property.Type));// 对于可空类型,使用基础类型进行比较Expression conditionExpression = BuildComparisonExpression(isNullable ? Expression.Property(property, "Value") : property,value,operatorType,targetType);return Expression.AndAlso(nullCheck, conditionExpression);}return BuildComparisonExpression(property, value, operatorType, targetType);}catch{// 类型转换失败时尝试字符串匹配if (property.Type == typeof(string)){return BuildStringCondition(property, stringValue, operatorType);}return null;}}/// <summary>/// 构建比较表达式/// </summary>/// <param name="property"></param>/// <param name="value"></param>/// <param name="operatorType"></param>/// <param name="targetType"></param>/// <returns></returns>private static Expression BuildComparisonExpression(Expression property,object value,string operatorType,Type targetType){var valueExpression = Expression.Constant(value, targetType);return operatorType switch{"=" => Expression.Equal(property, valueExpression),"!=" => Expression.NotEqual(property, valueExpression),">" => Expression.GreaterThan(property, valueExpression),"<" => Expression.LessThan(property, valueExpression),">=" => Expression.GreaterThanOrEqual(property, valueExpression),"<=" => Expression.LessThanOrEqual(property, valueExpression),_ => null};}/// <summary>/// 构建字符串条件/// </summary>/// <param name="property"></param>/// <param name="stringValue"></param>/// <param name="operatorType"></param>/// <returns></returns>private static Expression? BuildStringCondition(Expression property,string stringValue,string operatorType){switch (operatorType){case "=":return Expression.Equal(property,Expression.Constant(stringValue));case "!=":return Expression.NotEqual(property,Expression.Constant(stringValue));case "contains":case "":var containsMethod = typeof(string).GetMethod("Contains", [typeof(string)]);return Expression.Call(property, containsMethod!, Expression.Constant(stringValue));case "startswith":var startsWithMethod = typeof(string).GetMethod("StartsWith", [typeof(string)]);return Expression.Call(property, startsWithMethod!, Expression.Constant(stringValue));case "endswith":var endsWithMethod = typeof(string).GetMethod("EndsWith", [typeof(string)]);return Expression.Call(property, endsWithMethod!, Expression.Constant(stringValue));default:return null;}}/// <summary>/// 处理枚举条件/// </summary>/// <param name="property"></param>/// <param name="stringValue"></param>/// <param name="operatorType"></param>/// <param name="enumType"></param>/// <param name="isNullable"></param>/// <returns></returns>private static Expression? HandleEnumCondition(Expression property,string stringValue,string operatorType,Type enumType,bool isNullable){try{object enumValue = Enum.Parse(enumType, stringValue, true);var valueExpression = Expression.Constant(enumValue, enumType);// 如果是可空类型,需要访问.Value属性Expression comparisonProperty = isNullable ?Expression.Property(property, "Value") :property;return operatorType switch{"=" => Expression.Equal(comparisonProperty, valueExpression),"!=" => Expression.NotEqual(comparisonProperty, valueExpression),_ => null};}catch{return null;}}/// <summary>/// 处理Guid条件/// </summary>/// <param name="property"></param>/// <param name="stringValue"></param>/// <param name="operatorType"></param>/// <param name="isNullable"></param>/// <param name="parameter"></param>/// <returns></returns>private static Expression? HandleGuidCondition(Expression property,string stringValue,string operatorType,bool isNullable,ParameterExpression parameter){try{Guid guidValue = Guid.Parse(stringValue);var valueExpression = Expression.Constant(guidValue, typeof(Guid));// 如果是可空类型,需要访问.Value属性Expression comparisonProperty = isNullable ?Expression.Property(property, "Value") :property;return operatorType switch{"=" => Expression.Equal(comparisonProperty, valueExpression),"!=" => Expression.NotEqual(comparisonProperty, valueExpression),_ => null};}catch{return null;}}/// <summary>/// 获取嵌套属性表达式/// </summary>/// <param name="parameter"></param>/// <param name="path"></param>/// <returns></returns>private static Expression? GetNestedPropertyExpression(Expression parameter, string path){try{return path.Split('.').Aggregate(parameter, (current, property) =>Expression.Property(current, property));}catch{return null;}}/// <summary>/// 应用排序/// </summary>/// <typeparam name="T"></typeparam>/// <param name="source"></param>/// <param name="sortField"></param>/// <param name="sortDirection"></param>/// <returns></returns>private static IQueryable<T> ApplySorting<T>(IQueryable<T> source,string? sortField,string? sortDirection){if (string.IsNullOrWhiteSpace(sortField))return source;var parameter = Expression.Parameter(typeof(T), "x");var property = GetNestedPropertyExpression(parameter, sortField);if (property == null) return source;var lambda = Expression.Lambda(property, parameter);var methodName = string.Equals(sortDirection, "desc", StringComparison.OrdinalIgnoreCase)? "OrderByDescending": "OrderBy";var result = Expression.Call(typeof(Queryable),methodName,[typeof(T), property.Type],source.Expression,Expression.Quote(lambda));return source.Provider.CreateQuery<T>(result);}/// <summary>/// 应用分页/// </summary>/// <typeparam name="T"></typeparam>/// <param name="source"></param>/// <param name="pageIndex"></param>/// <param name="pageSize"></param>/// <returns></returns>private static IQueryable<T> ApplyPagination<T>(IQueryable<T> source,int pageIndex,int pageSize){if (pageIndex >= 1 && pageSize >= 1)return source.Skip((pageIndex - 1) * pageSize).Take(pageSize);else return source;}}public class DynamicQueryParams{// 分页参数public int PageIndex { get; set; } = 1;public int PageSize { get; set; } = 10;// 排序参数public string? SortField { get; set; }public string? SortDirection { get; set; } = "asc"; // asc/desc// 动态过滤字典 (Key: 字段名, Value: 搜索值)//public Dictionary<string, string> Filters { get; set; } = new ();public string? Filters { get; set; }}
测试
[Route("api/[controller]")][ApiController]public class ProductController : ControllerBase{[HttpGet]public IActionResult GetProducts([FromQuery] DynamicQueryParams parameters){var products = LoadProducts();var query = products.AsQueryable().BuildQuery(parameters, null);// 返回结果(包含分页元数据)var totalCount = query.Count();var results = query.ToList();return Ok(new{Data = results,PageIndex = parameters.PageIndex,PageSize = parameters.PageSize,TotalCount = totalCount,TotalPages = (int)Math.Ceiling(totalCount / (double)parameters.PageSize)});}private List<Product> LoadProducts(){return new List<Product>(){new Product{Id = 1, Code = "C001", Name = "产品001", Price = 10, CreateTime = Convert.ToDateTime("2025-07-11 08:00:00") },new Product{Id = 2, Code = "C012", Name = "产品012", Price = 20, CreateTime = Convert.ToDateTime("2025-07-12 08:00:00") },new Product{Id = 3, Code = "C003", Name = "产品003", Price = 10, CreateTime = Convert.ToDateTime("2025-07-11 10:00:00") },new Product{Id = 4, Code = "C004", Name = "产品004", Price = 30, CreateTime = Convert.ToDateTime("2025-07-11 16:00:00") },new Product{Id = 5, Code = "C005", Name = "产品005", Price = 25, CreateTime = Convert.ToDateTime("2025-07-13 08:00:00") },new Product{Id = 6, Code = "C006", Name = "产品006", Price = 10, CreateTime = Convert.ToDateTime("2025-07-15 08:00:00") },};}}public class Product{public int Id { get; set; }public string Code { get; set; }public string Name { get; set; }public int Price { get; set; }public DateTime CreateTime { get; set; }}