Search code examples
c#asp.netasp.net-mvcentity-frameworkcircular-reference

Fixing Circular Reference Error in C# Web API


I have a fairly simple Web API written in C#, using EF. The idea is pretty much a to do application, with the specific relationship being: A User can have many To Do Lists, and each To Do List can have many To Do Items.

Here are my Data Models:

public class User
{
    public Guid Id { get; set; }
    public string? Name { get; set; }
    public string? Email { get; set; }
    public string? Password { get; set; } // Be sure to properly hash and salt passwords
    public string? ProfilePhotoUrl { get; set; } // Store the URL to the user's profile photo
    public List<ToDoList>? ToDoLists { get; set; } // A user can have multiple to-do lists
}

public class ToDoList
{
    public Guid Id { get; set; }
    public string? Title { get; set; }
    public List<ToDo>? ToDos { get; set; } // Each to-do list contains multiple to-dos

    // Foreign Key to User
    public Guid UserId { get; set; }
    public User? User { get; set; }
}
public class ToDo
{
    public Guid Id { get; set; }
    public string? Name { get; set; }
    public string? Description { get; set; }
    public bool? InProgress { get; set; }
    public bool? IsComplete { get; set; }
    public DateTime Created { get; set; } = DateTime.Now;

    public ToDoList? ToDoList { get; set; } 
    public Guid? ToDoListId{ get; set; }
}

I have a controller that allows Users to register, login etc. One of the things I want to do is retrieve each To Do List a User may have. I have the code in a service:

    public Task<User> GetToDoListsForUsers(Guid userId)
    {
        return _context.Users
            .Include(u => u.ToDoLists)
            .FirstOrDefaultAsync(u => u.Id == userId);
    }

Which is then called by the controller:

    [HttpGet("{userId}/todolists")]
    public async Task<ActionResult<IEnumerable<ToDoList>>> GetToDoListsForUser(Guid userId)
    {
        var user = await _userService.GetToDoListsForUsers(userId);

        if (user == null)
        {
            return NotFound("User not found");
        }

        var toDoLists = user.ToDoLists;

        if (toDoLists.Count == 0)
        {
            return NotFound("user has no lists");
        }

        return Ok(toDoLists);
    }

However, when I run this I get the following Error:


      An unhandled exception has occurred while executing the request.
      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: $.User.ToDoLists.User.ToDoLists.User.ToDoLists.User.ToDoLists.User.ToDoLists.User.ToDoLists.User.ToDoLists.User.ToDoLists.User.ToDoLists.User.ToDoLists.Id.

I believe this is because the User has a reference to ToDoList, and the ToDoList has a reference back to User but I need this reference both ways as they are Foreign Keys.

I also came across this solution online to add in these lines:

        var jsonSettings = new JsonSerializerOptions
        {
            ReferenceHandler = ReferenceHandler.Preserve,
        };

        var serializedUser = JsonSerializer.Serialize(user, jsonSettings);
        var deserializedUser = JsonSerializer.Deserialize<User>(serializedUser, jsonSettings);

        var toDoLists = deserializedUser.ToDoLists;

To the controller method but this did not work either.

Any help available in fixing this?


Solution

  • TBH the found "solution" does not make any sense, using the same settings with ReferenceHandler.Preserve will just restore the references back.

    You can just return the serialized manually data:

    var serializedLists = JsonSerializer.Serialize(user.ToDoLists, jsonSettings);
    return Content(serializedLists);
    

    Or better apply the ReferenceHandler.Preserve setting globally:

    builder.Services.AddControllers()
        .AddJsonOptions(options =>
            options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.Preserve);
    

    Also option worth considering (one which I use by default) - creating DTOs and mapping to them so no loops are present and response has only required data.

    Read a little bit more - Avoid or control circular references in Entity Framework Core .