WCF Routing Service Deep Dive: Part II–Using Filters

In the first part of this series, I compared the WCF Routing Service with BizTalk Server for messaging routing scenarios.  That post was a decent initial assessment of the Routing Service, but if I stopped there, I wouldn’t be giving the additional information needed for you to make a well-informed decision.  In this post, we’ll take a look at a few of the filters offered by the Routing Service and how to accommodate a host of messaging scenarios.

Filtering by SOAP Action

If you have multiple operations within a single contract, you may want to leverage the ActionMessageFilter in the Routing Service.  Why might we use this filter?  In one case, you could decide to multicast a message to all services that implement the same action.  If you had a dozen retail offices who all implement a SOAP operation called “NotifyProductChange”, you could easily define each branch office endpoint in the Routing Service configuration and send one-way notifications to each service.  I

n the case below, we want to send all operations related to event planning to a single endpoint and let the router figure out where to send each request type.

I’ve got two services.  One implements a series of operations for occasions (event) that happen at a company’s headquarters.  The second service has operations for dealing with attendees at any particular event.  The WCF contract for the first service is as so:

 [ServiceContract(Namespace="http://Seroter.WcfRoutingDemos/Contract")]
    public interface IEventService
    {
        [OperationContract(Action = "http://Seroter.WcfRoutingDemos/RegisterEvent")]
        string RegisterEvent(EventDetails details);

        [OperationContract(Action = "http://Seroter.WcfRoutingDemos/LookupEvent")]
        EventDetails LookupEvent(string eventId);
    }

[DataContract(Namespace="http://Seroter.WcfRoutingDemos/Data")]
    public class EventDetails
    {
        [DataMember]
        public string EventName { get; set; }
        [DataMember]
        public string EventLocation { get; set; }
        [DataMember]
        public int AttendeeCount { get; set; }
        [DataMember]
        public string EventDate { get; set; }
        [DataMember]
        public float EventDuration { get; set; }
        [DataMember]
        public bool FoodNeeded { get; set; }
    }

The second contract looks like this:

[ServiceContract(Namespace = "http://Seroter.WcfRoutingDemos/Contract")]
    public interface IAttendeeService
    {
         [OperationContract(Action = "http://Seroter.WcfRoutingDemos/RegisterAttendee")]
         string RegisterAttendee(AttendeeDetails details);
    }

     [DataContract(Namespace = "http://Seroter.WcfRoutingDemos/Data")]
     public class AttendeeDetails
     {
         [DataMember]
         public string LastName { get; set; }
         [DataMember]
         public string FirstName { get; set; }
         [DataMember]
         public string Dept { get; set; }
         [DataMember]
         public string EventId { get; set; }
         
     }

These two services are hosted in a console-based service host that exposes the services on basic HTTP channels.  This implementation of the Routing Service is hosted in IIS and its service file (.svc) has a declaration that points it to the fully qualified path of the WCF Routing Service.

<%@ ServiceHost Language="C#" Debug="true" Service="System.ServiceModel.Routing.RoutingService,System.ServiceModel.Routing, version=4.0.0.0, Culture=neutral,PublicKeyToken=31bf3856ad364e35"  %>

The web configuration of the Routing Service is where all the fun is.  I first defined the Routing Service (with name System.ServiceModel.Routing.RoutingService) and contract System.ServiceModel.Routing.IRequestReplyRouter.  The Routing Service offers multiple contracts; in this case, I’m using the synchronous one which does NOT support multi-cast. My Routing Service has two client endpoints; one for each service created above.

Let’s check out the filters.  In this case, I have two filters with a filterType of Action. The filterData attribute of the filter is set to the Action value for each service’s SOAP action.

<filters>
     <filter name="RegisterEventFilter" filterType="Action" filterData="http://Seroter.WcfRoutingDemos/RegisterEvent"/>
     <filter name="RegisterAttendeeFilter" filterType="Action" filterData="http://Seroter.WcfRoutingDemos/RegisterAttendee"/>
</filters>

Next, the filter table maps the filter to which WCF endpoint will get invoked.

<filterTable name="EventRoutingTable">
      <add filterName="RegisterEventFilter" endpointName="CAEvents" priority="0"/>
      <add filterName="RegisterAttendeeFilter" endpointName="AttendeeService" priority="0"/>
</filterTable>

I also have a WCF service behavior that contains the RoutingBehavior with the filterTableName equal to my previously defined filter table.  Finally, I updated my Routing Service definition to include a reference to this service behavior.

<behaviors>
<serviceBehaviors>
       <behavior name="RoutingBehavior">
          <routing routeOnHeadersOnly="false" filterTableName="EventRoutingTable" />
          <serviceDebug includeExceptionDetailInFaults="true" />
       </behavior>
    </serviceBehaviors>
</behaviors>
<services>
	 <service behaviorConfiguration="RoutingBehavior" name="System.ServiceModel.Routing.RoutingService">
 		<endpoint binding="basicHttpBinding" bindingConfiguration=""
   name="RoutingEndpoint" contract="System.ServiceModel.Routing.IRequestReplyRouter" />
	 </service>
</services>

What all this means is that I can now send either Attendee Registration OR Event Registration to the exact same endpoint address and the messages will route to the correct underlying service based on the SOAP action of the message.

2011.1.13routing02

Filtering by XPath

Another useful way to route messages is by looking at the payloads themselves.  The WCF Routing Service has an XPath filter that lets you poke into the message body to find a match to a particular XPath query.  In this scenario, as an extension of the previous, we still have a service that processes events for California, and now we want a new service that receives events for Washington state.  Our Routing Service should steer requests to either the California service or Washington service based on the “location” node of the message payload.

Within the routing configuration, I have a namespace table that allows me to set up an alias used during XPath queries.

<namespaceTable>
    <add prefix="custom" namespace="http://Seroter.WcfRoutingDemos/Data"/>
</namespaceTable>

Next, I have two filters with a filterType of XPath and a filterData attribute that holds the specific XPath statement.

<filters>
    <filter name="CAEventFilter" filterType="XPath" filterData="//custom:EventLocation='CA'"/>
    <filter name="WAEventFilter" filterType="XPath" filterData="//custom:EventLocation='WA'"/>
</filters>

The filter table maps each XPath filter to a given endpoint.

<filterTables>
        <filterTable name="EventRoutingTable">
          <add filterName="CAEventFilter" endpointName="CAEvents" priority="0" />
          <add filterName="WAEventFilter" endpointName="WAEvents" priority="0" />
        </filterTable>
      </filterTables>

When I call my (routing) service now and pass in a Washington event and a California event I can see that each distinct service is called.

2011.1.13routing01

Note that you can build XPath statements using operations that combine criteria.  For instance, what if we wanted every event with an attendee count greater than 50 to go to the California service to be evaluated first.  My filters below include the California filter that has an “or” between two criteria.

<filter name="CAEventFilter" filterType="XPath" filterData="//custom:EventLocation='CA' or //custom:AttendeeCount > 50"/>
<filter name="WAEventFilter" filterType="XPath" filterData="//custom:EventLocation='WA'"/>

As it stands, if I execute the Routing Service again, and pass in a WA location for 60 users, I get an error because BOTH filters match.  The error tells me that “ Multicast is not supported with Request-Reply communication.”  So, we need to leverage the priority attribute of the filter to make sure that the California filter is evaluated first and if a match is found, the second filter is skipped.

<add filterName="RegisterAndCAFilter" endpointName="CAEvents" priority="3" />
<add filterName="RegisterAndWAFilter" endpointName="WAEvents" priority="2" />

Sure enough, when I call the service again, we can see that I have a Washington location, but because of the size of the meeting, the California service was called.

2011.1.13routing03

Complex Filters Through Joins

There may arise a need to do more complex filters that mix different filter types.  Previously we saw that it’s relatively easy to build a composite XPath query.  However, what if we want to combine the SOAP action filter along with the XPath filter?  Just enabling the previous attendee service filter (so that we have three total filters in the table) actually does work just fine.  However, for demonstration purposes, I’ve created a new filter using the And type and combine the registration Action filter to each registration XPath filter.

<filters>
        <filter name="RegisterEventFilter" filterType="Action" filterData="http://Seroter.WcfRoutingDemos/RegisterEvent"/>
        <filter name="RegisterAttendeeFilter" filterType="Action" filterData="http://Seroter.WcfRoutingDemos/RegisterAttendee"/>
        <filter name="CAEventFilter" filterType="XPath" filterData="//custom:EventLocation='CA' or //custom:AttendeeCount > 50"/>
        <filter name="WAEventFilter" filterType="XPath" filterData="//custom:EventLocation='WA'"/>
        <!-- *and* filter -->
        <filter name="RegisterAndCAFilter" filterType="And" filter1="RegisterEventFilter" filter2="CAEventFilter"/>
        <filter name="RegisterAndWAFilter" filterType="And" filter1="RegisterEventFilter" filter2="WAEventFilter"/>
</filters>

In this scenario, a request that matches both criteria will result in the corresponding endpoint being invoked.  As I mentioned, this particular example works WITHOUT the composite query, but in real life, you might combine the endpoint address with the SOAP action, or a custom filter along with XPath.  Be aware that an And filter only allows the aggregation of two filters.  I have not yet tried making the criteria in one And filter equal to other And filters to see if you can chain more than two criteria together.  I could see that working though.

Applying a “Match All” Filter

The final filter we’ll look at is the “match all” filter which does exactly what its name says.  Any message that arrives at the Routing Service endpoint will call the endpoint associated with the “match all” filter (except for a scenario mentioned later).  This is valuable if you have a diagnostic service that subscribes to every message for logging purposes.  We could also use this if we had a Routing Service receiving a very specific set of messages and we wanted to ALSO send those messages somewhere else, like BizTalk Server.

One critical caveat for this filter is that it only applies to one way or duplex Routing Service instances.  Two synchronous services cannot receive the same inbound message.  So, if I added a MatchAll filter to my current configuration, an error would occur when invoking the Routing Service.  Note that the Routing Service contract type is associated with the WCF service endpoint.  To use the MatchAll filter, we need another Routing Service endpoint that leverages the ISimplexDatagramRouter contract.  ALSO, because the filter table is tied to a service behavior (not endpoint behavior), we actually need an entirely different Routing Service definition.

I have a new Routing Service with its own XML configuration and routing table.  Back in my IEventService contract, I’ve added a new one way interface that accepts updates to events.

[ServiceContract(Namespace="http://Seroter.WcfRoutingDemos/Contract")]
    public interface IEventService
    {
        [OperationContract(Action = "http://Seroter.WcfRoutingDemos/RegisterEvent")]
        string RegisterEvent(EventDetails details);

        [OperationContract(Action = "http://Seroter.WcfRoutingDemos/LookupEvent")]
        EventDetails LookupEvent(string eventId);

        [OperationContract(Action = "http://Seroter.WcfRoutingDemos/UpdateEvent", IsOneWay=true)]
        void UpdateEvent(EventDetails details);
    }

I want my new Routing Service to front this operation.  My web.config for the Routing Service has two client endpoints, one for each (CA and WA) event service.  My Routing Service declaration in the configuration now uses the one way contract.

<service behaviorConfiguration="RoutingBehaviorOneWay" name="System.ServiceModel.Routing.RoutingService">
	<endpoint binding="basicHttpBinding" bindingConfiguration=""
		 name="RoutingEndpoint" contract="System.ServiceModel.Routing.ISimplexDatagramRouter" />
</service>

I have the filters I previously used which route based on the location of the event.  Notice that both of my filters now have a priority of 0.  We’ll see what this means in just a moment.

<filters>
	<filter name="CAEventFilter" filterType="XPath" filterData="//custom:EventLocation='CA' or //custom:AttendeeCount > 50"/>
	<filter name="WAEventFilter" filterType="XPath" filterData="//custom:EventLocation='WA'"/>		
</filters>
<filterTables>
	<filterTable name="EventRoutingTableOneWay">
		<add filterName="CAEventFilter" endpointName="CAEvents" priority="0" />
		<add filterName="WAEventFilter" endpointName="WAEvents" priority="0" />
	</filterTable>
</filterTables>

When I send both a California event update request and then a Washington event update request to this Routing Service, I can see that both one-way updates successfully routed to the correct underlying service.

2011.1.13routing04

Recall that I set my filter’s priority value to 0.  I am now able to multi-cast because I am using one-way services.  If I send a request for a WA event update with more than 50 attendees (which was previously routed to the CA service), I now have BOTH services receive this request.

2011.1.13routing05

Now I can also able to use the MatchAll filter.  I’ve created an additional service that logs all messages it receives.  It is defined by this contract:

[ServiceContract(Namespace = "http://Seroter.WcfRoutingDemos/Contract")]
    public interface IAdminService
    {
        [OperationContract(IsOneWay = true, Action = "*")]
        void LogMessage(Message msg);
    }

Note that it’s got an “any” action type.  If you put anything else here, this operation would fail to match the inbound message and the service would not get called.  My filters and filter table now reflect this new logging service.  Notice that I have a MatchAll filter in the list.  This filter will get called every time. 

<filters>
	<filter name="CAEventFilter" filterType="XPath" filterData="//custom:EventLocation='CA' or //custom:AttendeeCount > 50"/>
	<filter name="WAEventFilter" filterType="XPath" filterData="//custom:EventLocation='WA'"/>
	<!-- logging service -->
	<filter name="LoggingServiceFilter" filterType="MatchAll"/>
</filters>
<filterTables>
	<filterTable name="EventRoutingTableOneWay">
		<add filterName="CAEventFilter" endpointName="CAEvents" priority="0" />
		<add filterName="WAEventFilter" endpointName="WAEvents" priority="0" />
		<!-- logging service -->
		<add filterName="LoggingServiceFilter"  endpointName="LoggingService" priority="0"/>
	</filterTable>
</filterTables>

When I send in a California event update, notice that both the California service AND the logging service are called.

2011.1.13routing06

Finally, what happens if the MatchAll filter has a lower priority than other filters?  Does it get skipped?  Yes, yes it does.  Filter priorities still trump anything else.  What if the MatchAll filter has the highest priority, does it stop processing any other filters?  Sure enough, it does.  Carefully consider your priority values because only filters with matching priority values will guarantee to be evaluated.

Summary

The Routing Service has some pretty handy filters that give you multiple ways to evaluate inbound messages.  I’m interested to see how people mix and match the Routing Service contract types (two-way, one-way) in IIS hosted services as most demos I’ve seen show the Service being self-hosted.  I think you have to create separate service projects for each contract type you wish to support, but if you have a way to have both services in a single project, I’d like to hear it.

Author: Richard Seroter

Richard Seroter is currently the Chief Evangelist at Google Cloud and leads the Developer Relations program. He’s also an instructor at Pluralsight, a frequent public speaker, the author of multiple books on software design and development, and a former InfoQ.com editor plus former 12-time Microsoft MVP for cloud. As Chief Evangelist at Google Cloud, Richard leads the team of developer advocates, developer engineers, outbound product managers, and technical writers who ensure that people find, use, and enjoy Google Cloud. Richard maintains a regularly updated blog on topics of architecture and solution design and can be found on Twitter as @rseroter.

8 thoughts

  1. Richard very good article, yes of course this might not be the good scenario to use WCF routing but do you have any idea about how big message the WCF routing service can route, what I want to know is how WCF do this content based routing and how the size of message can impact its performance.

  2. Richard it is a very good article for people who are starting on WCF 4.0, I have a question if I am going to configure my Router configuration using C# code progrmmatically, how are we going to replace contract =”*” using ContractDescription.

  3. Hi,
    I found it out we cant use ISimplexDatagramRouter as our ContractDescription for the endpoints in the filter table, it has to be
    var cd = new ContractDescription(“contract”){ ConfigurationName = “*” };
    Thanks,
    Bhaskar.

  4. Hi,

    Please do let me know from where I can download the code for this tutorial? If you email me this would be great.

    /Rizwan

  5. Hi Richard,

    Here is response on “I’m interested to see how people mix and match the Routing Service contract types (two-way, one-way) in IIS hosted services …”

    Despite commonly perceived limitations of the Routing Service to handle service contracts with a mix of one-way and two-way operations, it is actually very much possible. In fact, there is more than one solution to this problem. What is common to all these solutions is an additional knowledge that the Routing Service has to be enabled with in regards to messages routed at runtime. For example, if a Routing Service is configured with the list of messages SOAP actions mapped to “one-way” or “two-way” flag then WCF RS can leverage this knowledge at runtime to intelligently process routed messages.

    Solution 1. This solution was prompted to me few years ago by Matt Snider and his team at Microsoft who developed WCF Routing Service in the first place. In this solution WCF RS is configured with two-way contracts only, but once a one-way message is received, WCF RS pushes a null Message back to its inbound WCF channel stack. When null Message is received by WCF RS inbound stack, it gracefully “eats it up”. Effectively this turns what was a design-time two-way invocation into runt-time one-way invocation.

    Solution 2. Solution 1 has its own limitations on supported use cases even though it still adds additional value to the out-of-the box WCF RS capabilities. A better (and complete) solution will be to create your own Routing Service class that wraps up WCF RS so that custom RS class implements all the WCF RS interfaces and exposes all their (differently shaped) operations. Now you can leverage WCF IDispatchOperationSelector interface and let custom Router Service delegate calls to either a one-way or two-way operation based on incoming message SOAP action.

    As an example, Nevatech’s Sentinet platform leverages both approaches at the core of its implementation, where RS is all deployed and operates in IIS Server hosted environment and adds many more capabilities on top of traditional WCF RS concepts, http://www.nevatech.com/overview.aspx

    Andrew Slivker

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.