Generating a BizTalk BRE Rule in .NET

As you can probably tell from other articles on this blog, I spend a great deal of time working with the Business Rules Engine (BRE) in BizTalk. Personally, I consider the BRE one of those great, often-overlooked technologies that should be used more than it is, but I digress…

One little-known thing you can do with the BRE is create rules on the fly in .NET. For this article, I’m going to provide a walk-through of how to do this.

First, though, you should spend a little time getting to understand the BRE if you don’t already. A great first step is to do the Virtual Lab provided by Microsoft.

The main artifacts in the BRE are Policies, Rules, Conditions, and Actions.  At a very high level, we can describe a Policy as a container for a number of Rules. Each Rule contains a set of Conditions, which if true, will cause Actions to be performed. A Condition can use any number of Predicates such as EQUALS or GREATER THAN and Operators like AND and OR to evaluate the state of Objects, XML Documents, etc. An Action can be an assignment, a method call, or something performed on the same or other objects that may be sent in at execution time.

Before writing your Rule-Generating code, understand exactly what it is you wish to accomplish with the rule. What will the input be? XML Document? Business Object? What conditions will need to exist for a rule to fire? What will happen when the rule does fire?

Once you know which type of rule or set of rules you’d like to generate, it’s usually a good idea to map it out a bit first. For our purposes, we’re going to update an ESB Toolkit BRE Resolver object to set Routing information for a message along an itinerary. If you’d like to learn more about using the ESB Toolkit, I have a an introductory article on this blog or you may also check out the excellent videos by Peter Kelcey. However it is not important for understanding the main points of this article.

Our policy will contain only a single rule. Here’s the logic:

IF
    The incoming messages is of type "http://myMessageTypeNamespace#messageRoot"
AND
    The ReceivePortName is "InboundMessagePort"
THEN
    Set TransportLocation to: "C:\FileDrops\FileOut"
    Set TransportType to:  "FILE"

First, reference the Rules Engine assemblies Microsoft.RuleEngine and Microsoft.Biztalk.RuleEngineExtension both located at <Program Files>\Common Files\Microsoft BizTalk\. Because we’ll also be accessing members of the Microsoft.Practices.ESB.Resolver.Resolution class for our conditions and actions, we’ll need a reference to Microsoft.Practices.ESB.Resolver as well.  If you have the ESB Toolkit 2.1 installed, it is located at <Program Files>\Microsoft BizTalk ESB Toolkit 2.1\Bin\ .  You may use classes in any .NET assembly here, just remember to alter your code and references accordingly.

After adding your references, add the following using statements:

using Microsoft.BizTalk.RuleEngineExtensions;
using Microsoft.RuleEngine;

Before we can construct our conditions, we need to access the artifacts that will be used. For our example, it will be the Resolution class mentioned above. Here is the code we need to create a binding to this class so we can use it in our Business Rule:

ClassBinding resolutionClassBinding = new ClassBinding(
    typeof(Microsoft.Practices.ESB.Resolver.Resolution));

We’ll also need to create bindings for the class members we want to evaluate. Note the names we use for the properties we’ll be evaluating. You’ll see names like “get_MessageType” instead of “MessageType” and “get_ReceivePortNameField” instead of “ReceivePortName”. At lower levels in the BRE code, properties are often accessed by calling a get_ or set_ function. An easy way to look up the names the engine will use is to load the class in the Facts Explorer in the Business Rules Composer and look at their names there.

Create instances of ClassMemberBinding, passing in the name of the members and the ClassBinding we created previously:

ClassMemberBinding messageTypeBinding = new ClassMemberBinding(
    "get_MessageType", resolutionClassBinding);
ClassMemberBinding receivePortNameBinding = new ClassMemberBinding(
    "get_ReceivePortNameField", resolutionClassBinding);

Next, we construct the logic. First, we’ll build each IF condition individually, add them to a collection of Logical Expressions, and ensure that we evaluate them all together with an And.

LogicalExpression messageTypeCondition = new Equal(
    new UserFunction(messageTypeBinding),
    new Constant("http://myMessageTypeNamespace#messageRoot"));
LogicalExpression portNameCondition = new Equal(
    new UserFunction(receivePortNameBinding),
    new Constant("InboundMessagePort"));

LogicalExpressionCollection conditionsList =
    new LogicalExpressionCollection();
conditionsList.Add(messageTypeCondition);
conditionsList.Add(portNameCondition);
LogicalAnd allConditions = new LogicalAnd(conditionsList);

After creation of the conditions, the next step will be to build out the actions that will take place should the conditions evaluate to true. For our purposes, the output values will be hard-coded, but you can use other rule inputs or method calls to create your values. First we start with an ActionCollection:

ActionCollection actions = new ActionCollection();

Now to add the actions to the list, we’ll need to create bindings to the class and its members that we’re interested in. As before, it’ll be references to members of the Resolution class. We’ll start by creating an ArgumentCollection and the ClassMember objects needed to bind to our list of Actions for the transport location method:

ArgumentCollection locationArgs = new ArgumentCollection();
locationArgs.Add(new Constant(@"C:\FileDrops\FileOut"));
ClassMemberBinding locationBinding = new ClassMemberBinding(
    "set_TransportLocation", resolutionClassBinding, locationArgs);

Next, the we’ll bind the Transport Type:

ArgumentCollection transportArgs = new ArgumentCollection();
transportArgs.Add(new Constant("FILE"));
ClassMemberBinding transportBinding = new ClassMemberBinding(
    "set_TransportType", resolutionClassBinding, transportArgs);

Now, add them to our Actions collection:

actions.Add(new UserFunction(locationBinding));
actions.Add(new UserFunction(transportTypeBinding));

Then we’ll create a Rule, add all the conditions and actions to it, and assign it to a new policy (or RuleSet). Our policy will use the default version, but you can also use the VersionInfo class to add your own major and minor version assignments.

Rule sampleRule = new rule("sampleRule", allConditions, actions);
RuleSet newPolicy = new RuleSet("samplePolicy");
newPolicy.Rules.Add(sampleRule);

Finally, we’ll need to deploy it to the BRE. We’ll need to publish and deploy the rule in two separate steps:

Microsoft.BizTalk.RuleEngineExtensions.RuleSetDeploymentDriver
    deploymentDriver =
    new Microsoft.BizTalk.RuleEngineExtensions.RuleSetDeploymentDriver();
RuleStore breStore = deploymentDriver.GetRuleStore();

// add the RuleSet to the database and publish it
try
{
    breStore.Add(newPolicy, true);
}
catch (RuleStoreRuleSetAlreadyPublishedException)
{
    Console.WriteLine("Warning: Ruleset \"" +
        newPolicy.Name +
        "\" is already published");
	return;
}

// now deploy the ruleset
try
{
    deploymentDriver.Deploy(new RuleSetInfo(newPolicy.Name,
        newPolicy.CurrentVersion.MajorRevision,
        newPolicy.CurrentVersion.MinorRevision));
}
catch (RuleEngineDeploymentAlreadyDeployedException)
{
    Console.WriteLine("Warning: Ruleset \"" +
        newPolicy.Name +
        "\" is already deployed");
	return;
}

And that’s it. 

Here’s an image of the policy we generated in the Business Rules Composer:

 Of course, there’s much more you can do than what is shown here but hopefully this sample code can help you get started.  In a future post, we’ll discuss how to use a regular XML message in your rules instead of just method calls in .NET assemblies as shown here.

What do you think?