.NET

Roll Your Own REST-ful WCF Router

Recently I’ve been tasked with building a WCF routing service and faced the choice of whether to go with the built-in router that ships with WCF 4.0, or to build one from scratch. The built-in router is great for a lot of different scenarios – it provides content-based routing, multicasting, protocol bridging, and failover-based routing. However, as the MSDN documentation for the WCF Router states, “The Routing Service does not currently support routing of WCF REST services.” The reason is fairly simple: the WCF Routing Service performs routing of messages in a way that is independent of the underlying transport, whereas a REST-based architecture is deeply rooted in the HTTP protocol and relies on the URI for delivery semantics. In fact, using the BasicHttpBinding with AspNetCompatibility enabled on the built-in WCF router results in a somewhat cryptic error: “Shouldn’t allocate SessionChannels if session-less and impersonating.”
Nevertheless, there are times when it might make sense to build a router that can talk to clients who don’t know anything about Soap, for example, an AJAX web application. You can also achieve a more compact data representation with plain old XML (POX) or Javascript Object Notation (JSON), which could result in greater throughput. While the ASP.NET MVC or the new ASP.NET Web API might seems like attractive options, WCF is the way to go if you need to accommodate both Soap-based and Rest-ful clients. WCF offers a unified programming model with the neutral Message type, which makes it easier to avoid serialization of the message body and the cost that could carry.

Here is the universal service contract for a routing service that can accept requests that are formatted as SOAP, POX or JSON.

[ServiceContract(Namespace = "urn:example:routing")]
public interface IRoutingService
{
    [WebInvoke(UriTemplate = "")]
    [OperationContract(AsyncPattern = true, Action = "*", ReplyAction = "*")]
    IAsyncResult BeginProcessRequest(Message requestMessage, AsyncCallback asyncCallback, object asyncState);
    Message EndProcessRequest(IAsyncResult asyncResult);
}
What makes this suitable for routing is that the Action and ReplyAction parameters of the OperationContract are set to “*” – allowing it to accept any request regardless of the Action. The contract also uses a request-response message exchange pattern, which is suitable for HTTP clients that generally follow this pattern when communicating with services.
Another thing you’ll notice is the AsyncPattern layout, with the Begin and End methods tied together by the IAsyncResult call object. This is a very important requirement for performance and scalability. WCF executes asynchronous contracts using the IO Completion Port Thread Pool, which economizes on server resources by exchanging a 100 byte IO request packet for a 1 MB thread stack. This makes sense only if you initiate async IO in the Begin method. An Async IO operation can be things like Socket.Begin[Send|Receive], NetworkStream.Begin[Read|Write], FileStream.Begin[Read|Write] (if created asynchronously), SqlCommand.BeginExecute[Reader|NonQuery|XmlReader], or invoking a WCF service asynchronously, which is precisely what a router is designed to do.

Here is the implementation of the IRoutingService interface.

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall,
    AddressFilterMode = AddressFilterMode.Any, ValidateMustUnderstand = false)]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class RoutingService : IRoutingService, IDisposable
{
    private IRoutingService _client;
    public IAsyncResult BeginProcessRequest(Message requestMessage, AsyncCallback asyncCallback, object asyncState)
    {
        // Select soap or rest client endpoint
        string endpoint = "service-basic";
        if (requestMessage.Version == MessageVersion.None)
            endpoint = "service-web";
        // Create channel factory
        var factory = new ChannelFactory<IRoutingService>(endpoint);
        // Set message address
        requestMessage.Headers.To = new Uri(factory.Endpoint.Address);
        // Create client channel
        _client = factory.CreateChannel();
        // Begin request
        return _client.BeginProcessRequest(requestMessage, asyncCallback, asyncState);
    }
    public Message EndProcessRequest(IAsyncResult asyncResult)
    {
        return _client.EndProcessRequest(asyncResult);
    }
    public void Dispose()
    {
        if (_client != null)
        {
            var channel = (IClientChannel)_client;
            if (channel.State != CommunicationState.Closed)
            {
                try
                    channel.Close();
                catch
                    channel.Abort();
            }
        }
    }
}
First, notice that the InstanceContextMode is set to PerCall, so that an instance of the RoutingService is created upon each request. This allows the service to be entirely stateless and function easily in a web farm environment without maintaining client state between method calls. Another thing to notice is that the client proxy (IRoutingService) is declared as a member variable and shared between the Begin and End methods.
When I first wrote the service implementation, I noticed something strange happen when the service was invoked by a non-Soap client. The call to _client.BeginProcessRequest was coming right back into the service, instead of invoking the remote service, even though the factory’s endpoint address pointed to the remote service. What I didn’t realize at the time is that the Http transport channel will use the “To” message header when manual addressing is set to true, which is the case with the WebHttpBinding. The “To” message header is naturally pointing to the routing service, so that’s where the message is directed. To correct this behavior, all you need to do is manually set the “To” message header to match the factory endpoint address.
Probably the most important task of a router is to, well, route messages. But you need to decide how to instruct the router to accomplish this task. The trick is to do it in a way that avoids having to look at the contents of the message. Messages in WCF consist of two parts: headers and a body. Headers are always deserialized and buffered, whereas the body comes in as a stream. If you can avoid creating a message buffer and deserializing the message stream, the router will perform more efficiently.

For soap-based messages, the natural place for routing instructions is the header. Here is part of a method that reads routing instructions from the incoming message header.

public Dictionary<string, string> GetRoutingHeaders(Message requestMessage)
{
    // Set routing namespace
    var routingHeaders = new Dictionary<string, string>();
    // Get soap routing headers
    if (requestMessage.Version != MessageVersion.None)
    {
        foreach (var header in requestMessage.Headers)
        {
            if (header.Namespace.ToLower() == _routingNamespace)
            {
                int headerIndex = requestMessage.Headers.FindHeader
                    (header.Name, _routingNamespace);
                if (headerIndex != -1)
                {
                    var headerValue = requestMessage.Headers.GetHeader<string>
                        (header.Name, _routingNamespace);
                    requestMessage.Headers.RemoveAt(headerIndex);
                    if (!string.IsNullOrWhiteSpace(headerValue))
                        routingHeaders.Add(header.Name, headerValue);
                }
            }
        }
    }
For non-Soap clients using HTTP, you have basically two choices: custom HTTP headers, or you can incorporate routing instructions into the URI. Personally, I like the second approach better, because it makes use of the URI in a more REST-like fashion. Here is some code that demonstrates both these approaches. It first looks at the HTTP headers for routing instructions, but if there aren’t any, it then gets them from query parameters in the URI.
WebHeaderCollection httpHeaders = WebOperationContext.Current.IncomingRequest.Headers;
foreach (string headerName in httpHeaders)
{
    if (headerName.ToLower().StartsWith(routingNamespace))
    {
        string name = headerName.Substring(routingNamespace.Length + 1);
        string value = httpHeaders.Get(headerName);
        routingHeaders.Add(name, value);
    }
}
if (routingHeaders.Count == 0)
{
    var queryParams = WebOperationContext.Current.IncomingRequest.UriTemplateMatch.QueryParameters;
    foreach (string paramKey in queryParams.AllKeys)
    {
        string name = paramKey.Substring(_routingNamespace.Length + 1);
        string value = queryParams[paramKey];
        routingHeaders.Add(name, value);
    }
}

Armed with routing metadata, you can look up the destination’s address in a routing table of some kind. This method selects GreetingService2 if the routing instructions specify the “western” region.

public string GetServiceAddress(Dictionary<string, string> routingHeaders,
    string defaultServiceAddress)
{
    // Select service address based on region
    string serviceAddress = defaultServiceAddress;
    var region = (from rh in routingHeaders
                    where rh.Key.ToLower() == "region"
                    select rh.Value).FirstOrDefault();
    if (region != null)
    {
        if (region.ToLower() == "western")
            serviceAddress = defaultServiceAddress
                .Replace("GreetingService1", "GreetingService2");
    }
    return serviceAddress;
}
If the router isn’t going to read the message body or alter it in any way, then clients will need to send messages that can be understood by the eventual recipient of the message. So if both Soap and non-Soap messages will be sent to the router, the downstream services will need to expose both soap and rest endpoints. Furthermore, if clients are going to want to transmit messages as Json, services will need to know how to understand and respond in kind. For example, here is the app.config file of the GreetingService. The webHttp endpoint behavior allows for a Json-formatted response if the Accept or ContentType header is set to “application/json”.
<system.serviceModel>
  <services>
    <service name="RoutingPrototype.Services.GreetingService1">
      <endpoint address="Soap"
                binding="basicHttpBinding"
                contract="RoutingPrototype.Interfaces.IGreetingService"
                name="service-basic"/>
      <endpoint address="Rest"
                binding="webHttpBinding"
                behaviorConfiguration="web"
                contract="RoutingPrototype.Interfaces.IGreetingService"
                name="service-web"/>
      <host>
        <baseAddresses>
          <add baseAddress="http://localhost:8000/GreetingService1" />
        </baseAddresses>
      </host>
    </service>
    <behaviors>
      <endpointBehaviors>
        <behavior name="web">
          <webHttp helpEnabled="true"
                    automaticFormatSelectionEnabled="true"
                    faultExceptionEnabled="true"/>
        </behavior>
      </endpointBehaviors>
    </behaviors>
  </system.serviceModel>

The client-side code for sending Soap-based messages looks like this:

private static string SendSoapMessage(string name, string addressType, string region, bool useFiddler)
{
    var factory = new ChannelFactory<IGreetingService>(addressType);
    string address = factory.Endpoint.Address.ToString()
        .Replace("localhost", ConfigurationManager.AppSettings["MachineName"]);
    if (useFiddler) factory.Endpoint.Address = new EndpointAddress(address);
            
    IGreetingService client = factory.CreateChannel();
    using ((IDisposable)client)
    {
        using (var contextScope = new OperationContextScope((IContextChannel)client))
        {
            if (region != null)
            {
                MessageHeader regionHeader = MessageHeader
                    .CreateHeader("region", _routingNamespace, region);
                OperationContext.Current.OutgoingMessageHeaders.Add(regionHeader);
            }
            return client.Hello(name);
        }
    }
}

The client-side code for sending Rest-ful messages looks like this:

private static string SendRestMessage(string name, string addressType, string region, bool useFiddler)
{
    string address = ConfigurationManager.AppSettings[addressType];
    if (useFiddler) address = address.Replace("localhost", ConfigurationManager.AppSettings["MachineName"]);
    var client = new WebClient {BaseAddress = address};
    // Set format
    string format = GetFormat();
    if (format == null) return null;
    string requestString;
    if (format == "xml")
    {
        requestString = SerializationHelper.SerializeXml(name);
        client.Headers.Add(HttpRequestHeader.ContentType, "application/xml");
    }
    else if (format == "json")
    {
        requestString = SerializationHelper.SerializeJson(name);
        client.Headers.Add(HttpRequestHeader.ContentType, "application/json");
    }
    // Set region header
    string addressParameters = string.Empty;
    if (region != null)
    {
        bool? useHeaders = UseHttpHeaders();
        if (useHeaders == null) return null;
        if ((bool)useHeaders)
        {
            string regionHeader = string.Format("{0}:{1}", _routingNamespace, "region");
            regionHeader = regionHeader.Replace(":", "-");
            client.Headers.Add(regionHeader, region);
        }
        else
        {
            addressParameters = string.Format
                ("?{0}:region={1}", _routingNamespace, region);
        }
    }
    // Send message
    string responseString = client.UploadString(addressParameters, requestString);
    // Deserialize response
    string response = null;
    if (format == "xml")
        response = SerializationHelper.DeserializeXml<string>(responseString);
    else if (format == "json")
        response = SerializationHelper.DeserializeJson<string>(responseString);
    return response;
}
If you look at the message received by the either the router or the downstream service, you won’t see a hint of Json, even when the message sent by the client is clearly Json. (SerializationHelper is a class I wrote to serialize Xml and Json using WCF’s data contract serializer.) The reason is that the translation from and to Json is performed by the webHttp endpoint behavior. If you want to see what is actually sent across the wire, you’ll need to employ an HTTP sniffer such as Fiddler. However, configuring it for use with .NET clients can be a joy (sarcasm intended). The easiest approach I found was to substitute “localhost” in client endpoint addresses with the actual machine name, which you can store as a setting in app.config.
Here you can see an HTTP POST message transmitted with a custom routing header, urn-example-routing-region, with a value of “western”. Both the request and response are formatted as a simple Json string.
WCF will give you all the tools you need to write a scalable, high-performance router with a minimal amount of code. Making it play nice with Rest, however, requires some effort, as well as familiarity with how WCF deals with Rest-based messages under the covers. Here are some resources I found helpful in getting my head around WCF addressing and message-handling and the mechanics of building a WCF routing service:

WCF Addressing In Depth (MSDN Magazine June 2007)
WCF Messaging Fundamentals (MSDN Magazine April 2007)
Building a WCF Router, Part 1 (MSDN Magazine April 2008)
Building a WCF Router, Part 2 (MSDN Magazine June 2008)

You can download the code for this post here. Enjoy.

Reference: Roll Your Own REST-ful WCF Router from our NCG partner Tony Sneed at the Tony Sneed’s Blog blog.

Related Articles

Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Mark Boyle
12 years ago

Hi Tony,

Great post, it’s really useful. I’m not able to download the source code from your other website – do you still have it?

Thanks again!
Mark

gsudhesh
gsudhesh
10 years ago

have to got any solutions ? .im also looking for that

Back to top button