Scripting in .Net
So you want to script C#? It’s very easy! For these examples in this article, we’ll use a script file called: “MyScript.cs” which is not compiled in the solution but simple marked as a content file. The contents of that file should look like this:
using System;
static class Program
{
[STAThread]
public static void Main()
{
System.Console.WriteLine("Hello world");
}
}
Next, let’s create a simple compiler to compile this script:
using System;
using System.Collections.Generic;
using System.Text;
using System.CodeDom.Compiler;
using Microsoft.CSharp;
using System.IO;
using System.Reflection;
namespace ScriptRunner
{
class Program
{
static void Main(string[] args)
{
CSharpCodeProvider provider = new CSharpCodeProvider();
CompilerParameters compilerParameters = new CompilerParameters();
compilerParameters.IncludeDebugInformation = true;
compilerParameters.GenerateExecutable = false;
compilerParameters.GenerateInMemory = true;
string source = File.ReadAllText("MyScript.cs");
CompilerResults results = provider.CompileAssemblyFromSource(compilerParameters, new string[] { source });
if (results.Errors.Count > 0)
{
foreach (var item in results.Errors)
{
Console.WriteLine(item);
}
}
else
{
if (results.CompiledAssembly != null)
{
BindingFlags flags = BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public;
Assembly asm = results.CompiledAssembly;
Type progType = asm.GetType("Program");
MethodInfo info = progType.GetMethod("Main", flags);
if (info != null)
{
info.Invoke(null, BindingFlags.NonPublic, null, null, null);
//asm.EntryPoint.Invoke(null, new object[0]);
}
else
{
Console.WriteLine("Entrypoint not found!");
foreach (var item in asm.GetTypes())
{
Console.WriteLine(item.FullName);
foreach (var method in item.GetMethods(flags))
{
Console.WriteLine(item.FullName + "." + method.Name);
}
}
}
}
}
Console.ReadLine();
}
}
}
No magic here, the result will be as expected:
Now what can we do with this? We can add scripting to our app… but wait, couldn’t that break my app? Yup, if someone decides to create a piece of script that generates an exception, loads huge files, etc, it will sooner or later break your app right with it… These are the kind of things you want to put inside an AppDomain, maybe even sandboxed with partial trust…
The idea is that a piece of code should be able to access certain items from your app. Interfaces and attributes are the preferred approach for these situations. Let’s create a plugin interface, and a marker attribute so we’ll be able to find it once the plugin gets compiled. For this, we’ll create a separate assembly which is called “PluginSDK” and this will contain a few interfaces.
The host interface:
namespace ScriptRunnerSDK
{
public interface IHost
{
string Name { get; }
}
}
The plugin interface:
namespace ScriptRunnerSDK
{
public interface IPlugin
{
string Execute();
IHost Host { get; set; }
}
}
And finally, the plugin attribute:
using System;
namespace ScriptRunnerSDK
{
[global::System.AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class PluginAttribute : Attribute
{
readonly string name;
// This is a positional argument
public PluginAttribute(string name)
{
this.name = name;
}
public string Name
{
get { return name; }
}
}
}
Now it’s time to change our sample script to something that actually uses this info.
using System;
using ScriptRunnerSDK;
[Plugin("MyPlugin")]
class Program : IPlugin
{
public Program()
{
}
public string Execute()
{
Console.WriteLine("I am running in domain: {0}", AppDomain.CurrentDomain.FriendlyName);
return String.Format("Hello {0}", Host.Name);
}
private IHost _host;
public IHost Host
{
get
{
return _host;
}
set
{
_host = value;
}
}
}
And finally, let’s create our compiler that loads the script, compiles it, runs it in a separate AppDomain providing a host and will terminate the script once it is done.
using System;
using System.CodeDom.Compiler;
using System.IO;
using System.Reflection;
using Microsoft.CSharp;
using ScriptRunnerSDK;
namespace ScriptRunner
{
class Program
{
static void Main(string[] args)
{
string source = File.ReadAllText("MyScript.cs");
DummyHost host = new DummyHost();
AppDomain scriptDomain = null;
try
{
scriptDomain = AppDomain.CreateDomain("Script domain");
Type workerType = typeof(ScriptWorker);
ScriptWorker worker = (ScriptWorker)scriptDomain.CreateInstanceAndUnwrap(workerType.Assembly.FullName, workerType.FullName);
string result = worker.RunScript("MyPlugin", source, host);
Console.WriteLine(result);
}
finally
{
if (scriptDomain != null)
AppDomain.Unload(scriptDomain);
}
Console.ReadLine();
}
}
public class DummyHost : MarshalByRefObject, IHost
{
public DummyHost()
{
}
#region IHost Members
public string Name
{
get { return "DummyHost"; }
}
#endregion
}
public class ScriptWorker : MarshalByRefObject
{
public string RunScript(string pluginName, string script, IHost host)
{
string result = String.Empty;
CompilerResults results = Compile(script);
if (results.Errors.Count > 0)
{
foreach (var item in results.Errors)
{
Console.WriteLine(item);
}
}
else
{
result = RunCompiledScript(results.CompiledAssembly, pluginName, host);
}
return result;
}
private string RunCompiledScript(Assembly assembly, string pluginName, IHost host)
{
Type pluginType = null;
// Find the plugin attribute in the assembly's types, see if it matches the plugin name and if so, construct the type, pass the host and execute the plugin.
foreach (var type in assembly.GetTypes())
{
PluginAttribute[] attr = type.GetCustomAttributes(typeof(PluginAttribute), false) as PluginAttribute[];
if (attr != null && attr.Length > 0 && String.Equals(attr[0].Name, pluginName, StringComparison.OrdinalIgnoreCase))
{
pluginType = type;
break;
}
}
// Did we find the type?
if (pluginType != null)
{
IPlugin plugin = pluginType.GetConstructor(new Type[0]).Invoke(new object[0]) as IPlugin;
plugin.Host = host;
return plugin.Execute();
}
// No, we didn't...
throw new InvalidOperationException("The plugin specified could not be found");
}
CompilerResults Compile(string source)
{
CSharpCodeProvider provider = new CSharpCodeProvider();
CompilerParameters compilerParameters = new CompilerParameters();
compilerParameters.IncludeDebugInformation = true;
compilerParameters.GenerateExecutable = false;
compilerParameters.GenerateInMemory = true;
// Add a reference assembly
compilerParameters.ReferencedAssemblies.Add("ScriptRunnerSDK.dll");
CompilerResults results = provider.CompileAssemblyFromSource(compilerParameters, new string[] { source });
return results;
}
}
}
This is all there’s to it
The result looks like this:
You can read in my previous post how to do error handling on remote AppDomains to provide additional stability to your application.
Sunday, June 07, 2009 3:03:50 PM (Pacific SA Standard Time, UTC-04:00)