Part 1, Part 2, Part 3
The SuffixFilter that I have shown in Part 3 of this little series interacts with the Indigo dispatch internals to figure out which endpoint shall receive an incoming request. If the filter reports true from it’s Match() method, the service endpoint that owns the particular filter is being picked and its channel gets the message. But at that point we still don’t know which of the operations on the endpoint’s contract shall be selected to handle the request.
We’ll take a step back and recap what we have by citing one of the contract declarations from Part 1:
[ServiceContract, HttpMethodOperationSelector] interface IMyApp { [OperationContract, HttpMethod("GET",UriSuffix="/customers/*")] CustomerInfo GetCustomerInfo(); [OperationContract, HttpMethod("PUT", UriSuffix = "/customers/*")] void UpdateCustomerInfo(CustomerInfo info); [OperationContract, HttpMethod("DELETE", UriSuffix = "/customers/*")] void DeleteCustomerInfo(); } |
If we implement this contract on a class and host the service endpoint for it at, say, http://www.example.com/myapp this particular endpoint will only accept requests on http://www.example.com/myapp/customers/* (whereby ‘*’ can really be any string) because our suffix filter that’s being hooked in my the HttpMethodOperationSelectorAttribute and populated with “/customers/*” suffix won’t let any other request pass. Only those requests for which a pattern match can be found when combining an operation’s suffix pattern with the endpoint URI are positively matched by the suffix filter. For a more complex example I’ll let you peek at a (shortened) snippet of one the contracts of the TV server I am working on:
/// <summary> /// Contract for the channel service /// </summary> [ServiceContract(Namespace = Runtime.ChannelServiceNamespaceURI), HttpMethodOperationSelector] public interface IChannelService { /// <summary> /// Gets the default RSS for this channel. /// </summary> /// <param name="message">Input message.</param> /// <returns>Reply message with 'text/xml' RSS content</returns> [OperationContract, HttpMethod("GET")] Message GetRss(Message message); /// <summary> /// Gets the channel logo as a raw binary image with appropriate /// media type, typically image/gif, image/jpeg or image/png /// </summary> /// <param name="message">Input message.</param> /// <returns>Reply message with 'image/*' binary content</returns> [OperationContract, HttpMethod("GET", UriSuffix = "/logo")] Message GetLogo(Message message); /// <summary> /// Gets the RSS for "now", which is typically including /// the next 12 hours of guide data from the current time /// onward and including currently running shows. /// </summary> /// <param name="message">Input message.</param> /// <returns>Reply message with 'text/xml' <a href="http://blogs.law.harvard.edu/tech/rss"> /// RSS 2.0</a> content</returns> [OperationContract, HttpMethod("GET", UriSuffix = "/now")] Message GetRssForNow(Message message); ...
/// <summary> /// Gets an ASX media metadata document containing a reference to /// the live TV stream for this channel and a reference to the /// HTMLView that provides the UI inside Windows Media Player. /// </summary> /// <param name="message">Input message.</param> /// <returns>Reply message with 'video/x-ms-asf' <a href="http://msdn.microsoft.com/library/default.asp?url=/library/en-us/wmplay10/mmp_sdk/asxelement.asp"> /// ASX 3.0</a> content.</returns> [OperationContract, HttpMethod("GET", UriSuffix = "/media")] Message GetMedia(Message message); /// <summary> /// Gets information about the current media session hosted by the provider. /// </summary> /// <param name="message">Input message.</param> /// <returns>Reply message with 'text/xml' content</returns> [OperationContract, HttpMethod("GET", UriSuffix = "/media/session")] Message GetMediaSession(Message message); /// <summary> /// Gets the "media display envelope". This is an HTML stream that is loaded /// by Windows Media Player to render an AJAX UI for accessing this service. /// </summary> /// <param name="message">Input message.</param> /// <returns>Reply message with 'text/html' content</returns> [OperationContract, HttpMethod("GET", UriSuffix = "/media/envelope")] Message GetMediaDisplayEnvelope(Message message); /// <summary> /// Gets a media display envelope collateral data element. This method /// acts as a web-server and serves up binary files or text files referenced /// by the media display envelope. Requests to this endpoint are HTTP GET /// requests to the service base URL with the suffix '/media/envelope' with an /// appended '/' and the file name of the file that is being requested from the /// service runtime's 'envelope' directory. /// </summary> /// <param name="message">Input message.</param> /// <returns>Reply message containing a raw binary file with appropriate media type</returns> [OperationContract, HttpMethod("GET", UriSuffix = "/media/envelope/*")] Message GetMediaDisplayEnvelopeCollateral(Message message); /// <summary> /// Gets the detail information for a particular episode /// in the EPG guide data (linked from RSS) or for a given /// recording. /// </summary> /// <param name="message">Input message.</param> /// <returns>Reply message containing 'text/xml' with detail information.</returns> [OperationContract, HttpMethod("GET", UriSuffix = "/item/?")] Message GetItemDetail(Message message); /// <summary> /// Adds detail information for a particular episode. Concretely this /// allows adding a recoding job to the episode data that will cause this /// show to be recorded. /// </summary> /// <param name="message">Input message.</param> /// <returns>Reply message with HTTP 200 OK status code</returns> [OperationContract, HttpMethod("POST", UriSuffix = "/item/?")] Message PostItemDetail(Message message); /// <summary> /// Deletes some of the item detail information for a particular episode. /// This is used to cacnel a recording for the episode. /// </summary> /// <param name="message">Input message.</param> /// <returns>Reply message with HTTP 200 OK status code.</returns> [OperationContract, HttpMethod("DELETE", UriSuffix = "/item/?")] Message DeleteItemDetail(Message message); /// <summary> /// Method receiving all unknown messages sent to this endpoint /// </summary> /// <param name="message">The message</param> /// <returns></returns> [OperationContract(Action = "*")] Message HandleUnknownMessage(Message message); } |
If you look at the individual operations in the above contract, you’ll see that the suffix filter would – given a base address of http://www.example.com/TV – match requests made on the URIs http://www.example.com/TV/logo, http://www.example.com/TV/now, and http://www.example.com/TV/media to name just a few. A special case is the GetRss() operation, which does not have an explicit suffix defined and therefore causes the suffix filter to match on the base address. An important aspect of the suffix filter is that it does not consider the HTTP method (GET, POST). Matching the HTTP method to an operation is the job of the HttpMethodOperationSelectorBehavior, which acts higher up on the endpoint level and picks out the exact method that the call is being dispatched to. The filter is only deciding whether the message is “ours” with respect to the namespace it is targeting.
The HttpMethodOperationSelectorBehavior is hooked into the service endpoint by the HttpMethodOperationSelectorAttribute’s implementation of IContractBehavior that you can look up in Part 3. In BindDispatch(), the dispatcher’s OperationSelector property is set to a new instance of our specialized operation selector. An “operation selector” is a class that takes an incoing request on an endpoint and figures out the proper operation to dispatch to. The default operation selector in Indigo acts according to the SOAP dispatch rules that I explained in Part 1 (see “Figuring out a programming model”).
However, in our REST/POX world that we’re building here we do not have a concept of “SOAP action”, but rather URIs and HTTP methods and therefore the default dispatch mechanism doesn’t take us very far. Hence, we need to replace the operation selection algorithm with our own and we do that by implementing IDispatchOperationSelector:
using System; using System.Collections.Generic; using System.Text; using System.Text.RegularExpressions; using System.ServiceModel; using System.ServiceModel.Configuration; using System.ServiceModel.Channels;
namespace newtelligence.ServiceModelExtensions { /// <summary> /// /// </summary> public class HttpMethodOperationSelectorBehavior : IDispatchOperationSelector { ContractDescription description; IDispatchOperationSelector defaultSelector;
/// <summary> /// Initializes a new instance of the <see cref="T:HttpMethodOperationSelectorBehavior"/> class. /// </summary> /// <param name="description">The description.</param> /// <param name="defaultSelector">The default selector.</param> public HttpMethodOperationSelectorBehavior(ContractDescription description, IDispatchOperationSelector defaultSelector) { this.description = description; this.defaultSelector = defaultSelector; }
/// <summary> /// Selects the operation. /// </summary> /// <param name="message">The message.</param> /// <returns></returns> public string SelectOperation(ref Message message) { if (message.Properties.ContainsKey(HttpRequestMessageProperty.Name)) { HttpRequestMessageProperty msgProp = message.Properties[HttpRequestMessageProperty.Name] as HttpRequestMessageProperty; string baseUriPath = message.Headers.To.AbsolutePath; List<OperationDescription> operationsWithSuffix = new List<OperationDescription>();
/* Check methods with UriSuffix first. For that we first add * operation descriptions that have the correct http method into * a list and then sort that list by the processing order */ foreach (OperationDescription opDesc in description.Operations) { HttpMethodAttribute methodAttribute = opDesc.Behaviors.Find<HttpMethodAttribute>(); if (methodAttribute != null && String.Compare(methodAttribute.Method, msgProp.Method, true) == 0 && methodAttribute.UriSuffix != null) { operationsWithSuffix.Add(opDesc); } }
/* * We are sorting the list based on two criteria: * a) ProcessingPriority value, and if that's equal: * b) Length of the UriSuffix expression */ operationsWithSuffix.Sort( delegate(OperationDescription descA, OperationDescription descB) { HttpMethodAttribute descAAttr = descA.Behaviors.Find<HttpMethodAttribute>(); HttpMethodAttribute descBAttr = descB.Behaviors.Find<HttpMethodAttribute>(); int result = descAAttr.Priority.CompareTo(descBAttr.Priority); if (result == 0) { result = Math.Sign(descAAttr.UriSuffix.Length - descBAttr.UriSuffix.Length); } return result; } );
for (int i = operationsWithSuffix.Count-1; i >= 0; i--) { OperationDescription opDesc = operationsWithSuffix[i]; HttpMethodAttribute methodAttribute = opDesc.Behaviors.Find<HttpMethodAttribute>(); // we have a method attribute, the attribute's method value matches // the incoming http request and we do have a regex. Match match = methodAttribute.UriSuffixRegex.Match(baseUriPath); if (match != null && match.Success) { return opDesc.Name; } }
/* now check the rest */ foreach (OperationDescription opDesc in description.Operations) { HttpMethodAttribute methodAttribute = opDesc.Behaviors.Find<HttpMethodAttribute>(); if (methodAttribute != null && methodAttribute.UriSuffixRegex == null) { // we have a http method attribute and the method macthes the request // method: match if (String.Compare(methodAttribute.Method, msgProp.Method, true) == 0) { return opDesc.Name; } } else if (String.Compare(opDesc.Name, msgProp.Method, true) == 0) { // we do not have a http method attribute, but the method name // equals the http method. return opDesc.Name; } }
// No match so far. Now lets find a wildcard method. foreach (OperationDescription opDesc in description.Operations) { if (opDesc.Messages.Count > 0 && opDesc.Messages[0].Action == "*" && opDesc.Messages[0].Direction == TransferDirection.Incoming) { return opDesc.Name; } } }
// No match so far, delegate to the default selector if one is present if (defaultSelector != null) { return defaultSelector.SelectOperation(ref message); } return ""; } } } |
As you can see, there is only one method: SelectOperation. The method will only do work on its own if the incoming request is an HTTP request received by Indigo’s HTTP transport. We can figure this out by looking into the message properties and looking for the presence of a property with the name HttpRequestMessageProperty.Name. The presence of this property is required, because that’s the vehicle through which Indigo gives us access to the HTTP method that was used for the request. What we’re looking for sits as an instance string property on HttpRequestMessageProperty.Method.
The algorithm itself is fairly straightforward:
1. We grab all operations whose HttpMethodAttribute.Method property matches (case-insensitively) the incoming HTTP method string and which have a suffix expression and throw them into a list.
2. We sort the list by the priority of the attributes amongst each other. I introduced the priorities, because I am allowing wildcards here and I want to allow the suffixes /item/detail and /item/* (read: “anything except detail”) to coexist on the same endpoint, but I need a something other than method order to specify that the match on the concrete expression should be done before the wildcard expression. In absence of priorities and/or in the case of collisions, longer suffixes always trump shorter expressions for matching priority.
3. We match the sorted list in reverse order (higher priority is better) and return the first operation in the list whose suffix expression matches the incoming messages “To” header (which is the same as the HTTP request URI).
4. If we don’t have a match, we proceed to iterate over all operations that do not have a suffix and see whether we can find a match solely based on the HttpMethodAttribute.Method value or, if the HttpMethodAttribute is absent, on the plain method name. (So if the method just named “Get” and there is no attribute, an HTTP GET request will still match).
5. If we still don’t have a match, we look for the common “all messages without a proper home” method with an OperationContract.Action value of “*”.
6. And as the very last resort we fall back to the default selector if we have been given one and else we fail out by returning an empty string, which means that there is no match at at all.
If we find a match, we return a string that’s the same as the name of the method we want to dispatch to and Indigo will them promptly do the right thing and call the respective method, either by passing the raw message outright (as in my TV app) or by breaking up the message body using the XmlFormatter or the XmlSerializer and passing a typed message or a set of parameters.
Step 4 is noteworthy insofar as that the [HttpMethod] attributes aren’t strictly necessary. If you name your methods exactly like the HTTP methods they should handle, the operation selector will figure this out. If that’s what you want, you don’t even need the [HttpMethodOperationSelector] attribute, if you choose to add that information in the configuration file instead. To enable that. I’ve built the required configuration class that you can register in the <behaviorExtensions> and map to the <behaviors> section of an endpoint’s configuration. The class is very, very simple:
using System; using System.Collections.Generic; using System.Text; using System.ServiceModel.Configuration;
namespace newtelligence.ServiceModelExtensions { public class HttpMethodOperationSelectorSection : BehaviorExtensionSection { public HttpMethodOperationSelectorSection() { }
protected override object CreateBehavior() { return new HttpMethodOperationSelectorAttribute(); }
public override string ConfiguredSectionName { get { return "httpMethodOperationSelector"; } } } } |
Alright, so where are we? We’ve got dispatch metadata, we’ve got an endpoint dispatch mechanism and we’ve got an operation dispatch mechanism. Furthermore we have a tool that conveniently grabs “parameter segments” from the URI and maps them to an out-of-band collection on the UriArgumentsMessageProperty from where we can conveniently fetch them inside the service implementation.
What we don’t have is POX. We’re still dealing with SOAP messages here. So the next step is to modify the wire encoding in a way that we unwrap the content and throw away the envelope on the way out and that we wrap incoming “raw” data into an envelope to make Indigo happy with incoming requests.
That’s plenty of material for Part 5 and beyond. Stay tuned.
Go to Part 5