如何在仓储中阻止非预期查询?以无索引字段查询管控为例
如何用表达式树拦截无索引字段的耗时查询
刚好做过类似的需求,其实核心就是遍历表达式树提取用到的实体属性,再和预定义的索引字段列表对比,一旦发现无索引字段就拦截查询。我给你一步步拆解实现思路:
第一步:先定义索引字段的规则
首先得给每个实体类型维护「允许用于查询/排序的索引字段」集合,两种常见实现方式:
方式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
相关产品推荐
相关产品推荐

