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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 23:15:34