You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过表达式复用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 03:46:16