Service Station

Building a WCF Router, Part 1

Michele Leroux Bustamante

Code download available at:ServiceStation2008_04.exe(238 KB)

Contents

Default Addressing Semantics
Routing Architecture
The Router Contract
Forwarding Messages
Logical and Physical Addressing
MustUnderstand Headers

Hosting and consuming a Windows® Communication Foundation (WCF) service usually requires a few fundamental steps: implement the service, configure endpoints where the service can be reached, host the service, generate a Web Services Description Language (WSDL) file or enable metadata exchange so that clients can generate a proxy to call the service, write code to instantiate the proxy with its associated configuration, and start calling service operations. You rarely need to take a look under the hood, but even in the simplest case, both the client and service channels rely on compatible configuration to handle addressing semantics and message filtering to ensure the correct operation is invoked.

Sometimes it is useful to introduce an intermediary or router service between a client and a target service to receive messages that flow between them and perform additional activities such as logging, priority routing, online/offline routing, load balancing, or to introduce a security boundary. When such an intermediate service is introduced, it becomes necessary to tweak some addressing and message filtering behaviors to accommodate.

So let's take a closer look at how you work with intermediate services, which for simplicity I'll refer to collectively as routers. In this issue, I'll explain WCF addressing and message filtering concepts while focusing on the router scenario, and I'll also explain some options for routing configuration along with the appropriate settings. In Part 2 of this series, I'll show you how to leverage this foundation for more advanced, practical routing implementations.

Default Addressing Semantics

In the June 2007 Service Station column (msdn.microsoft.com/msdnmag/issues/07/06/ServiceStation), Aaron Skonnard explained how WCF handles logical and physical endpoint addressing, addressing headers, and message filtering. In this section, I will review some of those fundamental addressing features and how they impact routing scenarios—but you will also find Aaron's column useful for additional details behind these WCF features.

Typically, clients send messages directly to the target service using a proxy generated from the service description. In order for the client and service to be compatible, they share equivalent contracts and endpoint configurations. Considering the service contract and configuration shown in Figure 1, you can derive several important addressing requirements for the service.

Figure 1 Service Contract and Endpoint Configuration

Service Contract

[ServiceContract(Namespace =
"https://www.thatindigogirl.com/samples/2008/01")]
public interface IMessageManagerService
{
  [OperationContract]
  string SendMessage(string msg);

  [OperationContract]
  void SendOneWayMessage(string msg);
}

Endpoint Configuration

<system.serviceModel>
  <services>
    <service name="MessageManager.MessageManagerService"
      behaviorConfiguration="serviceBehavior">
      <endpoint
        address="https://localhost:8000/MessageManagerService"
        contract="MessageManager.IMessageManagerService"
        binding="basicHttpBinding" />
    </service>
  </services>
</system.serviceModel>

First, the expected Action addressing header for a request to the SendMessage operation is:

https://www.thatindigogirl.com/samples/2008/01/IMessageManagerService/SendMessage

Since OperationContractAttribute doesn't specify an Action, this value is derived from the service contract namespace, the contract name (which defaults to the interface name), and the operation name (which defaults to the method name).

Second, the expected Action header for a response from SendMessage is:

https://www.thatindigogirl.com/samples/2008/01/IMessageManagerService/SendMessageResponse

Since the OperationContractAttribute doesn't specify a ReplyAction, this value is derived in the same way as the Action property, with the suffix "Response" appended.

Finally, the expected To header for messages targeting the service endpoint is:

https://localhost:8000/MessageManagerService

This value is derived from the address attribute for the endpoint element, which is considered the logical address of the endpoint. While it can be otherwise specified, the physical address of the endpoint usually matches the logical address. This means clients usually send messages to a physical address that matches the To header.

Service metadata describes these requirements such that clients can generate a compatible proxy and configuration. The service contract generated for the client reflects the same Action and ReplyAction settings of the service, and the client binding configuration will reflect an endpoint with the appropriate logical and physical address. For example, the following client endpoint is compatible with the service in Figure 1:

<client>
  <endpoint 
    address="https://localhost:8000/MessageManagerService"
    binding="basicHttpBinding" 
    contract="localhost.IMessageManagerService" 
    name="basicHttp" />
</client>

The client proxy uses the address property of the client endpoint element for both its logical and physical address. As a result, messages are sent to a physical address that matches the To header, as I indicated earlier. When the proxy calls the SendMessage operation, a message is sent with the To and Action headers that are shown in Figure 2.

Figure 2 Message Sent from Proxy

<s:Envelope xmlns:s="https://schemas.xmlsoap.org/soap/envelope/">
  <s:Header>
    <To s:mustUnderstand="1"
      xmlns="https://schemas.microsoft.com/ws/2005/05/addressing/none">
      https://localhost:8000/MessageManagerService
    </To>
    <Action s:mustUnderstand="1" 
      xmlns="https://schemas.microsoft.com/ws/2005/05/addressing/none">
      https://www.thatindigogirl.com/samples/2008/01
        /IMessageManagerService/SendMessage
    </Action>
  </s:Header>
  <s:Body>
    <SendMessage xmlns="https://www.thatindigogirl.com/samples/2008/01">
      <msg>test</msg>
    </SendMessage>
  </s:Body>
</s:Envelope>

The combination of the To and Action headers, respectively, indicate to the service model which channel dispatcher should process the message and which operation the dispatcher should invoke. By default, there must be an endpoint with a matching logical address to the To header and an operation that matches the Action header. Figure 3 illustrates this flow.

Figure 3 Typical Addressing without a Router

Figure 3** Typical Addressing without a Router **(Click the image for a larger view)

In the following sections I will explain the implications of the logical and physical address, the To and Action headers, and the message filtering rules when a router is introduced into the scenario.

Routing Architecture

Although there are some variations on how you might design a router, most routers must be able to receive messages that can target any service, and they must be able to forward the original message to the appropriate target service. There are two fundamental approaches to designing a router: a pass-through router or a processing router.

A pass-through router is transparent to the client. The client's relationship is with downstream services, but messages just happen to flow through the router. The client must send the message to the router using a compatible transport protocol and message encoder, but any security, reliable sessions, application sessions, or other messaging protocols required by the service channel are satisfied by the message generated at the client. The router may look at message headers or even inject headers, but the original message elements are still forwarded to the service unaltered. Figure 4 illustrates this relationship.

Figure 4 Operation of a Pass-Through Router

Figure 4** Operation of a Pass-Through Router **(Click the image for a larger view)

A processing router plays a more active role in processing messages for the application. Thus, the client's relationship is with the router as far as transport, encoding, and protocol compatibility go, though the client must still additionally be able to send messages that are compatible with downstream services. Messages flow through the router to services, and the message body—along with any required headers for the service—is left intact.

Security, reliable sessions, and headers or messages related to other communication protocols are usually handled by the router, and the router builds a new message with appropriate communication protocols for downstream services. Figure 5 illustrates compatibility for a processing router.

Figure 5 Operation of a Processing Router

Figure 5** Operation of a Processing Router **(Click the image for a larger view)

Each routing configuration has practical implementations. It is also possible to implement a hybrid solution somewhere between the two.

The Router Contract

Routers receive messages intended for downstream services and are responsible for forwarding those messages to the appropriate service. They are also responsible for receiving responses from the service and returning those to the client. A typical router contract exposes a single operation that can process any message request or response. In the following example, that operation is called ProcessMessage:

[ServiceContract(Namespace = 
"https://www.thatindigogirl.com/samples/2008/01")]
public interface IRouterService {
  [OperationContract(Action = "*", ReplyAction = "*")]
  Message ProcessMessage(Message requestMessage);
}

In a normal situation, the Action and ReplyAction properties of the OperationContractAttribute are derived as discussed earlier in this column—from the service contract namespace, the contract name, and the operation name. By default, when a message arrives, the channel dispatcher must find an operation to match the exact Action header. However, in a case when Action and ReplyAction are set to "*", the channel dispatcher will send any messages not mapped to a specific operation to that catch-all operation regardless of the Action header value. And in order to avoid ambiguity, only one operation can specify "*" for the Action or ReplyAction property.

A typical router supplies a single operation like ProcessMessage that can handle any message that reaches it. While the Action and ReplyAction will ensure the message is mapped to ProcessMessage by the channel dispatcher, the operation signature must also be capable of processing any message.

In order to address this, ProcessMessage receives and returns untyped messages in the form of the Message type. The router can access the header collection and the message body through this type, but no automatic serialization occurs beyond the common addressing headers, which are deserialized and made available through strongly typed properties.

Any further processing of the messages is up to the router implementation. A basic router will simply receive the untyped message and forward it as is to downstream services, awaiting a reply. Likewise, the reply is forwarded to the calling client in the same raw format.

Forwarding Messages

After the router receives a message and processes it according to its own requirements, it forwards the message to the appropriate downstream service for further processing. A simple router implementation for the contract discussed earlier is shown in Figure 6. ProcessMessage constructs a client channel (or proxy) with ChannelFactory<T> and uses this proxy to forward messages to a particular service endpoint, returning any responses.

Figure 6 A Simple Router Implementation

[ServiceBehavior(
  InstanceContextMode = InstanceContextMode.Single, 
  ConcurrencyMode = ConcurrencyMode.Multiple)]
public class RouterService : IRouterService {
  public Message ProcessMessage(Message requestMessage) {
    using (ChannelFactory<IRouterService> factory = 
      new ChannelFactory<IRouterService>("serviceEndpoint")) {

      IRouterService proxy = factory.CreateChannel();

      using (proxy as IDisposable) {
        return proxy.ProcessMessage(requestMessage);
      }
    }
  }
}

Proxies are usually strongly typed to the target service contract, but in this case the proxy should be able to forward any message and receive any reply—something that the router contract facilitates. In this simple example, the router merely forwards the original message to the target service and returns any response. If the operation at the target service is one-way, no reply is sent.

Since the contract works with untyped messages, the same message is forwarded to the service, as shown in Figure 7. But you must realize that there is, however, one change made to the message that you may not expect: the To header is altered before the message is sent to the service.

Figure 7 Addressing Semantics through a Simple Router

Figure 7** Addressing Semantics through a Simple Router **(Click the image for a larger view)

Recall that, by default, the proxy will use the logical address from its endpoint configuration to set the To header for outgoing messages—even if a raw Message instance is passed that already has a To header. While this may seem like a good thing—since all services require that the To header match the logical address of one of their service endpoints—it can cause other side effects. For one, if the updated To header is not signed and the service has security enabled, the message will be rejected.

Ideally, the client should send a message with a To header matching the target service, the router should accept that message despite the mismatch, and the router should forward this message to the service without altering the To header. This can be handled through binding configurations, which I'll discuss next.

Logical and Physical Addressing

When you introduce a router to the application architecture, it is always best if the client can send messages using the correct To header for the service while still sending the message to the router. One way to achieve this is to configure the client to use ClientViaBehavior, as in the configuration shown in Figure 8. This tells the client proxy to generate a message with a To header according to the endpoint's logical address, but to send it via the router's physical address. The problem is that this couples the client to the existence of a router.

Figure 8 Using ClientViaBehavior

<client>
  <endpoint address="https://localhost:8000/MessageManagerService"
    binding="wsHttpBinding"  
    bindingConfiguration="wsHttpNoSecurity"
    contract="localhost.IMessageManagerService" 
    name="basicHttp" 
    behaviorConfiguration="viaBehavior"/>
</client>
<behaviors>
  <endpointBehaviors>
    <behavior name="viaBehavior">
      <clientVia viaUri="https://localhost:8010/RouterService"/>
    </behavior>
  </endpointBehaviors>
</behaviors>

Another way to address this is to have the service configure the listenUri attribute for its endpoints so that the logical address of the service is that of the router while the physical address is specific to the service. Consider this service configuration:

<endpoint address="https://localhost:8010/RouterService" 
  contract="MessageManager.IMessageManagerService" 
  binding="wsHttpBinding"
  bindingConfiguration="wsHttpNoSecurity" 
  listenUri="https://localhost:8000/MessageManagerService"/>

The resulting service metadata publishes the router address to clients, so client endpoints reflect the router address. I don't really like this solution because it couples the service to the router and ideally the service doesn't need to know about it either.

The alternative is to have services use a logical address that is a type of URI that is not tied to the router or service, and then manually tell clients the physical address to send messages to since it won't be part of the metadata. Here is an example of this type of endpoint configuration:

<endpoint address="urn:MessageManagerService" 
  contract="MessageManager.IMessageManagerService" 
  binding="wsHttpBinding"  
  bindingConfiguration="wsHttpNoSecurity" 
  listenUri="https://localhost:8000/MessageManagerService"/>

In either case, the service will receive a To header matching its endpoint configuration and the router will receive the message first.

The router should really bear the burden of configuration so that clients and services are not dependent on its presence. Thus, the To header will never match the router's logical address. By default, services use the EndpointAddressMessageFilter to determine whether a message's To header matches any of its configured endpoints. Since a router can't expect this to work, it should install the MatchAllMessageFilter.

The ServiceBehaviorAttribute supports this through the AddressFilterMode property, which can be set to one of the AddressFilterMode enumerations: Exact (the default), Prefix, or Any. Since the router prefix can't be guaranteed to match all services it receives messages for, it makes sense to allow all To headers to pass through, like so:

[ServiceBehavior(InstanceContextMode = 
  InstanceContextMode.Single, 
  ConcurrencyMode = ConcurrencyMode.Multiple, 
  AddressFilterMode=AddressFilterMode.Any)]
public class RouterService : IRouterService

By default, the To header will always be updated to match a proxy's logical address, based on its endpoint configuration—regardless of whether the To address is already set to the correct value. To suppress this behavior so that the router can forward messages to the service using the original To header, the router must use a binding configuration with manual addressing. This is not a property that can be set on any of the standard bindings, so you must use custom binding to achieve this.

The following code snippet illustrates a customBinding section that sets this feature for the HTTP transport channel:

<customBinding>
  <binding name="manualAddressing">
    <textMessageEncoding />
    <httpTransport manualAddressing="true"/>
  </binding>
 </customBinding>

This facilitates the addressing flow shown in Figure 9 where headers are not altered.

Figure 9 Addressing Semantics through a Router with Manual Addressing

Figure 9** Addressing Semantics through a Router with Manual Addressing **(Click the image for a larger view)

MustUnderstand Headers

So far I have focused on a simple routing implementation to illustrate core router design considerations that also impact addressing, filtering, and binding configuration. This simple routing solution only works if the service does not enable security, reliable sessions, or any other rich protocol for its bindings. Figure 10 shows a simplified view of the binding protocols I'm assuming for the discussion thus far.

Figure 10 Service Contract and Endpoint Configuration

Figure 10** Service Contract and Endpoint Configuration **(Click the image for a larger view)

Figure 11 shows the same view for a pass-through router, when the service requires security and reliable sessions. Enabling these protocols means that the client and service channels will exchange additional messages to establish sessions, request security tokens, and other related messaging. Since the router allows all messages to pass through, these protocol-specific messages will also pass through to the service—and that is a good thing.

Figure 11 Pass-Through Configuration with Security and Reliable Sessions

Figure 11** Pass-Through Configuration with Security and Reliable Sessions **(Click the image for a larger view)

A problem arises, however, when the messages to and from the service include headers that must be understood by their recipient. Since the pass-through router purposely does not have security or reliable sessions enabled, those channels are not present to process associated protocol headers.

You can instruct the router service to ignore MustUnderstand headers by setting the ValidateMustUnderstand property of the ServiceBehaviorAttribute to false, as shown here:

[ServiceBehavior(InstanceContextMode = 
  InstanceContextMode.Single, 
  ConcurrencyMode = ConcurrencyMode.Multiple, 
  AddressFilterMode=AddressFilterMode.Any, 
  ValidateMustUnderstand=false)]
public class RouterService : IRouterService

This will address incoming messages from the client, but it won't address messages returned from downstream services.

To address this, you must also modify the router implementation to specify this behavior when initializing the channel factory for calling the downstream service, like so:

using (ChannelFactory<IRouterService> factory = 
  new ChannelFactory<IRouterService>("serviceEndpoint"))
{
  factory.Endpoint.Behaviors.Add(new MustUnderstandBehavior(false));
  IRouterService proxy = factory.CreateChannel();
  
  // remaining code
}

Now, protocol and service messages can freely flow between client and service, through the router—assuming that the protocol used is HTTP.

Another complication arises when duplex protocols such as TCP or named pipes are employed. This means that messages can be initiated by the service to the client, such as when reliable sessions are enabled. There is an advanced router configuration you can use to handle this special case, but I'll address that scenario and its practicality in Part 2 of this series.

Send your questions and comments to sstation@microsoft.com.

Michele Leroux Bustamante is Chief Architect of IDesign Inc., Microsoft Regional Director for San Diego, and a Microsoft MVP for Connected Systems. Her latest book is Learning WCF. Reach her at mlb@idesign.net or visit idesign.net. Michele blogs at dasblonde.net.