C# Corner
Extending a C# Application Through a Scripted DLR Language
The
Dynamic Language Runtime (DLR) is an open source library provided by Microsoft that that enables support for multiple dynamic programming languages. The DLR is not a replacement for the CLR but rather is built upon the CLR. There are many dynamic programming languages that have DLR implementations. I will be covering hosting
IronPython and
IronRuby, which are DLR implementations of Python and Ruby in a C# application.
DLR Hosting API Basics
The DLR Hosting API allows a DLR language to be scripted from a CLR language such as C# and VB.NET. Through the DLR hosting API, one can easily extend the functionality of a C# or VB.NET application through scripts coded in one of the many DLR supported languages. First let's get our feet wet by going over the basics of the DLR hosting API.
The major components of the DLR Hosting API are: ScriptRunTime, ScriptEngine, ScriptScope, ScriptSource and CompiledCode. A ScriptRunTime manages the global script scope and one or many ScriptEngines. A SciptScope acts as a namespace for scripted members. A ScriptEngine encapsulates a dynamic programming language. A ScriptSource is an abstraction of a piece of source code to execute or compile. CompiledCode represents a compiled script or expression that may be executed. CompiledCode objects are created from a ScriptSource and are optimized for fast execution.
Now that you have a basic understanding of how the hosting API works let's create a sample application. Our sample application will allow the user to create validation rules in either Python or Ruby for a form. The user will be able to select a scripting language and validation rule to apply to an email address form field. New rules will be able to be added simply by adding them to a common Rules folder for the application.
Project Setup
Create a new C# 4.0 WPF Application. Add references to IronPython, IronPython.Modules, IronRuby, IronRuby.Libraries, Microsoft.Scripting and Microsoft.Hosting.
Next add an Application Configuration file to the project. We will configure our application to be able to host both IronPython and IronRuby scripts.
App.Config
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="microsoft.scripting" type="Microsoft.Scripting.Hosting.Configuration.Section, Microsoft.Scripting"/>
</configSections>
<microsoft.scripting>
<languages>
<language names="IronPython;Python;py" extensions=".py" displayName="IronPython" type="IronPython.Runtime.PythonContext, IronPython"/>
<language names="IronRyuby;Ruby;rb" extensions=".rb" displayName="IronRuby" type="IronRuby.Runtime.RubyContext, IronRuby"/>
</languages>
</microsoft.scripting>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0,Profile=Client"/>
</startup>
</configuration>
Setting up the UI
Open up Window.xaml file and place the following markup in the root Grid element content.
<StackPanel>
<StackPanel Orientation="Horizontal">
<Label>Language</Label>
<ComboBox Name="cboLanguage" SelectedIndex="0"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Label>Validation Rule</Label>
<ComboBox Name="cboVaidatelRules" SelectedIndex="0"/>
</StackPanel>
<Label>Script Source</Label>
<TextBox Name="txtSource" Margin="2"></TextBox>
<StackPanel Orientation="Horizontal">
<Label>Email</Label>
<TextBox Name="txtEmail" MinWidth="200"></TextBox>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Label>Status</Label>
<Label Name="lblStatus"></Label>
</StackPanel>
<Button Click="Button_Click">Validate</Button>
</StackPanel>
[Click on image for larger view.] |
Figure 1. |
Now that the user interface of the application has been setup we will move on to implementing the core business logic.
Business Logic Setup
Next, add a new Class Library Project to the solution and name it "DLRScriptingVSM.BusinessLogic." Our BusinessLogic library will contain all validation logic, as well as the necessary scripting logic to load and execute our scripted validation rules. The IValidationRule interface will be our contract for our dynamic validation rules. The IValidationRule interface only has one method IsValid that returns a true/false and is given a dynamic type entity.
IValidationRule.cs
public interface IValidationRule
{
bool IsValid(dynamic entity);
}
Validation rules will be created by the ValidationEngine at run-time. The ValidationEngine relies on the DLRScript class, which encapsulates all scripting logic needed by the ValidationEngine. The DLRScript class creates a ScriptEngine for the given language. Language configuration is loaded from the App.Config configuration file. The created RunScript function executes the given script by a given file path and maintains its ScriptScope. The CreateClassInstance method instantiates a new dynamic object for the given class name.
DLRScript.cs
public class DLRScript
{
ScriptRuntime m_runTime;
public ScriptRuntime RunTime
{
get { return m_runTime; }
set { m_runTime = value; }
}
ScriptScope m_scope;
ScriptEngine m_engine;
public DLRScript(string languageName)
{
m_runTime = ScriptRuntime.CreateFromConfiguration();
m_engine = m_runTime.GetEngine(languageName);
m_scope = m_engine.CreateScope();
// set common search paths
var paths = m_engine.GetSearchPaths().ToList();
paths.Add(System.IO.Directory.GetCurrentDirectory());
m_engine.SetSearchPaths(paths);
}
public Microsoft.Scripting.Hosting.ScriptScope Scope
{
get { return m_scope; }
set { m_scope = value; }
}
public dynamic RunScript(string script)
{
return m_engine.Execute(script, m_scope);
}
public void RunScriptFromFile(string fileName)
{
m_scope = m_engine.ExecuteFile(fileName, m_scope);
}
public dynamic CreateClassInstance(string className)
{
dynamic dynType;
if (m_runTime.Globals.ContainsVariable(className))
{
dynType = m_runTime.Globals.GetVariable(className);
}
else
{
dynType = m_scope.GetVariable(className);
}
dynamic createdObj = m_engine.Operations.CreateInstance(dynType);
return createdObj;
}
}
Finally, we will implement our ValidationEngine class, which utilizes the DLRScript class to execute the given script for the given language.
ValidationEngine.cs
public IValidationRule CreateValidationRule(string scriptPath, string ruleName, string languageName)
{
DLRScript script = new DLRScript(languageName);
Assembly bllAssembly = Assembly.GetAssembly(typeof(DLRScriptingVSM.BusinessLogic.IValidationRule));
script.RunTime.LoadAssembly(bllAssembly);
script.RunScriptFromFile(scriptPath);
dynamic rule = script.CreateClassInstance(ruleName);
return rule as IValidationRule;
}
Tying it all together
Now we will get to integrating our business logic into our core application. First of all we need to add references to System.IO, DLRScriptingVSM.BusinessLogic, Microsoft.Scripting, and System.Configuration. Next we setup the path that will be used to load our dynamic business rules.
private const string RULE_SCRIPT_PATH = @"../../Rules/";
We'll also setup some view models to store the necessary information for the language and validation rule combo boxes.
private class ScriptLanguageViewModel
{
public string LanguageName { get; set; }
public string LanguageExtension { get; set; }
}
private class RuleScriptViewModel
{
public string ScriptPath { get; set; }
public string RuleName { get; set; }
}
Now we wire up the change events for the language and validation rule combo boxes. We'll also populate the languages combo box with the configured script languages from the App.Config file.
public MainWindow()
{
InitializeComponent();
cboLanguage.SelectionChanged += new SelectionChangedEventHandler(cboLanguage_SelectionChanged);
cboVaidatelRules.SelectionChanged += new SelectionChangedEventHandler(cboVaidatelRules_SelectionChanged);
LoadLanguages();
}
In the validation rule selected item change event, we get the selected rule and load up the source code view textbox with the rule's code.
void cboVaidatelRules_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
RuleScriptViewModel selectedRule = cboVaidatelRules.SelectedItem as RuleScriptViewModel;
if (selectedRule != null)
{
LoadSourceView(selectedRule.ScriptPath);
}
}
When the selected language changesm we retrieve the selected language file extension and refresh the validation rules combo box with rules for the language.
void cboLanguage_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
string langExt = (string)cboLanguage.SelectedValue;
LoadValidationRules(langExt);
}
The LoadSourceView function simply loads up the given script and displays it in the source code text box.
private void LoadSourceView(string scriptPath)
{
txtSource.Text = File.ReadAllText(scriptPath);
}
The LoadLanguages function populates the language combo box with the configured scripting languages in the <microsoft.scripting> config section in the App.Config configuration file.
private void LoadLanguages()
{
Microsoft.Scripting.Hosting.Configuration.Section scriptSettings = (Microsoft.Scripting.Hosting.Configuration.Section)ConfigurationManager.GetSection("microsoft.scripting");
var languages = scriptSettings.ElementInformation.Properties["languages"];
Microsoft.Scripting.Hosting.Configuration.LanguageElementCollection langElems = (Microsoft.Scripting.Hosting.Configuration.LanguageElementCollection)languages.Value;
List<ScriptLanguageViewModel> langItems = new List<ScriptLanguageViewModel>();
foreach ( Microsoft.Scripting.Hosting.Configuration.LanguageElement langElem in langElems)
{
string langName = langElem.Names.Split(';')[0];
string langExtension = langElem.Extensions.Split(';')[0];
langExtension = langExtension.Replace(".", string.Empty);
langItems.Add(new ScriptLanguageViewModel { LanguageExtension = langExtension, LanguageName = langName });
}
cboLanguage.DisplayMemberPath = "LanguageName";
cboLanguage.SelectedValuePath = " LanguageExtension";
cboLanguage.ItemsSource = langItems;
cboLanguage.SelectedIndex = 0;
}
The LoadValidationRules Function
The Load ValidationRules function loads up the validation rule combo box with validation rules for the given language extension.
private void LoadValidationRules(string languageExtension)
{
string searchPattern = string.Format("*.{0}", languageExtension);
IEnumerable<string> ruleFiles = Directory.EnumerateFiles(RULE_SCRIPT_PATH, searchPattern, SearchOption.AllDirectories);
var rules = from r in ruleFiles
select new RuleScriptViewModel()
{
ScriptPath = r,
RuleName = r.Replace(RULE_SCRIPT_PATH, string.Empty).Replace("." + languageExtension, string.Empty)
};
cboVaidatelRules.DisplayMemberPath = "RuleName";
cboVaidatelRules.SelectedValuePath = "ScriptPath";
cboVaidatelRules.ItemsSource = rules;
cboVaidatelRules.SelectedIndex = 0;
}
Finally, we wire up the validate button click event to run the selected validation rule on the form's email field. If the validation passes, we display "Valid Email Address" in the status label. If it fails, we display "InvalidEmail Address."
private void Button_Click(object sender, RoutedEventArgs e)
{
string emailAddress = txtEmail.Text;
string language = (string)cboLanguage.SelectedValue;
RuleScriptViewModel selectedRule = cboVaidatelRules.SelectedItem as RuleScriptViewModel;
if (IsValid(selectedRule.ScriptPath, selectedRule.RuleName, emailAddress, language))
{
lblStatus.Content = "Valid Email Address";
}
else
{
lblStatus.Content = "Invalid Email Address";
}
}
private bool IsValid(string scriptPath, string ruleName, string fieldValue, string language)
{
BusinessLogic.ValidationEngine valEngine = new ValidationEngine();
IValidationRule rule = valEngine.CreateValidationRule(scriptPath, ruleName, language);
return rule.IsValid(fieldValue);
}
Adding Python and Ruby Dynamic Validation Rules
Lastly, we'll add the actual Python and Ruby validation rules for the application. Both validation rules implement the IvalidationRule interface and use their respective language's regular expression libraries to validate a given email address.
ValidEmail.py
from DLRScriptingVSM.BusinessLogic import IValidationRule
import re
class ValidEmail(IValidationRule):
def IsValid(self,entity):
return re.match("^[a-zA-Z][\w\.-]*[a-zA-Z0-9]@[a-zA-Z0-9][\w\.-]*[a-zA-Z0-9]\.[a-zA-Z][a-zA-Z\.]*[a-zA-Z]$", entity) != None
ValidEmail.rb
class ValidEmail
include DLRScriptingVSM::BusinessLogic::IValidationRule
def IsValid(entity)
unless entity =~ /^[a-zA-Z][\w\.-]*[a-zA-Z0-9]@[a-zA-Z0-9][\w\.-]*[a-zA-Z0-9]\.[a-zA-Z][a-zA-Z\.]*[a-zA-Z]$/
return false
else
return true
end
end
end
Add both ValidEmail.py and ValidEmail.rb to a Rules folder in your WPF project. Make sure to save your Ruby script with ANSI file encoding to ensure proper execution. Python scripts will work correctly with UTF-8 encoding.
Conclusion
The DLR Hosting API is very flexible and provides the ability to add advanced scripting capabilities to your current and future .NET applications. As we have seen, it is possible to extend your application to run scripts from any supported DLR language. When you are looking for ways to extend your applications, consider it a very viable option.
About the Author
Eric Vogel is a Senior Software Developer for Red Cedar Solutions Group in Okemos, Michigan. He is the president of the Greater Lansing User Group for .NET. Eric enjoys learning about software architecture and craftsmanship, and is always looking for ways to create more robust and testable applications. Contact him at [email protected].