Teaching Indigo to do REST/POX: Part 3

7 minutes read

Part 1, Part 2

If you’ve read the first two parts of this series, you should know by now (if I’ve done a reasonable job explaining) about the fundamental concepts of how incoming web service messages (requests) are typically dispatched to their handler code and also understand how my Indigo REST/POX extensions are helping to associate the metadata required for dispatching plain, envelope-less HTTP requests with Indigo service operations using the HttpMethod attribute and how the HttpMethodParameterInspector breaks up the URI components into easily consumable, out-of-band parameters that flow into the service code the UriArgumentsMessageProperty.

What I have not explained is how the dispatching is actually done. There are two parts to that story: Dispatching to services on the listener level (which I will cover here) and dispatching to operations at the endpoint level (which I’ll cover in part 4).

When an HTTP request is received on a namespace that Indigo has registered with HTTP.SYS, the request is matched against a collection of “address filters”. “Registering a namespace” means that if you configure a service-endpoint to listen at the endpoint http://www.example.com/foo, the service-endpoint “owns” that URI.

What’s noteworthy is that if you have an Indigo/WCF application listening to endpoints at http://www.example.com/baz, http://www.example.com/foo and http://www.example.com/foo/bar, the demultiplexing (“demuxing” in short) of the requests is done by Indigo and not by the network stack. HTTP.SYS will push requests from any registered URI namespace of the particular application into the “shared” Indigo HTTP transport and leave it up to Indigo to figure out the right endpoint to dispatch to. And that turns out to be perfect for our purposes.

Whenever an incoming message needs to be dispatched to an endpoint, the message is matched against an address filter table. [For the very nosy: The place where it all happens is in the internal EndpointListenerTable class’s Lookup method, which you could probably look at if you had the right tools, but I didn’t say that.]

By default, the address filter that is used for any “regular” service is the EndpointAddressFilter, which reports a match if the incoming message’s “To” addressing header (which is constructed from the HTTP header information if it’s not immediately contained in the incoming message) is a match for the registered URI. Whether a match is found is dependent on the URI’s port and host-name (controllable by the HostNameComparisonMode in the HTTP binding configuration) and the URIs remaining path, which must be an exact match for the registered service endpoint URI. Since we want to introduce a slightly different dispatch scheme that is based on matching not only on the exact endpoint URI’s path but also on suffixes appended to that URI, we must put a hook into the dispatch mechanism and extend the default behavior. If a method marked up with [HttpMethod(“GET”,UriSuffix=”/bar”)] and the endpoint is hosted at http://www.example.com/foo, we want any HTTP GET request to http://www.example.com/foo/bar to be dispatched to that endpoint and, subsequently, to that exact method.

To infuse that behavior into Indigo, we need to tell it so. If you take a look at Part 2 and at the service contract declarations that I posted there, you will notice the HttpMethodOperationSelector attribute alongside the ServiceContract attribute. That attribute class does the trick:

using System;
using System.Collections.Generic;
using System.Text;
using System.ServiceModel;

namespace newtelligence.ServiceModelExtensions
{
   public class HttpMethodOperationSelectorAttribute :
                  Attribute, IContractBehavior, IEndpointBehavior
   {
      public void BindDispatch(
                ContractDescription description,
                IEnumerable<ServiceEndpoint> endpoints,
                DispatchBehavior dispatch,
                BindingParameterCollection parameters)
      {
         dispatch.OperationSelector =
              new HttpMethodOperationSelectorBehavior(description, dispatch.OperationSelector);
         foreach (ServiceEndpoint se in endpoints)
         {
            if (se.Behaviors.Find<HttpMethodOperationSelectorAttribute>() == null)
            {
               se.Behaviors.Add(this);
            }
         }
           
      }

      public void BindProxy(
                     ContractDescription description,
                     ServiceEndpoint endpoint,
                     ProxyBehavior proxy,
                     BindingParameterCollection parameters)
      {

      }

      public bool IsApplicableToContract(Type contractType)
      {
         return true;
      }

      public void BindServiceEndpoint(
                     ServiceEndpoint serviceEndpoint,
                     EndpointListener endpointListener,
                     BindingParameterCollection parameters)
      {
            SuffixFilter suffixFilter = null;

            if (endpointListener.AddressFilter == null ||
                !(endpointListener.AddressFilter is SuffixFilter))
            {
                suffixFilter = new SuffixFilter(endpointListener, endpointListener.AddressFilter);
                endpointListener.AddressFilter = suffixFilter;
                ((Dispatcher)endpointListener.Dispatcher).Filter = suffixFilter;
            }
            else
            {
                suffixFilter = endpointListener.AddressFilter as SuffixFilter;
            }

         foreach (OperationDescription opDesc in serviceEndpoint.Contract.Operations)
         {
            HttpMethodAttribute methodAttribute = opDesc.Behaviors.Find<HttpMethodAttribute>();
            if (methodAttribute != null)
            {
               if (methodAttribute.UriSuffixRegex != null)
               {
                  suffixFilter.AddSuffix(methodAttribute.UriSuffixRegex);
               }
            }
         }
      }
   }
}


In the attribute’s implementation of IEndpointBehavior.BindServiceEndpoint, which is invoked by Indigo as the endpoint is initialized (in response to ServiceHost.Open() ), we replace the service’s default endpoint filter with our own SuffixFilter class. Once we’ve done that, we iterate over the HttpMethodAttribute metadata elements that sit on the individual operations/methods in the contract description (this is the actual reason we put them there, see Part 2) and add any suffix we find to the filter’s suffix table. We’ll get back to this class in the next part while to investigate how the “operation selector” is hooked in; let’s investigate the suffix filter first.

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>
    /// This class implements a specialized ServiceModel address filter
    /// that allows matching URL suffixes.
    /// </summary>
    /// <remarks>
    /// The class aggregates an EndpointAddressFilter to helpi with the matching logic.
    /// </remarks>
    public class SuffixFilter : Filter
    {
        /// <summary>
        /// List for the suffixes.
        /// </summary>
        List<Regex> suffixes;
        /// <summary>
        /// Original filter that we delegate to if we can't match with this
        /// one.
        /// </summary>
        Filter originalFilter;
        /// <summary>
        /// The endpoint listener that this filter is applied to
        /// </summary>
        EndpointListener endpointListener;
        /// <summary>
        /// The aggregated endpoint address filter
        /// </summary>
        EndpointAddressFilter addressFilter;

        /// <summary>
        /// Creates a new instance of SuffixFilter
        /// </summary>
        /// <param name="endpointListener">EndpointListener this filter is attached to</param>
        /// <param name="originalFilter">Original AddressFilter of the EndpointListener</param>
        public SuffixFilter(EndpointListener endpointListener, Filter originalFilter)
        {
            this. suffixes = new List<Regex>();
            this. originalFilter = originalFilter;
            this. endpointListener = endpointListener;
        }

        /// <summary>
        /// Implements the matching logic
        /// </summary>
        /// <param name="message">Message that shall be matched</param>
        /// <returns>Returns an indicator for whether the message is considered a match</returns>
        public override bool Match(Message message)
        {
            // Workaround for Nov2006 CTP bug. GetEndpointAddress() cannot be
            // called on an EndpointListener before the listener is running.
            if ( addressFilter == null)
            {
                addressFilter = new EndpointAddressFilter( endpointListener.GetEndpointAddress(), false);
            }

            // check whether we have an immediate match, which means that the message's
            // To Header is an excat match for the EndpointListener's address
            if ( addressFilter.Match(message))
            {
                return true;
            }
            else
            {
                // no direct match. Save the original header value and chop off the
                // query portion of the URI.
                Uri originalTo = message.Headers.To;
                string baseUriPath = originalTo.AbsolutePath;
                string baseUriRoot = originalTo.GetLeftPart(UriPartial.Authority);

                // match against the suffix list
                foreach (Regex suffixExpression in suffixes)
                {
                    Match match = suffixExpression.Match(baseUriPath);
                    if (match != null && match.Success)
                    {
                        string filterUri = baseUriRoot+baseUriPath.Remove(baseUriPath.LastIndexOf(match.Value));
                        message.Headers.To = new Uri(filterUri);
                        if ( addressFilter.Match(message))
                        {
                            message.Headers.To = originalTo;
                            return true;
                        }
                        message.Headers.To = originalTo;
                    }                       
                }
            }
            if ( originalFilter != null)
            {
                // of no match has been found up to here, we match against the
                // original filter if that was provided.
                return originalFilter.Match(message);
            }
            return false;
        }

        /// <summary>
        /// Implements the matching logic by constructing a Message over
        /// a MessageBuffer and delegating to the Match(Message) overload
        /// </summary>
        /// <param name="buffer"></param>
        /// <returns></returns>
        public override bool Match(MessageBuffer buffer)
        {
            Message msg = buffer.CreateMessage();
            bool result = Match(msg);
            msg.Close();
            return result;
        }

        /// <summary>
        /// Adds a new suffix to the suffix table
        /// </summary>
        /// <param name="suffix">Suffix value</param>
        public void AddSuffix(Regex suffix)
        {
            suffixes.Add(suffix);
        }

        /// <summary>
        /// Removes a suffix from the suffix table
        /// </summary>
        /// <param name="suffix"></param>
        public void RemoveSuffix(Regex suffix)
        {
            suffixes.Remove(suffix);
        }
    }
}


The filter’s filet piece is in the Match method. To finally figure out whether a message is a match, we employ the matching logic of the default EndpointAddressFilter, which deals with matching the host names and the “base URI” at which the service was registered. What the suffix filter does in addition is to match the suffix regex pattern against the incoming message’s “To” header and if that is a match, the suffix is stripped and the remaining URI is matched against the aggregated EndpointAddressFilter. Only if we get a match for the suffix and for the remainder URI, we’ll report a positive match back to the infrastructure by returning true. And in that case and only in that case the service endpoint for which “this” suffix filter was installed and populated gets the request.

For each incoming request, Indigo goes through all registered endpoint address filters and asks them whether they want to service it. And that really means “all”. Indigo will refuse to service the request if two or more filters report ownership of the respective request and will throw a traceable (using Indigo tracing) internal exception that will cause none of the services to be picked due to this ambiguity. In the case of overlapping dispatch namespaces, none is indeed better than “any random”.

Next part: HttpMethodOperationSelectorBehavior

Go to Part 4

Updated:

Leave a Comment