0

I'm having an issue trying to convert an object to json. The error is a Newtonsoft.Json.JsonSerializationException:

Self referencing loop detected for property 'Project' with type 'System.Data.Entity.DynamicProxies.Project_F29F70EF89942F6344C5B0A3A7910EF55268857CD0ECC4A484776B2F4394EF79'. Path '[0].Categories[0]'.

The problem is that the object (it's actually a list of objects) has a property which is another object that refers back to the first object:

public partial class Project
{
...
    public virtual ICollection<Category> Categories { get; set; }
...
}

public partial class Category
{
...
    public virtual Project Project { get; set; }
...
}

This is all fine and dandy as far as Entity Framework is concerned, but to convert this to json would result in an infinite regress, hence the exception.

Here is my code:

    public async Task<HttpResponseMessage> GetProjects()
    {
        var projects = _projectService.GetProjects().ToList();
        string jsonString = JsonConvert.SerializeObject(projects); // <-- Offending line
        return Request.CreateResponse(HttpStatusCode.OK, jsonString);
    }

I've looked online for solutions to this and I found this stackoverflow post:

JSON.NET Error Self referencing loop detected for type

They suggest three solutions, none of which work:

1) Ignore the circular reference:

    public async Task<HttpResponseMessage> GetProjects()
    {
        var projects = _projectService.GetProjects().ToList();
        JsonSerializerSettings settings = new JsonSerializerSettings()
        {
            ReferenceLoopHandling = ReferenceLoopHandling.Ignore
        };
        string jsonString = JsonConvert.SerializeObject(projects, settings);
        return Request.CreateResponse(HttpStatusCode.OK, jsonString);
    }

This resulted in the call to SerializeObject(...) hanging for a bit then throwing a System.OutOfMemoryException (which tells me the circular references were NOT being ignored).

Mind you, the author of this proposed solution at stackoverflow says to set the ignore setting in WebApiConfig.cs but I tried that and it has no effect.

He also says:

"If you want to use this fix in a non-api ASP.NET project, you can add the above line to Global.asax.cs, but first add: var config = GlobalConfiguration.Configuration;"

Mine's a web API with no global file so I shouldn't have to do this.

I also don't want to ignore circular references because I don't want to lose data.

2) Preserve the circular reference:

    public async Task<HttpResponseMessage> GetProjects()
    {
        var projects = _projectService.GetProjects().ToList();
        JsonSerializerSettings settings = new JsonSerializerSettings()
        {
            ReferenceLoopHandling = ReferenceLoopHandling.Serialize,
            PreserveReferencesHandling = PreserveReferencesHandling.Objects
        };
        string jsonString = JsonConvert.SerializeObject(projects, settings);
        return Request.CreateResponse(HttpStatusCode.OK, jsonString);
    }

This just resulted in the request timing out because it would just hang.

Again, the author says to put this in WebApiConfig.cs, but again this had no effect.

3) Add ignore/preserve reference attributes to the objects and properties:

Ignoring Categories:

public partial class Project
{
...
    [JsonIgnore]
    public virtual ICollection<Category> Categories { get; set; }
...
}

This has no effect. I hover over the project list and see that it still has categories, and each category still has an instance of the project. I still get the same exception.

Again, even if this worked, I don't want to ignore the categories.

Preserve Categories:

[JsonObject(IsReference = true)]
public partial class Project
{
...
    public virtual ICollection<Category> Categories { get; set; }
...
}

Again, same results.

Even if this method worked, the attributes wouldn't be preserved. I'd be doing it on Entity Framework classes which are re-generated automatically every time I recompile. (Is there a way to tell it to set these attributes in the model? Can I set them on the other half of the partial class?)

Alternatively, I'm open to suggestions other than converting to json and sending back in the response. Is there another way to get the data back to the client?

What would be the fix to this problem? Thanks.

gib65
  • 151
  • 2
  • 12

2 Answers2

1

Briefly

The best way to fix this problem is to create completely brand-new Models (xxxModel, xxxViewModel, xxxResponse, etc..) on Presentation layer which will be returned to end-users. Than just cast one object to another using AutoMapper or your own custom methods.

Keep your database entities separate from real world!


In detail

There are so many problems that you will encounter:

  1. Disclosure of sensitive data. Your database entity could/will contain sensitive data which end-users shouldn't receive;
  2. Performance issues and waste of RAM and CPU. It would be better to load only those properties that end-users is required, instead all;
  3. Serialization problems. EF entities almost always contain Navigation properties which will be serialized together in case lazy-loading enabled. Imagine dozens related entities, which will be lazy-loaded when your composite root is being serialized. It will cause dozens unexpected requests to database;
  4. Fragility. Any changes related your EF entities will affect on Presentation Layer and on end-users. For instance, in case with API, new added property just extend response, but deleted or renamed will break logic in your customers' application.

There are a lot of other problems, just be careful.

Serhii Kyslyi
  • 1,575
  • 21
  • 40
  • I failed to mention. This function is NOT returning the json to the presentation layer (the browser). It is returning it to a controller in another MVC application. The flow goes like this: browser --> MVC controller --> web API. It returns along the same path. Why you ask? We are trying to resolve a problem where two Entity Framework caches sometimes get out of sync. The MVC application and the web API share the same database, which means they cache the same data, but if an update is made to the data in one, it will be out of sync with the other. Our solution is to only ever go through one. – gib65 Jan 17 '18 at 21:40
  • Anyway, you return data to another source, it does not matter is it end-users or your middleware. (you have problem with code design, I would have try to solve this problem as soon as possible) – Serhii Kyslyi Jan 18 '18 at 08:45
0

I would recommend not Serializing Entity Framework classes and creating a specific class that only inherits from Object and has only the data you need

Micah Armantrout
  • 6,439
  • 4
  • 36
  • 61