XML Files

WebMethod Validation, SOAP Validation, XmlSerializer, One-way Operations, and More

Aaron Skonnard

Code download available at:XMLFiles0211.exe(120 KB)

Q Is it possible to turn on validation in ASP.NET WebMethods? WebMethods don't seem to validate the request message against the implied XML Schema.

Q Is it possible to turn on validation in ASP.NET WebMethods? WebMethods don't seem to validate the request message against the implied XML Schema.

A Unfortunately there isn't a simple switch to turn on validation in WebMethods. There is a way to do it, however, if you're willing to write a little code. The problem is that ASP.NET takes advantage of System.Xml.Serialization.XmlSerializer to deserialize incoming SOAP messages into common language runtime (CLR) objects. XmlSerializer does not perform validation unless you explicitly force it through XmlValidatingReader.

A Unfortunately there isn't a simple switch to turn on validation in WebMethods. There is a way to do it, however, if you're willing to write a little code. The problem is that ASP.NET takes advantage of System.Xml.Serialization.XmlSerializer to deserialize incoming SOAP messages into common language runtime (CLR) objects. XmlSerializer does not perform validation unless you explicitly force it through XmlValidatingReader.

Let's briefly walk through the problem. Consider the XML Schema definition that models a person (see Figure 1). If you run this schema through xsd.exe with the /c switch (xsd.exe /c person.xsd), it will generate the following C# class that can be used with XmlSerializer to serialize or deserialize messages conforming to the schema in question:

using System.Xml.Serialization; using System.Xml.Schema; [XmlTypeAttribute(Namespace="urn:example-org:person")] [XmlRootAttribute(Namespace="urn:example-org:person", IsNullable=false)] public class Person { [XmlElementAttribute(Form=XmlSchemaForm.Unqualified)] public string name; [XmlElementAttribute(Form=XmlSchemaForm.Unqualified)] public System.Double age; }

Figure 1 XML Schema for Person

<xs:schema targetNamespace="urn:example-org:person" elementFormDefault="unqualified" xmlns:xs="https://www.w3.org/2001/XMLSchema" xmlns:tns="urn:example-org:person" > <xs:element name="Person" type="tns:Person" /> <xs:complexType name="Person"> <xs:sequence> <xs:element name="name" type="xs:string" /> <xs:element name="age" type="xs:double" /> </xs:sequence> </xs:complexType> </xs:schema>

For example, the following class creates an instance of Person and uses XmlSerializer to serialize it to a file:

public class Serialize { static void Main() { Person p = new Person(); p.name = "Monica"; p.age = 30; XmlSerializer ser = new XmlSerializer(typeof(Person)); FileStream fs = new FileStream("monica.xml", FileMode.Create); ser.Serialize(fs, p); fs.Close(); } }

The generated file (monica.xml) contains the following XML:

<Person xmlns="urn:example- org:person"> <name >Monica</ name> <age >30</age> </Person>

You could then write the following class that deserializes monica.xml into an instance of Person and prints the members to the console:

public class deserialize { static void Main() { XmlSerializer ser = new XmlSerializer(typeof(Person)); FileStream fs = new FileStream("monica.xml", FileMode.Open); Person p = (Person)ser.Deserialize(fs); fs.Close(); Console.WriteLine("name: {0}, age: {1}", p.name, p.age); } }

If you run this program against the monica.xml document shown before, you'll get the following output:

name: Monica, age: 30

Up to this point everything is fine, exactly what you'd expect. However, if you run the following document through the deserialize program, you'll get the exact same output:

<Person xmlns="urn:example-org:person"> <age >30</age> <name >Monica</name> </Person>

Notice that the order of the name and age elements is reversed, contradicting the original schema definition. In addition, consider the following invalid XML document that's missing the required age element and contains some extra gunk (gunk.xml):

<Person xmlns="urn:example-org:person"> <name >Monica</name> <extraGunk> <moreGunk>gunk...</moreGunk> </extraGunk> </Person>

Running this one through the deserialize program produces the following output:

name: Monica, age: 0

Notice that it just defaults the value for the missing age and silently ignores the extraGunk element even though both problems violate the schema.

This shows XmlSerializer is not performing schema validation. However, since there is an overload to Deserialize that takes an XmlReader instead of a Stream, it's possible to construct an XmlValidatingReader initialized with the correct schema file and pass it into the call, as shown here:

XmlReader xr = new XmlTextReader(fileName); XmlValidatingReader vr = new XmlValidatingReader(xr); vr.Schemas.Add("urn:example-org:person", "person.xsd"); Person p = (Person)ser.Deserialize(vr);

This code still succeeds with monica.xml but generates an XmlSchemaException when gunk.xml is processed, allowing you to catch such document errors. The exception contains the following helpful message:

System.Xml.Schema.XmlSchemaException: Element 'urn:example-org:person:Person' has invalid child element 'urn:example-org:person:extraGunk'. Expected 'age'. An error occurred at gunk.xml(3, 4).

As you can see, it's easy enough to force validation with XmlSerializer, but you don't have access to the underlying XmlSerializer used by ASP.NET WebMethods. You can, however, intercept the WebMethod messages and do something similar by preprocessing the message streams with XmlValidatingReader. Such interceptors are known as SOAP extensions in the realm of the Microsoft® .NET Framework.

A SOAP extension is a class that derives from SoapExtension, implements a few required overrides, and is configured to execute on either an entire vroot or on a method-by-method basis. You must use a custom attribute, derived from SoapAttribute in this case, to configure the SOAP extension on a given method. Once configured, the extension is called before and after the serialization and deserialization stages, as illustrated in Figure 2.

Figure 2 SOAP Serialization/Deserialization

Figure 2** SOAP Serialization/Deserialization **

To implement a SoapExtension that provides schema validation on the incoming SOAP messages, you need to write a custom attribute that can be used on WebMethods to indicate that validation is needed, as well as which schema to use:

[WebMethod] [Validation("urn:example-org:person", "person.xsd")] public override double AddPerson(Person p) { // implementation goes here... }

During initialization, the infrastructure will hand the extension the attribute instance so it can grab the declared schema information. You just need to add the schema to the cached collection (along with the schemas for SOAP, as you'll see in the question that follows this one):

ValidationAttribute va = attribute as ValidationAttribute; XmlSchemaCollection schemaCache = new XmlSchemaCollection(); schemaCache.Add(va.Namespace, server.MapPath(va.SchemaLocation)); schemaCache.Add("https://schemas.xmlsoap.org/soap/envelope/", server.MapPath("soapenvelope.xml")); schemaCache.Add("https://schemas.xmlsoap.org/soap/encoding/", server.MapPath("soapencoding.xml"));

ProcessMessage is then called each time a WebMethod executes. You can perform schema validation on the incoming stream during the BeforeDeserialization stage (see Figure 3).

Figure 3 Schema Validation

public override void ProcessMessage( System.Web.Services.Protocols.SoapMessage message) { switch (message.Stage) { case SoapMessageStage.BeforeDeserialize: try { XmlValidatingReader vr = new XmlValidatingReader( new XmlTextReader(message.Stream)); vr.Schemas.Add(this.schemaCache); vr.ValidationEventHandler += this.validateEventHandler; while (vr.Read()) ; // do nothing } ••• }

Now this code can be used on any WebMethod to include schema validation in the processing pipeline simply by adding an attribute declaration. SoapExtensions are an extremely valuable hook in the ASP.NET Web Services infrastructure, but they do require you to embrace the fundamental XML Web Services platform and work outside the comfort zone of objects and methods. This topic probably merits a complete article down the road.

You can download the ValidationExtension sample as well as all the source code for this column from the link at the top of this article.

Q Can you tell me how I validate a SOAP message from within my custom Web Service? In other words, how does the schema processor deal with the soap:Envelope and soap:Body elements along with my schema?

Q Can you tell me how I validate a SOAP message from within my custom Web Service? In other words, how does the schema processor deal with the soap:Envelope and soap:Body elements along with my schema?

A You need the schemas for SOAP, which can be downloaded from https://schemas.xmlsoap.org/soap/envelope/ and https://schemas.xmlsoap.org/soap/encoding/. Conveniently, these URLs are the same as the namespace names of their respective schemas.

A You need the schemas for SOAP, which can be downloaded from https://schemas.xmlsoap.org/soap/envelope/ and https://schemas.xmlsoap.org/soap/encoding/. Conveniently, these URLs are the same as the namespace names of their respective schemas.

Let's take a look at the definition for soap:Envelope:

<xs:element name="Envelope" type="tns:Envelope" /> <xs:complexType name="Envelope" > <xs:sequence> <xs:element ref="tns:Header" minOccurs="0" /> <xs:element ref="tns:Body" minOccurs="1" /> <xs:any namespace="##other" minOccurs="0" maxOccurs="unbounded" processContents="lax" /> </xs:sequence> <xs:anyAttribute namespace="##other" processContents="lax" /> </xs:complexType>

The previous code essentially says that the soap:Envelope element can contain an optional soap:Header element followed by a mandatory soap:Body element. It also allows for attributes from namespaces other than the targetNamespace to be included on soap:Envelope.

Here's the definition for soap:Body:

<xs:element name="Body" type="tns:Body" /> <xs:complexType name="Body" > <xs:sequence> <xs:any namespace="##any" minOccurs="0" maxOccurs="unbounded" processContents="lax" /> </xs:sequence> <xs:anyAttribute namespace="##any" processContents="lax"/> </xs:complexType>

This definition says that the soap:Body element may contain zero or more elements from any namespace. In other words, this is the wildcard slot meant for you to place instances of your own schema types. The processContents="lax" means that if the schema processor can locate the schema definition for the wildcard element, then go ahead and validate it; otherwise don't worry about it.

So to perform schema validation on SOAP messages, you simply need to load the SOAP schemas into your schema collection along with the rest of your schemas and continue using XmlValidatingReader as before. XmlValidatingReader will start by validating against the SOAP envelope schema and when it gets to the element inside soap:Body, it will validate against the corresponding schema for that element (the person.xsd schema in this case):

XmlValidatingReader vr = new XmlValidatingReader(new XmlTextReader(context.Request.InputStream)); vr.Schemas.Add("urn:example-org:person", server.MapPath("person.xsd")); vr.Schemas.Add("https://schemas.xmlsoap.org/soap/envelope/", server.MapPath("soapenvelope.xml")); vr.Schemas.Add("https://schemas.xmlsoap.org/soap/encoding/", server.MapPath("soapencoding.xml")); while (vr.Read()) ; // process stream and validate

A complete working example of SOAP validation was shown in the previous question.

Q How do you use XmlSerializer to take advantage of XML Schema type substitution?

Q How do you use XmlSerializer to take advantage of XML Schema type substitution?

A Now let's extend the person.xsd schema from the first question and take a look at an example (see Figure 4). If you run this schema through xsd.exe /c, you get a file that contains Person, Employee, and Manager classes (where Employee derives from Person, and Manager derives from Employee).

A Now let's extend the person.xsd schema from the first question and take a look at an example (see Figure 4). If you run this schema through xsd.exe /c, you get a file that contains Person, Employee, and Manager classes (where Employee derives from Person, and Manager derives from Employee).

Figure 4 Person.xsd Extended with Type Hierarchy

<xs:schema targetNamespace="urn:example-org:person" elementFormDefault="unqualified" xmlns:xs="https://www.w3.org/2001/XMLSchema" xmlns:tns="urn:example-org:person" > <xs:complexType name="Person"> <xs:sequence> <xs:element name="name" type="xs:string" /> <xs:element name="age" type="xs:double" /> </xs:sequence> </xs:complexType> <xs:complexType name="Employee"> <xs:complexContent> <xs:extension base="tns:Person"> <xs:sequence> <xs:element name="id" type="xs:string"/> <xs:element name="salary" type="xs:double"/> </xs:sequence> </xs:extension> </xs:complexContent> </xs:complexType> <xs:complexType name="Manager"> <xs:complexContent> <xs:extension base="tns:Employee"> <xs:sequence> <xs:element name="dept" type="xs:string"/> </xs:sequence> </xs:extension> </xs:complexContent> </xs:complexType> <xs:element name="Person" type="tns:Person" /> </xs:schema>

You can then use these classes in conjunction with XmlSerializer to support type-based substitution at run time. To take advantage of this, you have to supply all of the derived types that may be substituted for the base class when constructing the XmlSerializer object. The code in Figure 5 illustrates this.

Figure 5 XmlSerializer Object

XmlSerializer ser = new XmlSerializer(typeof(Person), /* substitutable derived types specified here */ new Type[] {typeof(Employee), typeof(Manager) }); FileStream fs = new FileStream(args[0], FileMode.Open); object obj = ser.Deserialize(fs); fs.Close(); // determine what type of object we got if (obj is Person) { Person p = (Person)obj; Console.WriteLine("name: {0}\nage: {1}", p.name, p.age); } if (obj is Employee) { Employee e = (Employee)obj; Console.WriteLine("id: {0}\nsalary: {1}", e.id, e.salary); } if (obj is Manager) { Manager m = (Manager)obj; Console.WriteLine("dept: {0}", m.dept); }

Once you've deserialized the message, you can then use standard type navigation techniques to determine what type of object you actually received. For example, suppose you supply the following Person document:

<tns:Person xmlns:tns="urn:example-org:person"> <name>Monica</name> <age>30</age> </tns:Person>

You should see the following output, indicating that the Person element is of type Person in the schema:

name: Monica age: 30

However, suppose you supply the following Manager document that uses xsi:type to perform type substitution:

<tns:Person xmlns:tns="urn:example-org:person" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xsi:type="tns:Manager" > <name>Monica</name> <age>30</age> <id>123-45-6789</id> <salary>1250.00</salary> <dept>Marketing</dept> </tns:Person>

Then you should see the following output, indicating that the Person element in this case is of type Manager:

name: Monica age: 30 id: 123-45-6789 salary: 1250 dept: Marketing

The code also determines that a Manager is an Employee and that an Employee is a Person, as you would expect.

Q Does ASP.NET support one-way Web Service operations?

Q Does ASP.NET support one-way Web Service operations?

A Yes, you can implement one-way operations in ASP.NET by setting the OneWay property of the SoapDocumentMethod attribute to true, as shown here:

[WebMethod] [SoapDocumentMethod(OneWay=true)] public void DoSomeWork(string someInfo, string someMoreInfo) { // do some real work here... System.Threading.Thread.Sleep(5000); }

Of course, one-way operations cannot have return values by definition (a SOAP fault is generated if you execute a one-way operation that returns something). By definition, a one-way operation has an input message and no output message, as illustrated by the WSDL generated from this .asmx endpoint:

••• <portType name="OneWayServiceSoap"> <operation name="DoSomeWork"> <input message="s0:DoSomeWorkSoapIn" /> </operation> </portType> •••

A Yes, you can implement one-way operations in ASP.NET by setting the OneWay property of the SoapDocumentMethod attribute to true, as shown here:

[WebMethod] [SoapDocumentMethod(OneWay=true)] public void DoSomeWork(string someInfo, string someMoreInfo) { // do some real work here... System.Threading.Thread.Sleep(5000); }

Of course, one-way operations cannot have return values by definition (a SOAP fault is generated if you execute a one-way operation that returns something). By definition, a one-way operation has an input message and no output message, as illustrated by the WSDL generated from this .asmx endpoint:

••• <portType name="OneWayServiceSoap"> <operation name="DoSomeWork"> <input message="s0:DoSomeWorkSoapIn" /> </operation> </portType> •••

As soon as ASP.NET finishes deserializing the SOAP message and before it invokes the method, it sends an HTTP response of "202 Accepted" back to the client, indicating it has started processing the request. The client doesn't have to wait for the server to finish processing the request, but the cost is that it won't know the final outcome.

If you invoke this WebMethod using the .asmx-generated HTML test form (not using SOAP), you'll see it wait five seconds before displaying the result. But if you invoke it using the following SOAP message and using an HTTP POST utility (like post.js) as shown in the following code

C:>post https://localhost/oneway/service1.asmx -h SOAPAction "urn:example-org:oneway/DoSomeWork" -h Content-Type "text/xml" -f req.xml

it will return immediately, which truly illustrates the nature of a OneWay request:

<soap:Envelope xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/"> <soap:Body> <DoSomeWork xmlns="urn:example-org:oneway"> <someInfo>string</someInfo> <someMoreInfo>string</someMoreInfo> </DoSomeWork> </soap:Body> </soap:Envelope>

Q Is there any way to avoid requiring a SOAPAction header when building Web Services with ASP.NET?

Q Is there any way to avoid requiring a SOAPAction header when building Web Services with ASP.NET?

A By default, ASP.NET dispatches SOAP methods to the appropriate WebMethod based on the value of the supplied SOAPAction header. This means that, by default, if you don't supply the SOAPAction header, it's not going to know which method to invoke. The other possibility would be to use the name of the request element (element within soap:Body). You can change to this behavior for a given class by using the RoutingStyle property of the SoapDocumentService attribute (see Figure 6).

A By default, ASP.NET dispatches SOAP methods to the appropriate WebMethod based on the value of the supplied SOAPAction header. This means that, by default, if you don't supply the SOAPAction header, it's not going to know which method to invoke. The other possibility would be to use the name of the request element (element within soap:Body). You can change to this behavior for a given class by using the RoutingStyle property of the SoapDocumentService attribute (see Figure 6).

Figure 6 RoutingStyle Property

[WebService(Name="RoutingStyleService", Namespace="urn:example-org:routing")] [SoapDocumentService( RoutingStyle=SoapServiceRoutingStyle.RequestElement)] public class RoutingStyleExample : WebService { [WebMethod] [SoapDocumentMethod(Action="")] public string SayHello() { return "Hello, world"; } [WebMethod] [SoapDocumentMethod(Action="")] public string SayGoodbye() { return "Goodbye, world"; } }

You can also advertise the fact that your operations don't require a SOAPAction header by setting the Action property of the SoapDocumentMethod attribute to an empty string, as shown in Figure 6. This will show up in the Web Services Description Language (WSDL) document so clients realize that they don't have to supply a value:

••• <operation name="SayHello"> <soap:operation soapAction="" style="document" /> •••

I should point out, however, that even though the SOAPAction header does not require a value in this case, you must still send along the empty header:

SOAPAction: ''

Here's an example of how to invoke the SayHello operation without sending along a SOAPAction value:

C:>post https://localhost/oneway/routingstyle.asmx -h SOAPAction "''" -h Content-Type "text/xml" -f sayhello.xml

If you remove the SoapDocumentService attribute and reissue the POST, you'll notice it no longer works because it's expecting to find a SOAPAction value.

Q I have an HTTP module that performs custom authentication. If the authentication fails, I would like to throw a SoapException, but that doesn't seem to work. How can I achieve this result?

Q I have an HTTP module that performs custom authentication. If the authentication fails, I would like to throw a SoapException, but that doesn't seem to work. How can I achieve this result?

A The SoapException class was designed for use within ASP.NET WebMethods or downstream code. If an exception is thrown anywhere within a WebMethod, it is automatically translated into a SOAP fault message before returning from the handler. This doesn't mean you can't generate SOAP faults from an HTTP module, you just can't expect the automatic translation to originate from there.

A The SoapException class was designed for use within ASP.NET WebMethods or downstream code. If an exception is thrown anywhere within a WebMethod, it is automatically translated into a SOAP fault message before returning from the handler. This doesn't mean you can't generate SOAP faults from an HTTP module, you just can't expect the automatic translation to originate from there.

Generating a SOAP fault from a module is straightforward, as you can see in Figure 7. This code wraps the HTTP response stream with an XmlTextWriter and writes out the SOAP fault message manually. Then, before returning from the call, it sets the HTTP status code to 501 and Content-Type to text/xml (as per the SOAP specification for faults). Finally, it calls CompleteRequest to indicate that this message shouldn't travel any further in the pipeline.

Figure 7 Generating a SOAP Fault

// message first arrives in the pipeline public void OnBeginRequest(object o, EventArgs ea) { HttpApplication httpApp = (HttpApplication)o; // always generate SOAP Fault XmlWriter xwr = new XmlTextWriter(httpApp.Response.Output); xwr.WriteStartDocument(); xwr.WriteStartElement("s", "Envelope", "https://schemas.xmlsoap.org/soap/envelope/"); xwr.WriteStartElement("s", "Body", "https://schemas.xmlsoap.org/soap/envelope/"); xwr.WriteStartElement("s", "Fault", "https://schemas.xmlsoap.org/soap/envelope/"); xwr.WriteElementString("faultcode", "s:Client"); xwr.WriteElementString("faultstring", "OUCH!!!"); xwr.WriteEndElement(); // Fault xwr.WriteEndElement(); // Body xwr.WriteEndElement(); // Envelope xwr.WriteEndDocument(); xwr.Close(); // set HTTP status code & headers httpApp.Response.StatusCode = 501; httpApp.Response.ContentType = "text/xml"; httpApp.CompleteRequest(); }

The SOAP fault returned from this module should automatically translate into a normal exception object on the client.

Q Can you recommend some XML and Web Services mailing lists or newsgroups?

Q Can you recommend some XML and Web Services mailing lists or newsgroups?

A I subscribe to a variety of groups that I try to read regularly. I'd have to make recommendations depending on several factors including topic, platform, programming language, and, most importantly, the amount of traffic you're willing to put up with.

A I subscribe to a variety of groups that I try to read regularly. I'd have to make recommendations depending on several factors including topic, platform, programming language, and, most importantly, the amount of traffic you're willing to put up with.

For general (mostly language/platform-neutral) XML discussions, the XML-DEV mailing list hosted by XML.org is the place to go. It can have high traffic but you'll find many of the brightest minds in XML there, including the authors of many XML specifications. XSL-List is another high traffic but reputable mailing list that focuses on general XPath/XSLT discussions as well as many of today's most commonly used processors.

If you're working with SOAP and Web Services, check out the SOAP list for current base-profile discussions and WS-FUTURES for evolving specification discussions (on WSDL, the GXA/ebXML specifications, and so on). If you're building SOAP infrastructure, check out SOAPBuilders where many toolkit developers hang out to discuss interoperability issues, and DOTNET-WEB if you're specifically building Web Services for the .NET platform. Although all of these lists have reasonable traffic, there are also plenty of related newsgroups available if that's your preference.

Figure 8 provides a reference for these lists and the URLs for where they're located. For more information on such resources, point your browser to the brand new, highly organized XML Web Services Development Center on MSDN Online.

Figure 8 XML and Web Services Mailing Lists

Mailing List Topic Discussed URL
XML-DEV General XML discussions https://www.xml.org/xml/xmldev.shtml
XSL-List General XPath/XSLT discussions https://www.mulberrytech.com/xsl/xsl-list/
SOAPBuilders Details of building SOAP https://groups.yahoo.com/group/soapbuilders/

Send questions and comments for Aaron to  xmlfiles@microsoft.com.

Aaron Skonnardis an instructor/researcher at DevelopMentor, where he develops the XML and Web Service-related curriculum. Aaron coauthored Essential XML Quick Reference (Addison-Wesley, 2001) and Essential XML (Addison-Wesley, 2000).