如何在IQueryExpressionInterceptor中查找查询引用的实体类型?
核心结论:优先依赖EF Core专属表达式类型,而非Type属性
在EF Core的查询表达式树中,实体类型的可靠标识是EF Core定义的专属表达式节点,而非通过Type属性或泛型参数间接提取——后者确实存在被复杂查询场景打破的可能。
关键表达式节点位置
- EntityQueryRootExpression
这是EF Core查询的根节点,所有从DbSet<T>发起的查询最终都会包含这个节点(即使被多层LINQ方法调用包裹)。它直接暴露EntityType属性,能直接获取对应的实体元数据,是最可靠的实体类型来源。
比如你测试的所有查询,db.Foos对应的底层就是EntityQueryRootExpression,后续的FirstAsync/Select/Include等LINQ方法都是以它为参数的MethodCallExpression。
IncludeExpression
当使用Include/ThenInclude加载关联数据时,表达式树中会出现IncludeExpression节点,通过它的Navigation.TargetEntityType属性可以获取关联的实体类型。FromSqlExpression
对于原生SQL查询(FromSqlRaw/FromSqlInterpolated),该节点的EntityType属性会指向查询对应的实体类型,即使查询最终投影到DTO,实体类型依然保存在这里。
会打破「Type属性提取」规律的场景
以下几种查询构造方式,仅通过递归检查Type属性或泛型参数会遗漏实体类型:
投影到DTO/匿名类型的查询
var dtoList = await db.Foos .Select(f => new FooDto { Id = f.FooId, Name = f.Name }) .ToListAsync();外层
MethodCallExpression的Type是FooDto,但实体类型Foo仅存在于底层的EntityQueryRootExpression中。多对多隐式连接查询
假设Foo和Bar是多对多关联,EF Core自动生成中间连接实体FooBar:var bars = await db.Foos .Where(f => f.Name == "Test") .SelectMany(f => f.Bars) .ToListAsync();中间连接实体
FooBar不会出现在外层的泛型参数或Type属性中,但会在表达式树的连接节点中通过EntityType暴露。无键实体(Keyless Entity)查询
var stats = await db.OrderStats .FromSqlRaw("SELECT COUNT(*) AS TotalOrders FROM Orders") .ToListAsync();OrderStats是无键实体,仅通过Type属性无法区分它是EF实体还是普通DTO,必须通过EntityQueryRootExpression.EntityType确认。
推荐的遍历实现思路
递归遍历表达式树,重点识别EF Core专属表达式类型:
public static IEnumerable<IEntityType> GetReferencedEntityTypes(Expression expression) { var entityTypes = new HashSet<IEntityType>(); Traverse(expression, entityTypes); return entityTypes; } private static void Traverse(Expression expression, HashSet<IEntityType> entityTypes) { if (expression == null) return; // 处理查询根实体 if (expression is EntityQueryRootExpression root) { entityTypes.Add(root.EntityType); } // 处理Include关联实体 else if (expression is IncludeExpression include) { entityTypes.Add(include.Navigation.TargetEntityType); Traverse(include.QueryExpression, entityTypes); } // 处理原生SQL对应的实体 else if (expression is FromSqlExpression fromSql) { entityTypes.Add(fromSql.EntityType); Traverse(fromSql.Subquery, entityTypes); } // 递归处理方法调用的参数 else if (expression is MethodCallExpression methodCall) { foreach (var arg in methodCall.Arguments) { Traverse(arg, entityTypes); } } // 递归处理Lambda表达式的主体 else if (expression is LambdaExpression lambda) { Traverse(lambda.Body, entityTypes); } // 处理其他表达式类型的子节点 else if (expression is BinaryExpression binary) { Traverse(binary.Left, entityTypes); Traverse(binary.Right, entityTypes); } else if (expression is MemberExpression member) { Traverse(member.Expression, entityTypes); } }
内容的提问来源于stack exchange,提问作者Kevin Krumwiede

