The Power of XmlElement Parameters in ASP.NET Web Methods

 

Matt Powell
Microsoft Corporation

April 18, 2003

Summary: Matt Powell explores different ways to use XmlElement parameters with your Web service to access raw XML data and gain high-level control. (11 printed pages)

In Tim Ewald's House of Web Services column, Accessing Raw SOAP Messages in ASP.NET Web Services in the March issue of MSDN Magazine, Tim describes an interesting way to manipulate the SOAP message stream directly using a SOAP Extension. This can be a beneficial way to access raw SOAP messages in a stream-based fashion, and it gives you full control of the message-parsing process. But Tim also alludes to an easier way of getting some of the same benefits—with the caveat that the data you access will already have been parsed once by the internal ASMX handler. The easy way is to use an XmlElement parameter for your Web method. In this month's At Your Service column, I explore different ways in which using an XmlElement parameter to your Web service can be a beneficial mechanism for accessing raw XML data, and how you can gain a high level of control over your Web service through this mechanism.

Web Method Serialization

Before we start digging into details of how to manipulate Web methods using XmlElement parameters, let's first take a look at how Web methods use XmlSerializer in the .NET Framework to invoke methods and build the response. Figure 1 symbolically shows what is going on when the XML body of a SOAP message is received.

Figure 1. The role of XmlSerializer in a standard ASP.NET Web method invocation

The XML is passed to the XmlSerializer to deserialize it into the instances of classes in managed code that map to the input parameters for the Web method. Likewise, the output parameters and return values of the Web method are serialized into XML in order to create the body of the SOAP response message.

If we create a Web method that adds two integers together and returns the results, then the managed code might look something like this:

[WebMethod]
public int Add (int x, int y)
{
    return x + y;
}

The SOAP message that would be sent to this Web method would look something like this:

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope 
    xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <Add 
        xmlns="https://msdn.microsoft.com/AYS/XmlElementService">
      <x>3</x> 
      <y>2</y> 
    </Add>
  </soap:Body>
</soap:Envelope>

So the XmlSerializer is used to convert the XML in red into the parameters for the Add method above. Since integers are types, this is pretty straightforward, but now we are going to look at a more complex example.

Suppose we have Web method that takes something other than a data type. Consider the following C# code:

public class MyClass
{
    public string child1;
    public string child2;
}

[WebMethod]
public void SubmitClass(MyClass input)
{
    // Do something with complex input parameters
    return;
}

This method only takes a single parameter, but that parameter is not one that maps to a simple XML type. In fact it maps to a complex type that is represented by the input element in the SOAP envelope below:

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope 
    xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <soap:Body>
    <SubmitClass
      xmlns="https://msdn.microsoft.com/AYS/XEService">
      <input> 
        <child1>foo</child1> 
        <child2>bar</child2> 
      </input> 
    </SubmitClass>
  </soap:Body>
</soap:Envelope>

The XmlSerializer takes the input element and deserializes it into an instance of the MyClass type that was defined in the code for this Web method before calling the method.

One more part of the Web service support in the .NET Framework that you should be aware of is the ease of development of Web service consumer code. The reason that this is possible is that a WSDL is automatically created for your Web method that thoroughly describes the details of your Web service interface. If you look at the WSDL generated by ASP.NET for this method, you'll notice that the input element has been defined in the types section as follows:

<s:complexType name="MyClass">
  <s:sequence>
    <s:element minOccurs="0" maxOccurs="1" 
               name="child1" type="s:string" />
    <s:element minOccurs="0" maxOccurs="1" 
               name="child2" type="s:string" />
  </s:sequence>
</s:complexType>

The Add Web Reference support in Visual Studio® .NET will define a class on the client that matches the previous definition of the MyClass class so that your calling code can look a lot like it is calling the Web method as if it were a function call. The method call is listed below.

localhost.XEService proxy = new localhost.XEService();
localhost.MyClass myInstance = new localhost.MyClass();
myInstance.child1 = "foo";
myInstance.child2 = "bar";
proxy.SubmitClass(myInstance);

The XmlSerializer is also used on the client to serialize the instance of the class into the XML that matches the defined schema from the WSDL.

Pulling XmlSerializer Out of Web Methods

The XmlSerializer is pretty useful and powerful when it comes to writing and consuming Web services. It makes the relationship between the XML on the wire and the classes in your managed code transparent. This is often a very useful thing and is a big part of why ASP.NET Web methods are one of the most efficient ways to write Web services.

But what if I don't like the way XmlSerializer works? What if I want more control over the serialization and deserialization process? Maybe I don't like the fact that XmlSerializer doesn't validate the XML being received against the schema for the message. Maybe I want to selectively decide which portions of a message I want to deserialize. Maybe I don't even know what the schema for an incoming message looks like. Is there an easy way that I can avoid the default usage of the XmlSerializer and control the message deserialization process myself? There are a couple of ways of approaching this problem: we will focus on using XmlElement parameters.

The Power of XmlElement

In the previous two examples we saw how parameters and a complex class get serialized into the parameters of a Web method. But what happens if the parameters themselves are XML?

Consider the following Web method:

[WebMethod]
public void SubmitXmlElement(XmlElement input)
{
    // Do something with the XML input
    return;
}

In this example the Web method takes a System.Xml.XmlElement object as input. The following SOAP envelope can be used to send a message to this Web service.

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope 
    xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <soap:Body>
    <SubmitXmlElement
        xmlns="https://msdn.microsoft.com/AYS/XEService">
      <input>
        <MyClass >
          <child1>foo</child1>
          <child2>bar</child2>
        </MyClass>
      </input>
    </SubmitXmlElement>
  </soap:Body>
</soap:Envelope>

Notice that I am sending something pretty close to what we sent to the previous Web method. The key difference however is that due to the definition of the Web method, XmlSerializer will deserialize the parameter into an XmlElement object instead of the MyClass object. The reality is that the XmlSerializer is deserializing the entire SOAP envelope. When it determines that the parameter for the Web method is of type XmlElement, the deserialization process becomes trivial and the corresponding portion of the Infoset is provided as an XmlElement.

The downside to this approach is that the automatic WSDL defined for this method now does not include information about the structure of the input parameter. After all, the Web method doesn't define a structure to the input parameter. This can be a good thing and a bad thing.

When you use Add Web Reference on this type of method in Visual Studio .NET, the input parameter on the proxy class is defined as an XmlNode (which is what XmlElement derives from). Therefore, we could send any XML to this method that we care to. This makes sense because if you look at the WSDL you will see the input element is defined as a complex type whose only element is of type xsd:any. There are certainly business situations where raw, schema-less XML is a really good thing to send to a Web service, but many times it is not. Many times our Web service will be expecting data to follow one or more known formats.

So if I know the format for my parameters, why would I ever want to use XmlElement? Because now you can control the deserialization process yourself, including the possibility of not even performing the deserialization. Performance improvements may be possible and you have an opportunity of doing things such as performing XML transforms, validating the XML, or performing content-based deserialization.

The other part of this problem is how you define your WSDL so that it advertises that it can handle multiple known parameter types. We will not go into this issue here, but check out Versioning Options for hints on writing Web methods that expose multiple parameter types and what effect that has on the WSDL generated.

XmlElement and Message Validation

XmlSerializer does not validate the XML it deserializes. It turns out that this may not normally be a big problem. For instance, if someone sends the following XML:

<MyClass>
  <param2>foo</param2>
  <param1>bar</param1>
</MyClass>

Based on the schema defined for our class previously, it would not pass validation. The problem in this case is that the type was defined as a sequence of elements that means that order is supposed to count. Of course we didn't write anything in our code that said we really care about the order of the parameters, so it might be okay if the XML being sent to us is not checked closely for validation. XML validation can be a relatively expensive process, which is probably why it is not performed on incoming messages by default.

However, there may be cases where a strictly validated message is critical. For instance, if you are performing XPath queries against the incoming XML, making assumptions on the XML structure can provide vastly unexpected results.

So we can use XmlElement to allow us to validate the incoming XML before we deserialize it into a class instance. Consider the following Web method:

[WebMethod]
public void SubmitValidatedTypedXml([XmlAnyElement]XmlElement input)
{
    XmlSchemaCollection schemaCollection = new XmlSchemaCollection();
    schemaCollection.Add("https://localhost/XEService/MyNewClass.xsd", 
        "https://localhost/XEService/MyNewClass.xsd");
    // XmlValidatingReader only accepts XmlTextReader as input
    // which is why we pass in input.OuterXml instead of an instance
    // of XmlNodeReader.
    XmlValidatingReader validator
        = new XmlValidatingReader(input.OuterXml, 
                                  XmlNodeType.Element, 
                                  null);
    validator.Schemas.Add(schemaCollection);
    XmlSerializer serializer 
        = new XmlSerializer(typeof(MyNewClassType));
    MyNewClassType newInstance 
        = (MyNewClassType)serializer.Deserialize(validator);
    // Do something with MyNewClassType object
}

This Web method does a couple things. Its goal is to take the XmlElement passed to the Web method and manually deserialize it as an instance of a class called MyNewClassType. To achieve this part of the process, create an instance of the XmlSerializer class, indicate the type involved as a parameter of the constructor, and then call the Deserialize method.

But there is something else that we do in this case. The Deserialize method takes an XmlReader object as input. We could have created an instance of an XmlNodeReader class (that inherits from XmlReader) and passed it to the Deserialize method. However, in this case we are passing an instance of the XmlValidatingReader class to the Deserialize method. The way that you validate XML with the .NET Framework is by using the XmlValidatingReader. We created an instance of the XmlValidatingReader by passing it the XML fragment from our input parameter. In order to validate XML against a schema, the validator needs to load the schema so it knows what to validate against. Adding the known schema to a schema collection accomplishes this.

The final validation of the XML occurs when the Deserialize method is called on the XmlSerializer object. This is because XmlValidatingReader also derives from XmlReader and the validation of the contained XML occurs as the various nodes are read through the XmlReader interface. These nodes are read when the Deserialize method walks through all the nodes to create the instance of the MyNewClassType class.

The MyNewClassType class is very similar to the MyClass class we created earlier, except I created it by defining an XML schema using the Visual Studio support for creating XSD files and then used the XSD.exe utility to create a managed class with the following command line:

Xsd /c MyNewClass.xsd

This way I had the XSD schema to pass to the XmlValidatingReader along with the class code to deserialize the XML into.

There is one more important thing that is included in this Web method that was not included in the previous version. In this method we decorate the input parameter with the XmlAnyElement parameter. This is a hint that this parameter will be deserialized from an xsd:any element. Since the attribute is not passed any parameters, it means that the entire XML element for this parameter in the SOAP message will be in the Infoset that is represented by this XmlElement parameter. This is a subtle difference from our previous Web method. Let's take a look at what the difference is.

The XmlAnyElement Attribute

To understand how the XmlAnyElement attribute works, let's take a look at two Web methods:

// Simple Web method using XmlElement parameter
[WebMethod]
public void SubmitXml(XmlElement input)
{return;}

// Simple Web method...using the XmlAnyElement attribute
[WebMethod]
public void SubmitXmlAny([XmlAnyElement] XmlElement input)
{return;}

The difference between the two is that for the first method, SubmitXml, the XmlSerializer will expect an element named input to be an immediate child of the SubmitXml element in the SOAP body. The second method, SubmitXmlAny, will not care what the name of the child of the SubmitXmlAny element is. It will plug whatever XML is included into the input parameter. The message style from ASP.NET Help for the two methods is shown below. First we look at the message for the method without the XmlAnyElement attribute.

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
    xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <SubmitXml xmlns="https://msdn.microsoft.com/AYS/XEService">
      <input>xml</input>
    </SubmitXml>
  </soap:Body>
</soap:Envelope>

Now we look at the message for the method that uses the XmlAnyElement attribute.

<!-- SOAP message for method using XmlAnyElement -- >
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
    xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <SubmitXmlAny xmlns="https://msdn.microsoft.com/AYS/XEService">
      Xml 
    </SubmitXmlAny>
  </soap:Body>
</soap:Envelope>

The method decorated with the XmlAnyElement attribute has one fewer wrapping elements. Only an element with the name of the method wraps what is passed to the input parameter.

If you want to control the deserialization process of the XmlSerializer for your Web method, XmlAnyElement is a nice trick to use to handle more of the body with your custom logic. But can we get even more of the body passed to an XmlElement instance? As a matter of fact we can, by using the ParameterStyle property of the SoapDocumentMethod attribute.

[WebMethod]
[SoapDocumentMethod(ParameterStyle = SoapParameterStyle.Bare)]
public void SubmitBody([XmlAnyElement]XmlElement input)
{
    return;
}

By setting the ParameterStyle property to SoapParameterStyle.Bare and using the XmlAnyElement, the message style now becomes the following:

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
    xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>xml</soap:Body>
</soap:Envelope>

Now the entire contents of the SOAP body will be passed to us in our input parameter.

The above mechanism can be pretty useful for a number of things, but we should be aware that the SOAP body is not an XML document on its own. In particular, it does not have to have a single root element. As it turns out, the WS-I Basic Profile does indicate that there should be a single child element of the Body; however, this is not guaranteed. If we really want access to the raw XML being passed to our Web method, we need to account for the situation where the body may have multiple child elements as shown below.

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
    xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <name>Fred</name>
    <name>Wilma</name>
    <name>Betty</name>
    <name>Barney</name>
  </soap:Body>
</soap:Envelope>

As it turns out, the SubmitBody method can accept this kind of a message, there just is no way to access all the data. When the message is deserialized, you will only be able to access the last element in the list through the input parameter.

We can handle such a situation by changing our single XmlElement input to an array of XmlElements. The following method will be able to read the entire body of any SOAP message sent to it.

[WebMethod]
[SoapDocumentMethodAttribute(ParameterStyle = SoapParameterStyle.Bare)]
public void SubmitAnything([XmlAnyElement]XmlElement [] inputs)
{
    return;
}

More Control... Harder Code

We have figured out how to access the entire contents of the SOAP body directly in XML from an ASP.NET Web method. This gives you the opportunity of doing many things, like validating the XML against a schema before deserializing it, avoiding deserialization in the first place, analyzing the XML to determine how you want to deserialize it, or using the many powerful XML APIs that are available to deal with the XML directly. This also gives you the control to handle errors in your own way instead of using the faults that the XmlSerializer might generate under the covers.

For those of you who want lower-level access to the XML being sent to your Web methods, using XmlElement parameters along with some of the Web method attribution tricks described in this article may give you more control in your Web service, but it does come at a cost of requiring more code and becoming familiar with a number of the basic XML technologies available in the .NET Framework. But the beauty of the .NET Framework is that it allows for the flexibility of having a lot of low-level control, at the same time that it provides the simple and efficient development environment for creating and consuming Web services.

 

At Your Service

Matt Powell is the lead Content Strategist for the MSDN XML Web Services Developer Center. Besides writing numerous articles for MSDN, various magazines, and co-authoring Running Microsoft Internet Information Server from Microsoft Press, Matt also helped develop the groundbreaking SOAP Toolkit 1.0. When he is not delving into the depths of Web services, Matt helps his wife chase their kids around the greater Seattle area.