Using WCF, you can send data from one service endpoint to another making use of a variety of contracts and transport protocols. The messages can be as simple as a single character or word sent as XML, or as complex as a stream of binary data. Service endpoints are commonly hosted using IIS, but can be hosted in other processes as well.
WCF services are a staple of multi-tiered architectures because they make it easy to move business logic or data operations into a service without having to worry much about the process that is hosting it, the transport, message formats, etc. The abstraction is so clean in fact, that I’ve actually spoken with developers who didn’t even realize that their code was running in a different process (or possibly on a different server altogether). This can be a both good thing and a bad thing.
Let’s say that you have an existing ASP.NET application that is using Entity Framework for data access, and all the code runs within this ASP.NET application. Later you decide that it’s time to move your data access into a WCF service, so you create a new service project (with the default bindings and message format), and begin porting your code into your new service. For the most part everything just works, you barely broke a sweat, and life is good.
Although your code probably compiles after only a few tweaks, most of the time you are going to run into at least a handful of serialization issues related to the parameters or return types of your service methods. And because these are run-time serialization exceptions (which are in some cases data-dependent), you may not even notice there is a problem until you’ve deployed your service and are exercising it with real data. This is the part that can be a little confusing if you are new to WCF or service-oriented applications in general.
This post is about common serialization issues that you are most likely to encounter when porting existing code to a WCF service or when leveraging legacy data models in a new project. We’ll wrap up with a simple approach you can use in your unit tests to ensure that new serialization bugs aren’t introduced down the road.
Classes with Circular References
Most object models have at least some circular references. For example, a parent object has collection of children, and each of those child objects holds a reference to the parent. This can pose a problem to a framework that uses text-based messages to represent those models.
Consider the following:
[ServiceContract]
public interface IService1
{
[OperationContract]
List GetEmployees();
}
[DataContract]
public class Employee
{
[DataMember]
public string Name { get; set;}
[DataMember]
public bool IsActive { get; set; }
[DataMember]
public Employee Manager { get; set; }
}
Each employee object holds a reference to another Employee object (Manager). To illustrate, let’s implement the GetEmployees() method with some sample data:
public class Service1 : IService1
{
public List GetEmployees()
{
var jon = new Employee()
{ Name = "Jon", IsActive = true };
var steve = new Employee()
{ Name = "Steve", IsActive = true, Manager = jon };
var mike = new Employee()
{ Name = "Mike", IsActive = false, Manager = steve };
return new List() { jon, steve, mike };
}
}
If we deploy and run this it works great, and this scenario seems harmless. But what if in reality we end up with a condition where the data has a circular reference? I realize that this is a contrived example, but for the sake of brevity, let’s make one quick change to the code above:
public List GetEmployees()
{
var jon = new Employee()
{ Name = "Jon", IsActive = true };
var steve = new Employee()
{ Name = "Steve", IsActive = true, Manager = jon };
var mike = new Employee()
{ Name = "Mike", IsActive = false, Manager = steve };
jon.Manager = mike;
return new List() { jon, steve, mike };
}
The code will still compile and deploy, but when we call the service method over a WCF channel we’ll get an exception:
“Object graph for type ‘Employee’ contains cycles and cannot be serialized if reference tracking is disabled.”
Fortunately, this common problem has an easy fix. We just need to set the “IsReference” property on the DataContractAttribute to tell the serializer to serialize the objects as references. Note that there is a small performance penalty for doing this (so only use it when necessary), and it will only work with XML message formats, but it allows us to salvage our existing data model without major refactoring.
[DataContract(IsReference=true)]
public class Employee
{
[DataMember]
public string Name { get; set;}
[DataMember]
public bool IsActive { get; set; }
[DataMember]
public Employee Manager { get; set; }
}
Derived Return Types
Let’s assume that some employees are hourly employees that require a special derived class, which again would be a very common thing to find in your object model. Let’s add an HourlyEmployee class that derives from Employee, and modify our GetEmployees() method to make use of it in our sample data:
[DataContract(IsReference = true)]
public class HourlyEmployee : Employee
{
}
public List GetEmployees()
{
var jon = new Employee()
{ Name = "Jon", IsActive = true };
var steve = new Employee()
{ Name = "Steve", IsActive = true, Manager = jon };
var mike = new HourlyEmployee()
{ Name = "Mike", IsActive = false, Manager = steve };
jon.Manager = mike;
return new List() { jon, steve, mike };
}
As soon as we encounter this new type after deploying the service and calling the method we will get a serialization exception:
“Type ‘HourlyEmployee’ with data contract name ‘HourlyEmployee:http://schemas.datacontract.org/2004/07/WCFSerialization’ is not expected.”
Again this is an easy fix. We just need to modify the data contract of the Employee type to allow the HourlyEmployee type as well. We do this by adding the KnownTypeAttribute:
[DataContract(IsReference=true)]
[KnownType(typeof(HourlyEmployee))]
public class Employee
{
[DataMember]
public string Name { get; set;}
[DataMember]
public bool IsActive { get; set; }
[DataMember]
public Employee Manager { get; set; }
}
Non-Serializable Collections
Certain types of collections can’t be serialized and should never be used as members in your data model. For example, IQueryable<T> can’t be serialized, but if you need to provide an IQueryable<T> member on your class, you can implement it as a wrapper for another (optionally private) collection that can be serialized. Just be sure to mark it with the IgnoreDataMemberAttribute so that the serializer will ignore it.
[DataContract(IsReference=true)]
[KnownType(typeof(HourlyEmployee))]
public class Employee
{
[DataMember]
public string Name { get; set;}
[DataMember]
public bool IsActive { get; set; }
[DataMember]
public Employee Manager { get; set; }
[DataMember]
private ICollection _directReports { get; set; }
[IgnoreDataMember]
public IQueryable DirectReports {
get { return _directReports.AsQueryable(); }
set { _directReports = value.ToList(); }
}
}
Another common issue is with collections that are marked as virtual and get overridden with a type that is not expected by the serializer. This is similar to the KnownType issue above, but they are usually anonymous types that can’t be accounted for at design time. You will most frequently see this with ORM tools (such as Entity Framework) where virtual collections are used as a mechanism to support lazy loading. Obviously you can’t lazy load a collection that has already been serialized and returned to the client, so lazy loading should just be disabled. The approach for doing this varies according to the specific framework and version you are using, but for Entity Framework you can find more information here.
Unit Testing for Serialization Issues
Up until now we’ve discussed problems you might discover after you’ve deployed your WCF service. But how do we catch serialization errors before we’ve deployed our code? You can write integration test scripts that will automatically deploy and test your service, but setting that up can be tricky and time-consuming. As an easier alternative, you can leverage the DataContractSerializer directly in your unit tests. The basic approach is to call your service methods directly, and then serialize and deserialize the result to simulate the process that is normally handled by WCF.
While I’m sure it’s not bullet proof, I’ve used this approach many times, and it will catch all of the scenarios discussed above. First, add a unit test project to your solution. Then simply create some serialization helper functions that you can from each of your tests. I’ve included a stripped-down version (below) that can be used as a starting point.
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Text;
using System.IO;
using System.Runtime.Serialization;
using System.Xml;
using System.Collections.Generic;
namespace UnitTestProject1
{
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestMethod1()
{
var service = new Service1();
var employees = service.GetEmployees();
//include any business logic tests
// { ... }
//test serialization
string xml = employees.SerializeToXml();
List employee2 = xml.DeserializeObject>();
}
}
public static class SerializationHelper
{
public static string SerializeToXml(this object obj)
{
var dataContractSerializer = new DataContractSerializer(obj.GetType());
using (var memoryStream = new MemoryStream())
{
dataContractSerializer.WriteObject(memoryStream, obj);
return Encoding.UTF8.GetString(memoryStream.ToArray());
}
}
public static T DeserializeObject(this string xml) where T : class
{
MemoryStream memoryStream = new MemoryStream(Encoding.Unicode.GetBytes(xml));
XmlDictionaryReader reader =
XmlDictionaryReader.CreateTextReader(memoryStream, Encoding.Unicode,
new XmlDictionaryReaderQuotas(), null);
DataContractSerializer dataContractSerializer =
new DataContractSerializer(typeof(T));
return dataContractSerializer.ReadObject(reader) as T;
}
}
}
Source :http://www.rdacorp.com/2013/04/solutions-to-common-wcf-serialization-problems/
No comments:
Post a Comment