Monday, November 23, 2015

EntityFramework and the challenge of Entity Serialization

Let's take the following couple of entities:


public class Parent
{
    public int Id { get; set; }

    public string Name { get; set; }

    public List Children { get; set; }
}


public class Child
{
    public int Id { get; set; }

    public string Name { get; set; }

    public int ParentId { get; set;  }

    public Parent Parent { get; set; }
}


And pass them through an Entity Framework query that looks like this:


var parent = db.Parent
                 .Include(par => par.Children)
                 .Where(par => par.Name == "Somename")
                 .FirstOrDefault();

It's interesting to see that we can then write the following LINQ to query the result:


var result = parent.Children[0]
                .Parent.Children[0]
                    .Parent.Children[0]
                        .Parent.Children[0]
                            .Parent.Children[0]
                                .Parent.Children[0]
                                    .Parent.Children[0]
                                        .Parent.Children[0]
                                            .Parent.Children[0]
                                                .Parent;

Here, the result variable refers to a Parent type which will have a collection of Children which will have a Parent ... oh never-mind, I'm sure you see where this ends!

When building a Web API application, we might think of exposing this type of query through a Controller action.  In such a case, how should the serializer deal with the cascading references.

One solution is to use a Serialization solution such as the ReferenceLoopHandler that is found in the Json.Net library to ignore circular references.  This switch tells the serializer to exclude reference properties after they have been found the first time.

Another solution is to shape the data to return specific fields from the service operation.

var parentView = new 
{
    ParentId = parent.Id,
    ParentName = parent.Name,
    ChildCount = parent.Children.Count,
    Children = parent.Children.Select(c =>
            new {
                Id = c.Id,
                Name = c.Name
            }
        )
};


This approach helps to control the shape of the data and to have greater certainty over what is being returned.

Taking this one step further we would create custom Data Contract classes and return those instead of the loosey-goosey approach of returning anonymous types.


var parentDataContract = db.Parent.Include(par => par.Children)
                            .Select(par =>
                                new ParentView
                                {
                                    Id = par.Id,
                                    Name = par.Name,
                                    Children = par.Children.Select(c =>
                                        new ParentView.ChildView
                                        {
                                            Id = c.Id,
                                            Name = c.Name
                                        }
                                    )
                                }
                            );

This approach gives us better static checks across the application, allows for reuse of Data Contracts across separate operations, and allows us to see where different contracts are being used.  From a versioning and maintenance point of view, this would be the gold standard.

What is your approach to designing service endpoints?   Do you mix RESTful with RPC-style design all in the same Controllers or do you separate them out into their own classes of service?

1 comment:

  1. Whilst I'm sure everyone does it, I'm a little bit cautious about returning objects from the internal domain model directly to consumers, so I think that anonymous type projection is a good solution for doing this. The challenge is consistency of course (whenever you return a parent, how do you ensure that you return the whole parent object (from a consumers point of view). So introducing a view isn't a bad approach - and I can't actually think of a better way of controlling the output.

    ReplyDelete