The Business Rules Framework is a technology supplied with BizTalk Server 2004 aimed at the kind of scenario described above. It is made up of a number of .NET components, tools, and wizards that provide design-time support for building business rules as well as a runtime environment for executing your rules. After you have defined and tested a set of rules, it is easy to integrate them into a BizTalk orchestration or even call them directly from your own .NET applications.
Before digging into the technology, I want to say a little about the motivations behind the Business Rules Framework. Every business process needs rules. When you' apply for a loan, pay for groceries at your local supermarket, or check in at an airport, you are part of a business process that uses specific criteria to decide an appropriate outcome. Traditionally, these rules have been developed as procedural code in languages like Microsoft® Visual Basic®. Unfortunately, this approach can have some significant downsides.
Developing and testing this kind of procedural code requires developers to translate the requirements identified by business analysts into a form that their chosen language can understand. Coding complex, interrelated rules can be a tricky business and once your rules are encapsulated in code they can be hard for non-developers to understand. After it is deployed, it is very likely that your business rules will need to change over time. The effort involved in modifying, re-testing and then redeploying your business rules can be significant and doesn't fit well with today's vision of the agile business.
The Business Rules Framework takes a very different approach. The business rules that it executes are defined declaratively without a need for procedural instructions. As a result, the rules that you define are much closer to the way that they were originally envisioned by a business analyst. To make rule development accessible to more people in your organization, a straightforward graphical user interface is provided for rule creation and management. This tool allows you to focus on what your rules need to achieve rather than dealing with complex implementation detail. At runtime, a sophisticated forward-chaining inference engine processes your rules and takes the actions that you specified.
At this point we need to introduce some terminology and fundamental concepts. The Business Rules Framework is based on facts, vocabulary, definitions, and policy. Facts are the entities that your rules consume and manipulate – examples include an element that appears in an XML document, a column in a database table, or even a method exposed by a .NET component. A vocabulary is a collection of facts, and its purpose is both to apply meaningful, user-defined definition names to those facts and to provide a unit of versioning. Vocabulary definitions provide a neat level of abstraction that keeps procedural detail (for example, the SQL statement needed to provide a value from a database) separate from the definition names you refer to when writing your rules. You can use the same vocabulary across many different rules and rule sets.
We've referred to 'rules' many times so far without really defining what they are. In terms of the Business Rules Framework, a rule is a statement that defines the behavior of one aspect of a business process. For example, you might have a rule that says "If the loan applicant has a credit rating of 'good' then immediately approve the loan." This example rule shows us that rules are made up of facts, conditions and actions.
Fact: loan applicant's credit rating
Condition: credit rating is equal to 'good'
Action: immediately approve the loan
Conditions always evaluate either to true or false based on one or more predicates being applied to facts. In this case, the predicate is "is equal to". You can combine predicates with the logical conjunctions AND, OR, and NOT to build up complex conditions. Actions define what happens when your condition evaluates to true.
The last term that we need to define is policy. A policy is a set of rules, packaged and versioned as a unit. Policies are what you develop, test, and deploy when working with the Business Rules Framework. When you publish a version of your policy it can not be changed. This makes it easy to manage well-defined versions that contain consistent, predictable behavior. One final - and really important - thing that about policy is that you can update a policy in your production environment without needing to recompile and redeploy the BizTalk orchestrations that make use of it. Your BizTalk Server application will start using the new version in near real-time without any downtime.
Here's a brief rundown of the software pieces that make up the Business Rules Framework:
Business Rules Composer: This application enables you to define vocabularies and build and test rules. The rule-building experience is as simple as dragging and dropping facts and setting properties. You can also publish and deploy policies from here.
Rules Engine Deployment Wizard: Vocabularies and policies are stored in a SQL Server rule store database. To help you move a vocabulary or policy to another computer, this wizard provides import/export functionality. You can also deploy or undeploy a policy.
Rules Engine: This is the runtime engine that processes your policies. The engine evaluates rules, based on their facts, and decides which actions need to be executed. It also supports complexities like dealing with forward chaining. Forward chaining refers to the situation where your rule conditions modify the facts used in your policy; as the underlying facts change, the engine determines when it needs to re-evaluate your rule conditions.
The rules engine is implemented as a standalone .NET component. It can be called from inside BizTalk Serveror loaded into a process of your choice via a comprehensive API. With this is mind, you can install the rules engine on its own using the BizTalk Server 2004 installer or as part of a larger BizTalk Server installation.
This paper does not go into too much more technical detail about the Business Rules Framework because the SDK documentation does a great job of providing that. See http://go.microsoft.com/fwlink/?LinkId=42042 for more detail.
Building Our Vocabulary
The first thing we need to do is to identify the facts involved in our business scenario. We have four key facts – our input XML document that contains applicant details, our output XML document that contains our applicant's risk profile, and two .NET components. Let's look at each of these in turn.
This scenario presents an entirely fictional process for a fictional company and works on data relating to fictional people. The data and business rules are kept simple – the important thing that we're examining here is the way that the tools and technologies can be used, not the intricacies of our fictional insurance company's business processes, or the XML schemas that they could involve.
Input XML Document
This document represents pertinent details about our applicant. The schema is very simple and contains some of the details that our fictional company could have captured on an application form. If you're not familiar with it, the <annotation> element that appears in the schema is being used here to distinguish one of the elements specified in the schema. Distinguishing an element makes it simpler to work with in expressions that appear in a BizTalk orchestration.
<?xml version="1.0" encoding="utf-16"?>
<xs:schema xmlns="http://RiskScoring" xmlns:b="http://schemas.microsoft.com/BizTalk/2003" elementFormDefault="qualified" targetNamespace="http://RiskScoring" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="ApplicantDetails">
<xs:annotation>
<xs:appinfo>
<b:properties>
<b:property distinguished="true" xpath="/*[local-name()='ApplicantDetails' and namespace-uri()='http://RiskScoring']/*[local-name()='ApplicationID' and namespace-uri()='http://RiskScoring']" />
</b:properties>
</xs:appinfo>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element name="ApplicationID" type="xs:long" />
<xs:element minOccurs="1" maxOccurs="1" name="PersonalDetails">
<xs:complexType>
<xs:sequence>
<xs:element name="Name" type="xs:string" />
<xs:element name="Age" type="xs:int" />
<xs:element name="CurrentHealth" type="HealthStatus" />
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element minOccurs="1" maxOccurs="1" name="Interests">
<xs:complexType>
<xs:sequence>
<xs:element name="LionTaming" type="xs:boolean" />
<xs:element name="Reading" type="xs:boolean" />
<xs:element name="ExtremeSports" type="xs:boolean" />
<xs:element name="WatchingTV" type="xs:boolean" />
<xs:element name="BearWrestling" type="xs:boolean" />
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element minOccurs="1" maxOccurs="1" name="FinancialDetails">
<xs:complexType>
<xs:sequence>
<xs:element name="NumTimesBankrupt" type="xs:int" />
<xs:element name="AnnualIncome" type="xs:int" />
<xs:element name="AnnualExpenditure" type="xs:int" />
<xs:element name="OwnProperty" type="xs:boolean" />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:simpleType name="HealthStatus">
<xs:restriction base="xs:string">
<xs:enumeration value="Great" />
<xs:enumeration value="OK" />
<xs:enumeration value="Poor" />
</xs:restriction>
</xs:simpleType>
</xs:schema>
Below is an example instance document, belonging to a fairly risky-looking applicant, which conforms to this schema:
<ns0:ApplicantDetails xmlns:ns0="http://RiskScoring">
<ns0:ApplicationID>10</ns0:ApplicationID>
<ns0:PersonalDetails>
<ns0:Name>John Dangerlover</ns0:Name>
<ns0:Age>99</ns0:Age>
<ns0:CurrentHealth>Poor</ns0:CurrentHealth>
</ns0:PersonalDetails>
<ns0:Interests>
<ns0:LionTaming>true</ns0:LionTaming>
<ns0:Reading>true</ns0:Reading>
<ns0:ExtremeSports>true</ns0:ExtremeSports>
<ns0:WatchingTV>true</ns0:WatchingTV>
<ns0:BearWrestling>true</ns0:BearWrestling>
</ns0:Interests>
<ns0:FinancialDetails>
<ns0:NumTimesBankrupt>10</ns0:NumTimesBankrupt>
<ns0:AnnualIncome>1000</ns0:AnnualIncome>
<ns0:AnnualExpenditure>10000</ns0:AnnualExpenditure>
<ns0:OwnProperty>false</ns0:OwnProperty>
</ns0:FinancialDetails>
</ns0:ApplicantDetails>
Output XML document
This document represents the output from our risk scoring rules. This document is a risk profile, detailing risk by category – our application can then use this risk profile to decide how much we should charge for insurance (or maybe whether we should insure the applicant at all!). We capture information about perceived risk organized by category (personal information, financial details and applicant's interests) with subtotals at each category level and a grand total that adds each subtotal together.
An important thing to note about our schema is that the score for each risk category needs to be built up incrementally as our rules get processed. Each rule adds or subtracts a value to the category that it relates to and adds an element into the relevant <Details> section to provide both an audit trail and commentary on the score adjustment. As rules execute, the risk scores in each category (and the corresponding totals) move up and down. As you can see, the risk profile needs to be constructed in a very dynamic manner. Finally, note that in our scenario a higher score in any risk profile category indicates higher risk.
Here's the schema for our risk profile:
<?xml version="1.0" encoding="utf-16"?>
<xs:schema xmlns="http://RiskScoring" xmlns:b="http://schemas.microsoft.com/BizTalk/2003" elementFormDefault="qualified" targetNamespace="http://RiskScoring" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="RiskProfile">
<xs:complexType>
<xs:sequence>
<xs:element name="ApplicationID" type="xs:long" />
<xs:element name="OverallRisk" type="xs:int" />
<xs:element name="DateCreated" type="xs:dateTime" />
<xs:element name="RiskElements">
<xs:complexType>
<xs:sequence>
<xs:element name="Personal" type="RiskElement"/>
<xs:element name="Financial" type="RiskElement" />
<xs:element name="Interests" type="RiskElement" />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:complexType name="RiskElement">
<xs:sequence>
<xs:element name="Details">
<xs:complexType>
<xs:sequence minOccurs="0" maxOccurs="10">
<xs:element name="Detail" type="xs:string" />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute name="Score" type="xs:int" />
</xs:complexType>
</xs:schema>
And here's an example risk profile instance document:
<?xml version="1.0" encoding="utf-16"?>
<RiskProfile xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://RiskScoring">
<ApplicationID>10</ApplicationID>
<OverallRisk>1000</OverallRisk>
<DateCreated>2004-11-26T16:22:01.2775888-08:00</DateCreated>
<RiskElements>
<Personal Score="0">
<Details />
</Personal>
<Financial Score="1000">
<Details>
<Detail>Applicant failed credit check</Detail>
</Details>
</Financial>
<Interests Score="0">
<Details />
</Interests>
</RiskElements>
</RiskProfile>
The final two facts that we need are two .NET components. Being able to incorporate .NET components into your rules as facts is an important feature that significantly increases the rules' flexibility. Rather than just working on static data, this feature allows your rules to interact with instances of your components – reading and writing properties, calling methods – to provide a programmable and dynamic element to your business rules. Our rules make use of two .NET components.
RiskProfileHelper Component
This component helps us build our output risk profile document. Why do we need a .NET component here at all? Well, if you've already played with the Business Rules Composer you will have seen that it makes it very easy for us to create rules that assign values to elements in a pre-existing XML document. However, our scenario is more complex than that. We need to dynamically alter the score of our risk elements as our rules are invoked, and also dynamically add new detail elements to our risk profile. Our component will do this for us, allowing our business analysts to define rules that perform this dynamic work without exposing them to any of the complexities involved in XML manipulation. Our component will also provide some other functionality like creating an empty risk profile document for our rules to work on and exposing the risk profile document to calling code. Let's take a quick look at the key members of this component in a little more detail.
Public RiskProfile RiskProfileDocument
This is a public property that exposes the risk profile document that we're working on. The type of this property is RiskProfile – this is a type generated for us by running xsd.exe against our risk profile schema. If you've never used it before, xsd.exe is a great way to leverage the .NET XML serializer to provide classes (or DataSets) that you can code against rather than having to directly manipulate XML. These classes are already decorated with everything they need to neatly serialize back into XML of the correct structure. Most developers find working with a class much more straightforward than working with XML, especially when you factor in Microsoft® IntellliSense® support in Visual Studio.
public void AddRisk(RiskCategory category, int scoreAdjustment, string detail)
The purpose of this method is to update our risk profile by adjusting the score of the relevant category by the specified value and also adding a new detail element to the relevant section.
private void SaveRiskProfileToFile(string filePath)
This method does just what you would expect – it serializes the risk profile out to a file. The ability to see the risk profile document will be very useful when we test our scoring rules.
A couple of other points of interest about this class:
-
The class includes a finalizer. It is not a good idea to include a finalizer unless you really need one, but this is included as a debug-only way of ensuring that the class always saves the risk profile out to a file for inspection. This option means that we don't need to pollute our policy with a special low-priority rule to call this method. When the code is built in release mode, the finalizer disappears.
-
The class includes a private ArrayList for each set of RiskElements/*/Details/Detail elements. When the document is requested using the RiskProfileDocument property, these ArrayLists are used to populate the corresponding arrays in the returned RiskProfile object. I did this because ArrayLists are a better fit for this situation – we know we will need to dynamically re-dimension our set of detail elements every time we call AddRisk(). Doing this against a regular Array would introduce an unnecessary overhead. An ArrayList allows us to grow our set of elements as required and only manipulate the regular arrays on our RiskProfile object when we need to.
CreditCheck Component
The final piece of our vocabulary is this second .NET component. This simple component represents some credit-scoring functionality that our rules need to call on. In this example the logic is some very simple local code – in a real system this component would call out to a real credit-scoring service, perhaps via a web service. The class has two key members:
public Status CurrentStatus
This is a read-only property that exposes a credit score status. The value of the status is selected from an enumeration exposed by the class. The status is initially set to NotChecked. Following credit scoring, this status is set to either CheckedOK or CheckedReject.
public void CheckCredit(int annualIncome, int annualExpenditure, int numberTimesBankrupt)
This method uses some simple credit-scoring logic to update the current status.
For convenience, both the RiskProfileHelper and CreditCheck classes are in the same project. This C# version of this project has a post-build event to install the output assembly into the Global Assembly Cache. We need our assembly to be installed here so that we can incorporate it into our vocabulary. If you are using the Visual Basic .NET version of the code, a batch file is provided to install the assembly into the GAC. Note that you may need to tweak the path used by either the post-build event or the batch file to point to the correct location of gacutil.exe on your development machine.
Putting the Vocabulary Together
To actually build our vocabulary we'll be using the Business Rules Composer. The user interface to do this is very straightforward. Here are the basic steps:
-
Open up the Business Rules Composer.
-
Connect to your local BizTalk Rules Engine database (the default name is BizTalkRuleEngineDb).
-
Select the Vocabularies tab in the Facts Explorer window.
-
Right-click on the Vocabularies folder and select Add New Vocabulary.
-
Give the vocabulary the name RiskScoring
Alternatively, the code for this article includes a completed vocabulary file – if you don't want to build the vocabulary for yourself, feel free to import the completed vocabulary via the Rules Engine Deployment Wizard. There is more information about using this tool later in this paper.
It is important to know that facts don't actually need to belong to a vocabulary. You are free to add facts via the Facts Explorer window and use them in a rule without adding them to a vocabulary first. On the other hand, a published vocabulary provides you with an immutable and reusable set of constraints and facts with user-friendly names. It is usually worth the effort involved to create a vocabulary, we will do that for our scenario in this paper.
We now have a vocabulary to hold our facts. Our facts will be a combination of elements from our applicant details schema as well as members from our two .NET components. These instructions assume that you are using the schema files included with this article's code and that you have already installed the RiskScoringHelper assembly into your local Global Assembly Cache. You can do this either by building the assembly - if you're using the C# code - or by running the supplied batch file corresponding to either the C# or VB.NET version of the code.
First, let's add the facts drawn from applicant details schema elements. By adding each element to our vocabulary we can provide an aliased name that will allow our users to focus on using the data in their business rules rather than dealing with any XPath:
-
Right-click on your vocabulary and select Add New Definition.
The Vocabulary Definition Wizard appears.
-
Select XML Document Element or Attribute and then click Next.
-
Click the Browse button and navigate to ApplicantDetails.xsd.
-
Expand the root ApplicantDetails element and then expand the child PersonalDetails element. Next, select the Age element and then click OK
-
Update the Definition Name to Applicant's age.
-
Update the Document Type to RiskScoringSchemas.ApplicantDetails.
This ensures that the schema has the same type as the corresponding message in the BizTalk Server application that we will see later in the paper.
-
Select the Perform "Get" operation radio button. We will be reading only this value.
-
Ensure that the Display Name is the same as the Definition Name.
-
Click Finish.
We need to repeat the steps above for the remaining schema elements that we will be referencing in our rules. The following table shows the complete set of elements that need to be added to our vocabulary.
Table 1 Vocabulary elements
|
Fact name
|
Schema element
|
|---|
|
Applicant's age
|
/PersonalDetails/Age
|
|
Applicant's current health status
|
/PersonalDetails/CurrentHealth
|
|
Applicant's interests include reading
|
/Interests/Reading
|
|
Applicant's interests include lion taming
|
/Interests/LionTaming
|
|
Applicant's interests include extreme sports
|
/Interests/ExtremeSports
|
|
Applicant's interests include wrestling bears
|
/Interests/BearWrestling
|
|
Applicant's annual income
|
/FinancialDetails/AnnualIncome
|
|
Applicant's annual expenditure
|
/FinancialDetails/AnnualExpenditure
|
|
The number of times that the applicant has been declared bankrupt
|
/FinancialDetails/NumTimesBankrupt
|
The second type of fact that we need to add to our vocabulary are the members from our .NET components that we will be using in our rules.
-
Right-click your vocabulary and select Add New Definition.
-
The Vocabulary Definition Wizard appears. This time, select .NET Class or Class and then click Next.
-
Click Browse and then scroll down the list of assemblies. Select RiskScoring.RiskScoringHelper and click OK.
-
A dialog box now prompts us for the class or member that we need to expose as a fact. Select the RiskProfileHelper.AddRisk() method and click OK.
-
Update the Definition Name to Update risk profile and then click Next.
-
This wizard step lets us define how parameters are handled. Leave the Step 1 section as it is. In the Step 2 section we can control how the fact appears when it's added to a rule. Update the string here from the default to a more user-friendly "Update risk profile using category = {0} score adjustment = {1} risk detail = {2}".
-
Click Finish.
We need to repeat steps 1 to 7 for the following .NET component members
-
CreditCheck.CurrentStatus property (definition name = "Credit check status", no need to specify display format string). Note that this property appears as "get_CurrentStatus" in the binding selection dialog.
-
CreditCheck.CheckCredit() method (definition name = "Run credit check", display format string = "Run credit check using income = {0} expenditure = {1} number of times bankrupt = {2}")
The last item that we need to add is a reference to an instance of our CreditCheck component. We will need this fact later on to provide a parameter to a rule engine control function in one of our more complex rules. Repeat steps 1 to 7, adding the CreditCheck class itself as a fact with a definition name of "CreditCheck".
Finally, we need to save and publish our new vocabulary. Right-click on your vocabulary and select Save. Then right-click it again and select Publish.
We now have everything we need to start building our rules.