如何在C# .NET Core中实现带筛选的Include().ThenInclude()查询
为Blazor通用组件实现带筛选条件的Include关联查询
当前实现情况
我正在开发一个带配置对象的Blazor通用组件,已单独实现.Where()和.Include().ThenInclude()功能,但需要为关联数据添加筛选条件。
当前配置类结构:
class Config<T> { public List<string> Includes { get; set; } public List<Expression<Func<T, bool>>> Conditions { get; set; } }
数据库结构示例:
Dataset (1)<--(n) DatasetItem DatasetItem (1)-->(1) Image DatasetItem (1)-->(1) Label Image (1)<--(n) Labels Label(1)<--(n) Tags
说明:一张图片可属于多个数据集,标签可能不在同一数据集中。例如城市公园图片包含狗、人、汽车标签,用户可能需要仅包含汽车标签的车辆数据集。
目前配置条件和关联的方式:
var config = new Config<DatasetItem>() { Includes = new() { "Image", "Image.Labels", "Image.Labels.Tags" }, Conditions = new() { di => di.DatasetId == 1, di => di.Label.Width > 100 } };
现有问题
- 字符串形式的Include重载无编译时检查,容易出错
- 无法为关联数据添加筛选条件,比如无法仅获取特定类别的Tags
- 尝试用
Any/All方法无法达到预期效果:
config.Conditions.Add( di => di.Images.Any(i => i.Labels.Any(l => l.Tags.Any(t => t.Name == "vehicle"))) );
这种写法只会过滤主实体(DatasetItem),不会过滤关联的Tags集合,返回的Images里仍会包含所有标签。
预期目标
实现类似EF Core原生的带筛选关联查询效果:
DatasetItems.Include(di => di.Images) .ThenInclude(i => i.Labels) .ThenInclude(l => l.Tags.Where(t => t.Name == "vehicle"));
希望配置类能支持类似条件表达式的结构,比如:
config.IncludeExpressions.Add(di => di.Images.Labels.Tags.Where(t => t.Name == "vehicle"));
解决方案实现
通过表达式树解析和反射调用EF Core的Include/ThenInclude方法,处理关联路径上的筛选条件。
1. 修改配置类
扩展配置类,添加支持带筛选的Include表达式列表:
class Config<T> { // 保留原字符串形式Include(兼容旧代码) public List<string> Includes { get; set; } = new(); // 新增带筛选的Include表达式 public List<Expression> IncludeExpressions { get; set; } = new(); // 主实体筛选条件 public List<Expression<Func<T, bool>>> Conditions { get; set; } = new(); }
2. 实现表达式解析与扩展方法
核心是解析包含Where的嵌套属性表达式,拆分关联路径和筛选条件,依次调用Include和ThenInclude并应用筛选:
public static class QueryableExtensions { public static IQueryable<T> ApplyConfig<T>(this IQueryable<T> query, Config<T> config) where T : class { // 先应用主实体筛选条件 foreach (var condition in config.Conditions) { query = query.Where(condition); } // 处理传统字符串路径Include foreach (var includePath in config.Includes) { query = query.Include(includePath); } // 处理带筛选的Include表达式 foreach (var includeExpr in config.IncludeExpressions) { query = ApplyFilteredInclude(query, includeExpr); } return query; } private static IQueryable<T> ApplyFilteredInclude<T>(IQueryable<T> query, Expression includeExpr) where T : class { // 解析表达式,拆分关联路径和筛选条件 var (pathSegments, filterExpr) = ParseFilteredIncludeExpression(includeExpr); if (pathSegments == null || pathSegments.Count == 0) return query; // 第一步:调用Include获取第一个关联 var currentQuery = query.Include(pathSegments[0]); var currentType = typeof(T); var previousPropertyType = currentType.GetProperty(pathSegments[0])?.PropertyType; // 后续步骤:循环调用ThenInclude遍历关联路径 for (int i = 1; i < pathSegments.Count; i++) { var propertyName = pathSegments[i]; var thenIncludeMethod = GetThenIncludeMethod(currentType, previousPropertyType!); if (thenIncludeMethod == null) break; // 构建属性访问表达式 var parameter = Expression.Parameter(previousPropertyType!, "x"); var propertyExpr = Expression.Property(parameter, propertyName); var lambda = Expression.Lambda(propertyExpr, parameter); // 执行ThenInclude调用 currentQuery = (IQueryable<T>)thenIncludeMethod.Invoke(null, new object[] { currentQuery, lambda })!; // 更新类型信息,用于下一次迭代 currentType = previousPropertyType!; previousPropertyType = previousPropertyType!.GetProperty(propertyName)?.PropertyType; } // 对最后一个关联节点应用筛选条件 if (filterExpr != null && previousPropertyType != null) { // 处理集合类型关联(如IEnumerable<T>) if (typeof(IEnumerable<>).IsAssignableFrom(previousPropertyType) || previousPropertyType.IsGenericType) { var elementType = previousPropertyType.GetGenericArguments()[0]; // 获取Queryable.Where方法 var whereMethod = typeof(Queryable).GetMethods() .First(m => m.Name == "Where" && m.GetParameters().Length == 2) .MakeGenericMethod(elementType); // 构建筛选表达式的Lambda var filterLambda = Expression.Lambda(filterExpr, Expression.Parameter(elementType, "t")); // 构建带筛选的属性访问表达式 var parameter = Expression.Parameter(currentType, "x"); var pathExpr = BuildPropertyExpression(parameter, pathSegments.Skip(1)); var filteredPathExpr = Expression.Call(whereMethod, pathExpr, filterLambda); var thenIncludeLambda = Expression.Lambda(filteredPathExpr, parameter); // 调用ThenInclude应用筛选 var thenIncludeMethod = GetThenIncludeMethod(currentType, previousPropertyType); if (thenIncludeMethod != null) { currentQuery = (IQueryable<T>)thenIncludeMethod.Invoke(null, new object[] { currentQuery, thenIncludeLambda })!; } } } return currentQuery; } private static (List<string> PathSegments, Expression? FilterExpr) ParseFilteredIncludeExpression(Expression expr) { var pathSegments = new List<string>(); Expression? filterExpr = null; // 遍历表达式树,拆解嵌套属性和Where调用 while (expr != null) { if (expr is MethodCallExpression methodCall && methodCall.Method.Name == "Where") { filterExpr = methodCall.Arguments[1]; expr = methodCall.Arguments[0]; } else if (expr is MemberExpression memberExpr) { pathSegments.Insert(0, memberExpr.Member.Name); expr = memberExpr.Expression; } else { break; } } return (pathSegments.Count > 0 ? pathSegments : null, filterExpr); } private static MethodInfo? GetThenIncludeMethod(Type sourceType, Type navigationType) { // 区分集合类型和引用类型的ThenInclude重载 var isCollection = typeof(IEnumerable<>).IsAssignableFrom(navigationType) || navigationType.IsGenericType; var thenIncludeMethod = typeof(EntityFrameworkQueryableExtensions) .GetMethods() .First(m => m.Name == "ThenInclude" && m.GetParameters().Length == 2 && m.GetGenericArguments().Length == (isCollection ? 3 : 2)); return isCollection ? thenIncludeMethod.MakeGenericMethod(sourceType, navigationType.GetGenericArguments()[0], navigationType) : thenIncludeMethod.MakeGenericMethod(sourceType, navigationType); } private static Expression BuildPropertyExpression(Expression parameter, IEnumerable<string> propertyNames) { Expression current = parameter; foreach (var name in propertyNames) { current = Expression.Property(current, name); } return current; } }
3. 使用示例
现在可以通过配置类添加带筛选的关联:
var config = new Config<DatasetItem>() { Conditions = new() { di => di.DatasetId == 1 }, IncludeExpressions = new() { (Expression)(di => di.Image.Labels.Tags.Where(t => t.Name == "vehicle")) } }; // 将配置应用到EF Core查询 var query = dbContext.DatasetItems.ApplyConfig(config);
关键说明
- 兼容EF Core原生
Include/ThenInclude逻辑,支持多层嵌套关联的筛选 - 保留原字符串路径Include的兼容性,可逐步迁移到表达式形式
- 通过表达式树解析避免了字符串路径的编译时错误风险
内容的提问来源于stack exchange,提问作者J Pablo F
相关产品推荐
相关产品推荐

