Code for Part 2 - Don't forget to Unblock before extracting
In Part 1, we introduced the concept of "scriptable business rules" using IronPython. That project consisted primarily of background information and some unit tests that showed the concepts in action.
Here in Part 2, we're going to get more practical and demonstrate a full implementation. Our sample application is a fictitious boat sales company. Here's the UI for the app:
The purpose of this sample is to show how to use the
Aim.Scripting framework to integrate dynamic business rules into any application. For this article, I've chosen WPF/MVVM as the vehicle to demonstrate the concepts. Where appropriate, I've included information about Caliburn.Micro MVVM, but keep in mind this is not a tutorial for that framework, nor for WPF in general. So, keep the following in mind:
- I'm not a WPF, MVVM, or
Caliburn.Micro guru. I have a good grasp of many of the concepts, but some things may not be implemented according to absolute best practices for said technologies.
- Most of the styling, and some of the custom controls for the app come from a DLL called
Aim.Wpf. Feel free to use it as you wish - it's pretty solid. I'm especially fond of my
FlowPanel custom controls :). However, don't expect it to remain constant across releases for this article series - it's a work in progress.
- The repository storage mechanism used for this sample is not really suitable for a real application. You would most likely want to use a database as a backing store, but I wanted to reduce the number of dependencies for the project, so I opted for a simple binary file repository rather than SQL or NoSQL.
Aim.Xxx DLLs used in this project are not yet hosted in an open-source repository such as GitHub. I'm working on it, but haven't gotten around to it yet.
- Many of the "implementations" that we use for samples are mostly placeholders with some comments about what a real implementation might look like.
IntegrationProvider, etc. - you shouldn't expect to start a real-time connection to your corporate SAP implementation!
At the foundation of business rules are two high-level concepts that any programmer understands: Events and Commands.
Events are "auto-wired" bits of Python code that respond to user interaction or system-level activity. They require the following pieces:
- A script-enabled event handler defined in your .NET code.
- An IronPython module (which is usually just another data model within your system, implementing the
IScriptDefinition interface) with a Type Key string to identify the types to listen to.
- Some IronPython code in that same module with a signature to match the event.
- Business rules event functions always have the signature
onEventName(sender, e), where
on (lowercase) is the prefix,
EventName is the name of the .NET event,
sender is the object triggering the event, and
e is the
EventArgs-derived argument expected by the event delegate contract.
Quick Reminder - Script-Enabled Event Handlers
To "script-enable" an event handler, include the
Aim.Scripting DLL in your project. You don't need to import any namespaces, because the
EventArgs extension methods are defined in the
Override script-enabled handler (defined in a class that derives from the class that defines the event):
protected override void OnPropertyChanged( PropertyChangedEventArgs e )
e.FireWithScriptListeners( base.OnPropertyChanged, this );
Virtual script-enabled handler (defined in the same class as the event):
protected virtual void OnSomeEvent( EventArgs e )
e.FireWithScriptListeners( () => SomeEvent, this );
Commands are quite a bit more involved and are composed of user interaction, Python functions, stored records, return values, and display contexts. Commands require the following pieces:
- An IronPython module with command functions that will be triggered by the user. This is the same data model type that is used to define events (
BizRuleDataModel in this implementation). The only difference is the way we write the Python code.
- While events have a specific signature that is understood by the scripting runtime, commands do not. A command function can take any number or type of arguments, which will be supplied by the user.
- A storable command data model (
BizRuleCmdDataModel in this implementation) with meta-information such as the command name, display context, active dates, etc.
- A method to determine the active display context, aggregate together the applicable commands, and display them to the user.
- An optional return value.
We'll be covering events first in this article. We'll also cover commands, but future articles will go into more depth on that subject, because command implementations are more complex.
Breaking Changes Since Part 1
I've added the
ICommandDefinition interface to the
Aim.Scripting library to represent a command. Previously, extracting active commands from a business rule module was somewhat kludgy, and required a lot of boilerplate code in the UI and composition root. I opted to formalize these commands under an
interface, allowing the script factory to handle some of the work of finding active commands for a module.
Since the concept of commands has been better formalized in the
Aim.Scripting library, it means that the
IScriptDefinition interface needs a way to query for these commands. Consequently, it got slightly more complex to implement, but is still pretty simple.
Here are the listings for
public interface IScriptDefinition
public interface ICommandDefinition
In our sample project, these two
interfaces are implemented as
BizRuleCmdDataModel, respectively. You can look at the code for those to see how simple the
interface implementation really is - every single implementation method of both
interfaces is a single line of code, essentially forwarding a value already defined as a stored property of the class.
Our First Dynamic Event Handler
Open up the Read Me - Events business rule (click the Fx button to the upper right - this will open the list of business rules modules. Double-click a line in the grid to open that record).
onInform(...) handler that is defined, then save the business rule.
Now, navigate to the product list (sailboat icon), and open an existing or create a new product record. Press the Save button. After you save, check out the status bar at the bottom of the screen. It should have printed a message that was supplied by the business rule event handler.
So how did this happen?
- We created a
BizRuleDataModel with a Type Key that tells it to listen to events of
- We added an
onInform(sender, e) Python method which will listen to
Inform events on
- We edited a
ProductDataModel instance, and the repository raised
Inform event. In this case, the event args was passed with the
Info property set to "
A More Useful Event Handler
Let's do something more real-world. Promotional codes are often used to give customers discounts based on the text of the code and maybe other information like the type of the product.
Handling promotional codes can be a pain unless you have a lot of built-in types to handle it. You need to:
- Recognize the code
- Have a time span for which the code is valid
- Determine when to apply the code (usually upon saving the record)
- Determine if the product meets the criteria for promotion
- Determine if the customer meets the criteria - perhaps they have a past due account and are not eligible for promotional discounts
- Do the math to determine the discount
- This can be further complicated if the customer has a standard discount
To look at the code for our July Promotion, open the business rule module with that name and navigate to the Scripts tab.
Got it? Ok, once you've looked that over (you don't need to change anything), create a new
SaleDataModel (click the money icon in the main toolbar to show the Sale list). Set the
PromoCode properties on the Main tab. Set the
Now, navigate to the Items tab of the sale and add a few items. For each item you add, select a product from the
Now, keeping your eye on the
DiscountPct column of the sale items list, press the Save button. You should see the number change from a 0 to a 10. (It may change to more than 10 if you happen to have selected a
customer with a standard discount that is more than 10%). You should also see a status message about a successful application of the promotional code.
One other thing you should notice about the July Promotion business rule module: If you look at the
ActiveThru properties, you'll see that it will only be active from 2015-06-01 thru 2015-08-01. Go ahead and set the
ActiveThru property of July Promotion to something like 2015-06-02, and save the business rule. Then, create another sale using a
Customer without a standard discount. You'll see that the
DiscountPct property remains at
It's recommended that you always supply an active date range for your modules. It should be composed of two nullable
BizRuleDataModel defines it like this:
public DateTime? ActiveFrom
return Get( m_ActiveFrom );
Set( ref m_ActiveFrom, value, "ActiveFrom" );
public DateTime? ActiveThru
return Get( m_ActiveThru );
Set( ref m_ActiveThru, value, "ActiveThru" );
public bool IsActive
var now = DateTime.Now;
if( ActiveFrom.HasValue && ActiveThru.HasValue )
return ActiveFrom.Value < now && now <= ActiveThru.Value;
if( ActiveFrom.HasValue )
return ActiveFrom.Value < now;
if( ActiveThru.HasValue )
return now <= ActiveThru.Value;
Some Other Events
Here are a couple other events that demonstrate other concepts.
A Fire-Once Startup Event
Our sample also includes a scripted handler in the Startup Events module that fires once, upon the
AppBootstrapper raising the
These types of fire-once startup events can be used for logging program usage, setting global settings, etc.
Sending an Email
We've also defined a rule that says to send an email to the big boss (and write a system log message) if a sale exceeds
$100,000. Keep in mind that our
EmailingProvider implementation is just a placeholder.
There are other events defined in other modules. All of the modules have good notes at the beginning of the Python code in the Scripts tab. Please take the time to read through all of them for ideas, how-tos, and best practices.
Moving On to Commands
We're going to move on now to cover commands. We'll first look at the modules and output from the commands that are already defined, then go into the how-to. Commands require a lot more context than events, so we need to show how we got things working.
Open the product list (sailboat icon). To show the Commands Panel, click the list-gear icon in the far upper right of the main toolbar. Commands are context-sensitive, based on the following:
- The Type Key of the associated business rule module.
- The Execution Context of the command.
ExecutionContext is an enumeration defined in
Aim.Scripting, with the following members:
Any - show the module commands based only on the Type Key. These commands will show when a list of items is active, or when a single item is active (such as an edit view).
Single - show the module commands based on the Type Key and whether a single record is active, such as is the case in an edit view.
List - show the module commands based on the Type Key and whether multiple records are active, such as is the case in a list view.
- The Type Key for business rule modules is not required for commands. In the case of a blank Type Key, the command will show based solely on the
ExecutionContext for that command definition.
With the Commands Panel still open, open the sale list (money icon). Notice that the Commands Panel now shows several group boxes with Run buttons inside.
Where did those come from? To get an idea, navigate to the business rule module list (Fx icon). Open the module called Read Me - Commands, and follow the directions in the Scripts tab.
Once you've done that, navigate back to the products list (sailboat icon). You should see something like the following:
Go ahead and click the Run button for our new command. You should see a status bar message that reflects the state of the check box control above the Run button.
Executing a command can return a result. This result can be anything. A simple
string message, a
We need some place to show those results, and that's where the Command Results panel comes in. Navigate back to the sale list. Once there, press the Run button under the command called Month to Date.
You should see a new tab at the bottom showing the results of executing the command. In this case, we returned an
To see how we got here, open the Sales Reports business rules module. First look at the Scripts tab, and read the comments and the code in there.
Next, look at the Commands tab to see how the commands are defined. Notice that each one has its
Context property set to
List - that's how we got the commands to show up when the active context is a list of sale records.
There are two parts to defining a command:
- The command function. This is the Python code that will run when the command is executed.
- The stored command, which references this function, but also adds a lot of metadata such as the human-readable command name, execution context, active from-thru dates, etc. See the previous image or the Sales Reports business rules module for examples.
Defining command functions isn't really that different from definining event handler functions - except that we have a lot more freedom. There are a few things to be aware of, however, when defining a command function in your Python code:
- The command function name must begin with
cmd (lowercase). For example,
- There is actually a good reason for this. When defining the stored commands, we show the available command functions in a
combobox. Since a business rule module could have any number of functions, some of which are event handlers, some of which are helper functions, etc., we need a way to differentiate those functions that will be attached to stored commands so that we don't clutter up the
combobox selection list with unwanted functions - we especially don't want someone calling an event handler by invoking a command!
- The command function can take any number or type of arguments. Since Python is a dynamically-typed language,
Aim.Scripting uses some naming convention magic to attempt setting the expected type (at dynamic function construction time) and converting the provided value (at dynamic function execution time). The recognized parameter name prefixes are
obj. I'll leave it as an exercise for the reader to divine what the prefixes mean :).
- This naming convention also has some beneficial side effects. If you have a sharp eye, you may have noticed that some of the controls in the Commands Panel render as checkboxes, some as text boxes, some as date pickers, etc. We use the parameter type inference along with a wonderful WPF concept called "template selectors" to achieve this.
- If the parameter prefix is recognized, the remainder of the parameter name will be split along uppercase lines to become the "human-readable" parameter name. For example,
numQtyInMeters will get a type of
double and a display name of
Qty In Meters.
Defining the stored command is really simple. Once you have a business rule module with some
cmdMyCommand() type functions, you can navigate to the Commands tab and enter a stored command that associates with the command function.
Notice that the combobox under
Function will show all the functions from the module that begin with
The Command _target Variable
When you execute a command, the
Aim.Scripting runtime sets a special variable called
_target that is accessible from the running function.
_target variable contains the context in which the command was executed - generally a list of objects in the
List context, and a single object in the
Single context. For example, here is an "integration" command that uses the
_target variable as the list of objects that will be "integrated". This is defined in the Sales Integration business rule module:
Simulated integration strategies. See the AppRuleHost.cs
C# class for more details.
In particular, the host.integrate() method shows one way
that you might wire up a Task<T>, send it off to do its
thing, then report back to the UI with status updates.
While it doesn't have a whole lot to do with scripted
business rules as such, the varying ways that things must
be integrated makes a good case for using business rules
to handle some of the variability points.
Note the use of the automatic _target variable here.
Because we set the Type Key of this module to "Sale", when
we run this command from a context of the list of sales,
that list will be what the _target variable is set to.
## Send sales information off to our Materials Requirements
## Planning software. Because our MRP system has such a simple
## API (hahHAHahAHhAHAhahhahaHah), we just coded up a quick
## PhonyIntegrator class to do the job.
## Call our phony local provider, which spins up a Task<T>
## and sends it off to do the work, reporting back on the
## status line for each item integrated. If any items fail,
## the host.integrate method will show an alert box with
## information about the number of successes and failures.
## See AppRuleHost.cs for more information.
We've already touched on how
Aim.Scripting uses naming conventions to enable type inference, but you can do more with command arguments. For example, you can provide default values for the parameters using a special comment above the command function. Here is the Python code for the Sales Reports business rule module. Pay special attention to the two special comments above the
Sales list context reports. The output of these will
show up in a new Command Results window at the bottom
of the screen.
Here, we're simply pulling in the repository and using
that as our data source. More likely you would bring
in System.Data and maybe run a custom stored procedure
that's specific to a certain tenant, for example.
Note how we can reference specific record ids in our
code. That would be a huge no-no in your compiled,
baked-in code, but for a business rule it's exactly
what we would expect to do. We can change it in just
a few seconds if the need arises.
This module shows the concept of "parameter defaults".
If you look at the cmdForYearMonth(...) function, you'll
see that it is decorated with two special comments.
These comments can be used to set parameter defaults
when the PyFunction is constructed (and prior to its
actual runtime execution). Notice that for the "value"
part of the comment, we are calling an actual function
that's defined in this module!
There are a couple rules about using a function as the
default parameter value:
1. The function must be defined in this module, or it
must be included via host.include(...)
2. The function must take no arguments, that is it
must be in the form of myFunction(), with nothing
between the parentheses. Think of the signature for
the function as a Func<T>, that is a method that
takes no arguments and returns a value.
3. The function for the default value is run in a C#
try/catch block. If the try fails, the parameter value
will not be set.
from System import DateTime
from System.Data import DataTable
from BizRules import RepositoryProvider
from BizRules.DataModels import SaleDataModel
prevYear = None
prevMonth = None
## Function for param value default
## If they have already run it, return the value they used before
## Function for param value default
## If they have already run it, return the value they used before
now = DateTime.Now
return cmdForYearMonth(now.Year, now.Month)
## This is how we set default parameters for a command.
## Notice that we can even call a function to get the
## default value.
# <param name="intYear" value="getYear()" />
# <param name="intMonth" value="getMonth()" />
def cmdForYearMonth(intYear, intMonth):
prevYear = intYear
prevMonth = intMonth
sales = allSales()
mSales = 
for sale in sales:
dos = sale.DateOfSale
if dos.Year == intYear and dos.Month == intMonth:
## Note how we are referring to a specific record id here.
## Not a big deal at all in business rules scripts.
sales = allSales()
mSales = 
for sale in sales:
if sale.CustomerId == '36':
The format of the special comment is:
# <param name="parameterName" value="parameterValue" />
The command parameter comment isn't too intelligent yet - for example, it doesn't associate itself with a specific command function, so if you have two command functions in the same module that have the same parameter name, you'll get the results of the first comment. This is still a work in progress - I plan to make it more robust. In the meantime, if you just use different parameter names inside your command function declarations, all should be well.
By the way, you may have wondered "why not just use Python's default argument values?" We could have done it that way, but (as far as I know), you cannot call another Python function to get the default value. The way we define it, since we bypass the normal syntax for Python default arguments, we can implement it however we want - in our case, we've added the ability to make the default get its value by calling another function.
Well, This is Getting Pretty Long
There is much more to cover. We have barely touched on AppRuleHost.cs, for example - our implementation of the
DefaultExecuter class. Read through the code and comments in
AppRuleHost for ideas about how you can make your business rules much simpler by defining a
host.do_something(...) method, so you can write parameterized boilerplate code in C# rather than IronPython.
What to Look For in the Code
If you want to know how the Commands Panel responds to the context and refreshes itself with the list of available and relevant commands, look at the following classes:
- Pay particular attention to the code in the
Handle(CommandTargetActivatedMessage message) method defined in
CommandModuleListViewModel. which is an implementation of the
Caliburn.Micro.IHandle<T> interface - one of my favorite interfaces.
- Notice how these three VMs combine to form a hierarchy that shows in the Commands Panel view: Modules -> Module -> Commands -> Command -> Parameters -> Parameter.
If you want to know more about extending
Aim.Scripting.DefaultExecuter, study the
BizRules.Client.Wpf.AppRuleHost class. This class has lots of comments about:
- Creating your own
- Writing to the UI on the UI thread
- Spinning up
Task<T> and letting it go off and do its thing, while reporting back in real time
If you need to know more about composition root, and where things are set up for scripting integration, look at the following classes:
- Pay particular attention to the overridden
- Look in the shell view model for lots more
See if you can figure out what's going on in Security Events, Admin Customer Reports, and Login Swap business rule modules. I'll leave it as an exercise for the reader.
- 2015-06-09: First release
Bruce Pierson is the CTO of Connexa Softools, Inc. (www.connexatools.com), a software company specializing in product configuration and build-to-order manufacturing tools.