I have this three entities Customer
, Product
and Review
.
A customer can have many products, and a product can have only one customer as owner. A customer can also have many reviews, and one review can have only one customer. A product can have many reviews.
It seems like I am having a reference loop and below is the JsonException
that I get when trying to get all customers:
Error message
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: $.rows.Reviews.Product.Reviews.Product.Reviews.Product.Reviews.Product.Reviews.Product.Reviews.Product.Reviews.Product.Reviews.Product.Reviews.Product.Reviews.Id.
Code:
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
configurations:
// Products configurations
builder.Ignore(e => e.DomainEvents);
builder.HasKey(t => t.Id);
// Customers configurations
builder.Ignore(e => e.DomainEvents);
builder.HasMany(e => e.Reviews)
.WithOne(e => e.Customer)
.HasForeignKey(uc => 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);
// Reviews configurations
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);
Any idea on how to fix this error?
Thanks in advance and if you need any more information please do let me know and I will provide asap.
Edit: this is the query that I am using for getting all customers:
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;
}
Edit #2: 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; }
}
}
CodePudding user response:
To fix this issue you need to add a ReviewDto class like this:
public partial class ReviewDto
{
public int Id { get; set; }
public int Stars { get; set; }
public string Description { get; set; }
// ...
}
And update the CustomerDto:
public partial class CustomerDto : IMapFrom<Customer>
{
public int Id { get; set; }
public string Name { get; set; }
public List<ReviewDto> Reviews { get; set; }
}
CodePudding user response:
As the comments suggest, the problem is not with EF; it is with the default mechanism of System.Text.Json
to serialize everything, even if there are loops. The problem with that is you eventually hit a limit giving you that exception. It is probably not your intent to send such a bloated payload back to API clients.
You can prevent that a number of different ways. You can null out the properties that would lead to cycles, but this "sort of" destroys data and could be misinterpreted by clients.
Another way would be to map your classes with cycles to DTOs that explicitly suppress the loop by not including that data, or substituting a reference property (e.g. an ID or some other reference value) to data that has been repeated.
If you don't want to do that, you can prevent the exception by using a ReferenceHandler set to ignore cycles.
This documentation explains how to do that. The effect is equivalent to the first solution of nulling out the values manually. An excerpt from that page
Employee tyler = new()
{
Name = "Tyler Stein"
};
Employee adrian = new()
{
Name = "Adrian King"
};
tyler.DirectReports = new List<Employee> { adrian };
adrian.Manager = tyler;
JsonSerializerOptions options = new()
{
ReferenceHandler = ReferenceHandler.IgnoreCycles,
WriteIndented = true
};
string tylerJson = JsonSerializer.Serialize(tyler, options);
...
Really, though, you're missing a step. It makes more sense to map your returned entities to DTOs. The purpose of the DTOs is to shape the response content to the needs of the API clients. That makes Ghassen's answer a good one.