Spring.NET and IronPython – scripting business rules

In many systems there comes a time when need of being able to change business rules without rebuilding and redeploying whole solution appears. In such projects it’s really reasonable to use Dynamic Language Runtime (like Ruby, Python or Lua to name a few). Example I’d like to present in this post shows how to accomplish this approach using Spring.NET (as IoC container) and IronPython (as DLR in which we implement our business rules).

In this example we have two interfaces which describe two main components:

  • ICustomerStatisticsService – interface of service which provides one method used to get total money spent in our shop by particular customer;
  • IDiscountPolicy – interface of service which provides one method used to calculate discount of particular customer;

Here are these interfaces declarations:

public interface ICustomerStatisticsService
{
    decimal GetTotalShoppingSpendings(int customerId);
}
public interface IDiscountPolicy
{
    decimal CalculateDiscountForCustomer(int customerId);
}

ICustomerStatisticsService could return result of  T-SQL query or invoke web service operation but for the sake of example below implementation is enough.

public class RandomCustomerStatisticsService : ICustomerStatisticsService
{
    public decimal GetTotalShoppingSpendings(int customerId)
    {
        return new Random().Next(0, 15000);
    }
}

Now we need to provide some generic mechanism of executing Python script to enforce business rules of calculating customer’s discount. This component will actually look for script DefaultDiscountPolicy.py placed in tenant specific folder, set two script variables – customer id and Spring.NET  context, run this script and return it’s value as our customer’s discount. Passing Spring.NET context into the script is quite powerful thing – it makes it possible for Python code to access any service registered in IoC container to send e-mail notification or even call some other service that will update customer data and still benefit from Spring.NET transactions management.

Here’s the scriptable discount policy host code:

public class PythonTenantDiscountPolicy : IDiscountPolicy
{
    public decimal CalculateDiscountForCustomer(int customerId)
    {
        //load tenant specific business rules script
        string pythonCode = string.Empty;
        string scriptFileName = Path.Combine(ConfigurationManager.AppSettings["TenantScriptsPath"], "DefaultDiscountPolicy.py");

        ScriptEngine engine = Python.CreateEngine();
        ScriptRuntime runtime = engine.Runtime;
        ScriptScope scope = runtime.CreateScope();

        using (var reader = new StreamReader(File.OpenRead(scriptFileName)))
        {   
            pythonCode = reader.ReadToEnd();
        }

        //set script variables
        scope.SetVariable("CustomerId", customerId);
        //inject application context into the script
        scope.SetVariable("AppContext", ContextRegistry.GetContext());

        ScriptSource source = engine.CreateScriptSourceFromString(pythonCode);

        //run script and return it's result - customer's discount
        return source.Execute<decimal>(scope);
    }
}

Below code presents really simple implementation of discount calculation policy.

import clr
from System import *

#get instance of statistics service registered in Spring.NET container
#"AppContext" is a variable set by host code
#"CustomerStatisticsService" is a name of service under which it's registered in IoC container 
statService = AppContext.GetObject("CustomerStatisticsService");

#use statistics service to find customer's total spendings
#"CustomerId" is a variable set by host code
spendings = statService.GetTotalShoppingSpendings(CustomerId);

#really simplistic policy
def GetDiscount(total):
    if total > 10000:
        return Decimal(0.15);
    elif total > 5000:
        return Decimal(0.10);
    elif total > 1000:
        return Decimal(0.05);
    elif total > 100:
        return Decimal(0.01);
    else:
        return Decimal(0);

#this line actually returns a discount value
GetDiscount(spendings)

Spring.NET configuration:

<spring>
  <context>
    <resource uri="config://spring/objects" />
  </context>
  <objects xmlns="http://www.springframework.net">
    <object name="CustomerStatisticsService" type="BusinessRulesDemo.Discounts.RandomCustomerStatisticsService, BusinessRulesDemo.Discounts">
    </object>
    <object name="DiscountPolicy" type="BusinessRulesDemo.Discounts.PythonTenantDiscountPolicy, BusinessRulesDemo.Discounts">        
    </object>
  </objects>
</spring>

Discount policy usage:

static void Main(string[] args)
{
    IDiscountPolicy policy = ContextRegistry.GetContext().GetObject("DiscountPolicy") as IDiscountPolicy;

    Console.WriteLine("Please enter customer ID...");
    int id = Convert.ToInt32(Console.ReadLine());
    decimal discount =  policy.CalculateDiscountForCustomer(id);

    Console.WriteLine("Customer discount = {0:0.00}%", discount);
}

What I like in this approach is that it makes your system open for extension and easier to adapt to your customers needs. This makes for a price of same indirection of code you have to deal with plus adding some “magic strings” to keep an eye on. Generally it makes your project more “dynamic” and less “static” with all its “pros” and “cons”.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: