System.Text.Json.JsonExceptionJsonSerializerOptions to support cycles
System.Text.Json.JsonException:“A possible object cycle was detected. This can either be due to a cycle or if the object depth is larger than the maximum allowed depth of 32. Consider using ReferenceHandler.Preserve on JsonSerializerOptions to support cycles. Path: $.Data.UserGroups.Group.UserGroups.Group.UserGroups.Group.UserGroups.Group.UserGroups.Group.UserGroups.Group.UserGroups.Group.UserGroups.Group.UserGroups.Group.UserGroups.Group.Id.”
System.Text.Json.JsonException:“A possible object cycle was detected. This can either be due to a cycle or if the object depth is larger than the maximum allowed depth of 32. Consider using ReferenceHandler.Preserve on JsonSerializerOptions to support cycles. Path: $.Data.UserGroups.Group.UserGroups.Group.UserGroups.Group.UserGroups.Group.UserGroups.Group.UserGroups.Group.UserGroups.Group.UserGroups.Group.UserGroups.Group.UserGroups.Group.Id.”
对象图中存在 循环引用(Object Cycle),而当前的 JSON 序列化配置 没有启用循环引用处理机制
看一下model // UserGroup 配置
modelBuilder.Entity<UserGroup>(entity =>
{
entity.ToTable("UserGroup");
entity.HasKey(e => e.Id);
entity.Property(e => e.Id).HasColumnName("Id");
entity.Property(e => e.UserId).HasColumnName("UserId");
entity.Property(e => e.GroupId).HasColumnName("GroupId");
entity.Property(e => e.JoinTime).HasColumnName("JoinTime").HasDefaultValueSql("CURRENT_TIMESTAMP");
entity.Property(e => e.Role).HasColumnName("Role").HasDefaultValue(UserRole.Member);
entity.HasOne(e => e.User);
//.WithMany(u => u.UserGroups)
//.HasForeignKey(e => e.UserId)
//.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(e => e.Group)
.WithMany(g => g.UserGroups)
.HasForeignKey(e => e.GroupId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasIndex(e => new { e.UserId, e.GroupId }).IsUnique();
});
,
// SysUser 配置
modelBuilder.Entity<SysUser>(entity =>
{
entity.ToTable("SysUser");
entity.HasKey(e => e.Id);
entity.Property(e => e.Id).HasColumnName("Id");
entity.Property(e => e.UserName).HasColumnName("UserName").HasMaxLength(50).IsRequired();
entity.Property(e => e.Password).HasColumnName("Password").HasMaxLength(100).IsRequired();
entity.Property(e => e.Email).HasColumnName("Email").HasMaxLength(100);
entity.Property(e => e.CreateTime).HasColumnName("CreateTime").HasDefaultValueSql("CURRENT_TIMESTAMP");
entity.Property(e => e.IsActive).HasColumnName("IsActive").HasDefaultValue(true);
entity.HasIndex(e => e.UserName).IsUnique();
});
// TodoGroup 配置
modelBuilder.Entity<TodoGroup>(entity =>
{
entity.ToTable("TodoGroup");
entity.HasKey(e => e.Id);
entity.Property(e => e.Id).HasColumnName("Id");
entity.Property(e => e.GroupName).HasColumnName("GroupName").HasMaxLength(100).IsRequired();
entity.Property(e => e.Description).HasColumnName("Description").HasMaxLength(500);
entity.Property(e => e.CreatorId).HasColumnName("CreatorId");
entity.Property(e => e.CreateTime).HasColumnName("CreateTime").HasDefaultValueSql("CURRENT_TIMESTAMP");
entity.Property(e => e.IsActive).HasColumnName("IsActive").HasDefaultValue(true);
entity.HasOne(e => e.Creator)
.WithMany()
.HasForeignKey(e => e.CreatorId)
.OnDelete(DeleteBehavior.Restrict);
}); 还有方法 public async Task<ApiResponse<TodoGroup>> CreateGroupAsync(int creatorId, string groupName, string? description = null)
{
try
{
var group = new TodoGroup
{
GroupName = groupName,
Description = description,
CreatorId = creatorId,
CreateTime = DateTime.Now,
IsActive = true
};
_context.TodoGroups.Add(group);
await _context.SaveChangesAsync();
// 创建者自动加入组并设为管理员
var userGroup = new UserGroup
{
UserId = creatorId,
GroupId = group.Id,
Role = UserRole.Admin,
JoinTime = DateTime.Now
};
_context.UserGroups.Add(userGroup);
await _context.SaveChangesAsync();
return ApiResponse<TodoGroup>.SuccessResult(group, "组创建成功");
}
catch (Exception ex)
{
return ApiResponse<TodoGroup>.ErrorResult($"创建组失败: {ex.Message}", 500);
}
}
感谢你提供完整的 DbContext 配置和 CreateGroupAsync 方法。现在我们可以 精准定位并解决 你遇到的 System.Text.Json.JsonException: A possible object cycle was detected 问题。
🔍 问题根源分析
你遇到的异常路径是:
Path: $.Data.UserGroups.Group.UserGroups.Group.UserGroups...
结合你的模型配置,我们可以推断出:
✅ 模型关系如下:
SysUser↔UserGroup↔TodoGroupUserGroup:UserId→SysUser.IdGroupId→TodoGroup.IdGroup导航属性 → 指向TodoGroupUser导航属性 → 指向SysUser
TodoGroup:UserGroups导航属性 →ICollection<UserGroup>(一个组有多个成员)Creator→ 指向SysUser(创建者)
SysUser:- 缺少
UserGroups导航属性!(关键点)
- 缺少
❌ 循环引用路径(导致异常)
当你查询 TodoGroup 并包含 UserGroups 和 UserGroups.Group 时,会形成以下链条:
TodoGroup
└── UserGroups → UserGroup
└── Group → TodoGroup
└── UserGroups → UserGroup
└── Group → TodoGroup
...(无限嵌套)
虽然 EF Core 不会无限加载,但如果你在返回 API 响应时 未断开循环,序列化器会尝试遍历这个结构,导致:
深度超过 32 层 → 抛出
object cycle异常
✅ 解决方案(推荐组合拳)
✅ 方案一:【立即修复】启用 ReferenceHandler.Preserve
在 Program.cs(或 Startup.cs)中配置 JSON 选项:
// Program.cs (.NET 6+)
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.Preserve;
options.JsonSerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase;
});
// 如果用了 SignalR
builder.Services.AddSignalR()
.AddJsonProtocol(options =>
{
options.PayloadSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.Preserve;
});
✅ 立即可解决循环引用问题
⚠️ JSON 会包含$id,$ref元数据(客户端需支持)
✅ 方案二:【最佳实践】使用 DTO 返回数据(推荐)
不要直接返回实体(TodoGroup),而是返回一个 扁平化的 DTO。
1. 创建 DTO 类
public class TodoGroupDto
{
public int Id { get; set; }
public string GroupName { get; set; } = string.Empty;
public string? Description { get; set; }
public int CreatorId { get; set; }
public DateTime CreateTime { get; set; }
public bool IsActive { get; set; }
// ✅ 返回成员信息,但不包含 Group 导航属性
public List<UserGroupDto> Members { get; set; } = new();
}
public class UserGroupDto
{
public int Id { get; set; }
public int UserId { get; set; }
public string UserName { get; set; } = string.Empty; // 来自 SysUser
public UserRole Role { get; set; }
public DateTime JoinTime { get; set; }
}
2. 修改 CreateGroupAsync 返回 DTO
public async Task<ApiResponse<TodoGroupDto>> CreateGroupAsync(int creatorId, string groupName, string? description = null)
{
try
{
var creator = await _context.SysUsers
.FirstOrDefaultAsync(u => u.Id == creatorId);
if (creator == null)
return ApiResponse<TodoGroupDto>.ErrorResult("用户不存在", 404);
var group = new TodoGroup
{
GroupName = groupName,
Description = description,
CreatorId = creatorId,
CreateTime = DateTime.Now,
IsActive = true
};
_context.TodoGroups.Add(group);
await _context.SaveChangesAsync();
// 创建者加入组
var userGroup = new UserGroup
{
UserId = creatorId,
GroupId = group.Id,
Role = UserRole.Admin,
JoinTime = DateTime.Now
};
_context.UserGroups.Add(userGroup);
await _context.SaveChangesAsync();
// ✅ 映射为 DTO 返回(避免循环)
var dto = new TodoGroupDto
{
Id = group.Id,
GroupName = group.GroupName,
Description = group.Description,
CreatorId = group.CreatorId,
CreateTime = group.CreateTime,
IsActive = group.IsActive,
Members = new List<UserGroupDto>
{
new UserGroupDto
{
Id = userGroup.Id,
UserId = creatorId,
UserName = creator.UserName,
Role = userGroup.Role,
JoinTime = userGroup.JoinTime
}
}
};
return ApiResponse<TodoGroupDto>.SuccessResult(dto, "组创建成功");
}
catch (Exception ex)
{
return ApiResponse<TodoGroupDto>.ErrorResult($"创建组失败: {ex.Message}", 500);
}
}
✅ 方案三:修复 SysUser 实体(可选但推荐)
你目前的 SysUser 缺少 UserGroups 导航属性,建议补上:
public class SysUser
{
public int Id { get; set; }
public string UserName { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public string? Email { get; set; }
public DateTime CreateTime { get; set; }
public bool IsActive { get; set; }
// ✅ 添加导航属性
public ICollection<UserGroup> UserGroups { get; set; } = new List<UserGroup>();
}
并在 DbContext 中启用关系:
modelBuilder.Entity<UserGroup>()
.HasOne(ug => ug.User)
.WithMany(u => u.UserGroups) // 启用双向导航
.HasForeignKey(ug => ug.UserId)
.OnDelete(DeleteBehavior.Cascade);
⚠️ 但返回
SysUser时仍需 DTO 或[JsonIgnore],否则也会循环。
✅ 最终建议(生产环境)
| 场景 | 推荐方案 |
|---|---|
| API 返回数据 | ✅ 使用 DTO(如 TodoGroupDto) |
| SignalR 实时消息 | ✅ DTO + ReferenceHandler.Preserve(备用) |
| 快速调试 | ✅ 临时启用 Preserve |
| 长期维护 | ✅ DTO + 明确的数据结构 |
✅ 总结
你遇到的循环引用是由于:
TodoGroup ←→ UserGroup ←→ Group形成闭环,序列化时无限嵌套。
🔧 立即修复:
options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.Preserve;
🏆 长期方案:
使用 DTO 返回数据,不直接暴露实体
这样既避免了循环引用,又提升了 API 安全性和性能。
如果你需要,我可以帮你生成完整的 DTO 映射(包括 AutoMapper 配置)。