Extending your C# application with IronPython

4 בדצמבר 2008

תגיות: ,
13 תגובות


Recently I’ve started messing around with IronPython; IronPython is the first language of a set of languages that rely on the DLR (Dynamic Language Runtime), the DLR is a hosting platform and a dynamic type system, capable of running dynamic languages on top of the CLR.


I really like the concept of a dynamic language, although it isn’t such a new concepts, I think it will be revived and gain more audience with the upcoming .Net Framework 4.0. One very useful thing you can do with IronPython (or any other language on top of the DLR) is to use it as an embedded scripting engine in your.Net application.


What can we gain from such a scripting engine?


I think that in almost every enterprise application project at some point you’ll come to the conclusion that you need extension points in the system. It can be a Rule Engine, a Workflow of some sort or a pluggable User Interface. So now you want to give someone else the ability to extend or modify the application’s behavior with a minimal effort and time. Of course it’s possible to achieve this with custom tools and code generators that compile and load at runtime, but these solutions are complex and time consuming. Utilizing the DLR as an embedded scripting engine is easy and for my opinion quite elegant (although this “magic” isn’t totally free, it comes with a certain performance penalty).


Embedding IronPython in a C# application


A very common scenario would be to allow a user to write some calculation expressions.




string code = @"100 * 2 + 4 / 3";


ScriptEngine engine = Python.CreateEngine();

ScriptSource source =

  engine.CreateScriptSourceFromString(code, SourceCodeKind.Expression);


int res = source.Execute<int>();

Console.WriteLine(res);

As you can see in this example I use the ScriptEngine to execute an expression that can easily be provided from various external sources. The ScriptEngine represents the language Hosting API (in this case the IronPython implementation on the DLR) It serves as the base entry point for embedding any DLR based language in other applications. The ScriptEngine can be used to create a ScriptSource and a ScriptScope which are used to execute the IronPython code.


In many cases we would like this calculation to operate on entities from our own domain, for example:




ScriptEngine engine = Python.CreateEngine();

ScriptRuntime runtime = engine.Runtime;

ScriptScope scope = runtime.CreateScope();


string code = @"emp.Salary * 0.3";


ScriptSource source =

  engine.CreateScriptSourceFromString(code, SourceCodeKind.Expression);


var emp = new Employee { Id = 1000, Name = "Bernie", Salary = 1000 };


scope.SetVariable("emp", emp);


var res = (double)source.Execute(scope);

In this example I also needed to define a ScriptScope object the ScriptScope object is an execution unit for code. The host can set or get variable values from it and more…


These are very simple scenarios where we have expressions coming from an external source (which can be a file, database etc.). Another possible implementation could be to use this within a Business Rule Engine when each expression / code snippet / .py file, becomes an external business rule.


You’ll see in the code sample that I’ve added a class library named berniea.ironPython.extending.ClassLib I’ve created two classes that will serve us in the following sample, SaleBasket and Line as you can see in Figure 1.


image


Figure 1 – Sample Classes


As you can see SaleBasket hold Lines and has a Total, the total is the Sum of Lines Amount. The Line Represents a product added to the basket, the Line class holds the Product’s name, Price and Quantity of items added. The Amount property represents Price * Quantity.


As in the previous samples first I’ve created the ScriptEngine, ScriptRuntime and ScriptScope. The I’ve instantiated a new SaleBasket with some lines:




var saleBasket = new SaleBasket

{

  Lines = new List<Line>

   {

    new Line { ProductName = "Prod1", ProductPrice = 100, Quantity = 2, Amount = 100 * 2},

    new Line { ProductName = "Prod2", ProductPrice = 20, Quantity = 1, Amount = 20 * 1},

    new Line { ProductName = "Prod3", ProductPrice = 45.8, Quantity = 2, Amount = 45.8 * 2},

    new Line { ProductName = "Prod4", ProductPrice = 3.9, Quantity = 10, Amount = 3.9 * 10},

    new Line { ProductName = "Prod5", ProductPrice = 555.5, Quantity = 10, Amount = 555.5 * 10}

   }

};

I’ve added a directory to store rules under the console application. We can store py files in this directory, when we wish execute the rules we can extract them from our Rules directory as shown in the following sample:


First getting all the files is quite easy; we can do something like this:




string rootDir = AppDomain.CurrentDomain.BaseDirectory;

string rulesDir = Path.Combine(rootDir, "Rules");


var files = new List<string>();



foreach (string path in Directory.GetFiles(rulesDir))

  if (path.ToLower().EndsWith(".py"))

     files.Add(path);

If we want the ScriptRuntime to be able to recognize our own classes we need to load the assembly into the ScriptRuntime. For example:




string path = Assembly.GetExecutingAssembly().Location;

string dir = Directory.GetParent(path).FullName;

string libPath = Path.Combine(dir,"berniea.ironPython.extending.ClassLib.dll");


Assembly assembly = Assembly.LoadFile(libPath);

runtime.LoadAssembly(assembly);

Without doing so our IronPython scripts will not recognize classes from our own domain. Like the following script:




from berniea.ironPython.extending.ClassLib import *


for line in saleBasket.Lines:


if line.ProductName == 'Prod1':

discount = line.Amount * 0.2

line.Amount = line.Amount – discount

print 'discount given: ' + discount.ToString()


if line.Quantity >= 10:

line.Amount = line.Amount – line.ProductPrice

print 'discount given: ' + line.ProductPrice.ToString()

You can see here that I’m importing all classes from berniea.ironPython.extending.ClassLib by using import * (this is similar to adding a “using” directive in a C# program). Then I use a “for loop” to iterate all the lines in the saleBasket in order to apply some business logic on them.


After all this preparations the rest is quite easy we need to set the “saleBasket” variable in the scope and execute all script files.




scope.SetVariable("saleBasket", saleBasket);


foreach (var file in files)

{

   source = engine.CreateScriptSourceFromFile(file);

   source.Execute(scope);

}

That’s about it, we have created an extensible rule engine which we can easily add new rules to and modify old ones without changing the core functionality of our application.


The samples I gave here are just the tip of the iceberg, there is a lot more that can be done with an embedded scripting engine in your C# application from writing your own DSL’s, enable plug-ins to your applications and allow other people to extend the applications UI. The nice thing about it is that you could actually build an entire new application using IronPython and very easily hook it to an existing application.


A word of caution, using these techniques is not a substitution for a good design. One very possible mistake could be to build an entire application around the ScriptEngine. I think that this approach will lead to a very messy and hard to maintain application. You need to carefully consider where you want to enable extension points in your application and only there provided the means.

הוסף תגובה
facebook linkedin twitter email

כתיבת תגובה

האימייל לא יוצג באתר. (*) שדות חובה מסומנים

13 תגובות

  1. Russell8 בינואר 2009 ב 2:55

    This gives a good introduction to using IronPython (thanks for that!), but I'm not quite so clear on the "Why". I love Python, but I have trouble seeing why you'd want to mix Python with C#, other than to show you can do it. Why can you do this way that you couldn't do as easily with C# (plus Spring.NET or Unity for dependency injection, etc)?

    I'm sure the answer is obvious and I'll probably see it as soon as I submit this comment, but for now it escapes me.

    להגיב
  2. BradJ26 בפברואר 2009 ב 20:18

    This is just what I was looking for. Thanks!

    להגיב
  3. Joe28 באפריל 2009 ב 2:25

    I agree with Russel.

    להגיב
  4. edvaldig24 ביוני 2009 ב 16:38

    Russel and Joe:
    Adding scripting support to your applications depends on your situation. Usually you have no use for it at all. For some cases though, you want to be able to change e.g. some logic/rules/calculations without having to recompile your application, scripting is essential. This is what most game developers are doing. After making a game engine which handles all the heavy work of rendering, networking, resources etc. much of the game logic can be scripted in a (higher level) scripting language. Python code is also much more readable and more compact than c# code, so scripts become easy to read and write even to people in your team that usually don't program.

    להגיב
  5. edvaldig24 ביוני 2009 ב 16:48

    By the way berniea, I believe this is called embedding, not extending:
    http://www.twistedmatrix.com/users/glyph/rant/extendit.html

    Extending would be to write modules in e.g. C/C++ which can be loaded into python, where Embedding is calling python code from within e.g. your C# code.

    להגיב
  6. Vadim31 באוגוסט 2009 ב 21:50

    Thank you very much for taking your time to write this well written article how to integrate IronPython and C#.

    I just started playing with IronPython and find your post one of the most usefull resources on the web.

    להגיב
  7. Fabzter2 באפריל 2010 ב 1:55

    It's both. He is extending when he exposes classes and objects to IronPython.

    להגיב
  8. zezito3 בספטמבר 2010 ב 13:58

    Man great work!
    very very helpfull!
    Thanks

    להגיב
  9. vinay22 בספטמבר 2010 ב 19:33

    Anything that would be "Elegant enough" but would not cause performance penalties??..

    להגיב
  10. Terry Barnes2 ביולי 2011 ב 17:39

    I'm trying to execute a script but it imports the subprocess.py file (which is in the IronPython/Lib folder.

    How do I set the engine up to reference this file as I currently receive an error stating "No module named subprocess".

    I'm new to Python and IronPython and struggling to find how to do this. Any help appreciated as this is the best article I've found on a similar topic. Thanks.

    להגיב
  11. Flanigan5 בספטמבר 2013 ב 10:45

    Howdy! I could have sworn I've been to this site before but after checking through
    some of the post I realized it's new to
    me. Anyhow, I'm definitely delighted I found it and I'll be book-marking and checking back
    often!

    להגיב
  12. Caudle2 בנובמבר 2013 ב 23:16

    No pensaba encontrar algo asi por estos lares ,en cambio hhe quedado bastante asombrado esta vez

    להגיב