Sdílet prostřednictvím


Loop Reference handling in Web API

The Issue

It's very common to have circular reference in models. For example, the following models shows a bidirection navigation property:

  1: public class Category 
  2: { 
  3:     public Category() 
  4:     { 
  5:         Products = new Collection<Product>(); 
  6:     } 
  7:     
  8:     public int Id { get; set; } 
  9:     public string Name { get; set; } 
  10:     public virtual ICollection<Product> Products { get; set; } 
  11: } 
  12:  
  13: public class Product 
  14: { 
  15:     public int Id { get; set; } 
  16:     public string Name { get; set; } 
  17:     public virtual Category Category { get; set; } 
  18: } 

When using with Web Api by generating an EF scaffolding api controller, it won't work by default, however. The following error will occur when serializing with json.net serializer:

 Self referencing loop detected for property 'Category' with type 
 'System.Data.Entity.DynamicProxies.Category_A97AC61AD05BA6A886755C779FD3F96E86FE903ED7C9BA9400E79162C11BA719'. 
 Path '[0].Products[0]' 

The error occurs because the serializer doesn't know how to handle cirular reference. (Similar error occurs in xml serializer as well)

Disable proxy and include reference

EF proxy doesn't work well with POCO data serialization. There are several workarounds. For simplicity, we just disable it in the data context class:

  1: public CircularReferenceSampleContext() : base("name=CircularReferenceSampleContext") 
  2: { 
  3:     Database.SetInitializer(new CircularReferenceDataInitializer()); 
  4:     this.Configuration.LazyLoadingEnabled = false; 
  5:     this.Configuration.ProxyCreationEnabled = false; 
  6: } 

However, after disable proxy, the navigation property won't be lazy loaded. So you have to include the reference when retrieving data from database. Change the scaffolding controller code to:

  1: public IEnumerable<Product> GetProducts() 
  2: { 
  3:     return db.Products.Include(p => p.Category).AsEnumerable(); 
  4: } 

The include call will include the reference data for all records.

Fix 1: Ignoring circular reference globally

json.net serializer supports to ignore circular reference on global setting. A quick fix is to put following code in WebApiConfig.cs file:

  1: config.Formatters.JsonFormatter.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore; 

The simple fix will make serializer to ignore the reference which will cause a loop. However, it has limitations:

  • The data loses the looping reference information
  • The fix only applies to JSON.net
  • The level of references can't be controlled if there is a deep reference chain

Fix 2: Preserving circular reference globally

This second fix is similar to the first. Just change the code to:

  1: config.Formatters.JsonFormatter.SerializerSettings.ReferenceLoopHandling 
  2:     = Newtonsoft.Json.ReferenceLoopHandling.Serialize; 
  3: config.Formatters.JsonFormatter.SerializerSettings.PreserveReferencesHandling 
  4:     = Newtonsoft.Json.PreserveReferencesHandling.Objects; 

The data shape will be changed after applying this setting.

  1: [{"$id":"1","Category":{"$id":"2","Products":[{"$id":"3","Category":{"$ref":"2"},"Id":2,"Name":"Yogurt"},{"$ref":"1"}],"Id":1,"Name":"Diary"},"Id":1,"Name":"Whole Milk"},{"$ref":"3"}] 

The $id and $ref keeps the all the references and makes the object graph level flat, but the client code needs to know the shape change to consume the data and it only applies to JSON.NET serializer as well.

Fix 3: Ignore and preserve reference attributes

This fix is decorate attributes on model class to control the serialization behavior on model or property level. To ignore the property:

  1: public class Category 
  2: { 
  3:     public int Id { get; set; } 
  4:     public string Name { get; set; } 
  5:     
  6:     [JsonIgnore] 
  7:     [IgnoreDataMember] 
  8:     public virtual ICollection<Product> Products { get; set; } 
  9: } 

JsonIgnore is for JSON.NET and IgnoreDataMember is for XmlDCSerializer.
To preserve reference:

  1: // Fix 3 
  2: [JsonObject(IsReference = true)] 
  3: public class Category 
  4: { 
  5:     public int Id { get; set; } 
  6:     public string Name { get; set; } 
  7:  
  8:     // Fix 3 
  9:     //[JsonIgnore] 
  10:     //[IgnoreDataMember] 
  11:     public virtual ICollection<Product> Products { get; set; } 
  12: } 
  13:  
  14: [DataContract(IsReference = true)] 
  15: public class Product 
  16: { 
  17:     [Key] 
  18:     public int Id { get; set; } 
  19:  
  20:     [DataMember] 
  21:     public string Name { get; set; } 
  22:  
  23:     [DataMember] 
  24:     public virtual Category Category { get; set; } 
  25: } 

[JsonObject(IsReference = true)] is for JSON.NET and [DataContract(IsReference = true)] is for XmlDCSerializer. Note that: after applying DataContract on class, you need to add DataMember to properties that you want to serialize.

The attributes can be applied on both json and xml serializer and gives more controls on model class.

Download sample code here

Comments

  • Anonymous
    November 07, 2012
    I finally got it: Disable proxy and include reference is a pre-requirement for other fixes.

  • Anonymous
    January 03, 2013
    Great article. Thanks.

  • Anonymous
    June 12, 2013
    Thanks!  It seems strange that the Web Api team would have created scaffolding that won't work unless you make these tweaks that you suggest.  Have you been working with them to come up with some fixes?