You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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; }
    }
}

问题分析

  1. 循环引用根源:CustomerDto直接引用了Review实体,而Review包含Customer和Product导航属性,Product又包含Customer和Reviews,形成Customer -> Review -> Product -> Customer的无限循环,导致Json序列化递归溢出。
  2. 配置错误:客户与评论的关联配置中,外键被错误设置为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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 09:50:34