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.


[Click on image for larger view.]
Figure 2.

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.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube