Digging into SOAP Headers with the .NET Framework
June 25, 2002
Even if you have played around with XML Web services, or perhaps even done some fairly involved development of an XML Web service, there is a good chance that you have never bothered with SOAP headers. In fact you may have found yourself, like Scott and I, putting information in the body of your SOAP message that really should be in the header section of your message. We will be looking at what sort of information should go into the header, how you can read and write message headers in the .NET Framework, and how you can augment the current SOAP infrastructure by using SOAP headers.
The SOAP Header Element
Before we dig into how or why we use the SOAP header, let's take a look at the specifics of how the SOAP Header element is defined. The SOAP 1.1 Specification and the SOAP 1.2 Working Draft indicate that a SOAP message consists of three elements: the top level Envelope element and two of its children (the Header element and the Body element). A SOAP message with all three of these elements is shown below.
<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:Header> <PriorityHeader xmlns="http://msdn.microsoft.com/AYS/6/2002/"> <Priority>HighPriority</Priority> </PriorityHeader> </soap:Header> <soap:Body> <PlaceOrder xmlns="http://msdn.microsoft.com/AYS/6/2002/"> <quantity>100</quantity> </PlaceOrder> </soap:Body> </soap:Envelope>
The Body element is where the main data in the message lives. The Header element is where any metadata that might describe the body, detail how the body should be processed, or simply provide extra information about the message can live. The Header element is optional, but if the Header element exists, it must be the first child of the Envelope element. The Header element itself consists of zero or more child elements referred to as header blocks. Each header block needs to be namespace-qualified.
The SOAP specifications define three attributes that can apply to header blocks: the encodingStyle attribute, the actor attribute and the mustUnderstand attribute. All three of these attributes are optional and may be used in addition to any other attributes you may want to include.
The encodingStyle attribute is used to indicate how the encapsulated data is encoded. The SOAP specification includes a mechanism for encoding data that includes data type information in XML attributes. This is becoming less popular as more and more people define their headers using XML Schema.
The actor attribute is used to indicate which node should process this particular header block. A SOAP message may be passed through a sequence of nodes, and it is conceivable that a header block may apply to one node in the sequence and not to others. You might set the actor attribute to the endpoint of the node, which will process the header block so that other nodes in the sequence will know to ignore it. The absence of the actor attribute implies that the header block is targeted for the ultimate recipient of the SOAP message. It appears that the actor attribute will be renamed to be the role attribute in future drafts of the SOAP 1.2 specification.
The mustUnderstand attribute is how a header block indicates that it must be understood and processed in accordance with any specifications that may be defined for the given qualified element name. If the recipient does not know how to process the header block then it must generate a MustUnderstand fault and not proceed with any further processing of the message. The mustUnderstand attribute is a binary value, and if it is not present then it is assumed to be false.
Information to Put in the Header
The SOAP specifications are fairly ambiguous concerning what information goes in the header. There is mention of "authentication, transaction management, payment, etc." but beyond that it is pretty quiet and for the most part sticks to descriptions of the schema. Those familiar with HTTP or MIME headers are probably used to seeing various sorts of metadata included with the main data in the message. In a lot of ways, the SOAP header is similar with one major difference.
HTTP uses the Content-Type header to indicate the MIME type of the data in the body of an HTTP request or response. Similarly, an HTTP client can request what kind of data it wants in the response by including the HTTP Accept header. From a high level, SOAP messages always contain XML data, so in that sense there is no need for specifying a MIME type to describe the data. In fact, the structure of the data in SOAP messages is much better defined through the use of XML Schema. An XML Web service that defines its interface through WSDL defines the schema of its data along with the bindings that correlate what response data types will be generated from which request data types. The well-defined nature of SOAP messages is what allows them to be so easily used from within applications. Therefore, because the data structure is already defined, using SOAP headers to describe the data structure in a SOAP message is unnecessary.
The focus of the SOAP header should be to help process the data in the body. It makes sense to include information about authentication or transactions, because this information will be involved in identifying the person or company who sent the body and in what context it will be processed. Expiration data might be included in the header to indicate when the data in the body may need to be refreshed. User account information could be included in order to insure that processing the message is only performed for a request that has been legitimately paid for.
Here's another factor in determining whether information should be included in SOAP headers: Will that information have broad application to a wide variety of SOAP messages? If so, include it in the header. It makes more sense to define a single schema and insert it into the definition of one header element then to force inclusion of the same data into the body schemas of a large number of message definitions. The various header blocks defined in the drafts created as part of the Global XML Web Services Architecture (GXA) are examples of headers that have a broad application. These specifications outline mechanisms for performing tasks like authentication and routing of SOAP messages. Authentication and routing are problems common to many XML Web services, so it makes sense that these specifications deal with information that lives in the header element.
Using SOAP Headers with the .NET Framework
Now that we have an idea of what type of information might be included in SOAP headers, let's look at a few different ways to use SOAP headers from within the .NET Framework. We will start by seeing how to read and write SOAP headers from within a Web method of your ASP.NET Web service. Next, we will see how a client consuming your Web service would read and write SOAP headers. Finally, we will look at how to extend the SOAP infrastructure using SOAP headers and the support for SoapExtensions in the .NET Framework. This mechanism can be used when you implement code that handles widely applicable header blocks, like those in the GXA drafts.
Creating an ASP.NET Web Method That Processes a Header Block
One of the main places you will want to process the data in a SOAP header block is in the XML Web service itself. In ASP.NET, that typically means within the logic of the public function that has been decorated with the WebMethod attribute. The code for a trivial PlaceOrder Web method is shown below.
<WebMethod(), SoapDocumentMethod()> _ Public Function PlaceOrder(ByVal quantity As Integer) As DateTime ' Return when the order will be processed Return CStr(DateAdd(DateInterval.Day, 5, Now())) End Function
The PlaceOrder function is in a class inherited from System.Web.Services.WebService. To add header-processing capabilities, we will create a class inherited from the System.Web.Services.Protocols.SoapHeader class. I wanted to add the ability to include a priority with an order, so I defined the following class:
Public Class PriorityHeader Inherits SoapHeader Public Priority As PriorityLevels Public Enum PriorityLevels LowPriority MediumPriority HighPriority End Enum End Class
The PriorityHeader class above is derived from the SoapHeader class and includes a new Enum for different priority levels. It also includes a public member called Priority, which is where the value will be set. To process the PriorityHeader from within my Web method, I first need to add a public member variable of the PriorityHeader type to the class where my Web method lives. I define the PriorityLevel member variable as follows:
Public PriorityLevel as PriorityHeader
I then need to set the SoapHeader attribute on the Web method declaration. Once I have done this, I can access the data in the header through the PriorityLevel member variable. The revised Web method might look like this:
<WebMethod(), _ SoapHeader("PriorityLevel", _ Direction:=SoapHeaderDirection.InOut, _ Required:=False), _ SoapDocumentMethod()> _ Public Function PlaceOrder(ByVal quantity As Integer) As DateTime ' Return the time when the order will be processed If PriorityLevel Is Nothing Then PriorityLevel = New PriorityHeader() PriorityLevel.Priority = _ PriorityHeader.PriorityLevels.MediumPriority End If Select Case PriorityLevel.Priority Case PriorityHeader.PriorityLevels.LowPriority Return CStr(DateAdd(DateInterval.Day, 10, Now())) Case PriorityHeader.PriorityLevels.MediumPriority Return CStr(DateAdd(DateInterval.Day, 5, Now())) Case PriorityHeader.PriorityLevels.HighPriority ' We don't support high priority so high priority ' requests will be changed to medium in the response PriorityLevel.Priority = _ PriorityHeader.PriorityLevels.MediumPriority Return CStr(DateAdd(DateInterval.Day, 5, Now())) End Select End Function
The SoapHeader attribute does three things. The first thing it does is to associate the PriorityLevel member variable with the header for this method. The second thing it does is to define the header as being both an In and an Out header. This means the header is defined for both the request and response messages. Including the header in the response is the method used to change the priority of the request. You can see that for HighPriority requests, we change the priority level for the response to MediumPriority. This will allow a client to see that we changed their order priority from what was submitted with the request. The third aspect of the SoapHeader attribute is the Required parameter, which in this case is set to False. This indicates whether SOAP messages will be processed if the indicated header is not present. In our case we are not going to require the Priority header.
For a look at a SOAP message for this Web method with the priority header block included, see the sample message at the beginning of this column.
Setting and Processing SOAP Headers on the client Using Microsoft Visual Studio .NET
Now that we have created an XML Web service that can use the Priority header block, we need to take a look at how we would set or read the header information from the client side. This turns out to be pretty simple, because the priority header information is included in the WSDL for our XML Web service. Therefore, the header functionality is imported for easy use by adding a Web reference to the project of our client application. The proxy object for our service will now include a PriorityHeaderValue member, which is of the type PriorityHeader that we created in our Web service code. To set the header for a call to the PlaceOrder Web method, we simply create an instance of the PriorityHeader class, set the Priority member appropriately, and then make the call. Code for making a HighPriority order is shown below.
Dim proxy As New localhost.Service1() proxy.PriorityHeaderValue = New localhost.PriorityHeader() proxy.PriorityHeaderValue.Priority = _ localhost.PriorityLevels.HighPriority Dim FullfillmentDate as DateTime FullfillmentDate = proxy.PlaceOrder(100)
Because the Priority header block may be included in the SOAP response as well, we can simply check the PriorityHeaderValue member after the call to see if the value was changed.
There are a number of other options available for dealing with header blocks on the client and server, including ways to use headers that are not defined as classes in your code and handling mustUnderstand issues. See Using SOAP Headers in the .NET Framework Developer's Guide for more information.
Handling SOAP Headers Using SoapExtensions
For header blocks specific to your particular Web service, adding a class that inherits from SoapHeader and writing specific code in your Web method may make sense, but there are also cases where headers will need to be processed in a common fashion for all XML Web services on your machine. For instance, if there is a definition for a standard authentication mechanism using SOAP header blocks, then you probably don't want to add the code that implements that authentication mechanism in every Web method on your server. It would be nice if we could create some sort of plug-in to be called for all SOAP requests, which would handle the authentication processing for us. As it turns out, you can do this using Soap Extensions in the .NET Framework.
For the purposes of this discussion, I will be expanding the use of our Priority header so that it can be used by any XML Web service on my machine. The heart of this code will exist in a class that I create that inherits from the System.Web.Services.Protocols.SoapExtension class. A SoapExtension gets called before and after the deserialization of an incoming SOAP message, and before and after the serialization of the outgoing SOAP message. This allows you to see and possibly manipulate the incoming request and the outgoing response. For generic information on creating a SOAP Extension, see Altering the SOAP Message Using SOAP Extensions in the .NET Framework Developer's Guide.
In the case of the Priority header, I want to look at all SOAP requests being received and see if the priority header is present. If it is present, I am going to read the value in the SoapExtension and provide an easy mechanism for getting at that value from within the Web method handling the request. The way I exposed the priority information is by creating a public class called PriorityWebService that inherits from the System.Web.Service.WebService class. The idea is that anyone writing a Web method that cares about an existing Priority header will use my PriorityWebService class as their base class instead of the typical WebService class.
The PriorityWebService class is identical to the WebService class, except it has an additional member called Priority. My SoapExtension will look for a Priority header and if it is found, it will set the Priority member of the PriorityWebService class to the value in the Priority header. This will allow the logic in the Web method to read and set the priority without having to declare a SoapHeader-derived class or worry about setting a SoapHeader attribute.
The new PriorityWebService base class looks like this:
Public Class PriorityWebService Inherits System.Web.Services.WebService Public Priority As PriorityLevels Public Enum PriorityLevels LowPriority MediumPriority HighPriority End Enum End Class
The code for implementing the same functionality that we implemented earlier with the PlaceOrder Web method can now be created like so…
Imports System.Web.Services Imports System.Web.Services.Protocols Imports PrioritySoapHeaderExtension <WebService(Namespace:="http://msdn.microsoft.com/AYS/6/2002/")> _ Public Class PriorityService Inherits PriorityWebService <WebMethod(), _ SoapDocumentMethod()> _ Public Function PlaceOrder(ByVal quantity As Integer) As DateTime ' Return when the order will be processed Select Case Priority Case PriorityLevels.LowPriority Return CStr(DateAdd(DateInterval.Day, 10, Now())) Case PriorityLevels.MediumPriority Return CStr(DateAdd(DateInterval.Day, 5, Now())) Case PriorityLevels.HighPriority ' We don't support high priority so high priority ' requests will be changed to medium in the response Priority = PriorityLevels.MediumPriority Return CStr(DateAdd(DateInterval.Day, 5, Now())) Case Else Priority = PriorityLevels.MediumPriority Return CStr(DateAdd(DateInterval.Day, 5, Now())) End Select End Function End Class
Notice that the class inherits from the PriorityWebService and that there is no knowledge of SOAP headers in this code at all.
You need to be aware that when you remove SoapHeader definitions from your Web service code that the header definitions in the dynamically generated WSDL are also removed. So if you browse to http://localhost/SoapHeaders/PriorityService.asmx?WSDL to view the WSDL associated with the PriorityService.asmx Web service, you will notice that there is no information in the WSDL about the Priority header. Therefore, if you import a Web reference for this Web service in Microsoft® Visual Studio .NET, you should do it with a separate WSDL file that does include the header data.
One way for doing this is to create a Web service in Visual Studio .NET that has the header defined, then save the WSDL returned with the ?WSDL option to a file that will serve as the interface definition for my particular Web service. I can then modify the Web service code to remove my priority header data, since my interface definition has already been locked down.
To avoid the possibility of clients seeing the WSDL with the removed header information, you might want to disable the ?WSDL functionality for your Web service. You can do this by manipulating the web.config file to remove the "Documentation" protocol for Web services. The crucial part of the web.config that removes WSDL documentation is shown below:
<system.web> <webServices> <protocols> <remove name="Documentation"/> </protocols> </webServices> </system.web>
There are some other options for dealing with SOAP extension header definitions that are not automatically included in the WSDL. You could simply omit the header definition from the WSDL and have clients manually add the header to their requests (see Using SOAP Headers for information on how to write client code that adds headers with the .NET Framework). Another option is to turn off WSDL generation as noted above, then simply manually edit the static WSDL file to include the header information.
Manually editing a WSDL may seem like an exercise only for the XML Schema elite, but if you go through the process once to define your header class and generate the corresponding WSDL using ?WSDL, it is not too hard to pull out the parts that deal with your header and manually plug them into other WSDLs. There are also a growing number of people who are defining their WSDL first then generating their code using the WSDL.EXE utility that is provided with the .NET Framework SDK. Another option is to create a client-side SoapExtension that automatically adds the header for you despite its absence in the WSDL. Obviously a solution like this would be very platform specific. One final option is to plug your own logic into the .NET Framework infrastructure that generates the response to ?WSDL requests. Apparently, this is possible using the ServiceDescriptionFormatExtension class, but this is under-documented at this time. There should be more information on how to do this in a future version of the .NET Framework Developer’s Guide.
For this example there is one more piece of work that the SoapExtension must do. Because the Priority header we created applies to both incoming messages and outgoing messages, the SoapExtension needs to add the Priority header to any outgoing messages based on the value of the Priority member in any Web services derived from our PriorityWebService class. To do this the SoapExtension gets called before serialization of the outgoing SOAP message occurs, and checks to see if the implementing class was inherited from the PriorityWebService class. If so, then we create an instance of the PriorityHeader class, set the priority accordingly, and add it to the Headers collection for the message. The entire code for my SoapExtension looks like this:
Imports System.Web.Services.Protocols Public Class PriorityHeader Inherits SoapHeader Public Priority As PriorityLevels Public Enum PriorityLevels LowPriority MediumPriority HighPriority End Enum End Class Public Class PriorityWebService Inherits System.Web.Services.WebService Public Priority As PriorityLevels Public Enum PriorityLevels LowPriority MediumPriority HighPriority End Enum End Class Public Class PriorityHeaderExtension Inherits SoapExtension Public Overrides Sub Initialize(ByVal initializer As Object) End Sub Public Overloads Overrides Function GetInitializer( _ ByVal serviceType As System.Type) As Object Return Nothing End Function Public Overrides Function ChainStream( _ ByVal stream As System.IO.Stream) As System.IO.Stream Return stream End Function Public Overloads Overrides Function GetInitializer(_ ByVal methodInfo As LogicalMethodInfo, _ ByVal attribute As SoapExtensionAttribute) As Object Return Nothing End Function Public Overrides Sub ProcessMessage( _ ByVal message As SoapMessage) If TypeOf message Is SoapServerMessage Then Dim ServerMessage As SoapServerMessage ServerMessage = message Select Case message.Stage Case SoapMessageStage.AfterDeserialize ' Incoming Message has been deserialized Dim Header As SoapUnknownHeader For Each Header In message.Headers If (Header.Element.Name = "PriorityHeader") _ And (Header.Element.NamespaceURI = _ "http://msdn.microsoft.com/AYS/6/2002/") _ Then If TypeOf ServerMessage.Server _ Is PriorityWebService Then Dim PriorityService As _ PriorityWebService PriorityService = ServerMessage.Server Dim PriorityHeader As PriorityHeader PriorityService.Priority = _ Priorityservice.Priority.Parse( _ PriorityService.Priority.GetType(), _ Header.Element.InnerText) End If End If Next Case SoapMessageStage.BeforeSerialize ' Outgoing Message is not yet serialized If TypeOf ServerMessage.Server _ Is PriorityWebService Then Dim PriorityService As PriorityWebService PriorityService = ServerMessage.Server Dim Header As New PriorityHeader() Header.Priority = PriorityService.Priority ServerMessage.Headers.Add(Header) End If End Select End If End Sub End Class
A lot of the code in the above extension is simply creating the functions that are declared as MustOverride. The meat of the code is in the ProcessMessage function, which is where the notifications occur that allow us to inspect the SOAP messages. The incoming message is handled under the AfterDeserialize case statement. I simply inspect the Headers collection of SoapUnknownHeader objects for a header that matches the Priority header by its name and namespace. (If you recall the earlier discussion, the fully qualified name of a header uniquely identifies what kind of a header it is.) If a Priority header is found, then I check to see if the implementation class for the Web method is derived from the PriorityWebService class that we created. If it is, then I set the Priority member of the PriorityWebService class accordingly.
After building our SoapExtension, we also have to configure ASP.NET to actually use it. We have the option of configuring it for use in a single directory, or configuring it for the entire Web server. We will look at both options.
If you are installing your SoapExtension in a single virtual directory, you can simply place the assembly for your extension in the bin directory below your virtual directory. Then you must modify the web.config to tell ASP.NET to load your SoapExtension. In my case, the assembly for my extension was PrioritySoapHeaderExtension.dll. SoapExtensions are configured under the webServices element using the soapExtensionTypes element in web.config. Below is the webServices element I used to configure the SoapExtension.
<webServices> <soapExtensionTypes> <add type="PrioritySoapHeaderExtension.PriorityHeaderExtension, PrioritySoapHeaderExtension" priority="1" group="0" /> </soapExtensionTypes> </webServices>
The webServices element simply needs to be created as a child of the system.web element. The type attribute of the add element indicates the class type that implements the SoapExtension with the added assembly name after the comma. The priority and group elements allow you to make settings so that you have some control over the order in which this extension will be loaded.
If you want the SoapExtension to be used for all virtual directories on your machine, you will need to place your extension in the Global Assembly Cache (GAC) and modify the machine.config file that is located with the core .NET Framework files. Your assembly must have a strong name in order to place it in the GAC. In order to give your assembly a strong name, the main things you will need are a version number and a Public Key Token. You can specify both of these by modifying the AssemblyInfo.vb file that Visual Studio .NET created for you. But before you modify AssemblyInfo.vb, you need a key file that holds the key pair that your assembly will be signed with. You can generate a key file using the SN.EXE utility that comes with the .NET Framework SDK. The "-k" flag tells SN.EXE to create a key file. The following command creates the key file Priority.snk.
SN –k Priority.snk
Once the key file is in place, we can add the AssemblyKeyFile attribute in AssemblyInfo.vb in order to properly sign the assembly during the build process. I also remove the default wildcard option in the AssemblyVersion attribute so that my strong name will not change during the development of my SoapExtension. Below are the modified AssemblyVersion and AssemblyKeyFile attributes in the AssemblyInfo.vb file.
<Assembly: AssemblyVersion("188.8.131.52")> <Assembly: AssemblyKeyFile( _ "c:\src\SoapHeaders\PrioritySoapHeaderExtension\Priority.snk")>
After rebuilding the assembly, I can use the GACUTIL.EXE utility that comes with the .NET Framework SDK to register the assembly. The following command line adds my assembly to the GAC:
GACUTIL /I PrioritySoapHeaderExtension.dll
Finally, I'm ready to modify the machine.config, which on my machine is located in the C:\WINDOWS\Microsoft.NET\Framework\v1.0.3705\CONFIG directory. You should find a soapExtensionTypes element in the machine.config that is most likely empty. An add element will need to be created similar to the add element we had in the web.config file earlier, except we need to specify the strong name of the assembly (which includes the version, public key token, and so forth). You can use GACUTIL /L to display the contents of the GAC and get the precise strong name for the extension. In my case, the modified soapExtensionTypes element in my machine.config looks like this:
<soapExtensionTypes> <add type="PrioritySoapHeaderExtension.PriorityHeaderExtension, PrioritySoapHeaderExtension, Version=184.108.40.206, Culture=neutral, PublicKeyToken=db43586593d328eb, Custom=null" priority="1" group="0" /> </soapExtensionTypes>
Now our custom SoapExtension that handles the Priority SOAP header block is ready to run for all XML Web services on the machine.
By this time you should have a pretty good feel for what is possible with the SOAP header, and how you can use header blocks for your own XML Web services. Similarly, you have seen how it is possible to extend the SOAP infrastructure by using SoapExtensions to handle header-block processing. SOAP header blocks promise to be more and more popular; a wide variety of specifications are being worked on now that use header blocks to empower XML Web services with more capabilities and will make them even simpler to use. By understanding the infrastructure available for using SOAP header data in the .NET Framework, you should have a good feel for how the environment might be extended in the future.
At Your Service