39

In my web API when I run project to get data from the database got this error .net core 3.1

JsonException: A possible object cycle was detected which is not supported. This can either be due to a cycle or if the object depth is larger than the maximum allowed depth of 32.

These are my codes:

my Model

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string ProductText { get; set; }
    public int ProductCategoryId { get; set; }
    [JsonIgnore]
    public virtual ProductCategory ProductCategory { get; set; }
}

my productCategory class is:

public class ProductCategory
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string CatText { get; set; }
    public string ImagePath { get; set; }
    public int Priority { get; set; }
    public int Viewd { get; set; }
    public string Description { get; set; }
    public bool Active { get; set; }
    public DateTime CreateDate { get; set; }
    public DateTime ModifyDate { get; set; }
    public virtual ICollection<Product> Products { get; set; }
}

my repo is

public async Task<IList<Product>> GetAllProductAsync()
    {
        return await  _context.Products.Include(p => p.ProductCategory).ToListAsync(); 
    }

my interface

public interface IProductRepository
{
   ...
   Task<IList<Product>> GetAllProductAsync();
   ...
}

and this is my controller in api project

[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
    private readonly IProductRepository _productRepository;

    public ProductsController(IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }
    [HttpGet]
    public ActionResult Get()
    {
        return Ok(_productRepository.GetAllProduct());
    }
}

When I run API project and put this URL: https://localhost:44397/api/products I got that error, I can't resolve it

user3748973
  • 449
  • 2
  • 8
  • 24
sunny
  • 2,670
  • 5
  • 24
  • 39

11 Answers11

55

this is happening because your data have a reference loop.

e.g

// this example creates a reference loop
var p = new Product()
     { 
        ProductCategory = new ProductCategory() 
           { products = new List<Product>() }
     };
    p.ProductCategory.products.Add(p); // <- this create the loop
    var x = JsonSerializer.Serialize(p); // A possible object cycle was detected ...

You can not handle the reference loop situation in the new System.Text.Json yet (netcore 3.1.1) unless you completely ignore a reference and its not a good idea always. (using [JsonIgnore] attribute)

but you have two options to fix this.

  1. you can use Newtonsoft.Json in your project instead of System.Text.Json (i linked an article for you)

  2. Download the System.Text.Json preview package version 5.0.0-alpha.1.20071.1 from dotnet5 gallery (through Visual Studio's NuGet client):

option 1 usage:

services.AddMvc()
     .AddNewtonsoftJson(
          options => {
           options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; 
      });
// if you not using .AddMvc use these methods instead 
//services.AddControllers().AddNewtonsoftJson(...);
//services.AddControllersWithViews().AddNewtonsoftJson(...);
//services.AddRazorPages().AddNewtonsoftJson(...);

option 2 usage:

// for manual serializer
var options = new JsonSerializerOptions
{
    ReferenceHandling = ReferenceHandling.Preserve
};

string json = JsonSerializer.Serialize(objectWithLoops, options);

// -----------------------------------------
// for asp.net core 3.1 (globaly)
 services.AddMvc()
  .AddJsonOptions(o => {
     o.JsonSerializerOptions
       .ReferenceHandling = ReferenceHandling.Preserve  
            });

these serializers have ReferenceLoopHandling feature.

  • Edit : ReferenceHandling changed to ReferenceHandler in DotNet 5

but if you decide to just ignore one reference use [JsonIgnore] on one of these properties. but it causes null result on your API response for that field even when you don't have a reference loop.

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string ProductText { get; set; }
    
    public int ProductCategoryId { get; set; }
    // [JsonIgnore] HERE or
    public virtual ProductCategory ProductCategory { get; set; }
}

public class ProductCategory
{
    public int Id { get; set; }
    // [JsonIgnore] or HERE
    public ICollection<Product> products {get;set;}
}
AliReza Sabouri
  • 4,355
  • 2
  • 25
  • 37
  • 8
    As of `System.Text.Json` v5 preview6, `ReferenceHandling` is now `ReferenceHandler`. See: https://github.com/dotnet/runtime/pull/37296/files – James Vickery Jul 17 '20 at 12:14
  • @AliReza I am using .net core and I want to eager load the table e.g `public List Category { get; set; }` So i use `context.Customer.Include(c=>c.Customer);` but i face that _possible object cycle_. If Iuse `[JsonIgnore]` how i will retrieve the data from the category in order to display them ? – noruk Feb 23 '21 at 17:24
  • @noruk I don't suggest using `JsonIgnore` you have 3 options now. first you can use Newtonsoft.Json or Dotnet 5 (because you can handel the loop using these frameworks) second option is reverse query (try to select your customers from categories `context.Category.Include(c=>c.Customer).Select(q=> q.Custormer)` ) the last one is ignoring one of the relations if you don't need it using `JsonIgnore`. – AliReza Sabouri Feb 23 '21 at 19:57
  • thank you for the response. I use .Net 5 web API and in its startup i placed `services.AddControllers().AddNewtonsoftJson(options => options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore);` from the controller is return: `return Ok(JsonSerializer.Serialize(getEmployeeById));` If I use `[JsonIgnore]` in the navigation property it works but as I want to include the navigation property, then I don;t use `[JsonIgnore]` but i get _possible object cycle_ . Is it something that i missed ? – noruk Feb 23 '21 at 20:02
  • @noruk you should open a separate question but looks like you are using both .net 5 and Newtonsoft frameworks. you don't need newtonsoft package if you are using .net 5. – AliReza Sabouri Feb 23 '21 at 20:14
  • yes i Use .net 5 and browsing questions here I found others that proposed to use Newtonsoft. but in my case did not work. could you assist me ? – noruk Feb 23 '21 at 20:17
  • `services.AddControllers().AddJsonOptions(o => o.JsonSerializerOptions .ReferenceHandler = ReferenceHandler.Preserve);` if this is not working open a new question ill check your code. – AliReza Sabouri Feb 23 '21 at 20:52
  • I used that too but I had an issue with the datatable.js that I use. Finally, I have just resolved it by adding to the controller `return Ok(JsonConvert.SerializeObject(getEmployeeById, Formatting.None, new JsonSerializerSettings() { ReferenceLoopHandling = ReferenceLoopHandling.Ignore }));` plus in the startup the similar configuration that i mentioned in my previous comment. I do not know why I had to use it to the return of controller too but worked – noruk Feb 23 '21 at 22:07
  • FYI ASP.Net6 builder.Services.Configure(jo => { jo.SerializerOptions.ReferenceHandler = ReferenceHandler.Preserve; }); – Benjamin McGill Feb 04 '22 at 21:41
13

.NET 5 Web API

    public static void ConfigureServices(this IServiceCollection services, IConfiguration configuration)
    {
        services.AddControllers()
            .AddJsonOptions(o => o.JsonSerializerOptions
                .ReferenceHandler = ReferenceHandler.Preserve);
    }
Erçin Dedeoğlu
  • 4,950
  • 4
  • 49
  • 69
8

I have the same issue, my fix was to add async and await keyword since I am calling an async method on my business logic.

Here is my original code:

[HttpGet]
public IActionResult Get()
{
   //This is async method and I am not using await and async feature .NET which triggers the error
   var results = _repository.GetAllDataAsync(); 
   return Ok(results);
}

To this one:

HttpGet]
public async Task<IActionResult> Get()
{
   var results = await _repository.GetAllDataAsync();
   return Ok(results);
}
Willy David Jr
  • 8,604
  • 6
  • 46
  • 57
8

In .Net 6, you can use System.Text.Json to initialize a startup action with AddControllersWithViews like this in Program.cs,

using System.Text.Json.Serialization;

builder.Services.AddControllersWithViews()
                .AddJsonOptions(x => x.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles);

also you can use AddMvc like this,

builder.Services.AddMvc()
                .AddJsonOptions(x => x.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles);

but quote from Ryan

asp.net core 3.0+ template use these new methodsAddControllersWithViews,AddRazorPages,AddControllers instead of AddMvc.

I will recommend to use the first solution.

Chester Chu
  • 395
  • 3
  • 11
4

Ensure you have [JsonIgnore] on the correct fields to avoid a circular reference.

In this case you will need

 public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string ProductText { get; set; }
    [JsonIgnore]
    public virtual ProductCategory ProductCategory { get; set; }
}

You probably don't need the ProductCategoryId field (depends if you are using EF and code first to define your DB)

Edit - In answer to noruk

There is often confusion in connected objects and navigation properties. You can get the data you want in JSON but also define the EF structures to get the correct DB structure (foreign keys, indexes, etc).

Take this simple example. A Product (for example a T-Shirt) has many sizes or SKUs (e.g. Small, Large, etc)

  public class Product
    {
     [Key]
     [MaxLength(50)]
     public string Style { get; set; }
     [MaxLength(255)]
     public string Description { get; set; }
     public List<Sku> Skus { get; set; }
    }
    
    public class Sku
    {
      [Key]
      [MaxLength(50)]
      public string Sku { get; set; }
      [MaxLength(50)]
      public string Barcode { get; set; }
      public string Size { get; set; }
      public decimal Price { get; set; }
      // One to Many for Product
      [JsonIgnore]
      public Product Product { get; set; }
    }

Here you can serialise a Product and the JSON data will include the SKUs. This is the normal way of doing things.

However if you serialise a SKU you will NOT get it's parent product. Including the navigation property will send you into the dreaded loop and throw the "object cycle was detected" error.

I know this is limiting in some use cases but I would suggest you follow this pattern and if you want the parent object available you fetch it separately based on the child.

var parent = dbContext.SKUs.Include(p => p.Product).First(s => s.Sku == "MY SKU").Product
Cueball 6118
  • 517
  • 4
  • 16
  • I do that and remove ProductId but Error still exist also try set option in services.AddControllers() but not working – sunny Feb 12 '20 at 22:25
  • 1
    @CueBall I use .Net 5 and In my case, as I want to include the navigation property. if i use the `[JsonIgnore]` it will be not added to the JSON result. So, if I do not use `[JsonIgnore]` i get: _possible object cycle was detected_ So, how can the navigation property be included ? – noruk Feb 23 '21 at 20:14
  • @noruk - Please see the edited answer. I am using .NET Core 3.1 at present. – Cueball 6118 Feb 24 '21 at 22:17
4

I fixed my API Core Net6.0 adding [JsonIgnore]:

public class SubCategoryDto
{

    public int Id { get; set; }
    public string Name { get; set; }
    public string Image { get; set; }
    public int CategoryId { get; set; }
    [JsonIgnore]
    public Category Category { get; set; }
   
   
}
Nick is tired
  • 6,860
  • 20
  • 39
  • 51
2

For net core 3.1 you have to add in Startup.cs:

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

and import at least this package using nuget.org include prerelease:

<PackageReference Include="System.Text.Json" Version="5.0.0-rc.1.20451.14" />
Macko
  • 906
  • 3
  • 11
  • 27
  • I 100% get why this is a good way of avoiding recursive serialisation and I ABSOLUTELY condone the use of a good JSON serializer such as NewtonSoft. However I would lean against this method as in many cases you will want to serialise data with multiple levels of hierarchy to use through an API, interface or similar. I would suggest understanding your data is the key here and knowing what to get and when. Ideally DB navigation properties are removed from serialisation in favour of getting the data in an alternative way. You will normally end up with a more generic solution. – Cueball 6118 Mar 04 '21 at 07:38
  • Set MaxDepth to 0 won't help the reference loop, if you see the document, set to 0 mean enlarge the depth from default 32 to 64, and finally a huge data content return from EF core. – Cheung Jul 26 '22 at 16:23
2

following code is working for me in dotnet 5.0 :

  services.AddControllersWithViews()
                        .AddJsonOptions(o => o.JsonSerializerOptions
                        .ReferenceHandler = ReferenceHandler.Preserve);
Uttam Ughareja
  • 842
  • 2
  • 12
  • 21
0

Finally fixed mine with System.Text.Json not NewtonSoft.Json using

var options = new JsonSerializerOptions()
        {
            MaxDepth = 0,
            IgnoreNullValues = true,
            IgnoreReadOnlyProperties = true
        };

Using options to serialize

objstr = JsonSerializer.Serialize(obj,options);
IObote
  • 159
  • 1
  • 4
  • So the problem here is that with max depth 0, you aren't goint to be able to get any relationships between data (I think). I.e. Include statements would just no longer work. – sbrevolution5 Oct 11 '21 at 16:21
  • @sbrevolution5 here MaxDepth = 0 indicates that max depth is 64, logically :) – SushiDynamite May 15 '23 at 12:08
0

My project built with a similar error.

Here's the code before

public class PrimaryClass {
  public int PrimaryClassId
  public ICollection<DependentClass> DependentClasses { get; set; }
}

public class DependentClass {
  public int DependentClassId { get; set; }
  public int PrimaryClassId { get; set; }
  public PrimaryClass primaryClass { get; set; }
}

I took away the PrimaryClass object from the DependentClass model.

Code after

public class PrimaryClass {
  public int PrimaryClassId
  public ICollection<DependentClass> DependentClasses { get; set; }
}

public class DependentClass {
  public int DependentClassId { get; set; }
  public int PrimaryClassId { get; set; }
}

I also had to adjust the OnModelCreating method from

modelBuilder.Entity<PrimaryClass>().HasMany(p => p.DependentClasses).WithOne(d => d.primaryClass).HasForeignKey(d => d.PrimaryClassId);

to

modelBuilder.Entity<PrimaryClass>().HasMany(p => p.DependentClasses);

The DbSet query that's running is

public async Task<List<DependentClass>> GetPrimaryClassDependentClasses(PrimaryClass p)
{
  return await _dbContext.DependentClass.Where(dep => dep.PrimaryClassId == p.PrimaryClassId).ToListAsync();
}

The error could have been with any of these 3 sections of code, but removing the primary object reference from the dependent class and adjusting the OnModelCreating resolved the error, I'm just not sure why that would cause a cycle.

IDrumsey
  • 45
  • 1
  • 9
0

In my case the problem was when creating the entity relationships. I linked the main entity using a foreign key inside the dependent entity like this

[ForeignKey("category_id")]
public Device_Category Device_Category { get; set; }

also I referred the dipendend entity inside the main entity as well.

 public List<Device> devices { get; set; }

which created a cycle.

Dependent Entity

  public class Device
    {
        [Key]
        public int id { get; set; }
        public int asset_number { get; set; }
        public string brand { get; set; }
        public string model_name { get; set; }
        public string model_no { get; set; }
        public string serial_no { get; set; }
        public string os { get; set; }
        public string os_version { get; set; }
        public string note { get; set; }
        public bool shared { get; set; }
        public int week_limit { get; set; }
        public bool auto_acceptance { get; set; }
        public bool booking_availability { get; set; }
        public bool hide_device { get; set; }
        public bool last_booked_id { get; set; }


        //getting the relationships category 1 to many 
        public int category_id { get; set; }

        [ForeignKey("category_id")]
        public Device_Category Device_Category { get; set; }

        public List<Booking> Bookings { get; set; } 
       
    }

Main Entity

    public class Device_Category
    {
        public int id { get; set; }
        public string name { get; set; }

        public List<Device> devices { get; set; }
    }
}

So I commented the

public List<Device> devices { get; set; }

inside main entity (Device_Category) and problem solved