Kirk Evans Blog

.NET From a Markup Perspective

High-Performance Web Services: Avoid XmlNode, Use IXmlSerializable

High-Performance Web Services: Avoid XmlNode, Use IXmlSerializable

  • Comments 6

While talking with a friend about web services design and "commonplace" guidance, the discussion turned ugly.  I was somewhat amazed to hear his perception that Microsoft commonly recommends returning XmlNode from all web services. 

I let him argue it out, looking for where in the world he derived this "commonplace" notion of untyped web services being the norm.  He cited Matt Powell's post about not returning strings from web services, favoring XmlNode instances, and Kzu's solution for avoiding the DOM in web services.  There was also a mention of Dare's guidance for representing XML in the .NET Framework, and Don Box's MSDN TV episode where he warned against passing XmlNode in methods, favoring IXPathNavigable.

There were several contexts here that, when mashed together, are confusing.  Matt's post says not to return strings containing encoded XML, Kzu shows how to use the XmlReader and XmlWriter types for your implementation in .NET 1.1, and Dare's paper talks about passing XML objects around within and across AppDomains... not services.  None of this ever says that web service calls should return XmlNode.

Let me be perfectly clear:

THERE IS NO GUIDANCE FROM MICROSOFT RECOMMENDING THAT YOU RETURN XmlNode FROM ALL WEB SERVICE METHODS.

If you go back to Matt Powell's original post, he stated what a monumentally bad idea it was to return a string containing encoded XML from a web service.  Here is his example, verbatim:

// Bad Bad Bad
[WebMethod]
public string MyLameWebMethod()
{
    XmlDocument dom = new XmlDocument();
    // load some XML ...
    return dom.OuterXml;
}

Matt then went on to recommend that IF you are a freak and don't like typed web services, and you enjoy the uncertainty of calling web services with no idea of what to pass in or out, then at least avoid encoded XML strings in favor of XmlNode:

// Better
[WebMethod]
public XmlDocument MyBetterWebMethod()
{
    XmlDocument dom = new XmlDocument();
    // load some XML ...
    return dom;
}

The difference is what is passed on the wire and what the experience will be for the consumer on the other end.  Let's put a concrete example together.

using System;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Xml;
using System.Xml.Serialization;

[WebServiceBinding(
    Name="ServiceBinding",
    Namespace="http://contoso.com",
    ConformsTo = WsiProfiles.BasicProfile1_1,
    EmitConformanceClaims=true)]
public interface IService
{
    [WebMethod]    
    string MyLameWebMethod();

    [WebMethod]    
    XmlNode MyBetterWebMethod();

    [WebMethod]
    Customer MyTypedWebMethod();
}

[WebService(Namespace="http://contoso.com")]
public class Service : IService
{        
    public string MyLameWebMethod()
    {
        return "<customer><name>Kirk Allen Evans</name><order>1234</order></customer>";
    }
    
    public XmlNode MyBetterWebMethod()
    {
        XmlDocument doc = new XmlDocument();
        doc.LoadXml("<customer><name>Kirk Allen Evans</name><order>1234</order></customer>");
        return doc.DocumentElement;
    }    

    public Customer MyTypedWebMethod()
    {
        Customer c = new Customer();
        c.Name = "Kirk Allen Evans";
        c.Order=1234;
        return c;
    }
}

[XmlType(TypeName="customer")]
public class Customer
{
    private string _name;
    private int _order;

    [XmlElement("name")]
	public string Name
	{
		get { return _name;}
		set { _name = value;}
	}

    [XmlElement("order")]
	public int Order
	{
		get { return _order;}
		set { _order = value;}
	}	
}

The MyLameWebMethod returns an encoded XML string.  You can inspect the actual payload on the wire using a tool like tcpTrace.

<?xml version="1.0" encoding="utf-8"?>
<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>
  <MyLameWebMethodResponse xmlns="
http://contoso.com">
   <MyLameWebMethodResult>&lt;customer&gt;&lt;name&gt;Kirk Allen Evans&lt;/name&gt;&lt;order&gt;1234&lt;/order&gt;&lt;/customer&gt;</MyLameWebMethodResult>
  </MyLameWebMethodResponse>
 </soap:Body>
</soap:Envelope>

That means that a consumer of this web service must re-parse the XML to determine the values that the XML contains.  You have no guarantee of structure, no guarantee what is contained in the message or where it is placed.

The alternative that Matt posed will return very different XML.

<?xml version="1.0" encoding="utf-8"?>
<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>
  <MyBetterWebMethodResponse xmlns="
http://contoso.com">
   <MyBetterWebMethodResult>
    <customer xmlns="">
     <name>Kirk Allen Evans</name>
     <order>1234</order>
    </customer>
   </MyBetterWebMethodResult>
  </MyBetterWebMethodResponse>
 </soap:Body>
</soap:Envelope>

The consumer can now use the typed XML that is returned from the web service without having to manually parse the result.  However, they are still at a disadvantage because they won't have an automated means to determine what the structure of the data being returned is.  Consider the "order" element:  it contains numeric data: is this a string or an integer?  Do you have any guarantees that it will always be an integer, or can this data ever contain non-numeric data?  You can see the problem highlighted in this snippet of the WSDL for the service:

      <s:element name="MyBetterWebMethodResponse">
        <s:complexType>
          <s:sequence>
            <s:element minOccurs="0" maxOccurs="1" name="MyBetterWebMethodResult">
              <s:complexType mixed="true">
                <s:sequence>
                  <s:any />
                </s:sequence>
              </s:complexType>
            </s:element>
          </s:sequence>
        </s:complexType>
      </s:element>

The translation for this basically says, "any XML structure can be returned as a result for the MyBetterWebMethod operation." 

Think about how you program APIs for class libraries.  Would you design all of your methods to accept System.Object and return System.Object?  Of course not, you would accept and return typed data.   If you did decide on this design pattern, then consumers of your methods would despise you, having to constantly cast to different objects, frustrated at their inability to determine what types you are actually returning.

One way around this problem is to return typed data.  In the implementation above, I included a MyTypedWebMethod that returns a Customer object.  On the wire, it looks pretty much like the MyBetterWebMethod implementation, but the difference is that it can now be represented in the WSDL:

      <s:element name="MyTypedWebMethodResponse">
        <s:complexType>
          <s:sequence>
            <s:element minOccurs="0" maxOccurs="1" name="MyTypedWebMethodResult" type="tns:customer" />
          </s:sequence>
        </s:complexType>
      </s:element>
      <s:complexType name="customer">
        <s:sequence>
          <s:element minOccurs="0" maxOccurs="1" name="name" type="s:string" />
          <s:element minOccurs="1" maxOccurs="1" name="order" type="s:int" />
        </s:sequence>
      </s:complexType>

Now, the consumer knows without any ambiguity that they can expect an element "customer" that contains an element "name" containing a string value, and another element "order" that contains an integer value.

The question that my friend raised when I brought this to his attention was that he wanted to take advantage of pulling XML from the database and returning that to the consumer, similar to what Kzu's returning well-formed XML from WebServices without XmlDocument post provides.

One of the great additions to the .NET Framework 2.0 is the ability to provide a schema for your service despite the actual service implementation.  Let's change our Customer object around a bit, using the IXmlSerializable interface to explicitly write to the response stream using an XmlWriter, just like Daniel's implementation does.

using System;
using System.Xml.Serialization;
using System.Xml;
using System.Xml.Schema;
using System.Collections;

[XmlSchemaProvider("GetServiceSchema")]
[XmlRoot("customer", Namespace = "
http://contoso.com")]
public class Customer : IXmlSerializable
{
    private static readonly string ns = "
http://contoso.com";

    public System.Xml.Schema.XmlSchema GetSchema()
    {
        return null;
    }

    public void ReadXml(System.Xml.XmlReader reader)
    {
        //no-op.  
    }

    public void WriteXml(System.Xml.XmlWriter writer)
    {
        writer.WriteElementString("name", Customer.ns, "Kirk Allen Evans");
        writer.WriteElementString("order", Customer.ns, "4321");           
    }

    // This is the method named by the XmlSchemaProviderAttribute applied to the type.
    public static XmlQualifiedName GetServiceSchema(XmlSchemaSet xs)
    {
        // This method is called by the framework to get the schema for this type.
        // We return an existing schema from disk.

        XmlSerializer schemaSerializer = new XmlSerializer(typeof(XmlSchema));
        string xsdPath = null;
        // NOTE: replace the string with your own path.
        xsdPath = System.Web.HttpContext.Current.Server.MapPath("App_Data/customer.xsd");
        XmlSchema s = (XmlSchema)schemaSerializer.Deserialize(
            new XmlTextReader(xsdPath), null);
        xs.XmlResolver = new XmlUrlResolver();
        xs.Add(s);

        return new XmlQualifiedName("customer", Customer.ns);
    }
}

What is going on here is pretty simple.  When the Customer object is to be serialized as a return type for our web service, the XmlSerializer sees that our object implements IXmlSerializable and calls its WriteXml method.  This gives us the fast performance that Daniel mentioned, avoiding the XmlDocument type.  it is typically not the case that you don't know what you are returning from a service method: the XmlNode is typically more of a crutch to get around other limitations of the XmlSerializer such as returning DataSets from web services using XmlDataDocument, it is definitely not a recommended best practice.

With the IXmlSerializable implementation, we are now able to provide a schema that will appear in the WSDL, allowing our consumers to know what the structure of the returned XML will be.  We achieved this by using the XmlSchemaProvider attribute on our IXmlSerializable class, which points to the method name in our class that provides the XmlSchema.  Our GetServiceSchema method looks in the App_Data folder in our ASMX project and gets the schema from disk.  The schema should look very familiar from earlier in our post:

<?xml version="1.0" encoding="utf-8" ?>
<s:schema id="customer"
    targetNamespace="
http://contoso.com"
    elementFormDefault="qualified"
    xmlns="
http://contoso.com"
    xmlns:mstns="
http://contoso.com"
    xmlns:s="
http://www.w3.org/2001/XMLSchema">

 <s:complexType name="customer">
   <s:sequence>
   <s:element minOccurs="0" maxOccurs="1" name="name" type="s:string" />
   <s:element minOccurs="1" maxOccurs="1" name="order" type="s:int" />
  </s:sequence>
 </s:complexType> 
</s:schema>

If you look at the WSDL for the web service for an operation that returns our Customer type, you will see the same schema that we used in our earlier typed version of our web method.

Using this method, I am able to return data from an XmlReader, avoiding the DOM, and still able to provide strong data typing to my consumers through WSDL.

I have never seen any guidance that says that you should favor untyped results from a web service operation: to the contrary, I have run across more documentation that favors typed web service operations. 

Page 1 of 1 (6 items)
Leave a Comment
  • Please add 3 and 3 and type the answer here:
  • Post
Translate This Page
Search
Archive
Archives