前言
最近在开发订单(Matno)查询接口时,遇到了一个困扰许久的Bug——接口调用后持续报错,查看日志发现全是System.Text.Json.JsonException: A possible object cycle was detected异常。
熟悉.NET开发的朋友看到这个错误,第一反应大概率和我一致:“坏了,业务实体肯定出现了循环引用,或者对象层级太深(默认超过32层)。”
可我反复检查LogMatno实体类,发现它只是一个普通的POCO类,既没有复杂的导航属性,也没有自引用的情况。排查一圈后才发现,问题根源竟是一个低级疏忽——await控制器中少写了一个。
问题场景
本次接口的代码结构十分简洁:Controller层调用Service层,Service层负责查询数据库,具体问题出在Controller的调用逻辑中。
出问题的控制器代码
以下代码为控制器中引发Bug的核心逻辑(已标注问题所在):
[HttpGet]
public async Task<ActionResult> GetMatno(int pages, int limit, string mname, string status, string matno, string apbmname, string apbmid)
{
var userInfo = JwtUser.GetRequestUser(this.Request);
// 问题所在:未使用await,导致接收的不是业务结果
var data = _MatnoService.GetMatno(pages, limit, mname, status, matno, apbmname, apbmid, userInfo.userid);
return Ok(data);
}
对应的Service方法签名
Service层的方法为异步方法,返回值为Task<ResultData>,具体签名如下:
public async Task<ResultData> GetMatno(...) { ... }
报错信息
接口请求后,直接抛出如下异常,无额外冗余信息:
System.Text.Json.JsonException: A possible object cycle was detected which is not supported. This can either be due to a cycle or if the object depth is larger than the maximum allowed depth of 32.
深度剖析:为什么漏掉await会报“循环引用”?
这是本次Bug最值得探讨的地方——表面是“循环引用”,本质是异步调用的疏忽,具体原因可分为4步拆解:
- 返回值并非业务结果:由于
_MatnoService.GetMatno是异步方法(返回Task<ResultData>),而Controller中未使用await,此时data接收的不是预期的ResultData业务对象,而是一个Task正在执行的对象。 - 序列化器触发异常:当代码执行到
Ok(data),准备将结果返回给前端时,ASP.NET Core 的System.Text.Json序列化器会对data进行JSON序列化,这是异常的触发点。 - Task对象的复杂结构:
Task是系统级复杂对象,包含Exception、AsyncState、CreationOptions等多个内部属性。更关键的是,其部分内部属性会通过父子任务、状态机关系,间接引用自身或形成复杂队列。 - 报错的本质原因:序列化器会递归解析
Task对象的所有属性,在深层解析中要么触发32层的深度限制,要么遇到内部循环引用,最终抛出A possible object cycle was detected异常。
修复方案
这个Bug的修复极其简单,只需补充6个字母——await,确保序列化的是业务对象而非Task对象。
修复后的控制器代码
[HttpGet]
public async Task<ActionResult> GetMatno(...)
{
var userInfo = JwtUser.GetRequestUser(this.Request);
// 补充await,确保获取到ResultData业务对象
var data = await _MatnoService.GetMatno(..., userInfo.userid);
return Ok(data);
}
简化写法
若无需单独处理data,可进一步简化代码:
return Ok(await _MatnoService.GetMatno(...));
总结与反思
这次低级疏忽,给我带来了3个重要的开发警示,也希望能帮到遇到类似报错的朋友:
- 编译器并非万能:虽然IDE会提示“此调用未完成等待,将在调用前继续执行”,但在复杂业务逻辑中,这类警告很容易被忽略,需格外留意。
- 异常信息需“穿透解读”:看到
JsonException,不要只局限于检查实体类的循环引用,还要确认传给序列化器的对象是否正确(本次就是误传了Task对象)。 - 异步调用需“首尾呼应”:只要方法定义为
async Task,调用的每一层都必须使用await,避免出现“异步不等待”的情况。
最后提醒:遇到JSON循环引用异常时,除了检查实体类,不妨多一步排查——你是不是在序列化一个Task对象?
补充注意事项
结合本次Bug排查,补充3个.NET开发中易忽略的细节,尤其适配后端接口开发场景:
- 关于重复写入:严禁在
Startup中手动执行AddNLog(),应统一在Program.cs中使用.UseNLog(),避免日志配置冲突。 - 关于流重置:读取
ResponseBody后必须执行Seek(0, Begin),否则前端将收到空响应,影响交互体验。 - 关于物理隔离:在
NLog.config规则中使用final="true",是实现日志分流、防止日志“冒泡”到通用日志的关键,便于后续问题排查。

Comments NOTHING