Sunteți pe pagina 1din 8

Contact

Tony Sneed, DevelopMentor Curriculum Lead

Contact Us
v s vetrivetri@hot

First Name Last Name

Email (we will keep your email completely private)

Message
Submit

Roll Your Own REST-ful WCF Router

Recently Ive 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: Couldnt 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 dont 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 youll 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 = "servicebasic"; if (requestMessage.Version == MessageVersion.None) endpoint = "service-web"; // Create channel factory var factory = new ChannelFactory<IRoutingService>(endpoint); // Set message address Uri(factory.Endpoint.Address); requestMessage.Headers.To = new

// 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 factorys endpoint address pointed to the remote service. What I didnt 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 thats 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 arent 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 destinations 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 isnt 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 Jsonformatted 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 wont 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 WCFs 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, youll 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, urnexample-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

S-ar putea să vă placă și