Summary: See how Microsoft ASP.NET Web services methods (WebMethods) provide a high-productivity approach to building Web services. WebMethods can expose traditional Microsoft .NET methods as Web service operations that support HTTP, XML, XML Schema, SOAP, and WSDL. The WebMethod (.asmx) handler automatically dispatches incoming SOAP messages to the appropriate method and automatically serializes the incoming XML elements into corresponding .NET objects. (20 printed pages)
Contents
Introduction
There are two fundamentally different ways to implement HTTP-based Web services in Microsoft?.NET today. The first and lowest-level technique is to write a custom IHttpHandler class that plugs into the .NET HTTP pipeline. This approach requires you to use the System.Web APIs to process the incoming HTTP message along with the System.Xml APIs to process the SOAP envelope found in the HTTP body. Writing a custom handler also requires you to manually author a WSDL document that accurately describes your implementation. Doing all of this properly requires a solid understanding of the XML, XSD, SOAP, and WSDL specifications, which is a daunting prerequisite for most.
A more productive way to implement Web services is to use the Microsoft ASP.NET WebMethods framework. ASP.NET ships with a special IHttpHandler class for .asmx endpoints (called WebServiceHandler), which provides the boilerplate XML, XSD, SOAP, and WSDL functionality that you need. Since the WebMethods framework shields you from the complexities of the underlying XML technologies, you're able to quickly focus on the business problem at hand.
Figure 1. Tradeoff between flexibility and productivity
Choosing between implementation techniques comes down to the common tradeoff between flexibility and productivity as shown in Figure 1. Writing a custom IHttpHandler gives you unbounded flexibility, but it's also going to take you longer to write, test, and debug the code. The WebMethods framework makes it easy to get your Web services up and running quickly, but you're obviously restricted to the boundaries of the framework. However, in cases where the WebMethods framework doesn't provide exactly what you need, it's possible to extend the framework by adding additional functionality of your own.
In general, unless you've mastered XML, XSD, SOAP, and WSDL and are willing to take on the burden of dealing with them directly, you're better off sticking with the WebMethods framework for your Web services needs. It provides the basic services that most Web service endpoints require along with some interesting extensibility points that make it possible to bend the framework to fit your precise needs. Based on that assumption, the rest of this article discusses the internals of how WebMethods work. If you're new to XML Schema and SOAP, you may want to read Understanding XML Schema and Understanding SOAP before continuing.
WebMethods Framework
The WebMethods framework revolves around mapping SOAP messages to methods on a .NET class. This is done by first annotating your methods with the [WebMethod] attribute found in the System.Web.Services namespace. For example, the following .NET class contains four methods, two of which are annotated with the [WebMethod] attribute:
using System.Web.Services;
public class MathService
{
[WebMethod]
public double Add(double x, double y) {
return x + y;
}
[WebMethod]
public double Subtract(double x, double y) {
return x - y;
}
public double Multiply(double x, double y) {
return x * y;
}
public double Divide(double x, double y) {
return x / y;
}
}
To use this class with the WebMethods framework, you need to compile the class into an assembly and copy it to the virtual directory's bin directory. In this example, the Add and Subtract methods can then be exposed as Web service operations, while Multiply and Divide cannot (since they weren't marked with [WebMethod]).
You expose Add and Subtract as Web service operations through an .asmx endpoint. To do this, create a new text file named Math.asmx that contains the following simple declaration and place it in the same virtual directory that contains the assembly (note: this goes in the virtual directory itself and not the child bin directory):
<%@ WebService class="MathService"%>
This declaration tells the .asmx handler which class to inspect for WebMethods and the handler magically takes care of everything else. For example, assuming the virtual directory is called 'math' and it contains Math.asmx along with a child bin directory containing the assembly, browsing to http://localhost/math/math.asmx causes the .asmx handler to generate the documentation page shown in Figure 2 (more on this later).
There is one major variation to how the .asmx handler works. The .asmx file usually only contains the WebService declaration which references the Web service class by name (like the one shown above). Hence, in this case, the assembly must already be compiled and deployed to the virtual directory's bin directory. The .asmx handler also provides just-in-time compilation of source code found in the .asmx file. For example, the following file (called Mathjit.asmx) contains the WebService declaration along with the source code for the referenced class.
<@% WebService class="MathServiceJit" language="C#"%>
using System.Web.Services;
public class MathServiceJit
{
[WebMethod]
public double Add(double x, double y) {
return x + y;
}
[WebMethod]
public double Subtract(double x, double y) {
return x - y;
}
public double Multiply(double x, double y) {
return x * y;
}
public double Divide(double x, double y) {
return x / y;
}
}
The first time this file is accessed over HTTP, the .asmx handler compiles the source and deploys the assembly to the correct location. Notice that the WebService declaration must also provide the language, so the .asmx handler can choose the correct compiler at runtime. The obvious downside to this approach is that you don't find out about compilation errors until you access the file for the first time.
Figure 2. MathService documentation
When you create a new Web service project in Visual Studio?.NET, it always uses the "two file" technique where the class source file is separate from the .asmx file that references it. The integrated development environment (IDE) does its best to hide this from you, but if you click Show All Files on the Solution Explorer toolbar, you'll notice there are two files for each Web service class in the project. In fact, Visual Studio .NET doesn't support .asmx files with syntax highlighting or IntelliSense? so you're on your own if you head that direction. With Web projects, Visual Studio .NET also takes care of creating a virtual directory and compiling the assembly to the virtual directory's bin directory automatically.
Before diving into the details of how the .asmx handler works, let's briefly discuss how the message makes it from Internet Information Server (IIS) into the .asmx handler to begin with. When the incoming HTTP message reaches port 80, IIS uses the information found in the IIS metabase to figure out which ISAPI DLL should be used to process the message. The .NET installation maps .asmx extensions to Aspnet_isapi.dll, as shown in Figure 3.
Figure 3. IIS Application mapping for .asmx
Aspnet_isapi.dll is a standard ISAPI extension provided by the .NET Framework, which simply forwards HTTP requests to a separate worker process called Aspnet_wp.exe. Aspnet_wp.exe hosts the common language runtime and the .NET HTTP pipeline. Once the message makes it into the .NET HTTP pipeline, the pipeline looks in the configuration files to see which IHttpHandler class should be used for the given extension. If you look in your Machine.config file, you'll see that it contains an httpHandler mapping for .asmx files as shown here:
<configuration>
<system.web>
<httpHandlers>
<add verb="*" path="*.asmx" type="System.Web.Services.Protocols.WebServiceHandlerFactory,
System.Web.Services, Version=1.0.3300.0, Culture=neutral,
PublicKeyToken=b03f5f7f11d50a3a" validate="false"/>
...
So when a message enters the .NET HTTP pipeline targeting an .asmx file, the pipeline calls into the WebServiceHandlerFactory class to instantiate a new WebServiceHandler object that can be used to process the request (by calling into the IHttpHandlerProcessRequest method). The WebServiceHandler object then opens the physical .asmx file to determine the name of the class that contains your WebMethods. For more information on how the .NET HTTP pipeline works, check out HTTP Pipelines: Securely Implement Request Processing, Filtering, and Content Redirection with HTTP Pipelines in ASP.NET.
Once the .asmx handler is called by the .NET HTTP pipeline, the magic begins to take care of the XML, XSD, SOAP, and WSDL processing. The remaining functionality provided by the .asmx handler can be broken down into three main areas: 1) message dispatching, 2) mapping XML to objects, and 3) automatic WSDL and documentation generation. Let's look at each of these areas in more detail.
Message Dispatching
When the .asmx handler is called by the HTTP pipeline, it figures out which .NET class to inspect by looking at the WebService declaration found in the .asmx file. Then it looks at the information in the incoming HTTP message to determine exactly which method to call in the referenced class. To invoke the Add operation shown in the previous examples, the incoming HTTP message needs to look something like this:
POST /math/math.asmx HTTP/1.1
Host: localhost
Content-Type: text/xml; charset=utf-8
Content-Length: length
SOAPAction: "http://tempuri.org/Add"
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
>
<soap:Body>
<Add xmlns="http://tempuri.org/">
<x>33</x>
<y>66</y>
</Add>
</soap:Body>
</soap:Envelope>
There are really two pieces of information in the incoming HTTP message that could be used to figure out which method to call in the class: the SOAPAction header or the request element's name (e.g., the name of the element within the soap:Body element). Either one, in this case, indicates the name of the method the sender wants to invoke.
The .asmx handler uses the value of the SOAPAction header by default to perform the message dispatching. Hence, the .asmx handler looks at the SOAPAction header in the message and then, using .NET reflection, inspects the methods in the referenced class. It only considers methods marked with the [WebMethod] attribute but it determines exactly which method to call by looking at each method's SOAPAction value. Since we didn't specify a SOAPAction value explicitly on the methods in our class, the .asmx handler assumes that the SOAPAction value will be a combination of the Web service's namespace followed by the name of the method. Since we didn't specify a namespace either, the handler assumes http://tempuri.org as the default. So the default SOAPAction value for the Add method is going to be http://tempuri.org/Add.
You can customize the namespace of your Web service by annotating your class with the [WebService] attribute as well as the exact SOAPAction value by annotating your WebMethods with the [SoapDocumentMethod] attribute as illustrated here:
using System.Web.Services;
using System.Web.Services.Protocols;
[WebService(Namespace="http://example.org/math")]
public class MathService
{
[WebMethod]
public double Add(double x, double y) {
return x + y;
}
[WebMethod]
[SoapDocumentMethod(Action="urn:math:subtract")]
public double Subtract(double x, double y) {
return x - y;
}
...
}
Now the .asmx handler expects the SOAPAction value to be http://example.org/math/Add for the Add method (using the default heuristic) and urn:math:subtract for the Subtract method (since we explicitly defined it to be that value). For example, the following HTTP request message invokes the Subtract operation:
POST /math/math.asmx HTTP/1.1
Host: localhost
Content-Type: text/xml; charset=utf-8
Content-Length: length
SOAPAction: "urn:math:subtract"
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
>
<soap:Body>
<Subtract xmlns="http://example.org/math">
<x>33</x>
<y>66</y>
</Subtract>
</soap:Body>
</soap:Envelope>
If the .asmx handler doesn't find a SOAPAction match for the incoming HTTP message, it simply throws an exception (more later on how exceptions are handled). If you'd rather not rely on the SOAPAction header for method dispatching, you can instruct the .asmx handler to use the request element's name by annotating the class with the [SoapDocumentService] attribute's RoutingStyle property. If you do this, you should probably also indicate that the WebMethods don't require a SOAPAction value by setting their values to the empty string as illustrated here:
using System.Web.Services;
using System.Web.Services.Protocols;
[WebService(Namespace="http://example.org/math")]
[SoapDocumentService(
RoutingStyle=SoapServiceRoutingStyle.RequestElement)]
public class MathService
{
[WebMethod]
[SoapDocumentMethod(Action="")]
public double Add(double x, double y) {
return x + y;
}
[WebMethod]
[SoapDocumentMethod(Action="")]
public double Subtract(double x, double y) {
return x - y;
}
...
}
In this case, the handler doesn't even look at the SOAPAction value梚t uses the name of the request element instead. For example, it expects the name of the request element to be Add (from the http://example.org/math namespace) for the Add method as illustrated in this HTTP request message:
POST /math/math.asmx HTTP/1.1
Host: localhost
Content-Type: text/xml; charset=utf-8
Content-Length: length
SOAPAction: ""
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
>
<soap:Body>
<Add xmlns="http://example.org/math">
<x>33</x>
<y>66</y>
</Add>
</soap:Body>
</soap:Envelope>
Hence, the first major thing the .asmx handler does when it receives an incoming HTTP message is figure out how to dispatch the message to the corresponding WebMethod. Before it can actually invoke the method, however, it needs to map the incoming XML into .NET objects.
Mapping XML to Objects
Once the WebMehod handler figures out which method to call, it then needs to deserialize the XML message into .NET objects that can be supplied during method invocation. As with message dispatching, the handler accomplishes this by inspecting the class via reflection to figure out how to process the incoming XML message. The XmlSerializer class performs the automatic mapping between XML and objects in the System.Xml.Serialization namespace.
XmlSerializer makes it possible to map any public .NET type to an XML Schema type, and with such a mapping in place, it can automatically map between .NET objects and XML instance documents (see Figure 4). XmlSerializer is limited to what's supported by XML Schema today so it can't deal with all of the complexities of today's modern object models such as complex non-tree-like object graphs, duplicate pointers, etc. Nevertheless, XmlSerializer can deal with most complex types developers tend to use.
For the Add example shown above, XmlSerializer would map the x and y elements to .NET double values, which could then be supplied when Add is called. The Add method returns a double value to the caller, which would then need to be serialized back into an XML element within the SOAP response.
Figure 4. Mapping XML to objects
XmlSerializer can also automatically deal with complex types (aside from the limitations described above). For example, the following WebMethod calculates the distance between two Point structures:
using System;
using System.Web.Services;
public class Point {
public double x;
public double y;
}
[WebService(Namespace="urn:geometry")]
public class Geometry {
[WebMethod]
public double Distance(Point orig, Point dest) {
return Math.Sqrt(Math.Pow(orig.x-dest.x, 2) +
Math.Pow(orig.y-dest.y, 2));
}
}
The SOAP request message for this operation will contain a Distance element, which contains two child elements, one named orig and the other dest, and each of these should contain child x and y elements as shown here:
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
>
<soap:Body>
<Distance xmlns="urn:geometry">
<orig>
<x>0</x>
<y>0</y>
</orig>
<dest>
<x>3</x>
<y>4</y>
</dest>
</Distance>
</soap:Body>
</soap:Envelope>
The SOAP response message in this case will contain a DistanceResponse element, which contains a DistanceResult element of type double:
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
>
<soap:Body>
<DistanceResponse
xmlns="urn:geometry">
<DistanceResult>5</DistanceResult>
</DistanceResponse>
</soap:Body>
</soap:Envelope>
The default XML mapping uses the name of the method as the name of the request element and the names of the parameters as the names of its child elements. The structure of each parameter depends on the structure of the type. The names of public fields and properties are simply mapped to child elements as in the case of x and y (within Point). The name of the response element by default is the name of the request element with "Response" tacked on the end. The response element also contains a child element named the same as the request element with "Result" tacked on the end.
It's possible to break free from the standard XML mapping by using a slew of built-in mapping attributes. For example, you can use the [XmlType] attribute to customize the type's name and namespace. You can use the [XmlElement] and [XmlAttribute] attributes to control how parameters or class members map to elements or attributes respectively. You can also use the [SoapDocumentMethod] attribute to control how the method itself maps to element names in the request/response messages. For example, check out the following version of Distance with various attributes sprinkled throughout:
using System;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Xml.Serialization;
public class Point {
[XmlAttribute]
public double x;
[XmlAttribute]
public double y;
}
[WebService(Namespace="urn:geometry")]
public class Geometry {
[WebMethod]
[SoapDocumentMethod(RequestElementName="CalcDistance",
ResponseElementName="CalculatedDistance")]
[return: XmlElement("result")]
public double Distance(
[XmlElement("o")]Point orig, [XmlElement("d")]Point dest) {
return Math.Sqrt(Math.Pow(orig.x-dest.x, 2) +
Math.Pow(orig.y-dest.y, 2));
}
}
This version of Distance expects the incoming SOAP message to look like this:
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
>
<soap:Body>
<CalcDistance xmlns="urn:geometry">
<o x="0" y="0" />
<d x="3" y="4" />
</CalcDistance>
</soap:Body>
</soap:Envelope>
And it will generate a SOAP response message that looks like this:
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
>
<soap:Body>
<CalculatedDistance xmlns="urn:geometry">
<result>5</result>
</CalculatedDistance>
</soap:Body>
</soap:Envelope>
The .asmx handler uses the SOAP document/literal style to implement and describe the default mapping shown above. This means that the WSDL definition will contain literal XML schema definitions describing both the request and response elements used in the SOAP messages (e.g., the SOAP encoding rules are not used).
The .asmx handler also makes it possible to use the SOAP rpc/encoded style. This means the SOAP Body contains an XML representation of an RPC call and the parameters are serialized using the SOAP encoding rules (e.g., XML Schema is not needed). To accomplish this, you use the [SoapRpcService] and [SoapRpcMethod] attributes instead of the [SoapDocumentService] and [SoapDocumentMethod] attributes. For more information on the differences between these styles, check out Understanding SOAP.
As you can see, it's possible to completely customize how a given method maps to a SOAP message. XmlSerializer provides a powerful serialization engine with many features that we don't have time to cover here. For more information on how XmlSerializer works, check out Moving to .NET and Web Services. I've also covered many of the nuances of XmlSerializer that are not obvious in my monthly MSDN Magazine column, XML Files (check out the list of columns in the online archives).
In addition to dealing with the deserialization of the parameters, the .asmx handler is also capable of deserializing/serializing SOAP headers. SOAP headers are handled differently than parameters since they're typically considered out-of-band information with no direct tie to a particular method. Due to this, header processing is typically done via interception layers, shielding WebMethods from having to deal with header processing at all.
If, however, you wish to get your hands on header information from within a WebMethod, you have to provide a .NET class, deriving from SoapHeader, which represents the header's XML Schema type (following the same mapping guidelines described above). Then you define a member variable of that type to serve as a placeholder for header instances. And finally, you annotate each WebMethod that needs access to the header, specifying the name of the field where you want it to go.
For example, consider the following SOAP request that contains a UsernameToken header for authentication purposes:
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
>
<soap:Header>
<x:UsernameToken xmlns:x="http://example.org/security">
<username>Mary</username>
<password>yraM</password>
</x:UsernameToken>
</soap:Header>
<soap:Body>
<CalcDistance xmlns="urn:geometry">
...
In order to make it possible for the .asmx handler to deserialize the header, first you need to define a .NET class that represents the implied XML Schema type (note: if you actually have the XML Schema for the header ahead of time, you can generate the class using xsd.exe /c). In this case, the corresponding class looks like this:
[XmlType(Namespace="http://example.org/security")]
[XmlRoot(Namespace="http://example.org/security")]
public class UsernameToken : SoapHeader {
public string username;
public string password;
}
Then, you simply need to define a member variable in your WebMethod class to hold an instance of the header class and annotate the WebMethods with the [SoapHeader] attribute as illustrated here:
using System;
using System.Web.Services;
using System.Web.Services.Protocols;
[WebService(Namespace="urn:geometry")]
public class Geometry {
public UsernameToken Token;
[WebMethod]
[SoapHeader("Token")]
public double Distance(Point orig, Point dest) {
if (!Token.username.Equals(Reverse(Token.password)))
throw new Exception("access denied");
return Math.Sqrt(Math.Pow(orig.x-dest.x, 2) +
Math.Pow(orig.y-dest.y, 2));
}
}
Then, within the WebMethod you can access the Token field and extract the information that was supplied in the header. You can also send headers back to the client using the same technique梱ou simply need to specify the direction of the header in the [SoapHeader] attribute declaration. For more information on processing SOAP headers within the WebMethods framework, check out Digging into SOAP Headers with the .NET Framework.
The .asmx handler also provides automatic serialization of .NET exceptions. Any unhandled exception caught by the .asmx handler is automatically serialized into a SOAP Fault element in the response. For example, in the previous example, if the username didn't match the reversed password, our code throws a.NET exception. The .asmx handler will then catch the exception and serialize it into the SOAP response as shown here:
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
>
<soap:Body>
<soap:Fault>
<faultcode>soap:Server</faultcode>
<faultstring>Server was unable to process request. --> access denied</faultstring>
<detail />
</soap:Fault>
</soap:Body>
</soap:Envelope>
If you'd like more control over the SOAP Fault element, you can also explicitly throw a SoapException object specifying all of the SOAP Fault element details such as the faultcode, faulstring, faultactor, and detail elements. Check out Using SOAP Faults for more information.
As you can see, the bulk of figuring out how WebMethods work has to do with understanding the underlying serialization engine and all of its various options. The benefit of the serialization engine is that it hides all of the underlying XML API code that you would normally have to write in a custom handler. However, while most developers find this positive, some developers consider it a deficiency because they still want to get their hands on the raw SOAP message within the WebMethod implementation. For more details on how to accomplish such a hybrid approach, check out Accessing Raw SOAP Messages in ASP.NET Web Services.
Automatic WSDL Generation
Once you write and deploy a WebMethod, clients need to know exactly what the SOAP messages need to look like in order to successfully communicate with it. The standard way to provide Web service descriptions is through WSDL (and embedded XSD definitions). To help accommodate this, the .asmx handler automatically generates both a human readable documentation page along with a WSDL definition that accurately reflects the WebMethod interface. If you've applied a bunch of mapping attributes to your WebMethods, they're all reflected in the generated documentation.
If you browse to the .asmx file, you'll see a human-readable documentation page like the one shown in Figure 2. This documentation page is generated by an .aspx page called DefaultWsdlHelpGenerator.aspx (found in C:\windows\Microsoft.NET\Framework\ v1.0.3705\config). If you open the file, you'll see it's just a standard ASP.NET page that uses .NET reflection to generate the documentation. This feature allows your documentation to always stay in sync with your code. You can customize the generated documentation by simply modifying this file.
You can also bypass the documentation generation on a virtual directory basis by specifying a different documentation file in your Web.config file:
<configuration>
<system.web>
<webServices>
<wsdlHelpGenerator href="MyDocumentation.aspx"/>
</webServices>
...
If a client issues a GET request for the .asmx endpoint with "?wsdl" in the query string, the .asmx handler generates the WSDL definition instead of the human-readable documentation. Clients can use the WSDL definition to generate proxy classes that automatically know how to communicate with the Web service (e.g., using Wsdl.exe in .NET).
To customize the WSDL generation process, you can write a SoapExtensionReflector class and register it with the WebMethods framework in your Web.config file. Then when the .asmx handler generates the WSDL definition, it will call into your reflector class and give you a chance to customize the final definition supplied to the client. For more information on how to write SoapExtensionReflector classes, check out SoapExtensionReflectors in ASP.NET Web Services.
You can also bypass the WSDL generation process altogether using two different techniques. First, you can provide a static WSDL document in your virtual directory for clients to access and then disable the documentation generator by removing it from your Web.config file as shown here:
<configuration>
<system.web>
<webServices>
<protocols>
<remove name="Documentation"/>
</protocols>
...
A slightly more automated technique is to use the [WebServicesBinding] attribute to specify the location of a static WSDL document in the virtual directory that the WebMethod class implements. You also have to specify the name of the WSDL binding that each WebMethod implements using the [SoapDocumentMethod] attribute. With this in place, the automatic WSDL generation process will import your static WSDL file and wrap a new service description around it. For more information on this technique, check out the article entitled Place XML Message Design Ahead of Schema Planning to Improve Web Service Interoperability.
WSDL is extremely hard to author by hand today because there still aren't many WSDL editors available. Hence, the automatic documentation/WSDL generation is a valuable part of the WebMethods framework many developers would have a hard time living without.
Conclusion
The ASP.NET WebMethods framework provides a high-productivity approach to building Web services. WebMethods make it possible to expose traditional .NET methods as Web service operations that support HTTP, XML, XML Schema, SOAP, and WSDL. The WebMethod (.asmx) handler automatically figures out how to dispatch incoming SOAP messages to the appropriate method, at which point it automatically serializes the incoming XML elements into corresponding .NET objects. And to simplify integrating clients, the .asmx handler also provides automatic support for generating both human-readable (HTML) and machine-readable (WSDL) documentation.
Although the WebMethods framework can be somewhat restrictive compared to custom IHttpHandlers, it also provides a powerful extensibility model known as the SOAP extension framework. SOAP extensions allow you to introduce additional functionality beyond what we've discussed here to meet your precise needs. As an example, Microsoft released the Web Services Enhancements 1.0 for Microsoft .NET (WSE), which simply provides a SoapExtension class that introduces support for several GXA specifications to the WebMethods framework. For more information on writing a SOAP extension, check out Fun with SOAP Extensions.
Resources
Accessing Raw SOAP Messages in ASP.NET Web Services by Tim Ewald.
Digging into SOAP Headers with the .NET Framework by Matt Powell.
Fun with SOAP Extensions by Keith Ballinger.
HTTP Pipelines: Securely Implement Request Processing, Filtering, and Content Redirection with HTTP Pipelines in ASP.NET by Tim Ewald and Keith Brown.
Moving to .NET and Web Services by Don Box.
Place XML Message Design Ahead of Schema Planning to Improve Web Service Interoperability by Yasser Shohoud.
SoapExtensionReflectors in ASP.NET Web Services by Matt Powell.
Understanding SOAP by Aaron Skonnard.
Using SOAP Faults by Scott Seeley.
Understanding XML Schema by Aaron Skonnard.
XML Files column index in MSDN Magazine by Aaron Skonnard.