Matt Milner
Pluralsight
Published: December, 2008
Articles in this series
Download the code for
this article
Windows Workflow Foundation (WF) ships with a robust
business rule engine that can be incorporated into workflows to assist in
managing business processes. This same business rule processing can be used in
various capacities with Windows Communication Foundation (WCF) to enrich its
message processing capabilities. This hands on article will walk through using
the rules engine to provide routing logic for a WCF message router.
Using rules in a web service router
When building web services there are times when it is
important to be able to write an intermediary service that acts as a router
making decisions about incoming messages and properly forwarding those messages
onto an actual service or services. This type of router provides an excellent
use case for applying business rules using the rules engine in Windows Workflow
Foundation. By defining the routing decisions as rules, the criteria and
endpoints can be expressed as a set of rules which can be managed separately
from the router logic and configuration. An example solution ships in the
samples that are part of the Windows Software Development Kit and can be found
in the WCF directory (WCF\Extensibility\Rules\WCF_Router\).
In this article we will build the rules used by the router
and review the code used to invoke those rules to control the message flow in
the router. This article also comes with a sample code implementation,
available at URL; the article will walk through how and why that sample code
project was created.
Understanding the WCF router basics
A router acts as an intermediary service accepting messages
from clients. After evaluating the message, the router acts as a proxy client,
forwarding the message to the service where it can be processed. A router can
receive many different messages and make dynamic decisions about the ultimate
destination of those messages based on message content and context. Routers can
be used for many different reasons in your solution including message filtering
for security purposes and service versioning. Figure 1 shows the interaction of
the client, services and router in this scenario.
.gif)
Figure 1: WCF routing
example
In order for this scenario to work, when the client sends
messages intended for the services, it must send them to an address that the
router is listening on instead of directly to the service. When configuring an
endpoint for a service, most WCF developers are familiar with configuring the
address for the endpoint. However, in addition to an address, a ServiceEndpoint
can also have a ListenUri property set. When no value is specified for the
ListenUri the endpoint is initialized with the address and begins listening.
When a value is present for the ListenUri, this becomes the physical address
that the endpoint registers and uses to listen for messages arriving.
Regardless of whether the endpoint has a ListenUri, it is the Address property
that is used to create the service description and is made public to the
client. In this scenario, the Address property describes an address that the
router is listening on while the ListenUri describes the physical address on
which the actual service is listening. Thus, when a client gets metadata from
the service it contains the correct contracts and binding information for the
service but the address of the router. Figure 2 shows how a given service
exposes both the ListenURI and the Address. The Address is the address of an
endpoint on the router which the client will use to call the service. The
ListenURI is the address the service is listening on and which the router uses
to communicate with the service to forward requests from the client.
.gif)
Figure 2: Listen URI
and address configuration
Figure 3 shows the service configuration for the EchoService
including the endpoint settings to handle the address. Notice that while both
the address and listenUri settings use the same server and port, the virtual
path is unique between them.
<service name="Microsoft.ServiceModel.Samples.EchoService"
behaviorConfiguration="metadataBehavior">
<host>
<baseAddresses>
<add baseAddress="http://localhost:8000/echo" />
</baseAddresses>
</host>
<endpoint address="http://localhost:8000/services/soap12/text"
listenUri="service"
contract="Microsoft.ServiceModel.Samples.IEchoService"
binding="wsHttpBinding"
bindingConfiguration="ServiceBinding" />
<!-- Echo service metadata endpoint. -->
</service>
Figure 3: Configuring
an endpoint with a ListenUri
In this example the routing decisions will be made by
examining the message headers to determine which service should receive the message.
For the calculator service, a custom header is used and is defined in the
endpoint configuration. The client and service each have their endpoint
configured with the details about the header to indicate that the header should
be sent with all messages passing through the endpoint. Figure 4 shows the
configuration in the app.config for the client. Notice the address points to
the router and the header will help the router know where to send the message.
<endpoint address="net.tcp://localhost:31080/services/soap12/binary"
binding="netTcpBinding" bindingConfiguration="NetTcpBinding_ICalculatorService"
contract="Microsoft.ServiceModel.Samples.ICalculatorService"
name="NetTcpBinding_ICalculatorService">
<headers>
<Calculator xmlns="http://Microsoft.ServiceModel.Samples/Router" />
</headers>
</endpoint>
Figure 4: Configuring
a header on an endpoint
Using rules in the router logic
The router is implemented as a WCF service and in the logic
for the service, it uses a class named RoutingTable to make decisions about
where to route messages. Open the Router.sln solution in the solution directory
found in the download this article and open the RoutingTable.cs file in the
router project. The fields in the RoutingTable class are shown in Figure 5 and
include several properties that are only used by the rules, as well as a
variable to hold a reference to the RuleEngine and the RuleSet.
public class RoutingTable
{
Random randomNumberGenerator;
Message currentMessage;
IList<EndpointAddress> possibleAddress;
EndpointAddress selectedAddress;
XmlNamespaceManager manager;
RuleSet ruleSet;
RuleEngine ruleEngine;
….
}
Figure 5: The
RoutingTable class definition
When the RoutingTable class is instantiated by an extension
to the ServiceHost, it initializes the RuleEngine and loads the RuleSet from an
XML file. The RuleSet name and path to the file are stored as values in the
app.config for the router application and are passed to a helper method,
examined next, which loads the XML and deserializes it into a RuleSet object.
In addition, two other helper variables are initialized which will be used by
the rules: a derivative of the XmlNamespaceManager and the Random class. Figure
6 shows the constructor for the RoutingTable class. Notice that the rule engine
is initialized with both a type and a ruleset as all rulesets are created based
on a given type and must be executed against an instance of that type.
public RoutingTable()
{
this.randomNumberGenerator = new Random();
this.manager = new XPathMessageContext();
this.ruleSet = GetRuleSetFromFile(
ConfigurationManager.AppSettings["SelectDestinationRuleSetName"],
ConfigurationManager.AppSettings["SelectDestinationRulesFile"]);
this.ruleEngine = new RuleEngine(ruleSet, typeof(RoutingTable));
}
Figure 6:
Initializing the router table
The GetRuleSetFromFile method is responsible for loading the
serialized ruleset from the file location specified, then finding and returning
the named RuleSet. The WorkflowMarkupSerializer class can be used to serialize
and deserialize a RuleSet object to XML. Figure 7 shows the code necessary to
load the rules from the file. In this example all exception handling code has
been removed for clarity.
private RuleSet GetRuleSetFromFile(string ruleSetName, string ruleSetFileName)
{
XmlTextReader routingTableDataFileReader = new XmlTextReader(ruleSetFileName);
RuleDefinitions ruleDefinitions = null;
WorkflowMarkupSerializer serializer = new WorkflowMarkupSerializer();
ruleDefinitions = serializer.Deserialize(routingTableDataFileReader) as RuleDefinitions;
RuleSet ruleSet = ruleDefinitions.RuleSets[ruleSetName];
return ruleSet;
}
Figure 7: Loading a
RuleSet from a file
The final bit of code needed for the address logic is the
method the router service can use to resolve the correct address. In the
SelectDestination method of the RouterTable class a Message object is passed
and the rules executed. This method is called by the router each time a message
arrives in order to determine the destination address to use for forwarding.
The business rules update the RoutingTable instance and set the selectedAddress
field which can then be returned as the chosen address. The SelectDestination
method is shown in Figure 8.
public EndpointAddress SelectDestination(Message message)
{
this.currentMessage = message;
this.ruleEngine.Execute(this);
return this.selectedAddress;
}
Figure 8: Executing
rules to select the destination
Defining the rules
With the router service in place and the RoutingTable
completed, the routing rules need to be written and saved to a file so they can
be consumed by the router. WF allows you to build your own UI for creating and
editing rules, and allows for rehosting the rules editor dialog that comes as
part of WF 3.0. For this example, rather than creating our own UI for creating
and editing rules, we will use the External RuleSet Toolkit sample. This sample
demonstrates how to create a ruleset outside of Visual Studio and save the
resulting XML to a file or a database; for our purposes, it provides an easy UI
that we don’t have to code in this article. You can download the sample from
MSDN using the link. Once you have downloaded the samples, run the installer to
expand them and you will find the External Ruleset Toolkit in the following
directory: \WCF\Extensibility\Rules\ExternalRuleSetToolkit.
To setup the database used by the External RuleSet Toolkit,
double-click the setup.cmd command file found with the sample. Note: The
command file assumes that your instance of SQL Server Express is named
.\sqlexpress. If you have named your instance something different or you are
using another version of SQL Server, you will need to modify the file before
executing it. In addition, you will need to update the configuration files in
the toolkit projects to point to the correct instance of SQL Server.
Once the command file has completed and the database has
been configured, open the ExternalRuleSetToolkit.sln solution in Visual Studio.
Make any necessary changes to the configuration file for your database to point
at the database you just created, and set the start up project by
right-clicking on the ExternalRuleSetTool project and selecting Set as startup
project from the context menu. Press F5 to run the application and you should see
a dialog as shown in Figure 9.
.jpg)
Figure 9: The RuleSet
editor tool
After starting the ExternalRuleSetTool project the main
dialog appears. Click the New button to create a new RuleSet definition and set
the Name field to “SelectDestination”. Next, the editor needs to know which CLR
type to build the rules against, so click the Browse button to bring up the
type selector dialog as shown in Figure 10. Click the Browse button in this new
dialog and select the WCF_Router.Router.exe application in the \solution\router\bin
directory. Then select the
Microsoft.ServiceModel.Samples.RoutingTable type from the list. Once the type
is selected the dialog shows all of the fields, properties and methods that
will be available in the rules.
.jpg)
Figure 10: Type
dialog browser in the ExternalRuleSetTool
After selecting the type, click OK to return to the main
window which shows the type and the RuleSet being edited as shown in Figure 11.
.jpg)
Figure 11: Main
dialog ready for editing
Click the Edit Rules button to open the Rule Set Editor
dialog that ships as part of the .NET Framework runtime components. This editor
can be re-hosted in Windows Forms and Windows Presentation Foundation (WPF)
applications to enable the viewing, creation, or editing of rules and will be
installed on a user’s machine as long as they have the runtime components; no
SDK or developer tools need to be installed on the user’s computer.
Creating rules involves defining a Condition, Then Actions,
and Else Actions. The concept of an If/Then/Else statement is familiar to all
.NET developers and makes creating rules
a fairly simple procedure. In the case of the rules being defined for this
scenario, there are three different steps in the rules in order to correctly
identify and choose a destination address: Initialize, Match, and Select. When
defining rules that need to execute in a particular order, each rule can be
given a priority which instructs the rule engine to evaluate those rules from
the highest priority to the lowest. For more information on priority-based
execution of rules, see the WF documentation on MSDN. The first rule used in
this example initializes several variables before the actual rule processing
occurs. Figure 12 shows the definition
of the InitializeVariables rule in the RuleSet Editor. Notice that the priority
for this rule has a value of “3”.
.jpg)
Figure 12:
IntializeVariables rule in the RuleSet Editor
To create this rule, click the Add Rule button and enter
“InitializeVariables” for the Name and “3” for the Priority. For the Condition,
enter “True” which will ensure that the rule always executes. In the
Then Actions add the following code to initialize the fields in the
RoutingTable class instance.
this.possibleAddress = new
System.Collections.Generic.List<System.ServiceModel.EndpointAddress>()
this.selectedAddress = null
this.manager = new System.ServiceModel.Dispatcher.XPathMessageContext()
this.manager.AddNamespace("rt", "http://Microsoft.ServiceModel.Samples/Router")
The second set of rules involves examining the message
received and determining if it is a match for a particular endpoint. This set
of rules includes one rule for the calculator service and one for the echo
service. The priority for both rules is “2” which means both will run after the
variables have been initialized. The table below shows the definition for these
rules, use the Add Rule button to add each rule and set the correct values for
each field.
Rule:CalculatorService | |
Condition: | new
System.ServiceModel.Dispatcher.XPathMessageFilter("/s12:Envelope/s12:Header/rt:Calculator",
this.manager).Match(this.currentMessage) |
Action: | this.possibleAddress.Add(new
System.ServiceModel.EndpointAddress("net.tcp://localhost:31000/calculator/service")) |
| |
Rule: EchoService | |
Condition: | new
System.ServiceModel.Dispatcher.XPathMessageFilter("/s12:Envelope/s12:Header/wsa10:Action/text()='http://Microsoft.ServiceModel.Samples/IEchoService/Echo'",
this.manager).Match(this.currentMessage) |
Action: | this.possibleAddress.Add(new
System.ServiceModel.EndpointAddress("http://localhost:8000/echo/service")) |
Each of these rules adds the endpoint address for the
service if a match is found. In the case of the CalculatorService, the rule
looks for the custom header to be present on the message using the
XPathMessageFilter class. For the EchoService rule the XPathMessageFilter looks
at the SOAP:Action header to determine if the message should be routed to the
service. Notice that the rules allow for creation of new object instances using
defined constructors, making it possible to initialize fields and properties
with new values for complex types.
Once each of the match rules has executed, the collection of
possible addresses has been populated with 0-2 addresses. The next set of rules
is responsible for selecting the address to return. These three rules all have
a priority of “1” indicating that they will run last. The priorities guarantee
that these rules will run after all rules with a higher priority, but there is
no guarantee about the order in which rules with the same priority will
execute. Use the Add Rule button and the information below to create these
three rules.
Rule:OneMatch | |
Condition: | this.possibleAddress.Count == 1 |
Action: | this.selectedAddress = this.possibleAddress[0] |
| |
Rule: Multiple Match | |
Condition: | this.possibleAddress.Count > 1 |
Action: | this.selectedAddress = this.possibleAddress[this.randomNumberGenerator.Next(this.possibleAddress.Count
- 1)] |
| |
Rule: No Match | |
Condition: | this.possibleAddress.Count == 0 |
Action: | This.selectedAddress = null |
If only a single address match was found, then that single
address is used to set the selectedAddress field in the RoutingTable class. In
the RoutingTable class, after the rules have executed, it is the
selectedAddress field that is returned to the caller of the SelectDestination
method. In the case where no matches are found, the selectedAddress is simply
set to a null value. The slightly more complex scenario involves handling
multiple matches. In this case the Random class is used to choose between the list
of endpoints that were possible matches.
Once all of the rules have been created the ruleset is
complete and ready to be saved to a file. Figure 13 shows the Rule Set Editor
dialog when all rules have been created.
.jpg)
Figure 13: All rules
in the editor
Once all of the rules have been configured, click the OK
button to complete the editing. Next choose the Data | Export command from the
menu and save the file to “SelectDestionation.rules” in the
\solution\router\bin directory. Once saved to the file as configured in the
app.config for the router service the rules are available to be used by the
router code.
Return to the Visual Studio instance with the Router.sln
solution loaded and run the solution by pressing F5. The client application
will send two messages to the router, the rules will execute for each message
and the messages will get forwarded to the correct service. You should be able
to see information in the router console about each message being processed
including requests and replies.
Conclusion
The rules engine in Windows Workflow Foundation is an
extremely powerful yet lightweight engine for executing complex business logic.
Developers can use the familiar concepts of conditional logic paired with
advanced capabilities such as prioritization, dependency detection, and rule
re-evaluation to create rich rule driven components and applications. This
hands on article provides one example of how the rule engine can be used as a
complementary technology to Windows Communication Foundation to provide rich
logic processing at various extension points.
About the Author
Matt Milner is a member of the technical staff at
Pluralsight, where he focuses on connected systems technologies and is the
author of the Windows Workflow and BizTalk Server courses. Matt is also an
independent consultant specializing in Microsoft .NET technologies and speaks
regularly at regional and national conferences such as Tech Ed. As a writer
Matt has contributed to several journals and magazines such as .NET Developers
Journal and MSDN Magazine where he currently authors the workflow content for
the Foundations column.
Related Links