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。
问题原因
异常核心原因有两点:
- 传入的
includes参数是普通Func委托而非表达式树,EF Core解析时会将委托调用识别为Invoke操作,而EF Core过滤Include要求表达式最开头必须是直接的导航属性访问,不能包含方法/委托调用。 - 表达式中两次
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ć

