Thursday, July 26, 2007
« Amazon Delivers Harry Potter and the Dea... | Main | "Jane, Stop this Crazy Thing!"... »

I sat down last week to migrate a customer's ASMX service to WCF, and in the process discovered a problem with complex type serialization when WCF is configured to use the XmlSerializer and the complex type is in an XML namespace that is different from that of the service.

By default, Windows Communication Foundation uses the new DataContractSerializer. This new serializer is lean and mean. It is limited in its schema (it does not support attributes, for example), but delivers a considerable performance boost and greater ease of interoperability because of its simpler schema. The XmlSerializer, on the other hand, yields a ton of control over the schema, is the default serializer used by ASMX web services and is the one we want Windows Communication Foundation to use for compatibility with ASMX.

So let's take a look at .NET Framework 2.0s XmlSerializer behavior when given a complex type as might be generated by XSD.EXE or by an explicitly intentioned message designer. Here's a class, MyComplexType, decorated with the XmlRootAttribute and intended to place serialized instances of this class in the http://schemas.casadehambone.com/samples/2007/07 namespace:

[XmlRoot(Namespace="http://schemas.casadehambone.com/samples/2007/07")]
public class MyComplexType
{
    private string m_firstname;

    [XmlElement(ElementName="FirstName", Order=1)]
    public string FirstName
    {
        get { return m_firstname; }
        set { m_firstname = value; }
    }
}

Remember, the class MyComplexType is marked as being in the XML namespace http://schemas.casadehambone.com/samples/2007/07. That's going to be important in a bit.

Now, when MyComplexType is passed through the XmlSerializer, the following XML is generated:

<?xml version="1.0" encoding="utf-8"?>
<MyComplexType xmlns="http://schemas.casadehambone.com/samples/2007/07">
  <FirstName>Kevin</FirstName>
</MyComplexType>

Note the XML namespace declaration xmlns="http://schemas.casadehambone.com/samples/2007/07". This XML namespace declaration declares the default namespace for all unqualified elements within this type including the root element itself. Therefore, MyComplexType in the namespace http://schemas.casadehambone.com/samples/2007/07, as is the FirstName element. What we see is the intended and correct behavior.

Let's now shift our focus to the definition of the Web service. Here's an ASMX Web service that uses MyComplexType:

[WebService(Namespace="http://www.casadehambone.com/samples/2007/07")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class SomeService : System.Web.Services.WebService
{
    [WebMethod(MessageName = "Hello")]
    public string Hello(MyComplexType myType)
    {
        return string.Format("Hello, {0}", myType.FirstName);
    }
}

Take a close look at the WebServiceAttribute on the class declaration. It places the service in the XML namespace http://www.casadehambone.com/samples/2007/07, and is distinctly different than MyComplexType which resides in the XML namespace http://schemas.casadehambone.com/samples/2007/07. This, too, will become important in a bit.

If we examine the SOAP message sent by a Visual Studio 2005 generated proxy (i.e., Add Web Reference and referred to herein as an ASMX proxy), we can see exactly the impact our namespace declarations in code have on the serialized XML:

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <soap:Body>
    <Hello xmlns="http://www.casadehambone.com/samples/2007/07">
      <MyComplexType xmlns="http://schemas.casadehambone.com/samples/2007/07">
        <FirstName>Kevin</FirstName>
    </Hello>
  </soap:Body>
</soap:Envelope>

Take a moment to digest what is going on here. The Hello element uses a default namespace of http://www.casadehambone.com/samples/2007/07 which corresponds to the namespace specified in the WebServiceAttribute. Furthermore, this default namespace is to be inherited by all subsequent unqualified elements, including the root element itself until another default XML namespace is declared.

Next up is the serialization of MyComplexType. As previously discussed, MyComplexType uses a default namespace of http://schemas.casadehambone.com/samples/2007/07. And again, because the MyComplexType itself is unqualified, it too is in the namespace http://schemas.casadehambone.com/samples/2007/07.

Up to this point, all is right with the world and everything works the way we expect. Now let's throw Windows Communication Foundation into the mix by migrating this service via a couple of well-placed System.ServiceModel attributes. Seems simple enough, right?

Here's the same ASMX Web service updated to include the requisite System.ServiceModel attributes to expose it as a Windows Communication Foundation service and serialize in a way that is supposed to be compatible with ASMX:

[WebService(Namespace="http://www.casadehambone.com/samples/2007/07")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[ServiceContract(Namespace="http://www.casadehambone.com/samples/2007/07")]
[XmlSerializerFormat]
public class SomeService : System.Web.Services.WebService
{
    [WebMethod(MessageName = "Hello")]
    [OperationContract(Action="http://www.casadehambone.com/samples/2007/07/Hello")]
    public string Hello(MyComplexType myType)
    {
        return string.Format("Hello, {0}", myType.FirstName);
    }
}

The ServiceContractAttribute, similar to the WebServiceAttribute, places the service in the same namespace as its ASMX counterpart, http://www.casadehambone.com/samples/2007/07.

The XmlSerializerFormatAttribute "instructs the Windows Communication Foundation (WCF) infrastructure to use the XmlSerializer instead of the XmlObjectSerializer." In other words, we're telling WCF that we want to serialize our classes in the same fashion as ASMX in order to maintain compatibility with our existing clients.

Last, but not least, is the OperationContractAttribute, similar to the WebMethodAttribute. By default, Windows Communication Foundation uses different action names than ASMX. Therefore to maintain compatibility with existing clients, we include the Action property and set it to the same value used by ASMX.

We're all set! We've exposed the same piece of ASMX code as a WCF service and have forced usage of the XmlSerializer to maintain serialization compatibility with existing clients.

And here is where the insidious problem rears its ugly head.

When the existing ASMX client calls the newly migrated WCF service, an exception is thrown and ... myType is null! What!? Huh? Excuse me? After repeatedly trying the client time and time again ... as if by magic I'm going to get a different result ... myType is null. Every time.

Knowing that this should work, I create a WCF client using SVCUTIL.EXE and call the exact same endpoint as that being used by the existing ASMX client and ... it works.

So for some reason WCF-to-WCF works fine for this service, but existing clients (i.e., ASMX-to-WCF) fail. Firing up tcpTrace and taking a look the XML sent by the WCF client reveals the problem. An insidious, subtle problem:

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
  <
s:Body>
    <
Hello xmlns="http://www.casadehambone.com/samples/2007/07" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
      <
MyComplexType xmlns:a="http://schemas.casadehambone.com/samples/2007/07">
        <
a:FirstName>Kevin</a:FirstName>
      </
MyComplexType>
    </
Hello>
  </
s:Body>
</
s:Envelope>

Remember that we declared the namespace of the service to be http://www.casadehambone.com/2007/07 and the namespace of MyComplexType to be http://schemas.casadehambone.com/2007/07? It's vitally important to what's going on here, but that's not what the serialized XML shows.

The Hello element, declares a default namespace of http://www.casadehambone.com/samples/2007/07. The Hello element itself is unqualified and therefore is also in this namespace. So far, so good ... but take a very close look at the serialized version of MyComplexType in the WCF serialized version of the XML.

MyComplexType does not declare a default namespace. Instead, WCF has serialized MyComplexType and assigns the namespace http://schemas.casadehambone.com/samples/2007/07 to the prefix a. Normally this is not an issue ... all we need is semantic equivalence of the XML infoset and there is nothing wrong with using fully qualified QNames to achieve that goal. But look closely (very closely) at the serialized XML.

WCF proceeds to use the a: prefix for the serialized elements within MyComplexType but neglects to associate the prefix a: to MyComplexType! The result is that MyComplexType, being unqualified and not declaring its own default namespace, inherits the namespace from its parent! MyComplexType is now in the namespace http://www.casadehambone.com/samples/2007/07, the namespace of the service not the namespace we specified for the type!

If we had originally placed MyComplexType in the same namespace as the service (i.e., if both were in the namespace http://www.casadehambone.com/samples/2007/07) we would never see this problem. Both ASMX and WCF would generate semantically equivalent XML.

If we used simple parameters to our WebMethod/OperationContract (i.e., public string Hello(string FirstName)) we would never see this problem.

This problem only surfaces with WCF when the complex type is placed in a namespace different than that of the service.

So what are we to do?

Well, our guidance on MSDN for Migrating ASP.NET Web Services to WCF works, but is terribly more complex than adding three additional attributes to your existing ASMX code. In fact, our guidance has you reverse engineer the service contract and data contract using SVCUTIL.EXE (as if you were building a client) and then re-implement a brand new service using the interfaces and classes created by SVCUTIL.EXE. It works because the auto generated service contract, data contract and message contract do result in semantically equivalent XML when the message is serialized, but WCF must use a message contract to get the work done. And a message contract is tantamount to saying, "You know what ... you don't know how to serialize things properly, let me tell you exactly how I want it done." Not surprisingly, this is a very true statement about this condition in WCF, and considerably raises the complexity bar. I shouldn't have to rely upon a MessageContract to get this done.

A second option was offered up by my esteemed colleague, Dino Chiesa. Dino created a custom ServiceHost that explicitly looks for complex types with an XmlTypeAttribute or XmlRootAttribute and a namespace that disagrees with the service's namespace. When a mismatch is found, Dino's custom ServiceHost changes the MessageParts to properly align the namespace with that of the complex type. Furthermore, because this is done before any instances of the service are ever created, you also get a proper schema in the WSDL!

In Dino's words, his solution is only 35 lines of boilerplate code once you remove the comments, white space and logging - and is completely reusable, whereas our MSDN guidance requires you to re-implement each and every service you have to get compatibility. You can use his custom ServiceHost with IIS-hosted services and migrate your ASMX services to WCF while maintaining compatibility with existing clients.

What surprises me is that I've not heard anyone bring this up before. Is no one migrating at all? Is no one migrating services with complex types with their own namespace declarations?

What do you have to say? Have you run into this issue in migrating any of your complex ASMX services to WCF? How did you deal with the problem?

Technorati Tags: , ,
Thursday, July 26, 2007 8:08:07 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Related posts:
Tracked by:
"Complex Types Serialized to the Wrong Namespace by WCF" (Kevin W. Hammond) [Trackback]

Comments are closed.