sup { vertical-align:text-top; }

Service Station

Building A WCF Router, Part 2

Michele Leroux Bustamante

Contents

Pass-Through Router Scenarios
Forwarding by Action Header
Forwarding with Custom Headers
Registering Services
Inspecting Messages
Routers and Transport Sessions
Duplex Routers
Mixed Transport Sessions

Send your questions and comments to sstation@microsoft.com.
Code download available at msdn.microsoft.com/en-us/magazine/cc135911.aspx.

This article is based on a prerelease version of ASP.NET 3.5 and the Microsoft AJAX Library. All information herein is subject to change.

In the April 2008 installment of Service Station, I showed you how to create a simple router that allows messages to flow transparently between the calling client and the target service. In the process, I reviewed important Windows® Communication Foundation (WCF) addressing and message filtering semantics, you learned how to design a router contract to work with untyped messages, and you learned how to configure the bindings and behaviors to allow messages to pass through untouched by the router. In this issue I'll continue the discussion by looking at further implementation details that arise when more practical scenarios for routers are employed.

Pass-Through Router Scenarios

As I mentioned in Part 1, when a pass-through router is inserted between a client and a service, the client's relationship is with the target service, not the router. Although certainly messages must be sent with a transport protocol and message encoder that the router can understand, the entire contents of the message—including security headers and reliable sessions, for example—are not processed by the router. A few examples where a pass-through router might apply are load balancing, content-based routing, or message transformation.

Load balancing and distribution of work across server resources is best suited to Network Load Balancing (NLB) or, better yet, hardware load-balancing devices. Still, a WCF router can be useful for load balancing if services are hosted in environments without these luxuries, when services are installed to physical infrastructures out of your direct control, when you need routing based on domain-specific logic, or when the application simply calls for a lightweight routing solution that is simple to configure. Such a WCF router can be used to distribute messages to services hosted in multiple processes on the same machine or distributed across machines.

Regardless of the distribution model, a load-balancing router certainly requires a few core features. Services must somehow register with the router in order to be included in load distribution. The router must be able to determine the service type and associated endpoint so that it can correctly forward messages. And the router must have an algorithm for distributing load such as a classic round-robin approach or some form or priority-based routing.

Sometimes distribution of messages among services is handled based on the content of the message, as opposed to load balancing. A content-based router will typically inspect either the message headers or the message body for routing information. For example, messages from clients with a valid license key may be forwarded as high priority to a large pool of server machines with more processing power, while those with a trial license are forwarded to a smaller pool of less powerful servers. In this scenario the router must not only know where to forward messages, but it must also be able to inspect each message, its headers or body content, before making the decision on where to forward. The following sections will discuss relevant routing features that support these scenarios.

Forwarding by Action Header

Messages received at the router have two addressing headers that can be useful in forwarding messages to the correct service:

To This header indicates the name of the endpoint. If the header matches the target service and not the router, it will indicate a URL for the service endpoint that the message was intended for.

Action This header indicates the service operation that the message was intended for, but it may not represent a valid URL per se.

In many cases, however, the To header will match the router address and not the service, leaving the Action header as the more reliable source of information as to the correct destination for the message. Recall that the Action header is derived from the service contract namespace, the service contract name, and the operation name. This is sufficient information for a router to uniquely identify the target service, assuming that contracts are not shared across different service types. Consider the following service contracts, each implemented on different service types:

[ServiceContract(Namespace = "https://www.thatindigogirl.com/samples/2008/01")]
public interface IServiceA {
  [OperationContract]
  string SendMessage(string msg);
}
[ServiceContract(Namespace = "https://www.thatindigogirl.com/samples/2008/01")]
public interface IServiceB {
  [OperationContract]
  string SendMessage(string msg);
}
public class ServiceA : IServiceA {...}
public class ServiceB : IServiceB {...}

As Figure 1 illustrates, the router can rely on a mapping between contract namespaces for each service contract and the service endpoints to which messages should be directed.

fig01.gif

Figure 1 Mapping Contract Namespace to Service Endpoint

The following shows a dictionary initialized so that each contract namespace entry maps to a configuration element that indicates the correct channel configuration settings to use:

static public IDictionary<string, string> RegistrationList = 
  new Dictionary<string, string>();

RegistrationList.Add
    ("https://www.thatindigogirl.com/samples/2008/01/IServiceA","ServiceA");
RegistrationList.Add
    ("https://www.thatindigogirl.com/samples/2008/01/IServiceB","ServiceB");

The code to initialize the channel would then look like this:

string contractNamespace = 
  requestMessage.Headers.Action.Substring(0, 
  requestMessage.Headers.Action.LastIndexOf("/"));

string configurationName = RouterService.RegistrationList[contractNamespace];

using (ChannelFactory<IRouterService> factory = 
  new ChannelFactory<IRouterService>(configurationName))
{...}

There are a few important design dependencies in this scenario that you should note:

  • The map of contracts to services would most likely live in a database to simplify configuration and to support multiple router instances.
  • The service contract cannot be implemented on multiple service types unless messages can be processed by any service implementing the contract.
  • If there are multiple instances of the service in a server farm, the configuration for each endpoint should map to a virtual address that the physical load balancers can then distribute accordingly.
  • There is no support for messages that contain an action header other than those for application services.

This last point is important, because if secure sessions or reliable sessions are enabled for application services, then there will be additional messages sent to establish those sessions prior to actual application service messages. These messages use an Action header for their respective protocols and are completely independent of any application service. This means that an alternative to the Action header must be used for message forwarding.

Forwarding with Custom Headers

To ensure that every message contains a routing header that can properly indicate the application service the client is trying to communicate with, a custom header can be specified in the application service endpoint configuration section, as shown here:

<service behaviorConfiguration="serviceBehavior" name="MessageManager.ServiceA">
  <endpoint address="https://localhost:8010/RouterService"
    binding="wsHttpBinding" bindingConfiguration="wsHttp" 
    contract="IServiceA" listenUri="ServiceA">
    <headers>
      <Route 
        xmlns="https://www.thatindigogirl.com/samples/2008/01">
        https://www.thatindigogirl.com/samples/2008/01/IServiceA
      </Route>
    </headers>
  </endpoint>
</service>

Custom headers have a name, a namespace, and a value. In some cases, headers are more dynamic, but in this case the header is fixed to represent the service contract namespace. The Route element indicates the header name, and the namespace is indicated by the xmlns attribute. Since this header is specified as part of the endpoint configuration, it is included in the metadata for the service. As such, when clients generate a proxy, they also generate a client configuration that includes the header, as shown here:

<client>
  <endpoint address="https://localhost:8010/RouterService" 
    binding="wsHttpBinding" bindingConfiguration="wsHttp"
    contract="localhost.IServiceA" >
    <headers>
      <Route xmlns="https://www.thatindigogirl.com/samples/2008/01">
        https://www.thatindigogirl.com/samples/2008/01/IServiceA
      </Route>
    </headers>
  </endpoint>
</client>

This makes the presence of the header transparent to the client coding effort and ensures that all messages, including those to establish secure sessions or reliable sessions, include the header. The router can retrieve the header value from any message by its name and namespace, as follows:

string contractNamespace = requestMessage.Headers.GetHeader<string>
  ("Route","https://www.thatindigogirl.com/samples/2008/01");

The only change to this implementation from the previous example is in how the router discovers the contract namespace—from the custom Route header instead of the Action header. This allows the router to forward messages related to secure sessions or reliable sessions to the appropriate service endpoint.

Registering Services

Rather than hardcoding endpoints for application services, the router can expose a service endpoint for services to register and unregister as they come online and go offline. In the absence of a software or hardware load balancer, this reduces the configuration overhead of the router when the application services must be scaled out, or when ports or machine names change in their respective endpoint addresses. To support this model, the following steps can be taken:

  • Implement a service registration contract for the router and expose that endpoint to app services behind the firewall.
  • Maintain a registration list for the router.
  • After each ServiceHost is initialized, have it register service endpoints with the router.
  • As each ServiceHost is faulted or closed, unregister service endpoints with the router.

The diagram in Figure 2 illustrates the registration process, which adds entries that hold the contract namespace mapped to a physical endpoint address.

fig02.gif

Figure 2 Registering Services with the Router

With this approach, registration only requires a contract namespace and physical address for each service endpoint. Figure 3 shows the IRegistrationService service contract and associated RegistrationInfo details that are passed to the router to register and unregister.

Figure 3 IRegistrationService Contract with Data Contract

[ServiceContract(Namespace = 
  "https://www.thatindigogirl.com/samples/2008/01")]
public interface IRegistrationService {
  [OperationContract]
  void Register(RegistrationInfo regInfo);

  [OperationContract]
  void Unregister(RegistrationInfo regInfo);
}

[DataContract(Namespace = 
  "https://schemas.thatindigogirl.com/samples/2008/01")]
public class RegistrationInfo {
  [DataMember(IsRequired = true, Order = 1)]
  public string Address { get; set; }

  [DataMember(IsRequired = true, Order = 2)]
  public string ContractName { get; set; }

  [DataMember(IsRequired = true, Order = 3)]
  public string ContractNamespace { get; set; }

  public override int GetHashCode()   {
    return this.Address.GetHashCode() + 
    this.ContractName.GetHashCode() + 
    this.ContractNamespace.GetHashCode();
  }
}

The router could store a single entry per contract—but that would not allow for more than one service for each contract. In order to support distribution across multiple entries, the router should use a unique key per registration. This code uses a dictionary that uniquely associates each entry with a hash code for the Registration­Info instance:

// registration list
static public IDictionary<int, RegistrationInfo> 
  RegistrationList = 
  new Dictionary<int, RegistrationInfo>();

// to register
if (!RouterService.RegistrationList.ContainsKey(
  regInfo.GetHashCode())) {
  RouterService.RegistrationList.Add(regInfo.GetHashCode(), 
    regInfo);
  }

  // to unregister
  if (RouterService.RegistrationList.ContainsKey(
    regInfo.GetHashCode())) {
    RouterService.RegistrationList.Remove(
      regInfo.GetHashCode());
  }

When the router receives messages, it should gather the contract namespace and look for a suitable item in the dictionary that matches—and if more than one exists, use selection criteria to forward the message to an appropriate service endpoint (see Figure 4).

Figure 4 Matching Messages to Appropriate Endpoints

string contractNamespace = 
  requestMessage.Headers.Action.Substring(0, 
  requestMessage.Headers.Action.LastIndexOf("/"));

// get a list of all registered service entries for 
// the specified contract
var results = from item in RouterService.RegistrationList
  where item.Value.ContractNamespace.Contains(contractNamespace)
  select item;

int index = 0;
// find the next address used ...

// create the channel 
RegistrationInfo regInfo = results.ElementAt<KeyValuePair<int, 
  RegistrationInfo>>(index).Value;

Uri addressUri = new Uri(regInfo.Address);
Binding binding = ConfigurationUtility.GetRouterBinding    (addressUri.Scheme);
EndpointAddress endpointAddress = new EndpointAddress(regInfo.Address);

ChannelFactory<IRouterService> factory = new 
  ChannelFactory<IRouterService>(binding, endpointAddress)
// forward message to the service ...

Aside from filling load balancing needs to services across machines, dynamic registration can be very useful in scenarios where multiple instances of a service may be hosted on the same machine—which requires multiple port assignments if they are hosted in a Windows service.

In order to support this, services should select a dynamic port assignment for the machine. For TCP services, this can be accomplished by setting the listen URI mode to Unique in the endpoint configuration:

<endpoint address="net.tcp://localhost:9000/ServiceA" 
  contract=" IServiceA" binding="netTcpBinding"
  listenUriMode="Unique"/>

For named pipes and HTTP, however, this setting does not select a unique port. Instead it appends a GUID to the address:

net.tcp://localhost:64544/ServiceA
https://localhost:8000/ServiceA/66e9c367-b681-4e4f-8d12-80a631b7bc9b
net.pipe://localhost/ServiceA/6660c07e-c9f5-450b-8d40-693ad1a71c6e

To ensure a unique port for TCP and HTTP service endpoints, you can initialize base addresses or explicit endpoint addresses in code:

Uri httpBase = new Uri(string.Format(
  "https://localhost:{0}", 
  FindFreePort()));
Uri tcpBase = new Uri(string.Format(
  "net.tcp://localhost:{0}", 
  FindFreePort()));
Uri netPipeBase = new Uri(string.Format(
  "net.pipe://localhost/{0}", 
  Guid.NewGuid().ToString()));

ServiceHost host = new ServiceHost(typeof(ServiceA), 
  httpBase, tcpBase, netPipeBase);

Figure 5 illustrates multiple services hosted on the same machine, registering to the router. This diagram also shows that, in order to remove the single point of failure of the router, a software or physical load balancer may still be necessary to distribute registration calls among instances. Of course, this also implies that the registration list is stored in a shared database.

fig05.gif

Figure 5 Registering Services with Dynamic Ports through a Load Balanced Router

Inspecting Messages

Although routers typically forward the original message to application services, they may perform activities based on the content of the message, such as inspecting headers or body elements for content-based routing or rejecting messages based on validity of headers or body elements.

Inspecting headers is trivial since the Message type exposes a Headers property to retrieve addressing headers directly and custom headers by their name and namespace. Consider the following service operation, which uses a message contract to add a custom LicenseKey header for the incoming operation:

// operation
[OperationContract]
SendMessageResponse SendMessage(SendMessageRequest message);

// message contract
[MessageContract]
public class SendMessageRequest {
  [MessageHeader]
  public string LicenseKey { get; set; }

  [MessageBodyMember]
  public string Message { get; set; }
}

Clients will send messages including a LicenseKey header, possibly empty if they don't yet have a license key. The router can retrieve this header, as follows:

string licenseKey = 
  requestMessage.Headers.GetHeader<string>(
  "LicenseKey", 
  "https://www.thatindigogirl.com/samples/2008/01");

If the same LicenseKey value were to be passed inside the message body, the router must read the message body to access the value (since this information is not directly available through the Message type). The method GetReaderAtBodyContents returns an XmlDictionaryReader that can be used to read the message body, as follows:

XmlDictionaryReader bodyReader = requestMessage.GetReaderAtBodyContents();

The State property of the Message can be any of the following MessageType enumeration values: Created, Copied, Read, Written, or Closed. The message begins in Created state—and routers receiving Message parameters to operations do not process messages, thus the state remains Created.

Reading the message body causes the request message to move from Created to Read state. Once read, it cannot be forwarded to application services because the message can only be read once, written to once, or copied once.

Before reading the message, a content-based router implementation should copy the message to a buffer. Using this buffered copy of the message, new copies of the original message can be created and used for processing, as follows:

MessageBuffer messageBuffer = 
  requestMessage.CreateBufferedCopy(int.MaxValue);
Message messageCopy = messageBuffer.CreateMessage();
XmlDictionaryReader bodyReader = 
  messageCopy.GetReaderAtBodyContents();

XmlDocument doc = new XmlDocument();
doc.Load(bodyReader);
XmlNodeList elements = doc.GetElementsByTagName("LicenseKey");
string licenseKey = elements[0].InnerText;

The same buffer can be used again to create a message for forwarding to application services. The call to CreateMessage returns a new Message instance based on the original message.

Routers and Transport Sessions

In a pass-through router situation, clients must send messages using the transport protocol and encoding format that the router expects, and the router must forward the message to application services using the transport protocol and encoding format they expect. All of the routing features discussed so far work great when both ends are HTTP—with or without sessions. However, when you introduce a transport session such as TCP, some interesting challenges arise. Everything is fine in the simplest case where security is disabled and there are no reliable sessions, but challenges are introduced when these features are added.

Once security is enabled for the application service, the router must provide a signed To header. Normally this means leaving the To header untouched—as it was sent by the client—but by default the router will modify the To header to match the address of the service when messages are sent, unless manual addressing is enabled. If, for example, the router uses TCP protocol to forward messages to a service, manual addressing is not allowed if the outgoing channel is based on a request-reply contract.

Another problem arises if reliable sessions are enabled and the router uses TCP protocol to call the service. In this case, asynchronous acknowledgments are sent back through the router. That requires the router to maintain a session with the service to receive those asynchronous acknowledgments. As a result, the client must maintain a duplex session with the router to receive the same asynchronous acknowledgments.

Both problems can be solved in part by implementing a router that supports sessions and relies on duplex incoming and outgoing channels. Neither the calling client nor the application service need be directly aware of this—it is an implementation detail within the router. There is, however, a dependency on session-aware bindings, and on duplex communication when asynchronous reliable session acknowledgements are introduced.

Duplex Routers

The code in Figure 6 shows an example of a duplex router contract in order to support the scenario where messages are sent over TCP between client, router, and application services. It is different from the traditional request-reply router contract in the following ways:

Figure 6 Duplex Router Contract

[ServiceContract(Namespace = 
  "https://www.thatindigogirl.com/samples/2008/01", 
  SessionMode = SessionMode.Required, 
  CallbackContract = typeof(IDuplexRouterCallback))]

public interface IDuplexRouterService {
  [OperationContract(IsOneWay=true, Action = "*")]
  void ProcessMessage(Message requestMessage);
}

[ServiceContract(Namespace = 
  "https://www.thatindigogirl.com/samples/2008/01", 
  SessionMode = SessionMode.Allowed)]
public interface IDuplexRouterCallback {
  [OperationContract(IsOneWay=true, Action = "*")]
  void ProcessMessage(Message requestMessage);
}
  • ProcessMessage is now a one-way operation.
  • The service contract requires sessions and has an associated callback contract. It is important for you to note that this does not require the client to implement a callback; this is internal to the router.
  • The callback contract has a single one-way method to receive responses from router calls to application services. Note also that services are not aware that their responses are being sent to a callback channel; they can be request-reply messages.

The architecture for a duplex router is illustrated in Figure 7. As far as the client is concerned, requests are sent and a synchronous reply is expected. The router receives requests to a one-way operation and saves the client's callback channel in order to send the reply. In the meantime, the router forwards messages using a duplex channel and provides a callback channel to receive the reply from the service.

fig07.gif

Figure 7 Duplex Router Architecture

The service receives the request and sends a synchronous reply, received by the router's callback channel. This callback channel in turn uses the client callback channel to send a response back to the client. From start to finish, the operation behaves synchronously—but the router decouples activities and relies on duplex communication in the underlying receive and send channels to correlate messages.

The router implementation for this is shown in Figure 8. There are a few relevant changes to the request-reply router implementations. First, the router supports sessions and implements a duplex contract. When the router forwards messages to services, a duplex channel is created using DuplexChannelFactory<T>, which means supplying a callback object that will receive responses from the service.

Figure 8 Duplex Router Implementation

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession, 
  ConcurrencyMode = ConcurrencyMode.Multiple, 
  AddressFilterMode=AddressFilterMode.Any, 
  ValidateMustUnderstand=false)]

public class DuplexRouterService : IDuplexRouterService, IDisposable {
  object m_duplexSessionLock = new object();
  IDuplexRouterService m_duplexSession;

  public void ProcessMessage(Message requestMessage)  {
    lock (this.m_duplexSessionLock)  {
      if (this.m_duplexSession == null) {
        IDuplexRouterCallback callback = 
          OperationContext.Current.GetCallbackChannel
          <IDuplexRouterCallback>();

        DuplexChannelFactory<IDuplexRouterService> factory = 
          new DuplexChannelFactory<IDuplexRouterService>
          (new InstanceContext(null, 
          new DuplexRouterCallback(callback)), "serviceEndpoint");
        factory.Endpoint.Behaviors.Add(new MustUnderstandBehavior(false));
        this.m_duplexSession = factory.CreateChannel();
      }
    }

    this.m_duplexSession.ProcessMessage(requestMessage);
  }
  public void Dispose() {
    if (this.m_duplexSession != null) {
      try {
        ICommunicationObject obj = this.m_duplexSession as 
          ICommunicationObject;
        if (obj.State == CommunicationState.Faulted)
          obj.Abort();
        else
          obj.Close();
      }
      catch {}
    }
  }
}

public class DuplexRouterCallback : IDuplexRouterCallback {

  private IDuplexRouterCallback m_clientCallback;

  public DuplexRouterCallback(IDuplexRouterCallback clientCallback) {
    m_clientCallback = clientCallback;
  }

  public void ProcessMessage(Message requestMessage) {
    this.m_clientCallback.ProcessMessage(requestMessage);
  }
}

Figure 8 Duplex Router Implementation

The callback object implements the callback contract, and it will receive responses from the service. This callback object must use the client callback channel to return the response back to the client.

The router service instance, the client callback channel reference, and the router callback channel all live for the duration of the session with the client. For this reason, the router must expose endpoints that support sessions and the downstream service must support sessions for this to work.

Mixed Transport Sessions

In some scenarios it may be desirable for the client to send messages to the router over HTTP, while the router forwards those messages over TCP to application services. When security features or reliable sessions are enabled, even the duplex router configuration isn't sufficient to support the scenario.

As I mentioned, manual addressing is only supported with request-reply channels. Otherwise, the service model relies on addressing features to correlate messages. Since TCP doesn't natively support request-reply, manual addressing is not an option unless the contract is one-way. Thus, the send channel from Figure 7 must be created from a one-way contract like IDuplexRouterService. The callback channel is provided to receive the response.

The router's callback channel must also be kept alive until the response is sent, and the client's callback channel must likewise be kept alive. To support this, the client must have a session with the router, and the router must have a session with the service.

Assuming that application services called by the router are secure, manual addressing is a likely necessity to forward messages untouched by the router. If the router calls application services over TCP, this requires a duplex router implementation, as discussed previously—so that the outgoing call can be a one-way channel. This forces the client to send messages over a session-aware binding, which means enabling secure sessions or reliable sessions over HTTP.

If the router is a pass-through router, the entire point is to let the application service process security and reliable session headers. If the router requires secure sessions or reliable sessions for its client endpoints in order to support sessions over HTTP, the router will process those headers and the session will not be established with application services.

Thus, mixing protocols only works in limited scenarios—unless you reach lower into the channel layer to override default behaviors. When security and reliable sessions are disabled, clients can send messages to the router over HTTP while the router forwards over TCP to application services. If security or reliable sessions are enabled, the client must send messages over TCP to the router so the session can be established without enabling reliable sessions or secure sessions for the router channel.

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 design.net. Michele blogs at dasblonde.net.