Windows Workflow Tutorial: Rules-Driven WCF Services

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.

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.

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="https://localhost:8000/echo" />
          </baseAddresses>
        </host>
        <endpoint address="https://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.

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.

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.

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”.

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("https://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.

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.