4

I've got two entities, Speakers and Sessions where speakers can be part of multiple sessions and sessions can have multiple speakers. I've defined them as follows and have, I think, successfully seeded the speakers table (I believe it worked because a link table SessionRecSpeakerRec was created as I expected.

Here are my definitions:

public class SessionRec
{
    public int Id { get; set; }
    public string Title { get; set; }
    public virtual List<SpeakerRec> Sessions { get; set; }
}

public class SpeakerRec
{
    public int Id { get; set; }
    public string First { get; set; }
    public string Last { get; set; }
    public virtual List<SessionRec> Sessions { get; set; }
}

public DbSet<SessionRec> SessionRecs { get; set; }
public DbSet<SpeakerRec> SpeakerRecs { get; set; }

In my c# asp.net controller code, I do this trying to get a list of speakers with sessions:

[HttpGet]
 public IEnumerable<SpeakerRec> GetSpeakerRecs()
 {
        return _context.SpeakerRecs.Include(a=>a.Sessions).ToList();
 }

And I get the cycle error. Any clues as to why and how to fix?

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.

Lee Taylor
  • 7,761
  • 16
  • 33
  • 49
Peter Kellner
  • 14,748
  • 25
  • 102
  • 188
  • 1
    This problem has nothing to do with Entity Framework. It would occur for any two objects who have a public reference to each other. – Flater Dec 06 '20 at 01:20
  • Have you read the error message that you posted? It answers your question at the end of the message. – Flater Dec 06 '20 at 01:22
  • Yes @Flater, I did see that but could not figure out how to set that option. When I google'd for it, I found lots of references to NewtonSoft which I assume are obsolete in .net 5. – Peter Kellner Dec 06 '20 at 01:33
  • Does this answer your question? [How to avoid the circular referencing asp.net core mvc \[HttpPost("add-recipe")\] web api](https://stackoverflow.com/questions/64341036/how-to-avoid-the-circular-referencing-asp-net-core-mvc-httppostadd-recipe) – Flater Dec 06 '20 at 01:36
  • Have you tried removing the `virtual` to disable lazy loading? – Hooman Bahreini Dec 06 '20 at 01:38
  • @flater, still not getting it. I think that link you posted recommends newtonsoft which I thought is not part of .net 5 – Peter Kellner Dec 06 '20 at 01:44
  • @HoomanBahreini, I did try removing virtual and the model creation failed. – Peter Kellner Dec 06 '20 at 01:45
  • @PeterKellner: If you're getting a Newtonsoft error, that's Newtonsoft throwing an error. – Flater Dec 06 '20 at 01:45
  • @Flater, I'm not getting a newtonsoft error. My understanding is NewtonSoft is not part of .net core 5 and the link you send me references newtonsoft. My error is. "JsonException: A possible object cycle was detected" (not newtonsoft) – Peter Kellner Dec 06 '20 at 01:50
  • 1
    @PeterKellner That's because before .Net 5 there wasn't a way to handle this with just System.Text.Json (i.e. without Newtonsoft.Json). This question/answer might be more appropriate: https://stackoverflow.com/questions/60197270/jsonexception-a-possible-object-cycle-was-detected-which-is-not-supported-this – devNull Dec 06 '20 at 01:54
  • @Flater, this question/answer is getting cyclic. It seems you know the problem and answer. If you could post that it would be appreciated. The last link you sent me still does not make it clear. – Peter Kellner Dec 06 '20 at 01:58
  • Added working answer, but I still don't get what the core problem is and why this fixes it. – Peter Kellner Dec 06 '20 at 02:03
  • @devNull , I added more questions at the bottom of my answer. Any help would be appreciated in understanding what is happening here and how I can get a clean JSON output with speakers and session details. That is, just Sessions: [{..},{..}... – Peter Kellner Dec 06 '20 at 02:14

4 Answers4

12

To clarify the issue: the error is occurring because in C# land your instances have references to each other in memory:

var session = new SessionRec
{
    Id = 1,
    Title = "Session",
    Speakers = new List<SpeakerRec>()
};
var speaker = new SpeakerRec
{
    Id = 1,
    First = "Speaker",
    Last = "Speaks",
    Sessions = new List<SessionRec>()
};
session.Speakers.Add(speaker);
speaker.Sessions.Add(session);
// session.Speakers[0] == speaker 
// speaker.Sessions[0] == session 

But during serialization when representing these object instances as text (JSON), the serializer will essentially try to do the following:

{
  "Id": 1,
  "First": "Speaker",
  "Last": "Speaks",
  "Sessions": [
    {
      "Id": 1,
      "Title": "Session"
      "Speakers": [
        {
          "Id": 1,
          "First": "Speaker",
          "Last": "Speaks",
          "Sessions": [
            {
              "Id": 1,
              "Title": "Session",
              // You see where this is going...
            }
          ]
        }
      ]
    }
  ]
}

And so you get JsonException: A possible object cycle was detected. As you found, there is one way to handle this in the System.Text.Json SerializerOptions in .NET 5: ReferenceHandler.Preserve. But as you also found, this approach adds metadata properties in order to properly represent the object cycle as JSON.

So on to your question:

how I can get a clean JSON output with speakers and session details

The short answer is that there is currently no support to to simply ignore object cycles with System.Text.Json (in .NET 5). See this open GitHub issue to follow the request for it, which is currently slated for .NET 6.

With that said, your options to handle this scenario are (in order of what I would personally recommend):

1 - Create a separate object layer for the API

This one's pretty simple, and you don't need to worry about serializing objects with potential cycles or decorating your data objects with serialization attributes. For this, you'd create corresponding DTOs for your data objects, map them, and simply return them:

public class SessionRecDto
{
    public int Id { get; set; }
    public string Title { get; set; }
}

public class SpeakerRecDto
{
    public int Id { get; set; }
    public string First { get; set; }
    public string Last { get; set; }
    public List<SessionRecDto> Sessions { get; set; }
}

This entirely removes the object cycle in the API and represents exactly how (I assume) you expect it to look:

{
  "Id": 1,
  "First": "Speaker",
  "Last": "Speaks",
  "Sessions": [
    {
      "Id": 1,
      "Title": "Session"
    }
  ]
}

2 - Use Newtonsoft.Json

Newtonsoft.Json does have out of the box support for ignoring object cycles (ReferenceLoopHandling):

string json = JsonConvert.SerializeObject(speaker, new JsonSerializerSettings
{
    ReferenceLoopHandling = ReferenceLoopHandling.Ignore
});

{
  "Id": 1,
  "First": "Speaker",
  "Last": "Speaks",
  "Sessions": [
    {
      "Id": 1,
      "Title": "Session",
      "Speakers": []
    }
  ]
}

3 - Ignore serializing the property by decorating the data class

public class SessionRec
{
    public int Id { get; set; }
    public string Title { get; set; }
    [JsonIgnore]
    public virtual List<SpeakerRec> Speakers { get; set; }
}

This will effectively look the same as option 1.

devNull
  • 3,849
  • 1
  • 16
  • 16
3

For ASP.NET CORE 5.0, I was able to achieve the proper serialization following this documentation in Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews()
        .AddNewtonsoftJson(options =>
        {
            options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
        });
}

As a result, the client receives the JSON object without any additional metadata that you get with the new ReferenceHandler.

Nata
  • 986
  • 9
  • 4
1

For .Net Core 3.0 this was working for me pretty good

var settings = new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore };
return Json(new { items= items}, settings);

But for .NET Core 5.0 the above raises this exception

System.InvalidOperationException: Property 'JsonResult.SerializerSettings' must be an instance of type 'System.Text.Json.JsonSerializerOptions'.

So you have to use an instance of type System.Text.JsonSerializerOptions. The following is working for me fine in .Net Core 5.0

var settings = new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve };
return Json(new { items= []}, settings);
Abdulhakim Zeinu
  • 3,333
  • 1
  • 30
  • 37
0

This seems to work, but I don't quite understand the MaxDepth of 0 returning anything. I'm also not understanding why this is necessary and how it changes the processing. @Flater suggested it in the comments which led me to this.

services.AddControllersWithViews().AddJsonOptions(o =>
        {
            o.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.Preserve;
            o.JsonSerializerOptions.MaxDepth = 0;
        });

However, I now get a lot of extra data in my serialization that will make parsing this data very problematic.

Is there a way to just get clean JSON without all the extra $id, $value type data?

{
  "$id": "1",
  "$values": [
    {
      "$id": "2",
      "id": 620,
      "first": "Ron",
      "last": "Kleinman",
      "bio": "Ron teaches Object Oriented Analysis and Design at De Anza College ",
      "favorite": false,
      "twitterHandle": null,
      "company": "De Anza College",
      "sessions": {
        "$id": "3",
        "$values": [
          {
            "$id": "4",
            "id": 86,
            "title": "The Performance Limitations  of the Java Platform ... and how to avoid them",
            "day": "Not Assigned",
            "eventYear": "2008",
            "level": "Intermediate",
            "favorite": false,
            "time": "Not Assigned",
            "sessions": {
              "$id": "5",
              "$values": [
                {
                  "$ref": "2"
                }
              ]
            }
          },
Peter Kellner
  • 14,748
  • 25
  • 102
  • 188
  • See https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.referencehandler.preserve?view=net-5.0 "When writing complex reference types, the serializer also writes metadata properties ($id, $values, and $ref) within them." – Lee Taylor Dec 06 '20 at 02:22
  • 1
    Thanks @LeeTaylor, Is there a way to omit that data and just get one level of output deserialized? – Peter Kellner Dec 06 '20 at 02:34
  • From what I can tell, if you omit the ReferenceHandler.Preserve line, you will have problems with the self-referential nature of your data. I think if you want to avoid those values you would have to flatten your data and use IDs to rebuild the references after deserialization. – Lee Taylor Dec 06 '20 at 02:40