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

EF Core自定义Include权限过滤通用方法报无效表达式异常求解

EF Core通用过滤Include扩展方法抛出无效表达式异常

我尝试编写如下通用扩展方法,用于集中校验用户对导航属性的读取权限,仅将符合权限要求的导航属性纳入查询结果:

public static IQueryable<TModel> IncludeByUserCondition<TModel, TIncludable>(this IQueryable<TModel> query, Func<TModel, IQueryable<TIncludable>> includes, List<int> userIDs)
        where TModel : class
        where TIncludable: class
    {
        Expression<Func<TModel, object>> result = x => includes(x);

        if(typeof(ASqlBase).IsAssignableFrom(typeof(TIncludable)))
        {
            result = x =>
                includes(x)
                .Select(prop => prop as ASqlBase)
                .Where(prop => 
                    prop.DeleteDate == null
                )
                .Where(prop => 
                    userIDs != null && userIDs.Count > 0 ? userIDs.Contains(prop.IdentityUnitID) : true
                )
                .Select(prop => prop as TIncludable);
        }

        query = query.Include(result);
        return query;
    }

方法设计背景

我的应用读取权限采用层级设计,登录用户可查看自身记录以及自己添加到系统的所有用户的记录,因此编译期无法确定全部可见记录范围,无法为不同用户组配置独立数据库上下文;同时该权限过滤只是众多数据过滤规则的一种,无法使用EF Core全局过滤器实现。

调用方式

我使用如下方式调用该方法:

qry = qry.IncludeByUserCondition<AllocatedFund, AllocatedFundDetailPaymentMade>(p => p.AllocatedFundDetailPaymentsMade.AsQueryable(), allowedUserIDs);

运行时异常

运行时抛出如下异常:

The expression 'Invoke(__includes_0, x).Select(prop => (prop As ASqlBase)).Where(prop => ((prop.DeleteDate == null))).Where(prop => True).Select(prop => (prop As AllocatedFundDetailPaymentMade))' is invalid inside an 'Include' operation, since it does not represent a property access: 't => t.MyProperty'. To target navigations declared on derived types, use casting ('t => ((Derived)t).MyProperty') or the 'as' operator ('t => (t as Derived).MyProperty'). Collection navigation access can be filtered by composing Where, OrderBy(Descending), ThenBy(Descending), Skip or Take operations.

对照测试

如果手动在Include中直接编写相同的过滤逻辑,查询可以正常运行:

qry = qry.Include(p => 
                    p.AllocatedFundDetailPaymentsMade
                    .AsQueryable()
                    .Where(prop => 
                        prop.DeleteDate == null
                    )
                    .Where(prop => 
                        userIDs != null && userIDs.Count > 0 ? userIDs.Contains(prop.IdentityUnitID) : true
                    )

应用中有30余个模型都需要使用相同逻辑过滤被包含的导航属性,不希望在每个查询中重复编写相同的Where子句。


补充说明

ASqlBase是部分模型继承的抽象基类(并非所有模型都继承该类,例如User模型就不继承ASqlBase),其定义如下:

public abstract class ASqlBase
    {
        [Key]
        public int ID { get; set; }

        [Required]
        public int UserID { get; set; }
        [ForeignKey("UserID")]
        public virtual User User { get; set; }

        public DateTime? DeleteDate { get; set; }
}

该方法主要用于获取报表展示数据,以Person模型为例,方法调用形式如下:

var qry = dbContext.Person.IncludeByUserCondition<Person, Athlete>(p => p.Athletes.AsQueryable(), athleteAllowedUserIDs);
qry = qry.IncludeByUserCondition<Person, Employee>(p => p.Employees.AsQueryable(), employeeAllowedUserIDs);
qry = qry.IncludeByUserCondition<Person, Student>(p => p.Students.AsQueryable(), studentAllowedUserIDs);

Person模型定义如下:

public class Person : ASqlBase
{
    ...
    public virtual ICollection<Athlete> Athletes { get; set; }
    public virtual ICollection<Employee> Employees { get; set; }
    public virtual ICollection<Student> Students { get; set; }
}

上述模型中Athlete、Employee和Student均继承自ASqlBase。


更正说明

之前方法名存在笔误,正确方法名为IncludeByUserCondition,而非之前使用的IncludeMultiple。


问题原因

异常核心原因有两点:

  1. 传入的includes参数是普通Func委托而非表达式树,EF Core解析时会将委托调用识别为Invoke操作,而EF Core过滤Include要求表达式最开头必须是直接的导航属性访问,不能包含方法/委托调用。
  2. 表达式中两次Select强转操作多余,破坏了EF Core要求的导航属性访问表达式结构。

解决方案

调整扩展方法参数为导航属性访问表达式树,直接在原生导航访问表达式上拼接过滤逻辑,去掉多余类型转换:

public static IQueryable<TModel> IncludeByUserCondition<TModel, TIncludable>(
    this IQueryable<TModel> query, 
    Expression<Func<TModel, IEnumerable<TIncludable>>> navigationProperty, 
    List<int> userIDs)
        where TModel : class
        where TIncludable : ASqlBase
{
    // 取lambda参数和导航属性访问表达式
    var entityParam = navigationProperty.Parameters[0];
    var navAccessExpr = navigationProperty.Body;

    // 构建集合元素过滤条件
    var propParam = Expression.Parameter(typeof(TIncludable), "prop");
    // 软删除过滤条件:DeleteDate == null
    var deleteDateProp = Expression.Property(propParam, nameof(ASqlBase.DeleteDate));
    var softDeleteCondition = Expression.Equal(deleteDateProp, Expression.Constant(null, typeof(DateTime?)));
    
    // 权限过滤条件:用户ID列表匹配,无有效ID列表时不过滤
    Expression permissionCondition = Expression.Constant(true);
    if (userIDs != null && userIDs.Count > 0)
    {
        // 注意:如果你的实体权限字段不是IdentityUnitID,请替换为实际属性名
        var unitIdProp = Expression.Property(propParam, "IdentityUnitID");
        var containsMethod = typeof(List<int>).GetMethod(nameof(List<int>.Contains), new[] { typeof(int) });
        permissionCondition = Expression.Call(Expression.Constant(userIDs), containsMethod, unitIdProp);
    }
    var combinedCondition = Expression.AndAlso(softDeleteCondition, permissionCondition);
    var whereLambda = Expression.Lambda<Func<TIncludable, bool>>(combinedCondition, propParam);

    // 拼接Where到导航属性表达式
    var whereMethod = typeof(Enumerable).GetMethods()
        .Where(m => m.Name == nameof(Enumerable.Where) && m.GetParameters().Length == 2)
        .First()
        .MakeGenericMethod(typeof(TIncludable));
    var filteredNavExpr = Expression.Call(whereMethod, navAccessExpr, whereLambda);

    // 组装最终Include表达式
    var finalIncludeLambda = Expression.Lambda<Func<TModel, IEnumerable<TIncludable>>>(filteredNavExpr, entityParam);
    return query.Include(finalIncludeLambda);
}

如果需要支持不继承ASqlBase、不需要权限过滤的导航属性,补充一个简单重载即可:

public static IQueryable<TModel> IncludeByUserCondition<TModel, TIncludable>(
    this IQueryable<TModel> query, 
    Expression<Func<TModel, IEnumerable<TIncludable>>> navigationProperty)
        where TModel : class
        where TIncludable : class
{
    return query.Include(navigationProperty);
}

调用说明

原有调用逻辑基本不变,可省略多余的.AsQueryable():

var qry = dbContext.Person
    .IncludeByUserCondition<Person, Athlete>(p => p.Athletes, athleteAllowedUserIDs)
    .IncludeByUserCondition<Person, Employee>(p => p.Employees, employeeAllowedUserIDs)
    .IncludeByUserCondition<Person, Student>(p => p.Students, studentAllowedUserIDs);

该实现生成的表达式结构和手动编写的Include逻辑完全一致,EF Core可正常解析执行,同时避免了30余处重复编写过滤条件的问题。


内容的提问来源于stack exchange,提问作者Goran Petrović

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 13:45:33