Entity Framework实体引用循环问题排查与解决求助
解决实体循环引用引发的JsonException问题
错误信息
System.Text.Json.JsonException: 检测到可能的对象循环。这可能是由于存在循环引用,或者对象深度超过了允许的最大深度32。请考虑在JsonSerializerOptions上使用ReferenceHandler.Preserve来支持循环引用。 路径: $.rows.Reviews.Product.Reviews.Product.Reviews.Product.Reviews.Product.Reviews.Product.Reviews.Product.Reviews.Product.Reviews.Product.Reviews.Product.Reviews.Id.
实体代码
namespace Domain.Entities { public partial class Customer { public int Id { get; set; } public string? Name { get; set; } public virtual ICollection<Review> Reviews { get; set; } } public partial class Product { public int Id { get; set; } public string Name { get; set; } public string Description { get; set; } public int Price { get; set; } public int CustomerId { get; set; } public Customer Customer { get; set; } public virtual ICollection<Review> Reviews { get; set; } } public partial class Review { public int Id { get; set; } public int Stars { get; set; } public string Description { get; set; } public int CustomerId { get; set; } public int ProductId { get; set; } public Customer Customer { get; set; } public Product Product { get; set; } } }
ModelBuilder配置
// 产品配置 builder.Ignore(e => e.DomainEvents); builder.HasKey(t => t.Id); // 客户配置 builder.Ignore(e => e.DomainEvents); builder.HasMany(e => e.Reviews) .WithOne(e => e.Customer) .HasForeignKey(uc => uc.Id); // 注意:此处配置错误,外键应为uc.CustomerId而非uc.Id builder.HasMany(e => e.MessagesSent) .WithOne(e => e.Receiver) .HasForeignKey(uc => uc.SenderId) .OnDelete(DeleteBehavior.Cascade); builder.HasMany(e => e.MessagesReceived) .WithOne(e => e.Sender) .HasForeignKey(uc => uc.ReceiverId) .OnDelete(DeleteBehavior.Cascade); // 评论配置 builder.HasKey(t => t.Id); builder.HasOne(d => d.Customer) .WithMany(p => p.Reviews) .HasForeignKey(t => t.CustomerId) .OnDelete(DeleteBehavior.Cascade); builder.HasOne(d => d.Product) .WithMany(p => p.Reviews) .HasForeignKey(t => t.ProductId) .OnDelete(DeleteBehavior.Cascade);
查询代码
public async Task<PaginatedData<CustomerDto>> Handle(CustomersWithPaginationQuery request) { var filters = PredicateBuilder.FromFilter<Customer>(""); var data = await _context.Customers .Where(filters) .OrderBy("Id desc") .ProjectTo<CustomerDto>(_mapper.ConfigurationProvider) .PaginatedDataAsync(1, 15); return data; }
CustomerDto代码
namespace Application.Customers.DTOs { public partial class CustomerDto : IMapFrom<Customer> { public int Id { get; set; } public string Name { get; set; } public List<Review> Reviews { get; set; } } }
问题分析
- 循环引用根源:
CustomerDto直接引用了Review实体,而Review包含Customer和Product导航属性,Product又包含Customer和Reviews,形成Customer -> Review -> Product -> Customer的无限循环,导致Json序列化递归溢出。 - 配置错误:客户与评论的关联配置中,外键被错误设置为
uc.Id,应改为uc.CustomerId,否则EF Core无法正确建立关联关系。
解决方案
1. 优先方案:用DTO隔离实体(消除循环根源)
不要在DTO中直接引用数据库实体,创建精简的DTO仅保留业务所需字段:
步骤1:创建子DTO
namespace Application.Reviews.DTOs { public class ReviewDto { public int Id { get; set; } public int Stars { get; set; } public string Description { get; set; } // 仅保留产品核心信息,避免引用完整Product实体 public ProductShortDto Product { get; set; } } public class ProductShortDto { public int Id { get; set; } public string Name { get; set; } public int Price { get; set; } } }
步骤2:修改CustomerDto
namespace Application.Customers.DTOs { public partial class CustomerDto : IMapFrom<Customer> { public int Id { get; set; } public string Name { get; set; } public List<ReviewDto> Reviews { get; set; } } }
步骤3:配置AutoMapper映射
public class MappingProfile : Profile { public MappingProfile() { CreateMap<Customer, CustomerDto>(); CreateMap<Review, ReviewDto>(); CreateMap<Product, ProductShortDto>(); // 显式忽略Product的Reviews属性,彻底切断循环 CreateMap<Product, ProductShortDto>().ForMember(dest => dest.Reviews, opt => opt.Ignore()); } }
2. 应急方案:修改Json序列化配置
若暂时无法调整DTO结构,可通过配置允许循环引用,但会增加响应体积,且前端需处理额外标记:
// 在Program.cs/Startup.cs中配置 builder.Services.AddControllers() .AddJsonOptions(options => { options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.Preserve; // 可选:提高最大深度上限,但仅为临时缓解,不推荐长期使用 // options.JsonSerializerOptions.MaxDepth = 64; });
3. 必须修正的ModelBuilder错误
先修复客户与评论的关联配置:
// 客户配置中的评论关联修正 builder.HasMany(e => e.Reviews) .WithOne(e => e.Customer) .HasForeignKey(uc => uc.CustomerId); // 将uc.Id改为uc.CustomerId
内容的提问来源于stack exchange,提问作者Sachihiro
相关产品推荐
相关产品推荐

