C# scripting example using CSharpCodeProvider

Jun 26, 2014

This example shows how to utilize C# as a scripting language in your C# application. This approach provides great benefits:

  • Script with a powerful, modern, and fast programming language
  • No need to learn a new scripting language
  • Use Visual Studio to edit and debug the scripts (use breakpoints, step into code)
  • Scripts can be merged with the application executable for release
  • Performance same as native C#

You can download the source code or check it out at GitHub directly. Visual C# 2010 Express is required to compile the code.

How it works:
  1. .cs files from the Scripts directory are compiled into a .dll file
  2. Assembly is searched for all classes that implement IScript interface
  3. Each of these classes is instantiated, and the interface method is called

This is similar to using a .dll plugin, except the .dll is compiled at runtime by the application.

Note: Why separate the scripts from the application? They are prone to change and we don't want to recompile the whole application every time a tweak is made.

Relevant classes and interfaces explained:

  • IScript: Interface that all external script classes must implement to be recognized as scripts
  • MyCustomScript: Demo script class that implements the IScript interface
  • IScriptableComponent: Interface of some component that is exposed for the scripts to control
  • DummyComponent: Dummy implementation of the IScriptableComponent
  • Helper: A static class that handles compiling and finding scripts in an assembly

Compiling at runtime

In order to compile .cs files, we will use the CSharpCodeProvider:

public static Assembly CompileAssembly(string[] sourceFiles, string outputAssemblyPath)
{
    var codeProvider = new CSharpCodeProvider();

    var compilerParameters = new CompilerParameters
    {
        GenerateExecutable = false,
        GenerateInMemory = false,
        IncludeDebugInformation = true,
        OutputAssembly = outputAssemblyPath
    };

    // Add CSharpSimpleScripting.exe as a reference to Scripts.dll to expose interfaces
    compilerParameters.ReferencedAssemblies.Add(Assembly.GetExecutingAssembly().Location);

    var result = codeProvider.CompileAssemblyFromFile(compilerParameters, sourceFiles); // Compile

    return result.CompiledAssembly;
}
Warning: At this point the compiled assembly is loaded in the current AppDomain and cannot be unloaded. Loading many assemblies will consume memory.

Demo script

Every class that implements IScript interface is considered a script. In this example, IScript only has the Run() method:

// CSharpSimpleScripting.exe

public interface IScript
{
    void Run(IScriptableComponent component);
}

public interface IScriptableComponent
{
    void DoSomething(string parameter);
}

// Scripts.dll
// -----------
// In order to use IScript and IScriptableComponent,
// Scripts.dll must reference the CSharpSimpleScripting.exe
// This is handled by the Helper.CompileAssembly method.
public class MyCustomScript : IScript
{
    public void Run(IScriptableComponent component)
    {
        // Once the script gets the scriptable component from the application,
        // it can do what ever it wants with it
        component.DoSomething("Hello from MyCustomScript.");
    }
}

Creating and running scripts

A helper class is used to find all types that implement IScript interface. Each of these types is instantiated using the Activator, and then executed:

var scriptTypes = Helper.GetTypesImplementingInterface(scriptAssembly, typeof(IScript));

foreach (var scriptType in scriptTypes)
{
    // Creates instances of type and pass component to the constructor
    var script = (IScript)Activator.CreateInstance(scriptType);
    script.Run(component);
}
Security Warning: Letting your end users run arbitrary code can be dangerous. If the host executable has full trust, a malicious script can do bad things on the system.
comments powered by Disqus