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:
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:
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:
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:
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:
[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:
[TestMethod]
public void Serializer_ProgramAttrJsonSerlialize()
{
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:
[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:
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:
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:
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:
[TestMethod]
public void Resolver_ProgramContractResolver()
{
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:
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:
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:
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.