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

如何在仓储中阻止非预期查询?以无索引字段查询管控为例

如何用表达式树拦截无索引字段的耗时查询

刚好做过类似的需求,其实核心就是遍历表达式树提取用到的实体属性,再和预定义的索引字段列表对比,一旦发现无索引字段就拦截查询。我给你一步步拆解实现思路:

第一步:先定义索引字段的规则

首先得给每个实体类型维护「允许用于查询/排序的索引字段」集合,两种常见实现方式:

方式1:用静态字典配置

适合集中管理所有实体的索引规则:

public static class IndexedFieldConfig
{
    public static readonly Dictionary<Type, HashSet<string>> EntityIndexedFields = new()
    {
        { typeof(Product), new HashSet<string> { "Id", "CategoryId", "Price" } },
        { typeof(User), new HashSet<string> { "Id", "Email", "CreatedAt" } }
    };
}

方式2:用自定义特性标记

更贴合代码的注解式风格,给实体的索引属性加标记:

[AttributeUsage(AttributeTargets.Property)]
public class IndexedAttribute : Attribute { }

public class Product
{
    [Indexed]
    public int Id { get; set; }
    
    [Indexed]
    public int CategoryId { get; set; }
    
    public string Description { get; set; } // 无索引字段,禁止用于查询/排序
}

第二步:编写表达式树分析工具

接下来要写一个工具类,递归遍历表达式树,把其中用到的实体属性全部提取出来。需要处理常见的表达式类型,比如属性访问、逻辑/比较运算符组合的条件:

public static class ExpressionPropertyExtractor
{
    public static HashSet<string> GetReferencedEntityProperties<T>(Expression<Func<T, bool>> expression)
    {
        var properties = new HashSet<string>();
        TraverseExpression(expression.Body, properties, typeof(T));
        return properties;
    }

    private static void TraverseExpression(Expression expr, HashSet<string> props, Type targetEntityType)
    {
        switch (expr.NodeType)
        {
            // 处理属性访问(比如 x => x.Price > 100 里的 x.Price)
            case ExpressionType.MemberAccess:
                var memberExpr = (MemberExpression)expr;
                // 只收集目标实体类型的属性,忽略常量、参数的其他属性
                if (memberExpr.Expression?.Type == targetEntityType)
                {
                    props.Add(memberExpr.Member.Name);
                }
                // 递归处理嵌套属性(比如 x => x.Category.Id == 5 里的 x.Category)
                TraverseExpression(memberExpr.Expression, props, targetEntityType);
                break;
            
            // 处理二元表达式(&&、||、>、<、== 等组合条件)
            case ExpressionType.AndAlso:
            case ExpressionType.OrElse:
            case ExpressionType.GreaterThan:
            case ExpressionType.LessThan:
            case ExpressionType.Equal:
                var binaryExpr = (BinaryExpression)expr;
                TraverseExpression(binaryExpr.Left, props, targetEntityType);
                TraverseExpression(binaryExpr.Right, props, targetEntityType);
                break;
            
            // 常量、参数这类不需要提取属性的表达式,直接跳过
            case ExpressionType.Constant:
            case ExpressionType.Parameter:
                break;
            
            // 遇到未处理的表达式类型,根据业务需求抛出异常或忽略
            default:
                throw new NotSupportedException($"不支持的表达式类型:{expr.NodeType}");
        }
    }
}

第三步:在仓储的Find方法中加入检查逻辑

把上面的分析工具集成到仓储的Find方法里,检查WhereClause和SortClause是否用到了无索引字段:

public class Repository<T> where T : class
{
    public IEnumerable<T> Find(QueryConstraints<T> constraints)
    {
        // 检查查询条件(WhereClause)
        if (constraints.WhereClause != null)
        {
            var usedProps = ExpressionPropertyExtractor.GetReferencedEntityProperties(constraints.WhereClause);
            var indexedProps = GetIndexedFieldsForEntity<T>();
            
            var nonIndexedProps = usedProps.Except(indexedProps);
            if (nonIndexedProps.Any())
            {
                throw new InvalidOperationException($"查询条件使用了无索引字段:{string.Join(", ", nonIndexedProps)},请改用索引字段查询");
            }
        }

        // 检查排序条件(SortClause)——无索引排序会触发全表扫描,同样需要拦截
        if (constraints.SortClause != null)
        {
            var usedSortProps = ExpressionPropertyExtractor.GetReferencedEntityProperties(constraints.SortClause);
            var indexedProps = GetIndexedFieldsForEntity<T>();
            
            var nonIndexedSortProps = usedSortProps.Except(indexedProps);
            if (nonIndexedSortProps.Any())
            {
                throw new InvalidOperationException($"排序使用了无索引字段:{string.Join(", ", nonIndexedSortProps)},请改用索引字段排序");
            }
        }

        // 这里执行正常的查询逻辑
        // ...
    }

    // 根据你选择的配置方式,获取当前实体的索引字段
    private HashSet<string> GetIndexedFieldsForEntity<T>()
    {
        // 如果用字典配置:
        if (IndexedFieldConfig.EntityIndexedFields.TryGetValue(typeof(T), out var fields))
        {
            return fields;
        }
        throw new ArgumentException($"未找到实体{typeof(T).Name}的索引配置");

        // 如果用特性配置,替换成下面的逻辑:
        // return typeof(T).GetProperties()
        //     .Where(p => Attribute.IsDefined(p, typeof(IndexedAttribute)))
        //     .Select(p => p.Name)
        //     .ToHashSet();
    }
}

额外优化建议

  • 嵌套属性处理:如果你的实体有嵌套对象(比如Product.Category.Name),可以根据业务需求决定是否允许嵌套属性查询,或者需要检查嵌套属性的索引情况。
  • 复杂度限制:如果允许少量无索引字段查询,可以修改检查逻辑,比如限制无索引字段的数量不超过1个,而不是直接拦截。
  • 缓存分析结果:如果同一个查询表达式会被多次使用,可以缓存提取后的属性列表,提升性能。

内容的提问来源于stack exchange,提问作者BTajahmadi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 04:17:14