Wednesday, May 06, 2009

Problems with recursion in WCF serialization (hierarchical structures)

I know the title isn't too hot, but I wanted to write a post about a problem I've encountered lately with WCF serialization in hierarchical structures.  These come up a lot in the real world, and in the normal world of .Net, when relationships between classes are object references, there is no problem.  However, when you serialize these objects into XML using the WCF data contract serializer you find that the object references are replaced by child XML nodes.  When your references are circular you get into an in infinite loop.

Let me explain by example.  Imagine that we have a database table containing a list of employees.  It might look something like this:

This encapsulates a simple hierarchy.  An employee can have a line manager and so the table is keyed back onto itself to form the hierarchy.  The data might be this:



As you can see, you have 3 records.  One manager and two direct reports.  If we were to represent this in classes we might have the following (note that I have implemented them as data contracts):

    [DataContract(Namespace="http://andrew.com/employee")]
    public class Employee
    {
        [DataMember(Order = 0)]
        public string Name { get; set; }

        [DataMember(Order = 1)]
        public string JobTitle { get; set; }

        [DataMember(Order = 2)]
        public Employee LineManager {get; set;}

        [DataMember(Order = 3)]
        public EmployeeCollection DirectReports { get; set; }
    }

    [CollectionDataContract(Namespace="http://andrew.com/employee")]
    public class EmployeeCollection : List {}

Now, we can see that for an employee we have a collection of their direct reports, and for each employee we can create a reference back to their line manager.  So, what I will do now is use a quick bit of LINQ-to-SQL to retrieve the employees from the database and an extension method to perform the conversion into the data contracts.  My code, if I were to create a simple service, might like this:

namespace EmployeeService
{
    public class EmployeeServiceInstance : IEmployeeService
    {

        #region IEmployeeService Members

        public Employee GetEmployeeByName(string name)
        {
            using (DataAccess.EmployeesDataContext ctx = new DataAccess.EmployeesDataContext())
            {
                var q = from a in ctx.Employees
                        where a.Name == name
                        select a.Translate();

                return (Employee)q.SingleOrDefault();
            }
        }

        #endregion
    }

    public static class Translator
    {
        public static Employee Translate(this DataAccess.Employee entity)
        {
            Employee employee = new Employee()
            {
                Name = entity.Name,
                JobTitle = entity.JobTitle,
                DirectReports = new EmployeeCollection()
            };

            foreach (DataAccess.Employee e in entity.Employees)
            {
                employee.DirectReports.Add(e.Translate());
            }

            //This is where you get the problem, by adding the reference back to the parent entity
            employee.DirectReports.ForEach(p => p.LineManager = employee);

            return employee;
        }
    }
}

[Disclaimer:  This code is for illustration only.  My production code and production designs would not look like this!]

So, what happens when we run this and search for "Bob" using the service?  We get the following error:

Test method EmployeeService.Tests.UnitTest1.TestMethod1 threw exception:  System.ServiceModel.CommunicationException: The underlying connection was closed: The connection was closed unexpectedly. --->  System.Net.WebException: The underlying connection was closed: The connection was closed unexpectedly..

If you look this error up on the net it will tell you that it has occurred when the WCF message size limits have been breached.  This usually happens when you need to bring back lots of rows or when you are handling binary data.  In reality, in this case the error has occurred because the data contract serializer is stuck in an infinite loop passing backwards and forwards between the line manager and then employee records.  Because it is serializing as XML it loses the object references!

So all we have to do is comment out the line where we add the parent reference in...

            //employee.DirectReports.ForEach(p => p.LineManager = employee);

...and it all works!  It's a shame because I still like to use lambda expressions whereever possible because they're still new and cool (to me anyway).

In conclusion, be careful with hierarchies in WCF, and note that the error message that you get does not neccessarily reflect the underlying cause.




6 comments:

Anonymous said...

It's a shame because you can't reflect in your Dto what you really have in your domain model.
At least there should be an attribute able to give a depth limit when you scale your domain tree

andregsilv said...

Could you find a workaround? I have a similar problem, but I need to backreference...

andregsilv said...

I have found the solution!
You need to add the argument IsReference=true in the DataContractAttribute, like this:
[DataContract(IsReference)].

It will tell the framework to not serialize the referenced members inside the element nodes. Instead, it will create a reference to the member, allowing you to do recursion.

John Barness said...

Thank you for the good article.
When it comes to databases, it should be very good data security systems involved. As for me, virtual data room solutions are the best in this field.

Unknown said...

Thank y, very good script.
Very important to understand how it works, join cyber security blog.
security online

nasrradder said...

This platform has your back in case you have been to bodily casinos and need to play basic slots. However, the profitable won't be as excessive as many different free slot games. Since they offer bonuses that require slot machines, you’ll by no means see this website run out of 1xbet them. Gambling can be fun, however it’s designed to half you out of your hard-earned money. If you’re going to hit the tables, persist with blackjack, aka twenty-one.