roll your own rest-ful wcf router

8
Contact Tony Sneed, DevelopMentor Curriculum Lead Contact Us First Name v Last Name s Email (we will keep your email completely private) vetrivetri@hot Message S ubmit Roll Your Own REST-ful WCF Router

Upload: junkyard

Post on 01-Dec-2015

22 views

Category:

Documents


3 download

DESCRIPTION

Roll Your Own REST-Ful WCF Router

TRANSCRIPT

Page 1: Roll Your Own REST-Ful WCF Router

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

Page 2: 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.

Page 3: Roll Your Own REST-Ful WCF Router

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.

Page 4: Roll Your Own REST-Ful WCF Router

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.

Page 5: Roll Your Own REST-Ful WCF Router

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"

Page 6: Roll Your Own REST-Ful WCF Router

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;

Page 7: Roll Your Own REST-Ful WCF Router

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.

Page 8: Roll Your Own REST-Ful WCF Router

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