.NET Core EF Core 3.1 API 返回重复嵌套JSON,如何调整输出格式且少改模型
问题根源
当前接口的冗余嵌套来自两个点:
- 接口返回根类型是
MotorParts集合,序列化时会自动展开实体定义的Motor导航属性,而Motor实体的MotorParts集合又会被递归展开,形成循环引用 - 如果启用了EF Core 3.1的延迟加载代理,序列化触发导航属性访问时会自动加载关联实体,进一步放大冗余数据
适配方案(完全不修改现有实体模型)
方案1:调整查询逻辑为Motor端投影查询(推荐,完全匹配期望结构)
不需要修改现有实体的任何配置,直接从MotorList实体端发起查询,通过投影只取需要的字段,从根源避免冗余和循环引用。
修改后的控制器代码:
[HttpGet("GetPartsByMotorId/{id}")] public async Task<ActionResult<IEnumerable<object>>> GetPartsByMotorId(Guid id) { // 直接按MotorId查询Motor基础信息,同时投影排序后的关联零件 var motorData = await _context.MotorList .Where(m => m.MotorId == id) .Select(m => new { motor = new { motorId = m.MotorId, motorText = m.MotorText, // 按partText排序零件,只取需要的字段,避免冗余 motorParts = m.MotorParts .OrderBy(p => p.PartText) .Select(p => new { partId = p.PartId, motorId = p.MotorId, partText = p.PartText }) .ToList() } }) .FirstOrDefaultAsync(); if (motorData == null) return NotFound(); // 包装为数组,完全匹配你提供的期望返回结构 return new[] { motorData }; }
优势:
- 不改动原有
MotorParts、MotorList实体的定义和配置 - 数据库端直接过滤不需要的字段,查询效率更高
- 序列化时无嵌套导航属性,彻底消除冗余和循环引用
方案2:配置全局JSON序列化规则(辅助适配)
如果暂时不想调整原有查询逻辑,可通过配置ASP.NET Core的序列化规则,拦截循环引用导致的冗余嵌套:
若使用.NET Core 3.1默认的System.Text.Json
在Startup.cs的ConfigureServices中添加配置:
services.AddControllers() .AddJsonOptions(options => { // 遇到循环引用直接停止序列化 options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles; // 可选:忽略null值减少冗余 options.JsonSerializerOptions.DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull; });
若使用Newtonsoft.Json
配置如下:
services.AddControllers() .AddNewtonsoftJson(options => { options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore; });
注意:该方案仅能解决无限嵌套的问题,原有返回的根结构仍是零件列表,和你期望的「Motor为根、嵌套零件」的结构不匹配,建议搭配方案1使用。
可选:使用DTO定义强类型返回结构(不影响原有实体)
如果不想返回匿名类,可额外定义DTO数据传输对象(不属于原有实体,不违反要求),让返回结构更规范:
// 零件DTO public class MotorPartResponse { public Guid PartId { get; set; } public Guid? MotorId { get; set; } public string PartText { get; set; } } // Motor DTO public class MotorResponse { public Guid MotorId { get; set; } public string MotorText { get; set; } public List<MotorPartResponse> MotorParts { get; set; } } // 外层包装DTO,匹配你要的结构 public class MotorWrapper { public MotorResponse Motor { get; set; } }
查询时直接投影为强类型DTO即可:
var motorData = await _context.MotorList .Where(m => m.MotorId == id) .Select(m => new MotorWrapper { Motor = new MotorResponse { MotorId = m.MotorId, MotorText = m.MotorText, MotorParts = m.MotorParts .OrderBy(p => p.PartText) .Select(p => new MotorPartResponse { PartId = p.PartId, MotorId = p.MotorId, PartText = p.PartText }) .ToList() } }) .FirstOrDefaultAsync();
常见注意点
- 原代码中排序用的
c.partText为小写,和实体定义的PartText(首字母大写)不一致,注意匹配大小写避免查询报错 - 如果已启用EF Core延迟加载代理,投影查询不会触发额外的导航属性加载,不会影响现有实体的配置
内容的提问来源于stack exchange,提问作者SkyeBoniwell
相关产品推荐
相关产品推荐

