如何以编程方式分解EF Core Include子句并获取引用实体类型
我在EF Core应用中采用了一套支持运行时指定导航属性加载的灵活查询模式,核心包含三个要素:
核心模式实现
- 声明Include表达式委托
public delegate IIncludableQueryable<T, object> IncludeClause<T>(IQueryable<T> value);
- 预定义Include子句实例
针对不同场景定义对应的Include链式调用:
public static IncludeClause<Blog> BlogFull => entities => entities .Include(e => e.Author) .Include(e => e.Posts) .ThenInclude(e => e.Comments);
- 动态调整查询的实体方法
通过传入Include子句控制导航属性加载:
public async Task<Blog> GetBlog(int id, IncludeClause<Blog> include) { return await include(_context.Set<Blog>()).FirstAsync(blog => blog.Id == id); }
当前需求
这套模式运行正常,但需要以编程方式分解任意预定义的Include子句,获取它所引用的所有实体类型集合。例如解析上述BlogFull后,期望得到类型数组:
new[] { typeof(Blog), typeof(Author), typeof(Post), typeof(Comment), }
补充背景
实际需求是解决Web API响应的JSON循环引用问题:响应模型与实体架构大致匹配,深度查询会生成嵌套响应,但实体间的循环引用(如Post包含父级Blog导航属性)会导致序列化失败。
我的初步方案是:用Include子句解析出的类型列表控制响应模型的嵌套填充——根模型拿到完整类型列表,移除自身类型后传递给子模型,子模型仅填充列表中存在的类型,从而避免循环引用。同时也希望了解更简单的替代方案。
解决方案:解析Include表达式树
要实现这个需求,需要将IncludeClause委托转换为表达式树,然后遍历识别Include/ThenInclude调用,提取对应的实体类型。
1. 保留Include子句的表达式形式
首先,确保预定义的Include子句以Expression<IncludeClause<T>>类型保存(而非直接编译为委托),这样才能获取表达式树:
public static Expression<IncludeClause<Blog>> BlogFullExpr => entities => entities .Include(e => e.Author) .Include(e => e.Posts) .ThenInclude(e => e.Comments); // 若需要委托实例,可随时编译 public static IncludeClause<Blog> BlogFull = BlogFullExpr.Compile();
2. 实现表达式访问器
创建ExpressionVisitor子类,遍历表达式树中的Include/ThenInclude方法调用,提取导航属性对应的实体类型:
using System.Linq.Expressions; using Microsoft.EntityFrameworkCore.Query; public class IncludeTypeExtractor<T> : ExpressionVisitor { private readonly HashSet<Type> _entityTypes = new HashSet<Type>(); private Type _currentSourceType = typeof(T); public HashSet<Type> ExtractEntityTypes(Expression<IncludeClause<T>> includeExpression) { _entityTypes.Clear(); _entityTypes.Add(typeof(T)); _currentSourceType = typeof(T); Visit(includeExpression.Body); return _entityTypes; } protected override Expression VisitMethodCall(MethodCallExpression node) { // 处理Include方法 if (node.Method.Name == nameof(EntityFrameworkQueryableExtensions.Include) && node.Method.DeclaringType == typeof(EntityFrameworkQueryableExtensions)) { ExtractNavigationEntityType(node.Arguments[1] as LambdaExpression); } // 处理ThenInclude方法(兼容不同泛型重载的命名) else if (node.Method.Name.StartsWith(nameof(EntityFrameworkQueryableExtensions.ThenInclude)) && node.Method.DeclaringType == typeof(EntityFrameworkQueryableExtensions)) { ExtractNavigationEntityType(node.Arguments[1] as LambdaExpression); } return base.VisitMethodCall(node); } private void ExtractNavigationEntityType(LambdaExpression navigationExpr) { if (navigationExpr == null) return; var propertyType = navigationExpr.ReturnType; // 处理可空类型与集合类型 propertyType = Nullable.GetUnderlyingType(propertyType) ?? propertyType; if (propertyType.IsGenericType && typeof(System.Collections.IEnumerable).IsAssignableFrom(propertyType)) { propertyType = propertyType.GetGenericArguments()[0]; } _entityTypes.Add(propertyType); _currentSourceType = propertyType; } }
3. 使用示例
调用提取器获取实体类型集合:
var extractor = new IncludeTypeExtractor<Blog>(); HashSet<Type> entityTypes = extractor.ExtractEntityTypes(BlogFullExpr); // 输出结果:Blog、Author、Post、Comment foreach (var type in entityTypes) { Console.WriteLine(type.Name); }
更简单的替代方案:直接处理JSON序列化循环引用
其实无需解析Include表达式树,针对循环引用问题,有更直接的解决方案:
方案1:配置JSON序列化忽略循环引用
在ASP.NET Core中,直接配置JsonOptions忽略循环引用:
builder.Services.AddControllers() .AddJsonOptions(options => { options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles; });
此配置会自动检测并跳过循环引用的属性,无需修改响应模型或实体。
方案2:使用AutoMapper映射实体到响应模型
通过AutoMapper控制实体到响应模型的映射,显式忽略会导致循环的属性:
public class MappingProfile : Profile { public MappingProfile() { CreateMap<Blog, BlogResponseModel>() .ForMember(dest => dest.Posts, opt => opt.MapFrom(src => src.Posts)); CreateMap<Post, PostResponseModel>() .ForMember(dest => dest.Blog, opt => opt.Ignore()); // 忽略父级Blog属性,避免循环 CreateMap<Author, AuthorResponseModel>(); CreateMap<Comment, CommentResponseModel>(); } }
AutoMapper不仅能避免循环引用,还能更灵活地控制响应模型的字段结构,是Web API开发中常用的实体映射方案。
内容的提问来源于stack exchange,提问作者Tim Coulter

