Click here to Skip to main content
15,887,828 members
Articles / Web Development / ASP.NET

Web API Circular References with Many to Many Relationships

Rate me:
Please Sign up or sign in to vote.
4.50/5 (4 votes)
25 Aug 2015CPOL4 min read 26.2K   5   5
Web API Circular References with Many to Many Relationships

Many-to-many relationships are tough to work with in Web API. In this article, I would like to take on a circular reference challenge. A circular exception occurs when a parent model has many children and a child points back to the same parent. We will investigate different alternatives and see the limitations of each approach.

I would like to focus on the data models we will be working with. These models create tables in a database using code first in entity framework. Below is a program model with many users:

C#
public class Program
{
    public long Id { get; set; }
    [Required]
    [StringLength(25)]
    public string Name { get; set; }
    public virtual ICollection<User> Users { get; set; }
}

And here are the users:

C#
public class User
{
    public long Id { get; set; }
    [Required]
    [StringLength(25)]
    public string Name { get; set; }
    public virtual ICollection<Program> Programs { get; set; }
}

Background

You can begin to wonder what happens when I try to serialize this beast using Web API. A program has many users but each user has a many-to-many relationship with the parent. When I serialize this through Json.NET, which is the default serializer in Web API, I get the following error:

Newtonsoft.Json.JsonSerializationException: Self referencing loop detected for property 
'Programs' with type 'System.Collections.Generic.List`1
[WebApiCircularSerialization.Models.Program]'. Path '[0].Users[0]'.

This issue gets exacerbated by the fact that we turned on lazy loading in entity framework. By using the virtual keyword, entity framework loads up child-parent relationships ad infinitum. Your gut reaction could be to turn this “feature” off, but, is there a better solution? To start, imagine this has nothing to do with entity framework nor cumbersome relationships. Imagine this is just a simple dataset, like:

JavaScript
var prgs = new List<Program>
{
    new Program { Id = 1, Name = "P1" },
    new Program { Id = 2, Name = "P2" }
};
var usrs = new List<User>
{
    new User { Id = 1, Name = "U1" },
    new User { Id = 2, Name = "U2" }
};
prgs.ForEach(x => x.Users = usrs);
usrs.ForEach(x => x.Programs = prgs);

When I try to serialize either prgs or usrs, this does in fact reproduce this same issue. So, the problem does not lie in the data but in our serialization strategy.

Let’s look at some of the alternatives.

Serialization Attributes

Json.NET offers the ability to tag model properties through C# attributes. There many attributes, and the one we need is [JsonIgnore]. With this, we can add it to one of the circular relational properties:

C#
public class ProgramAttr
{
    public long Id { get; set; }
    [Required]
    [StringLength(25)]
    public string Name { get; set; }
    [JsonIgnore]
    public virtual ICollection<UserAttr> UserAttrs { get; set; }
}

I created a separate data model for this since C# attributes lock it down. To differentiate it, I added Attr at the end. The JsonIgnore attribute tells the engine to ignore this property, hence stop circular serialization.

Now, like any sound code solution, let’s make sure this works through some awesome unit tests. I like to start with the repository pattern and make sure we can get data from the database:

C#
[TestMethod]
public async Task Repository_ProgramAttrAllAsync()
{
    Database.SetInitializer(new ApplicationTestDatabaseInitialize());
    var context = new ApplicationTestDbContext();
    context.Database.Log = s => Debug.WriteLine(s);
    var repo = new ProgramAttrRepository(context);
    var result = await repo.AllAsync();
    Assert.AreEqual<int>(2, result.Count());
    Assert.AreEqual<int>(2, result.First().UserAttrs.Count());
}

It is a nice and simple API with an AllAsync that does all the magic. I am making sure we get programs and the list of users from the database. I am also logging any SQL that comes from entity framework to the output window. This way, I am aware of any crazy SQL statements getting sent. Moving to the next layer, let’s make sure we get what we want from the serializer:

C#
[TestMethod]
public void Serializer_ProgramAttrJsonSerlialize()
{
    // Dummy "prgs" data that simulates circular references
    string result = JsonConvert.SerializeObject(prgs);
    var expected = "[{\"Id\":1,\"Name\":\"P1\"},{\"Id\":2,\"Name\":\"P2\"}]";
    Assert.AreEqual<string>(expected, result);
}

Sweet, we are gaining confidence this solution will work. To end it, let’s move on to the Web API controller. Following SOLID principles, I am coding to abstractions not implementation details:

C#
[TestMethod]
public async Task Controller_ProgramAttrGet()
{
    var prgs = new List<ProgramAttr>();
    var mock = new Mock<IProgramAttrRepository>();
    mock.Setup(m => m.AllAsync()).ReturnsAsync(prgs);
    var target = new ProgramAttrController(mock.Object);
    var result = await target.Get() as IEnumerable<ProgramAttr>;
    mock.Verify(m => m.AllAsync(), Times.Once);
    Assert.IsNotNull(result);
}

The controller returns a contract of IEnumerable<ProgramAttr>. Since Web API uses Json.NET by default, we have reassurance this will not have circular data. This is what the controller does:

C#
public async Task<IEnumerable<ProgramAttr>> Get()
{
    return await repo.AllAsync();
}

Boring! The genius behind this is the default serialization that does the magic under the covers. To show off, we can run all unit tests in unison:

Serialization attributes unit tests

Contract Resolvers

One other alternative is to use contract resolvers in Json.NET. This gains you the flexibility to define how you wish to serialize data. So, for example, to prevent circular serialization in a program model:

C#
public class ProgramContractResolver : DefaultContractResolver
{
    protected override JsonProperty CreateProperty(
        MemberInfo member,
        MemberSerialization memberSerialization)
    {
        var property = base.CreateProperty(member, memberSerialization);
        if (property.PropertyName == "Programs")
        {
            property.ShouldSerialize = i => false;
        }
        return property;
    }
}

This contract resolver looks for a property.PropertyName called Programs and sets the ShouldSerialize setting. We inherit from DefaultContractResolver and override CreateProperty. The serialization engine handles the rest of the heavy lifting. The beauty here is I can append this without locking anything down. Now, to test this sweet little resolver:

C#
[TestMethod]
public void Resolver_ProgramContractResolver()
{
    // Dummy "prgs" data that simulates circular references
    var settings = new JsonSerializerSettings
    {
        ContractResolver = new ProgramContractResolver()
    };
    var result = JsonConvert.SerializeObject(prgs, Formatting.None, settings);
    Assert.IsNotNull(result);
    var expected = "[{\"Id\":1,\"Name\":\"P1\",\"Users\":" +
        "[{\"Id\":1,\"Name\":\"U1\"},
        {\"Id\":2,\"Name\":\"U2\"}]}," +
        "{\"Id\":2,\"Name\":\"P2\",\"Users\":
        [{\"Id\":1,\"Name\":\"U1\"}," +
        "{\"Id\":2,\"Name\":\"U2\"}]}]";
    Assert.AreEqual<string>(expected, result);
}

Json.NET gives you the capability to override the ContractResolver. The SerializeObject method takes in the settings parameter. The program data model now has the list of users. But, no circular references to a program here. I’ve already shown you unit tests for all the other layers. So, let’s look at the Web API controller:

JavaScript
public async Task<IEnumerable<Program>> Get()
{
    var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
    json.SerializerSettings.ContractResolver = new ProgramContractResolver();
    return await repo.AllAsync();
}

This one is a bit more complicated since we need to set up the proper settings. Luckily, ASP.NET has a GlobalConfiguration object where I can set ContractResolver settings. The rest is nothing you are not already familiar with. Now to run all unit tests:

Contract resolvers unit tests

Survey Says!

No point in doing all this without at least showing how it all comes together. I’ve used Fiddler to help me compose requests to the web API:

Fiddler says

Final Thoughts

So there you have it, a way to serialize many-to-many relationships through Web API. Let’s go over the pros and cons of each approach.

Serialization attributes:

  • Simple setup, all it needs is an attribute.
  • We leverage the default serialization engine with zero lines of code in the controller.
  • It locks down your data model, so you only get a one-to-many relationship. In C#, attributes are not configurable.

Contract resolvers:

  • Ultimate flexibility. We are able to configure the serialization to fit the current need.
  • ASP.NET affords us the capability to configure the default serializer.
  • Because of this flexibility, it requires more lines of code. We have to write the resolver and configure the controller.

I hope you’ve enjoyed this psychedelic trip around the Json.NET serializer. If interested, I encourage you to go spelunking through the code up on GitHub.

The post Web API Circular References with Many to Many Relationships appeared first on BeautifulCoder.NET.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Engineer
United States United States
Husband, father, and software engineer living in Houston Texas. Passionate about JavaScript, C#, and webbing all the things.

Comments and Discussions

 
QuestionThere is other way control: JavaScriptSerializer with JavaScriptConverter Pin
rpokrovskij18-Apr-17 2:48
rpokrovskij18-Apr-17 2:48 
QuestionBidirectional, and not many-to-many, associations create circular references Pin
Gerd Wagner26-Aug-15 5:26
professionalGerd Wagner26-Aug-15 5:26 
AnswerRe: Bidirectional, and not many-to-many, associations create circular references Pin
Camilo Reyes26-Aug-15 5:53
professionalCamilo Reyes26-Aug-15 5:53 
GeneralRe: Bidirectional, and not many-to-many, associations create circular references Pin
Gerd Wagner26-Aug-15 6:02
professionalGerd Wagner26-Aug-15 6:02 
GeneralRe: Bidirectional, and not many-to-many, associations create circular references Pin
Camilo Reyes26-Aug-15 6:35
professionalCamilo Reyes26-Aug-15 6:35 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.