roll your own rest-ful wcf router
DESCRIPTION
Roll Your Own REST-Ful WCF RouterTRANSCRIPT
Contact
Tony Sneed, DevelopMentor Curriculum Lead
Contact Us
First Namev
Last Names
Email (we will keep your email completely private)vetrivetri@hot
Message
Submit
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: “Couldn’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
newASP.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.
Home | Training | Onsite | Webcasts | Resources | About Us | Contact
© 2013 Digital Age Learning. All Rights Reserved | Terms of Use | Please Read our Privacy Policy