Jumat, 28 Januari 2011

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.



Filed under: SOA, WCF/WF

Tidak ada komentar: