Routing SOAP Messages with Web Services Enhancements 1.0

 

Aaron Skonnard
DevelopMentor

January 2003

Applies to:
    SOAP messaging
    WS-Routing specification
    WS-Referral specification
    Web Services Enhancements 1.0 for Microsoft® .NET

Summary: Covers how to use WS-Routing to define SOAP message paths and WS-Referral to influence message paths dynamically, and includes the Web Services Enhancements 1.0 for Microsoft .NET support available for both specifications. (30 printed pages)

Download the WS-Routing.exe sample for this article.

Contents

Introduction
The SOAP Processing Model
WS-Routing
WSE and Routing
WSE Routers
Specifying Paths Programmatically
WS-Referral
WSE and Referral
Updating the Referral Cache
Updating the Client
Summary

Introduction

Routing is a common requirement in most distributed applications and one that developers shouldn't have to implement themselves. The SOAP and WSDL specifications enable basic communication between heterogeneous environments but don't define a standard routing protocol. They do, however, provide an extensible framework that allows such services to be defined as independent layered modules. The Microsoft Global XML Architecture (GXA) addresses routing along with other common needs such as security and provides convenient implementations in the recently released Web Services Enhancements 1.0 for Microsoft .NET (WSE). This article examines WS-Routing, WS-Referral, and how you can begin using them today.

Routing Defined

Most distributed applications take advantage of routing in one form or another, even if the developers don't realize it. Routing is the process of an intermediary (a piece of hardware or software between the sender and the ultimate receiver) deciding where to send an incoming message as illustrated in Figure 1.

Figure 1. Basic router

In this case the client is configured to call a logical endpoint, the router in this case, which forwards the message to another server at runtime.

Routing can be implemented in hardware or software (e.g., Cisco's LocalDirector vs. Microsoft® Windows® NLB service), using a variety of different algorithms and networking protocols. Routing algorithms can perform anything from simple address translation to more advanced content analysis, where routing decisions are based on what's in the message (also known as content-based routing).

The fundamental benefit of routing is that of network virtualization, the ability to make the application's physical network topology transparent to the client. For example, the client in Figure 2 always sends messages to a logical endpoint (the router) and doesn't realize that the message is actually forwarded to one of three possible servers that are ultimately responsible for processing the request, as is the case with load balancing.

Figure 2. Routing and load balancing

Another possibility is illustrated in Figure 3 where the client sends a message to an initial router, which in turn propagates the message through a chain of additional routers before reaching the ultimate receiver that processes the request. This model is often used to build reusable units of functionality that can be selectively composed into a complete processing pipeline.

Figure 3. Multiple router intermediaries

Network virtualization decouples the client from the physical network, making it possible to introduce additional servers or redefine routing paths without affecting clients. In general, routing makes it possible to implement more flexible network architectures that are easier to scale and maintain over time. Although various forms of routing can be found on the Web today, Web services need a customized solution designed specifically for SOAP.

The SOAP Processing Model

Since the dawn of SOAP several years ago, the specification has experienced several changes and shifts in priority but the one area that has remained fairly constant is the definition of the core messaging framework, which provides an extensible message format for carrying and annotating XML messages.

SOAP defines several XML elements that are used in the messaging framework including Envelope, Header, and Body (from the https://schemas.xmlsoap.org/soap/envelope/ namespace in SOAP 1.1). The following illustrates the structure of a typical SOAP envelope:

<s:Envelope 
  xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/">
  <s:Header>
    <!-- extensible headers go here -->
  </s:Header>
  <s:Body>
    <!-- payload (XML message) goes here -->
  </s:Body>
</s:Envelope>

The Envelope element encapsulates the entire message including all headers and the payload. The Body element contains the actual payload, while the Header element provides an extensibility point for defining layered infrastructure or application-specific protocols.

This framework is similar to the way HTTP works. An HTTP message contains an extensible headers section that's separate from the message body. HTTP clients and servers use headers to negotiate things like application capabilities, security, connection management, and many other things. SOAP was also designed to support such interactions.

The HTTP specification shipped with various built-in headers to address the most common needs of higher-level applications. The SOAP specification, on the other hand, doesn't define any headers, just the framework in which they may be used. The full value of SOAP will become apparent as more and more standardized SOAP headers become widely available, like the ones we're discussing in this piece for implementing SOAP-based routing.

SOAP can also be used over multiple transports, as illustrated in Figure 4. Hence, the core messaging framework and processing model are defined at the XML message-level in order to remain transport neutral.

Figure 4. SOAP intermediaries

Those responsible for creating SOAP 1.1 clearly believed in architectures like the one depicted in Figure 4, and in the importance of message routing. This becomes evident while reading the specification—it explicitly outlines a processing model that takes these factors into account. The processing model is currently being extended in SOAP 1.2.

The SOAP processing model defines three types of nodes that make up a SOAP pipeline: a SOAP sender, an ultimate SOAP receiver, and a SOAP intermediary (a SOAP intermediary is a node that acts as both a sender and a receiver at the same time). While processing a message, a node assumes one or more roles that determine how headers are processed. Roles are given unique names (URIs) so they can be identified during processing. The SOAP 1.1 only defines a single role, https://schemas.xmlsoap.org/soap/actor/next/, which all SOAP nodes must assume. Applications are free to define custom roles as well.

SOAP headers may target specific roles through the SOAP actor attribute and be marked as mandatory for that role through SOAP's mustUnderstand attribute as illustrated here:

<soap:Envelope 
  xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/">
  <soap:Header>
    <wsrp:path soap:mustUnderstand="1"
    soap:actor="https://schemas.xmlsoap.org/soap/actor/next"
    xmlns:wsrp="https://schemas.xmlsoap.org/rp">
      ...

When a message arrives at a SOAP node, the roles assumed by the node are determined, then all mandatory headers targeted for the given node (through actor) are identified and processed. If the node wasn't designed to understand a mandatory header, it's required to generate a SOAP fault and discontinue processing. Otherwise, the header is processed and removed from the message, although nodes are allowed to (re-)insert headers if desired. So in the case of the sample path header above, the first SOAP intermediary to receive the message is required to understand and process the path header before continuing. Headers with no actor attribute can be safely ignored by intermediaries; such headers can be used to carry information required by other SOAP headers (e.g., timestamps) but are never formally processed alone.

As you can see, the SOAP specification stops just short of defining a routing protocol. It provides the necessary message framework and processing model for implementing routing in an interoperable, transport-independent manner. All we need now is a standard routing header.

WS-Routing

Most GXA specifications simply define new header elements to be used in the SOAP processing model described above. WS-Routing is one such specification that defines a new header element for routing information.

WS-Routing makes it possible to define a forward path for a SOAP message, an optional reverse path for a response SOAP message, as well as a way to correlate the two. This makes is possible for WS-Routing to support one-way messaging, two-way messaging (request/response), peer-to-peer messaging, and long-running dialogs. WS-Routing does not, however, define the means for reliable or secure messaging, although additional SOAP extensions could provide these capabilities over WS-Routing.

WS-Routing defines a single header element named path (from the https://schemas.xmlsoap.org/rp/ namespace) for specifying routing details. Here is the overall structure of the WS-Routing path header with comments describing each component:

    <!-- WS-Routing header -->
    <wsrp:path xmlns:wsrp="https://schemas.xmlsoap.org/rp">
       <!-- indicates message intent -->
       <wsrp:action />
       <!-- identifies ultimate receiver -->
       <wsrp:to />
       <!-- identifies forward intermediaries -->
       <wsrp:fwd>
         <!-- identifies an intermediary node -->
         <wsrp:via />
       </wsrp:fwd>
       <!-- identifies reverse intermediaries -->
       <wsrp:rev>
         <wsrp:via />
       </wsrp:rev>
       <!-- identifies sender -->
       <wsrp:from />
       <!-- uniquely identifies this message -->
       <wsrp:id />  
       <!-- correlates this message with another message -->
       <wsrp:relatesTo />
       <!-- provides extra routing-specific fault details,
            used in conjunction with standard SOAP Fault -->
       <wsrp:fault />
    </wsrp:path>

All of these elements are optional except for action, and typically, they're not all used at the same time. For example, the fault element will only appear in a response message if a WS-Routing fault occurred. Or in the case of HTTP-specific paths, reverse paths and correlation typically aren't necessary since the underlying request/response model implies such information.

For example, here is a path element that was injected into a SOAP message sent by a WSE client:

...
<wsrp:path soap:mustUnderstand="1"
  soap:actor="https://schemas.xmlsoap.org/soap/actor/next"
  xmlns:wsrp="https://schemas.xmlsoap.org/rp">
<wsrp:action>http://example.org/ws-routing/Echo</wsrp:action>
<wsrp:to>https://localhost/endpoints/endpoint1.asmx</wsrp:to>
<wsrp:fwd>
   <wsrp:via>https://localhost/RouterA/echo.rp</wsrp:via>
   <wsrp:via>https://localhost/RouterB/echo.rp</wsrp:via>
   <wsrp:via>https://localhost/RouterC/echo.rp</wsrp:via>
</wsrp:fwd>
<wsrp:id>uuid:fa858133-3835-4fd9-8063-c9e2ab93be73</wsrp:id>
</wsrp:path>
...

Notice it only uses action, to, fwd, and id, in order to route the message through three intermediaries, RouterA, RouterB, and RouterC, on the way to the ultimate receiver, endpoint1.asmx.

When a message arrives at a given node, and it targets one of the node's assumed roles, the node is responsible for removing the first via element and forwarding the message to the node identified by the next via in the fwd element. This continues until the message arrives at the node identified by the to element or the last via element in the forward path.

While a node is processing a forward path, it may optionally build a reverse path with the via's removed from the fwd element. For example, here is the return path for the forward path shown above:

...
<wsrp:path soap:mustUnderstand="1"
  soap:actor="https://schemas.xmlsoap.org/soap/actor/next"
  xmlns:wsrp="https://schemas.xmlsoap.org/rp">
<wsrp:action>http://example.org/ws-routing/Echo</wsrp:action>
<wsrp:to>https://localhost/endpoints/endpoint1.asmx</wsrp:to>
<wsrp:fwd/>
<wsrp:rev>
   <wsrp:via>https://localhost/RouterC/echo.rp</wsrp:via>
   <wsrp:via>https://localhost/RouterB/echo.rp</wsrp:via>
   <wsrp:via>https://localhost/RouterA/echo.rp</wsrp:via>
</wsrp:rev>
<wsrp:id>uuid:fa858133-3835-4fd9-8063-c9e2ab93be73</wsrp:id>
</wsrp:path>
...

In cases where the return path is implied by the underlying transport (e.g., two-way channels like HTTP), empty via elements can be used or the return path may be omitted altogether. For example, this return path simply used empty via's:

...
<wsrp:path soap:mustUnderstand="1"
  soap:actor="https://schemas.xmlsoap.org/soap/actor/next"
  xmlns:wsrp="https://schemas.xmlsoap.org/rp">
<wsrp:action>http://example.org/ws-routing/Echo</wsrp:action>
<wsrp:to>https://localhost/endpoints/endpoint1.asmx</wsrp:to>
<wsrp:fwd/>
<wsrp:rev>
   <wsrp:via/>
   <wsrp:via/>
   <wsrp:via/>
</wsrp:rev>
<wsrp:id>uuid:fa858133-3835-4fd9-8063-c9e2ab93be73</wsrp:id>
</wsrp:path>
...

Reverse paths are processed using the same model: it's just a forward path from the receiver back to the sender (e.g., a reverse path can be built on the way back to the sender, creating the original forward path we started with). Now let's look at the support for WS-Routing paths in the WSE.

WSE and Routing

The WSE provides an implementation of WS-Routing for use in Microsoft .NET. To get started with the WSE, download the software, run the installation, and review the documentation and quick start samples. This will give you an overview of how everything fits together.

Although WS-Routing was designed for use over various transports, such as TCP or UDP, the WSE only supports HTTP out of the box. The WSE hooks directly into the WebMethod framework on the server and into the proxy framework on the client through extension classes. This makes it extremely easy to begin using the WSE with existing or new Web service code.

To get started, the first thing you need to do is add a reference to the Microsoft.Web.Services assembly to your project (see Figure 5).

Figure 5. Reference to WSE assembly

You'll also want to add some using/import statements to your code in order bring the relevant WSE namespaces into scope. The Microsoft.Web.Services namespace contains the core WSE classes, and we'll also be using Microsoft.Web.Services.Routing and Microsoft.Web.Services.Referral as shown here:

using Microsoft.Web.Services;
using Microsoft.Web.Services.Routing;
using Microsoft.Web.Services.Referral;

In the code examples below, assume that we've added these namespace declarations to each source file even if you don't see them.

If you intend to use the WSE in conjunction with WebMethods, you also need to register the WebServiceExtension class in your Web.config file:

<configuration>
  <system.web>
     <webServices>
     <soapExtensionTypes>
        <add type="Microsoft.Web.Services.WebServicesExtension,
         Microsoft.Web.Services, Version=1.0.0.0, Culture=neutral,
         PublicKeyToken=31bf3856ad364e35" priority="1" group="0"/>
      </soapExtensionTypes>
    </webServices>
  </system.web>
</configuration>

This addition hooks into the WebMethod extensibility framework making it possible for the WSE to provide us with additional functionality before and after invocation. With this in place, you can access the WSE-provided SoapContext objects within your WebMethods as seen here:

[WebService(Namespace="http://example.org/ws-routing/")]
public class Endpoint1 : WebService
{
   [WebMethod]
   public string Echo()
   {
      SoapContext reqCtx = HttpSoapContext.RequestContext;
      SoapContext resCtx = HttpSoapContext.ResponseContext;
       ... // use Soapcontext to interface with the GXA
           // implementation of WS-Routing

   }
}

The HttpSoapContext class exposes two static properties, RequestContext and ResponseContext, both of which are of type SoapContext. SoapContext is the focal point of the WSE as it provides an interface to the supported GXA headers like those for WS-Routing, WS-Referral, and WS-Security.

To make things easier for you on the client, the WSE provides a new proxy base class called WebServicesClientProtocol. Normally Wsdl.exe (or Add Web Reference) generates a proxy class deriving from System.Web.Services.Protocols.SoapHttpClientProtocol. To take advantage of the WSE functionality on the client, you'll need to manually change the base class to Microsoft.Web.Services.WebServicesClientProtocol as shown here:

// wsdl.exe generated proxy
...
public class EndpointsProxy : 
  //manually changed base class
  Microsoft.Web.Services.WebServicesClientProtocol {
    ...
}

As with the WebMethod approach, the WebServicesClientProtocol class exposes two properties, RequestSoapContext and ResponseSoapContext for accessing GXA-header information on the client:

EndpointsProxy e = new EndpointsProxy();
SoapContext reqCtx = e.RequestSoapContext;
SoapContext resCtx = e.ResponseSoapContext;
... // use Soapcontext to interface with the GXA
    // implementation of WS-Routing

Now that you know how to configure both the client and the server, we're ready to implement more sophisticated application interactions. But before continuing, I want to highlight tracing, a very useful feature of the WSE.

It's helpful to trace the messages moving through a SOAP pipeline. You can attempt to use traditional trace utilities (like tcpTrace or the Microsoft Trace Utility that comes with the SOAP Toolkit), but it's quite cumbersome, especially in the case of routing. To simplify things, you can configure the WSE to automatically maintain trace logs by adding an entry to your configuration file.

<configuration>
  <configSections>
    <section name="microsoft.web.services" type=
     "Microsoft.Web.Services.Configuration.WebServicesConfiguration,
     Microsoft.Web.Services, Version=1.0.0.0, Culture=neutral,
PublicKeyToken=31bf3856ad364e35" />
  </configSections>
  <microsoft.web.services>        
    <diagnostics>
      <trace enabled="true" input="inputtrace.xml"
output="outputtrace.xml"/>
    </diagnostics> 
  </microsoft.web.services>
</configuration>

Notice that you have to add a configSection entry for microsoft.web.services before you can begin using it. You can add this to Web.config to trace WebMethod input/output messages and you can also add it to your client application configuration file (e.g., Client.exe.config) to trace the Wsdl.exe-generated proxy's input/output messages. You can also add it to any virtual directory acting as a WSE router to trace the input/output messages of Microsoft.Web.Services.Routing.RouterHandler.

WSE Routers

RoutingHandler is a class that knows how to process WS-Routing paths. All you have to do to configure a router is add an entry to your Web.config that specifies when to use RoutingHandler. For example, consider the following Web.config:

<configuration>
  <system.web>
    <httpHandlers>
      <add verb="*" path="*.rp"
       type="Microsoft.Web.Services.Routing.RoutingHandler,
       Microsoft.Web.Services, Version=1.0.0.0,
       Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    </httpHandlers>
  </system.web>
</configuration>

In this case I decided to make up a new file extension, .rp (routing protocol), to use as logical endpoint identifiers. The httpHandlers section contains an entry mapping incoming HTTP requests with an .rp extension to the RoutingHandler class. Hence, placing this Web.config into a virtual directory enables WS-Routing on .rp requests in that directory.

However, since I invented the .rp extension, I also have to configure IIS to hand off .rp requests to the Microsoft .NET HTTP pipeline (through aspnet_isapi.dll), otherwise they'll never make it to RouterHandler. You can do this by adding a new mapping in the IIS administration console as shown in Figure 6 (make sure you disable the Verify that file exists option).

Figure 6. IIS application configuration

To experiment with routing, I created three virtual root directories called RouterA, RouterB, and RouterC and placed this Web.config file into each of them, effectively making each one a WSE router. I also created a Microsoft® Visual Studio® .NET Web service project called Endpoints, which contains several simple .asmx classes (endpoint1.asmx, endpoint2.asmx, and endpoint3.asmx), each of which contains an identical Echo method that echoes back the page's URL along with the WS-Routing action value as shown here:

[WebMethod]
public string Echo()
{
   SoapContext sctx = HttpSoapContext.RequestContext;
   return string.Format("Echo: {0}, action={1}",
      HttpContext.Current.Request.Url, 
      ((sctx != null) && (sctx.Path != null)) ? 
            sctx.Path.Action : "NONE");
}

Now it's possible to post SOAP messages containing WS-Routing paths to any of the router endpoints and the forward path information will be processed accordingly. For example, consider the following SOAP message that contains a forward path of RouterA, RouterB, and RouterC, followed by the ultimate receiver, in this case endpoint1.asmx (just like Figure 4, but using HTTP):

<soap:Envelope 
  xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/">
  <soap:Header>
    <wsrp:path soap:mustUnderstand="1"
     soap:actor="https://schemas.xmlsoap.org/soap/actor/next" 
     xmlns:wsrp="https://schemas.xmlsoap.org/rp">
    <wsrp:action>http://example.org/ws-routing/Echo</wsrp:action>
 <wsrp:to>https://localhost/endpoints/endpoint1.asmx</wsrp:to>
    <wsrp:fwd>
        <wsrp:via>https://localhost/RouterA/echo.rp</wsrp:via>
        <wsrp:via>https://localhost/RouterB/echo.rp</wsrp:via>
        <wsrp:via>https://localhost/RouterC/echo.rp</wsrp:via>
    </wsrp:fwd>
    <wsrp:id>uuid:fa858133-3835-4fd9-8063-c9e2ab93be73</wsrp:id>
    </wsrp:path>
  </soap:Header>
  <soap:Body>
    <Echo xmlns="http://example.org/ws-routing/" />
  </soap:Body>
</soap:Envelope>

Posting this message to https://localhost/RouterA/echo.rp (using a simple HTTP post utility) produces the following SOAP response message:

<soap:Envelope xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/">
  <soap:Header>...</soap:Header>
  <soap:Body>
    <EchoResponse xmlns="http://example.org/ws-routing/">
      <EchoResult>Echo:
       https://localhost/endpoints/endpoint1.asmx,
       action=http://example.org/ws-routing/Echo
      </EchoResult>
    </EchoResponse>
  </soap:Body>
</soap:Envelope>

This indicates that the message traveled through the specified forward path to the ultimate receiver (endpoint1.asmx). Let's briefly look at what happened to the message as it traveled through the pipeline by inspecting the WSE trace files.

We know what the original message looks like since we created it (see above). RouterA processed the original message and forwarded it on to RouterB with a modified path. The message leaving RouterA and entering RouterB contained the following path header:

...
<wsrp:path soap:mustUnderstand="1"
 soap:actor="https://schemas.xmlsoap.org/soap/actor/next"
 xmlns:wsrp="https://schemas.xmlsoap.org/rp">
<wsrp:action>http://example.org/ws-routing/Echo</wsrp:action>
<wsrp:to>https://localhost/endpoints/endpoint1.asmx</wsrp:to>
<wsrp:fwd>
   <wsrp:via>https://localhost/RouterB/echo.rp</wsrp:via>
   <wsrp:via>https://localhost/RouterC/echo.rp</wsrp:via>
</wsrp:fwd>
<wsrp:id>uuid:5efb4bcc-3682-4c20-9e1c-c22e95fcf927</wsrp:id>
</wsrp:path>
...

Notice that the via element identifying RouterA was removed from the forward message path as it's already been traversed. The message leaving RouterB, headed for RouterC, contained the following path header:

...
<wsrp:path soap:mustUnderstand="1"
 soap:actor="https://schemas.xmlsoap.org/soap/actor/next"
 xmlns:wsrp="https://schemas.xmlsoap.org/rp">
<wsrp:action>http://example.org/ws-routing/Echo</wsrp:action>
<wsrp:to>https://localhost/endpoints/endpoint1.asmx</wsrp:to>
<wsrp:fwd>
   <wsrp:via>https://localhost/RouterC/echo.rp</wsrp:via>
</wsrp:fwd>
<wsrp:id>uuid:5efb4bcc-3682-4c20-9e1c-c22e95fcf927</wsrp:id>
</wsrp:path>
...

And finally, the message leaving RouterC headed for the ultimate receiver (endpoint1.asmx) contains the following path information, which no longer has a forward message path:

...
<wsrp:path soap:mustUnderstand="1"
 soap:actor="https://schemas.xmlsoap.org/soap/actor/next"
 xmlns:wsrp="https://schemas.xmlsoap.org/rp">
<wsrp:action>http://example.org/ws-routing/Echo</wsrp:action>
<wsrp:to>https://localhost/endpoints/endpoint1.asmx</wsrp:to>
<wsrp:id>uuid:5efb4bcc-3682-4c20-9e1c-c22e95fcf927</wsrp:id>
</wsrp:path>
...

After endpoint1.asmx invokes the operation, the return message follows the same path, in reverse, back to the sender through the underlying HTTP connections.

Specifying Paths Programmatically

The WSE makes it possible to specify routing paths programmatically, through the SoapContext Path property. The Path property gives you access to the all the elements that make up a WS-Routing path header. For example, the following code illustrates how to generate the WS-Routing header shown above:

...
EndpointsProxy e = new EndpointsProxy();
e.Url = "https://localhost/endpoints/endpoint1.asmx";
SoapContext ctx = e.RequestSoapContext;
ctx.Path.Fwd.Add(new Via(new 
  Uri("https://localhost/RouterA/echo.rp")));
ctx.Path.Fwd.Add(new Via(new 
  Uri("https://localhost/RouterB/echo.rp")));
ctx.Path.Fwd.Add(new Via(new 
  Uri("https://localhost/RouterC/echo.rp")));
Console.WriteLine(e.Echo());

The program simply adds a new Via object for each intermediary node in the desired forward path. The proxy automatically takes care of filling in certain parts of the path header like action (SOAPAction value), to (proxy's Url property), and id (autogenerated unique id).

Running this client program produces the following console output:

Echo: https://localhost/endpoints/endpoint1.asmx,
 action=http://example.org/ws-routing/Echo

The handler doesn't build reverse paths while processing forward paths unless it finds a rev element in the message. You can add a rev element to the original message by assigning the Rev property an empty ViaCollection:

EndpointsProxy e = new EndpointsProxy();
// build forward path (see above)
...
// creates an empty rev element in original message
ctx.Path.Rev = new ViaCollection();
Console.WriteLine(e.Echo());

With this in place, you'll see the correct reverse path in the final message entering endpoint1.asmx (check out the input trace file).

As you can see, WS-Routing simply provides a mechanism for specifying routing paths within SOAP messages. This implies that something or someone has to build the path and place it in the SOAP message. We obviously don't want to force client applications to know the intimate details of our network architecture in order to take advantage of routing. So what we need is a way to dynamically discover and influence routing paths at runtime.

One way to accomplish this is through content-based routing. You can implement content-based routing with the WSE by writing a custom handler that derives from Microsoft.Web.Services.Routing.RoutingHandler and overrides ProcessRequestMessage. Inside of ProcessRequestMessage, you can inspect the incoming SOAP message and programmatically modify the path header based on what you find (see the WSE samples for a complete example). Another technique for influencing message paths is through declarative referral instructions.

WS-Referral

WS-Referral is another GXA specification that makes it possible influence the dynamic creation of routing paths. The WS-Referral elements make it possible to describe simple routing instructions based on the URI of an incoming request. The following illustrates the basic structure of the WS-Referral referrals element:

<!-- WS-Referral header -->
<r:referrals
    xmlns:r="https://schemas.xmlsoap.org/ws/2001/10/referral">
    <!-- referral routing instruction -->
    <r:ref>
        <!-- URI this referral applies to -->
        <!-- either an exact match or a prefix match -->
        <r:for>
            <r:exact /> | <r:prefix />
        </r:for>
        <!-- referral conditions -->
        <r:if > 
            <!-- time to live -->
            <r:ttl />
            <!-- existing refId this referral invalidates -->
            <r:invalidates>
                <r:rid />
            </r:invalidates>
        </r:if>
        <!-- new route location -->
        <r:go>
            <r:via/>
        </r:go>
        <!-- unique referral identifier -->
        <r:refId />
    </r:ref>
</r:referrals>

The referrals element may contain multiple ref elements, each of which provides a separate routing instruction. The for element identifies the URI(s) the routing instruction applies to, either through an exact URI match or a more general URI-prefix match, and the go element specifies how to re-route the request through child via elements. Referrals can also be assigned a unique id, through the refId element, as well as a set of conditions (if).

For example, the following referral re-routes requests for https://localhost/referralrouter/echo.rp to https://localhost/endpoints/endpoint2.asmx:

<r:referrals 
  xmlns:r="https://schemas.xmlsoap.org/ws/2001/10/referral">
  <r:ref>
    <r:for>
     <r:exact>https://localhost/referralrouter/echo.rp</r:exact>
    </r:for>
    <r:if />
    <r:go>
     <r:via>https://localhost/endpoints/endpoint2.asmx</r:via>
    </r:go>
    <r:refId>uuid:fa469956-0057-4e77-962a-81c5e292f2ae</r:refId>
  </r:ref>
</r:referrals>

We can use this information in combination with SOAP and WS-Routing to dynamically build message paths.

WSE and Referral

The WSE makes it possible to take advantage of WS-Referral in various ways. One of the most advantageous uses is in building smarter routers. You can configure RouterHandler to read referrals from an XML file on disk, also known as a referral cache, and it will take the routing instructions into account while processing message paths.

To make this possible, add a referral element to the microsoft.web.services section of your Web.config file specifying the location of the referral cache file:

<configuration>
  <configSections>
    <section name="microsoft.web.services" type=
     "Microsoft.Web.Services.Configuration.WebServicesConfiguration,
     Microsoft.Web.Services, Version=1.0.0.0, Culture=neutral,
     PublicKeyToken=31bf3856ad364e35" />
  </configSections>
  <microsoft.web.services>
    <referral>
      <cache name="referralCache.config" />
   </referral>                   
  </microsoft.web.services>
</configuration>

You may want to use a file extension like .config to prevent clients from surfing to the file. You'll also have give the ASPNET account read/write permissions to the file (IIS_WPG on Microsoft® Windows® Server 2003), otherwise you'll get an access denied error when the router attempts to use the file.

To experiment, let's create a new virtual root directory called ReferralRouter that contains a Web.config file configured for routing .rp requests using the referral cache shown above. Then your clients can target https://localhost/referralrouter/echo.rp as shown here:

EndpointsProxy e = new EndpointsProxy();
e.Url = "https://localhost/referralrouter/echo.rp";
Console.WriteLine(e.Echo());

This code produces the following console output:

Echo: https://localhost/endpoints/endpoint2.asmx,
 action=http://example.org/ws-routing/Echo

The original message generated by this code contains a minimal WS-Routing header (no forward path information). When the message arrives at the ReferralRouter virtual directory, the RouterHandler inspects the referral cache, determines where it should go from there, and updates the message path based on the routing instructions. In this case, it goes directly to the endpoint (https://localhost/endpoints/endpoint2.asmx). The message leaving ReferralRouter looks like this:

<soap:Envelope 
  xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/">
 <soap:Header>
  <wsrp:path soap:mustUnderstand="1"  
    soap:actor="https://schemas.xmlsoap.org/soap/actor/next"   
    xmlns:wsrp="https://schemas.xmlsoap.org/rp">
    <wsrp:action>http://example.org/ws-routing/Echo</wsrp:action>
    <wsrp:to>https://localhost/referralrouter/echo.rp
    </wsrp:to>
    <wsrp:fwd>
      <wsrp:via>https://localhost/endpoints/endpoint2.asmx
      </wsrp:via>
    </wsrp:fwd>
    <wsrp:id>uuid:5ae8b275-c863-4b6d-97d9-734adcd50348
    </wsrp:id>
  </wsrp:path>
 </soap:Header>
 <soap:Body>
  <Echo xmlns="http://example.org/ws-routing/" />
 </soap:Body>
</soap:Envelope>

Notice that the forward routing path was dynamically generated based on the routing instructions found in the referral cache. This technique makes it possible to build Web service clients in terms of logical endpoints that translate into physical locations at runtime, thus easing maintenance issues and providing more long-term flexibility in terms of network architecture.

As long as the client is configured to call the virtual endpoint, https://localhost/referralrouter/echo.rp, we can temporarily redirect all Echo invocations to a different server by simply updating the referral cache file—no changes to existing clients whatsoever.

The easiest way to do this while the existing referral cache file is in use is to prepare a new referral cache file and modify Web.config. Then, the next time the Web service is invoked, the new referral cache will be used. For example, assume that I've created a temporary referral cache file named ReferralCacheTemp.config that redirects requests for https://localhost/referralcache/echo.rp to a new destination of https://localhost/endpoints/endpoint3.asmx, and that I update my Web.config as follows:

<configuration>
  <configSections>
    <section name="microsoft.web.services"
     type="Microsoft.Web.Services.Configuration.WebServicesConfiguration,
     Microsoft.Web.Services, Version=1.0.0.0, Culture=neutral,
     PublicKeyToken=31bf3856ad364e35" />
  </configSections>
  <microsoft.web.services>
    <referral>
      <cache name="referralCacheTemp.config" />
   </referral>                   
  </microsoft.web.services>
</configuration>

Then, the next time the client application runs it produces the following console output:

Echo: https://localhost/endpoints/endpoint3.asmx,
 action=http://example.org/ws-routing/Echo

The downside to this technique is that it requires you to manually update configuration files. It would be better if it were possible to dynamically update routing instructions found in the referral cache.

Updating the Referral Cache

Referrals can be used to dynamically update routing instructions at runtime. This can be accomplished by sending the referrals element as a SOAP header that travels with the message. Routers or clients that receive such referrals can choose to either accept or reject the updates. This technique brings with it serious security concerns that must be addressed in order to safely identify the sender and trust the referral. This is exactly the type of situation that WS-Security was designed to address, but requires you to write some additional code today.

Due to these security concerns, the RoutingHandler doesn't automatically update the referral cache while processing SOAP messages. It's not difficult, however, to write an administration utility that makes this possible. The WSE comes with a class called ReferralCache that provides a static Register method for registering routing instructions with the referral cache. Plus, you can traverse the referrals found in a SOAP message through the SoapContext Referrals property.

The following WebMethod blindly reads all referrals from the incoming message and registers them with the virtual directory's referral cache:

[WebService(Namespace="http://example.org/ws-referral")]
public class RouterConfiguration : WebService
{
   [WebMethod]
   public void UpdateReferralCache()
   {
     SoapContext reqCtx = HttpSoapContext.RequestContext;
     foreach (Referral r in reqCtx.Referrals)
       ReferralCache.Register(r);
   }
}

You can place this WebMethod in any virtual directory that performs routing and invoke it to dynamically update routing instructions. I've provided a sample administration application that makes this possible (see Figure 7).

Figure 7. Router configuration utility

You have to specify the router you'd like to update, the URI the routing instruction applies to (for), and the new routing information (go). If it's an update, you also have to specify the ID of the previous referral that the new referral invalidates. When the administrator clicks Send the code adds a referral to the SOAP message before invoking the dummy method:

// Web service proxy class
RouterConfiguration rc = new RouterConfiguration();
rc.Url = txtRouter.Text;
Referral r = new Referral();
r.For.Exact = new Uri(txtFor.Text);
r.Go.Add(new Via(new Uri(txtGo.Text)));
r.If.Invalidates.Add(new Uri(txtInvalidates.Text));
rc.RequestSoapContext.Referrals.Add(r);
rc.UpdateReferralCache();
MessageBox.Show("Referral cache updated!");

You can invoke a Web service, modify the referral cache using this utility, and then invoke it again to see the update take effect. If you decide to do something like this, make sure you think through the security issues to ensure that only your administrators are able to invoke the UpdateReferralCache operation.

Using referral-driven routing decouples the client from the network architecture and makes the application easier to evolve over time. It requires, however, routing logic to be executed for every incoming request. You may want to per-request routing if you're using a DMZ machine or more sophisticated content-based routing algorithms. It's a tradeoff between flexibility and efficiency.

Updating the Client

You can get around some of the per-request routing logic by configuring clients with a local referral cache that the proxy infrastructure looks at to build the initial routing header. You configure a client's referral cache file in the application configuration file (Client.exe.config instead of Web.config). This reduces the burden on intermediary routers but makes it harder to update clients.

You could, however, make it possible to dynamically update the client's referral cache by writing some code to process referrals received in the SOAP response. For example, the following code invokes the Echo method and then updates the local referral cache with any referrals found in the message:

EndpointsProxy e = new EndpointsProxy();
e.Url = "https://localhost/RouterA/echo.rp";
Console.WriteLine(e.Echo());

SoapContext respCtx = e.ResponseSoapContext;
foreach(Referral r in respCtx.Referrals)   
   ReferralCache.Register(r);

Now it's possible for the implementation of Echo to return referrals to the client as part of the message response as shown here:

[WebMethod]
public string Echo()
{
   SoapContext respCtx = HttpSoapContext.ResponseContext;

   // build referral to update client
   Referral r = new Referral();
   r.For.Exact = 
      new Uri("https://localhost/RouterA/echo.rp");
   r.Go.Add(new Via(
      new Uri("https://localhost/endpoints/endpoint1.asmx")));
   r.If.Invalidates.Add(
      new Uri("uuid:fa469956-0057-4e77-962a-81c5e292f2ae"));
   respCtx.Referrals.Add(r);

   // use an empty SOAP actor so intermediaries don't 
   // swallow the referral on the way back to the client
   // (same as SOAP 1.2's none role:
// http://www.w3.org/2002/06/soap-envelope/role/none)
   respCtx.Referrals.Actor = "";

   SoapContext sctx = HttpSoapContext.RequestContext;
   return string.Format("Echo: {0}, action={1}",
      HttpContext.Current.Request.Url, 
      ((sctx != null) && (sctx.Path != null)) ? 
           sctx.Path.Action : "NONE");
}

Assuming the original client referral cache points to endpoint3.asmx, the first invocation of the client would print:

Echo: https://localhost/endpoints/endpoint3.asmx,
 action=http://example.org/ws-routing/Echo

And after receiving the response, future invocations would print:

Echo: https://localhost/endpoints/endpoint1.asmx,
 action=http://example.org/ws-routing/Echo

This proves the client was dynamically updated by the first SOAP response. As with dynamic router updates, you need to think about security when approaching this type of design. Again, WS-Security provides the necessary framework for such safeguarding but you'll have to write some code to integrate your security needs with the routing techniques presented here—check out Understanding WS-Security for more information.

Summary

Routing is needed in most distributed applications because it helps provide more flexible network architectures. Although the SOAP specification introduced a processing model based on intermediary nodes, it stopped short of defining a complete routing protocol. WS-Routing fills that need by defining a SOAP header for specifying routing paths. WS-Referral enhances the solution by making it possible to influence routing paths with dynamic instructions. The WSE provides nice support for both specifications, neatly integrated with WebMethods on the server and generated proxies on the client.