简体   繁体   中英

System.Text.Json.JsonException: A possible object cycle was detected

I am working on a.Net blog style API as a practice project and have come across this error. I have a User class, Post class and Comment class. Comment has reference to both Post and User, each one-to-many.

Here is my User class:

 public class User
    {
        //Other properties..
        public List<Post> Posts { get; set; }
        public List<Comment> Comments { get; set; }
    }

My Post class:

public class Post
    {
        //Other properties..
        public List<Comment> Comments { get; set; }
        [Required]
        public User CreatedBy { get; set; }
    }

and my Comment class:

  public class Comment
    {
        //Other properties..
        [Required]
        public User CreatedBy { get; set; }
        [Required]
        public Post Post { get; set; }
    }

I am attempting to save a comment like this (I have Post.Id and CreatedBy.Id populated from a previous method). This works as in it inserts into the database as expected, however I get the JsonException.

comment.Post = await _context.Posts.SingleOrDefaultAsync(p => p.Id == comment.Post.Id);
comment.CreatedBy = await _context.Users.FindAsync(comment.CreatedBy.Id);

await _context.Comments.AddAsync(comment);

await _context.SaveChangesAsync();

return comment;

I am able to get around this by adding Microsoft.AspNetCore.Mvc.NewtonsoftJson and adding this to my startup class

 services.AddControllers()
 .AddNewtonsoftJson(options =>
 options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore);

But I am wondering if there is something I should be doing in my code to avoid this rather than just providing an option to say that it's ok.

An instance of a Post contains a list of Comments. Each instance of Comment contains a Post. An instance of a Post contains a list of Comments. Each instance of Comment contains a Post.... and so on.

While this is not against the rules of C# to do this, I prefer to avoid this because it introduces an unknown complexity to a collection of data. Text serialization is one of the biggest pain points you will run into when you do this. Although there are ways of working with these pain points (ReferenceLoopHandling and Ignore attribute tags to name a few), I have found it much easier to omit reference loops from my class definitions as much as possible even if it means defining two differently named classes with the same properties, like Comment and PostComment (where PostComment would be a Comment contained under a Post).

For example a structure like the following would break the looping reference and allow it to be easily serializable and persisted to a database. Notice the type change on the Comment attribute of Post and the related omission of Post from PostComment so that the loop is broken.

public class User
{
    //Other properties..
    public List<Post> Posts { get; set; }
    public List<Comment> Comments { get; set; }
}

public class Post
{
    //Other properties..
    public List<PostComment> Comments { get; set; }
    [Required]
    public User CreatedBy { get; set; }
}

public class Comment
{
    //Other properties..
    [Required]
    public User CreatedBy { get; set; }
    [Required]
    public Post Post { get; set; }
}

public class PostComment
{
    //Other properties..
    [Required]
    public User CreatedBy { get; set; }
}

I had similiar issue with my api (.net 5). The exception thrown, 500, could not be catched by the API. To view the actual problem I had to go to windows "Event Logger".

To solve the problem I added below in the configuration (startup.cs):

   services.AddMvc()
                .AddJsonOptions(opt =>
                 {
                     opt.JsonSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.Preserve;
                 });

Documentation can be found at: https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to?pivots=dotnet-5-0

To avoid cycling you can try to use Eager loading with adding IgnoreAutoIncludes() extension method. In such a way using Include() extension method, you are able to specify which navigational properties to pull back with your query (but in your specific case you do not need to include anything since you are interested only in Post entity):

 comment.Post = await _context.Posts.IgnoreAutoIncludes()
        .SingleOrDefaultAsync(p => p.Id == comment.Post.Id);

Also, if you're using Fluent API you can Exclude a property from mapping by adding Ignore in the OnModelCreating() method:

modelBuilder.Entity<Post>().Ignore(c => c.Comments);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM