如何通过表达式复用C#中IQueryable的通用范围过滤逻辑?
实现ASP.NET Core中通用的IQueryable范围过滤扩展
在ASP.NET Core开发中,经常会遇到重复的范围过滤逻辑——比如针对时间、价格等字段的区间查询,每次都要写判断上下限、构建Where条件的样板代码,仅字段名和类型不同。我们可以通过特性标记+表达式树的方式封装通用逻辑,实现一行代码完成多字段的范围过滤。
原重复代码示例
比如针对ProductSearch的时间范围查询:
public class ProductSearch { public DateTimeOffset? From { get; set; } public DateTimeOffset? To { get; set; } }
对应的查询逻辑:
var query = _dbContext.Products.AsNoTracking(); if (search.From > search.To) { // 上下限颠倒时忽略上限 search.To = null; } if (search.From.HasValue && search.From == search.To) { query = query.Where(x => x.CreationTime.UtcDateTime == search.From.Value.UtcDateTime); } else { if (search.From.HasValue) { query = query.Where(x => x.CreationTime.UtcDateTime >= search.From.Value.UtcDateTime); } if (search.To.HasValue) { query = query.Where(x => x.CreationTime.UtcDateTime <= search.To.Value.UtcDateTime); } }
实现步骤
1. 定义枚举和特性类
首先定义范围限制的枚举,以及标记搜索属性的特性:
// 范围限制类型 public enum RangeLimit { LowerLimit, // 下限 UpperLimit // 上限 } // 标记搜索属性对应的数据库字段和范围类型 [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] public class DBRangeFilterAttribute : Attribute { public string ColumnName { get; set; } public RangeLimit Limit { get; set; } }
2. 编写IQueryable通用扩展方法
核心是通过反射读取搜索类的特性,动态构建表达式树生成Where条件:
using System.Linq.Expressions; using System.Reflection; public static class QueryableRangeExtensions { public static IQueryable<TEntity> ApplyRangeFilters<TEntity, TSearch>(this IQueryable<TEntity> query, TSearch searchModel) where TEntity : class where TSearch : class { if (searchModel == null) return query; // 按ColumnName分组,收集同一字段的上下限值 var rangeGroups = searchModel.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance) .Select(prop => new { Prop = prop, Attr = prop.GetCustomAttribute<DBRangeFilterAttribute>() }) .Where(x => x.Attr != null) .GroupBy(x => x.Attr.ColumnName) .Select(g => new { ColumnName = g.Key, LowerValue = g.FirstOrDefault(x => x.Attr.Limit == RangeLimit.LowerLimit)?.Prop.GetValue(searchModel), UpperValue = g.FirstOrDefault(x => x.Attr.Limit == RangeLimit.UpperLimit)?.Prop.GetValue(searchModel) }); foreach (var range in rangeGroups) { query = ApplySingleRangeFilter(query, range.ColumnName, range.LowerValue, range.UpperValue); } return query; } private static IQueryable<TEntity> ApplySingleRangeFilter<TEntity>(IQueryable<TEntity> query, string columnName, object lowerValue, object upperValue) where TEntity : class { // 处理上下限颠倒的情况 if (lowerValue != null && upperValue != null) { var compareResult = Comparer.Default.Compare(lowerValue, upperValue); if (compareResult > 0) { upperValue = null; } else if (compareResult == 0) { // 上下限相等时,构建等于条件 var equalExpr = BuildEqualExpression<TEntity>(columnName, lowerValue); return query.Where(equalExpr); } } // 构建下限>=条件 if (lowerValue != null) { var greaterOrEqualExpr = BuildComparisonExpression<TEntity>(columnName, lowerValue, ExpressionType.GreaterThanOrEqual); query = query.Where(greaterOrEqualExpr); } // 构建上限<=条件 if (upperValue != null) { var lessOrEqualExpr = BuildComparisonExpression<TEntity>(columnName, upperValue, ExpressionType.LessThanOrEqual); query = query.Where(lessOrEqualExpr); } return query; } // 构建等于表达式 private static Expression<Func<TEntity, bool>> BuildEqualExpression<TEntity>(string columnName, object value) { return BuildComparisonExpression<TEntity>(columnName, value, ExpressionType.Equal); } // 构建比较表达式(>=、<=、==) private static Expression<Func<TEntity, bool>> BuildComparisonExpression<TEntity>(string columnName, object value, ExpressionType expressionType) { var parameter = Expression.Parameter(typeof(TEntity), "x"); var property = Expression.Property(parameter, columnName); // 处理DateTimeOffset转UtcDateTime的情况 if (property.Type == typeof(DateTimeOffset)) { property = Expression.Property(property, nameof(DateTimeOffset.UtcDateTime)); value = ((DateTimeOffset)value).UtcDateTime; } // 可扩展其他类型转换,比如Nullable类型拆箱 var constant = Expression.Constant(value, property.Type); var comparison = Expression.MakeBinary(expressionType, property, constant); return Expression.Lambda<Func<TEntity, bool>>(comparison, parameter); } }
3. 使用示例
在搜索类中用特性标记属性:
public class ProductSearch { [DBRangeFilter(ColumnName = "CreationTime", Limit = RangeLimit.LowerLimit)] public DateTimeOffset? From { get; set; } [DBRangeFilter(ColumnName = "CreationTime", Limit = RangeLimit.UpperLimit)] public DateTimeOffset? To { get; set; } // 新增价格范围过滤示例 [DBRangeFilter(ColumnName = "Price", Limit = RangeLimit.LowerLimit)] public decimal? PriceFrom { get; set; } [DBRangeFilter(ColumnName = "Price", Limit = RangeLimit.UpperLimit)] public decimal? PriceTo { get; set; } }
在Action中直接调用扩展方法:
var query = _dbContext.Products.AsNoTracking(); query = query.ApplyRangeFilters(search);
说明
- 该扩展支持任意实体类型
TEntity和搜索模型TSearch,只要通过DBRangeFilterAttribute标记对应字段即可 - 自动处理上下限颠倒、相等的场景
- 已内置
DateTimeOffset转UtcDateTime的逻辑,可根据需求扩展其他类型转换(比如数值类型、Nullable类型) - 同一字段的上下限会自动分组处理,无需额外关联
内容的提问来源于stack exchange,提问作者Parsa99
相关产品推荐
相关产品推荐

