How to handle a Circular Reference Problem in ASP Dot Net MVC Core

How to ​​ handle a Circular Reference Problem in ASP.NET MVC Core?

 

The code​​ below​​ contains a problem that causes so much confusion that it is worth exploring in​​ detail. To see the problem, restart the ASP.NET Core MVC application and use a browser to request the URL

http://localhost:5000/api/products/1. The application will report the following exception:

...

Newtonsoft.Json.JsonSerializationException: Self referencing loop detected with

type 'SportsStore.Models.Product'. Path 'supplier.products'.

...

Ans :​​ The​​ Newtonsoft.Json​​ namespace contains the excellent Json.NET package that Microsoft uses for JSON serialization in ASP.NET Core. Json.NET keeps track of the objects it serializes to avoid circular references that would lead to the same data being endlessly serialized. For example, in a situation where object A has a reference object B and object B has a reference to object A, there is a risk that the serializer will get stuck in a loop endlessly following the references between the objects. To avoid this situation, the serializer throws an exception when it follows a reference to an object that it has already serialized.

Looking at the code below, you might struggle to see why using the Include method has created a circular reference. The problem is caused by a well-intentioned Entity Framework Core feature that attempts to minimize the amount of data read from the database but that causes problems in ASP.NET Core MVC applications. To see what is happening, change the configuration of the JSON serializer in the Startup class so that it doesn’t report an exception when it detects a circular reference.

Configuring the JSON Serializer in the Startup.cs File in the SportsStore Folder

using​​ Microsoft.AspNetCore.Builder;

using​​ Microsoft.AspNetCore.Hosting;

using​​ Microsoft.Extensions.Configuration;

using​​ Microsoft.Extensions.DependencyInjection;

using​​ Microsoft.Extensions.Logging;

using​​ Microsoft.AspNetCore.SpaServices.Webpack;

using​​ SportsStore.Models;

using​​ Microsoft.EntityFrameworkCore;

using​​ Newtonsoft.Json;

 

namespace​​ SportsStore {

 ​​ ​​ ​​​​ public​​ class​​ Startup​​ {

 ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ public​​ Startup(IHostingEnvironment env) {

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ var​​ builder =​​ new​​ ConfigurationBuilder()

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ .SetBasePath(env.ContentRootPath)

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ .AddJsonFile("appsettings.json", optional:​​ false, reloadOnChange:​​ true)

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional:​​ true)

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ .AddEnvironmentVariables();

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ Configuration = builder.Build();

 ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ }

 

 ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ public​​ IConfigurationRoot Configuration {​​ get; }

 

 ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ public​​ void​​ ConfigureServices(IServiceCollection services) {

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ services.AddDbContext<DataContext>(options =>

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ options.UseSqlServer(Configuration

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ ["Data:Products:ConnectionString"]));

 

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ services.AddMvc().AddJsonOptions(opts => {

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ opts.SerializerSettings.ReferenceLoopHandling  ​​​​ 

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ = ReferenceLoopHandling.Serialize;

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ opts.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ });

 ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ }

 

 ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ public​​ void​​ Configure(IApplicationBuilder app,

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ IHostingEnvironment env, ILoggerFactory loggerFactory) {

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ loggerFactory.AddConsole(Configuration.GetSection("Logging"));

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ loggerFactory.AddDebug();

 

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ app.UseDeveloperExceptionPage();

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ app.UseWebpackDevMiddleware(new​​ WebpackDevMiddlewareOptions {

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ HotModuleReplacement =​​ true

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ });

 

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ app.UseStaticFiles();

 

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ app.UseMvc(routes => {

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ routes.MapRoute(

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ name:​​ "default",

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ template:​​ "{controller=Home}/{action=Index}/{id?}");

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ });

 

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ SeedData.SeedDatabase(app.ApplicationServices

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ .GetRequiredService<DataContext>());

 ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ }

 ​​ ​​ ​​​​ }

}

 

This doesn’t fix the problem, but it does reveal what is causing it, albeit by allowing a circular reference to create a terminal stack overflow. Restart the ASP.NET Core MVC application and use a browser to​​ request the​​ http://localhost:5000/api/products/1​​ URL. Instead of the exception shown earlier, the application

will terminate with this error:

...

Process is terminated due to​​ StackOverflowException

...

This is the situation that the JSON serializer tries to avoid. You can see what caused the problem by examining the JSON content that was displayed by the browser before the application stopped, which is easier to understand if you format the data like this:

...

{ "productId":1, "name":"Kayak", "category":"Watersports",

"description":"A boat for one person", "price":275.00,

"supplier":{

"supplierId":1, "name":"Splash Dudes", "city":"San Jose",

"state":"CA",

"products":[

{ "productId":1, "name":"Kayak", "category":"Watersports",

"description":"A boat for one person", "price":275.00,

"supplier":{

"supplierId":1,"name":"Splash Dudes", "city":"San Jose",

"state":"CA",

"products":[

{ "productId":1, "name":"Kayak", "category":"Watersports",

...

When Entity Framework Core creates objects, it tries to populate navigation properties with objects​​ that have already been created by the same database context. This can be a useful feature in some kinds of​​ application, such as desktop apps, where a database context object has a long life and is used to make many​​ requests over time. It isn’t useful for ASP.NET Core MVC applications where a new context object is created​​ for each HTTP request. In the example application, the only objects that Entity Framework Core creates are​​ the ones for the current query, which starts with a​​ Product​​ object and includes the related​​ Supplier​​ and​​ Rating​​ objects.

When Entity Framework Core creates the​​ Supplier​​ object, it looks at the objects it has already created​​ to see whether any of them can be used to populate the​​ Products​​ navigation property. There is one such

object, which is the​​ Product​​ object that has already been created, so Entity Framework Core adds this to the​​ collection assigned to the​​ Supplier.Products​​ property. This creates the circular reference, which isn’t a​​ problem when the references are between objects in the .NET Core runtime but cause problems when they​​ are serialized to JSON, which doesn’t have any way to represent references between objects.

The application terminates with a​​ StackOverflowException​​ because the JSON serializer follows these​​ the references and writes out each object it encounters. The​​ Product​​ object has a reference to the​​ Supplier​​ object through its​​ Supplier​​ property, which has a reference to the​​ Product​​ object through its​​ Products​​ property, which has a reference to the​​ Supplier​​ object, and so on.

 

Breaking the Circular References

There is no way to stop Entity Framework Core from using existing objects for navigation properties.Preventing the problem means breaking the references between objects after they have been created by​​ Entity Framework Core and before they are processed by the JSON serializer.

Breaking References in the ProductValuesController.cs File in the Controllers Folder

using​​ Microsoft.AspNetCore.Mvc;

using​​ SportsStore.Models;

using​​ Microsoft.EntityFrameworkCore;

using​​ System.Linq;

using​​ System.Collections.Generic;

 

namespace​​ SportsStore.Controllers {

 

 ​​ ​​ ​​​​ [Route("api/products")]

 ​​ ​​ ​​​​ public​​ class​​ ProductValuesController​​ : Controller {

 ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ private​​ DataContext context;

 

 ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ public​​ ProductValuesController(DataContext ctx) {

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ context = ctx;

 ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ }

 

 ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ [HttpGet("{id}")]

 ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ public​​ Product GetProduct(long​​ id) {

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ Product result = context.Products

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ .Include(p => p.Supplier).ThenInclude(s => s.Products)

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ .Include(p => p.Ratings)

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ .First(p => p.ProductId == id);

 

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ if​​ (result !=​​ null) {

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ if​​ (result.Supplier !=​​ null) {

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ result.Supplier.Products = result.Supplier.Products.Select(p =>

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ new​​ Product {

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ ProductId = p.ProductId,

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ Name = p.Name,

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ Category = p.Category,

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ Description = p.Description,

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ Price = p.Price,

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ });

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ }

 

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ if​​ (result.Ratings !=​​ null) {

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ foreach​​ (Rating r​​ in​​ result.Ratings) {

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ r.Product =​​ null;

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ }

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ }

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ }

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ return​​ result;

 ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ }

 

 ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ [HttpGet]

 ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ public​​ IEnumerable<Product> GetProducts(string​​ category,​​ string​​ search,​​ 

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ bool​​ related =​​ false) {

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ IQueryable<Product> query = context.Products;

 

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ if​​ (!string.IsNullOrWhiteSpace(category)) {

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ string​​ catLower = category.ToLower();

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ query = query.Where(p => p.Category.ToLower().Contains(catLower));

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ }

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ if​​ (!string.IsNullOrWhiteSpace(search)) {

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ string​​ searchLower = search.ToLower();

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ query = query.Where(p => p.Name.ToLower().Contains(searchLower)

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ || p.Description.ToLower().Contains(searchLower));

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ }

 

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ if​​ (related) {

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ query = query.Include(p => p.Supplier).Include(p => p.Ratings);

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ List<Product> data = query.ToList();

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ data.ForEach(p => {

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ if​​ (p.Supplier !=​​ null) {

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ p.Supplier.Products =​​ null;

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ }

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ if​​ (p.Ratings !=​​ null) {

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ p.Ratings.ForEach(r => r.Product =​​ null);

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ }

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ });

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ return​​ data;

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ }​​ else​​ {

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ return​​ query;

 ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ }

 ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ }

 

 ​​ ​​ ​​ ​​ ​​ ​​ ​​​​ 

 ​​ ​​ ​​​​ }

}

To break the relationships, the listing sets the​​ Supplier.Products​​ property to​​ null. There is also a circular reference between the​​ Product​​ and​​ Rating​​ objects, so the listing enumerates the​​ Rating​​ objects returned by the​​ Product​​ object’s​​ Ratings​​ property to set their​​ Product​​ property to​​ null. To see the result, restart the ASP.NET Core MVC application and use a browser to request the​​ http://localhost:5000/api/products/1​​ URL, which will produce the following data:

{

"productId":1,"name":"Kayak","category":"Watersports",

"description":"A boat for one person","price":275.00,

"supplier":{

"supplierId":1,"name":"Splash Dudes","city":"San Jose",

"state":"CA","products":null},

"ratings":[{"ratingId":1,"stars":4,"product":null},

{"ratingId":2,"stars":3,"product":null}]

}

 

AVOIDING PROBLEMATIC SOLUTIONS

There are two other ways to prevent the JSON serializer from throwing exceptions about circular references, both of which will show up if you search online for the error text, but neither of which really solves the problem.

The most common advice is to change the configuration of the serializer so that it simply ignores any object that it has already serialized, using a configuration statement like this in the​​ Startup​​ class:

...

services.AddMvc().AddJsonOptions(opts =>

opts.SerializerSettings.ReferenceLoopHandling =​​ ReferenceLoopHandling.Ignore);

...

The​​ Ignore​​ setting prevents exceptions, but it doesn’t stop extraneous data from being returned by the web service, which can be a problem when you are handling requests for multiple objects. Each object​​ retrieved from the database gives Entity Framework Core more scope to populate navigation properties, adding to the data sent to the client.

The other approach is to add the​​ JsonIgnore​​ attribute to navigation properties in the model classes to stop the serializer following them when creating JSON data. This solves the problem in the short term​​ but prevents you from being able to create useful web services that deal with the model classes directly,rather than as related data.

(Visited 369 times, 3 visits today)

Leave a Reply

Your email address will not be published. Required fields are marked *