Introduction to the Roslyn Scripting API

by Brian Rasmussen

In this post we take a look at how the Roslyn Scripting API can enable applications to evaluate code at runtime. While this has been possible since the dawn of .NET through the use of Reflection, Lightweight Code Generation, CodeDom, etc., it has never been particularly easy. All of these mechanisms are either hard to use, require MSIL knowledge, or just have inherent limitations.

Roslyn not only makes dynamic code evaluation a lot easier, it also provides the necessary APIs to implement a fully featured scripting experience. This post covers the basics of the Scripting API in Roslyn. For an example of how Roslyn can be used to implement a rich environment for entering and executing code at runtime take a look at the C# Interactive window implemented in the Roslyn CTP. The C# Interactive window provides syntax highlighting, intellisense, and even refactoring for code supplied at runtime.

This post uses C# as an example because that is what Roslyn supports for scripting in this first CTP. 

The Basics

Let’s start by looking at a very simple example. Let’s just execute some code dynamically. To do that all we need is an instance of the ScriptEngine class. Specifically we need the C# version defined in Roslyn.Scripting.CSharp. All we have to do is create an instance of ScriptEngine and call the Execute method with the code to be evaluated. The Roslyn scripting version of Hello World is as simple as:

 using Roslyn.Scripting.CSharp;
 
namespace RoslynScriptingDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            var engine = new ScriptEngine();
            engine.Execute(@"System.Console.WriteLine(""Hello Roslyn"");");
        }
    }
}

The solution needs references to Roslyn.Compilers and Roslyn.Compilers.CSharp.

Notice, we did not have to use APIs that take MSIL or that carefully build up a tree of code that then needs to be compiled and executed. We can simply send the ScriptEngine text snippets of the code we write all the time. The ScriptEngine takes care of the rest to execute it.

Execution Context

Unfortunately the code above does have a few limitations. The first of which is that there’s no cumulative execution context, so if we define a variable as part of one code snippet and then try to access the variable from another code snippet, we will get an exception. For example, if we change the body of Main to something like this:

 var engine = new ScriptEngine();
 
engine.Execute(@"var a = 42;");
engine.Execute(@"System.Console.WriteLine(a);");

We will get a compilation error from the scripting engine as the statements are treated as separate compilations. Fortunately, the Scripting API provides the Session type that can be used as an execution context when calling Execute. A session stores the context of a series of submissions, so by providing a session we can execute the code above and get the expected result.

 using Roslyn.Scripting.CSharp;
using Roslyn.Scripting;
 
namespace RoslynScriptingDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            var engine = new ScriptEngine();
            var session = Session.Create();
 
            engine.Execute(@"var a = 42;", session);
            engine.Execute(@"System.Console.WriteLine(a);", session);
        }
    }
}

We can even define a new method, store it in the session, and then invoke it like this.

 engine.Execute(@"void Foo() { System.Console.WriteLine(""Foo""); }", session);
engine.Execute(@"Foo();", session);

Interacting with the Host Application

However, we still cannot interact with the hosting application. Obviously scripting is of limited use if the user supplied code cannot interact with the application somehow. To address this we need to provide a host object to the session.

The code submissions will automatically have access to any public members of the host object. To supply a host object, we pass in an instance of any type we would like to use as an interface between the host application and the session. Furthermore, we have to make the ScriptEngine aware of this type as well. To do that we pass in a reference to the assembly defining our host object type. With that the setup code now looks as follows:

 var hostObject = new HostObject();
var engine = new ScriptEngine(new[] { hostObject.GetType().Assembly.Location });
var session = Session.Create(hostObject);

The host object can be of any type. The ScriptEngine binds any free identifier to public members of the type, as if the type's member names were globally accessible from the script. For the sake of this demo, let’s just make it as simple as possible.

 public class HostObject
{
    public int Value = 0;
}

With the code above, we can now access the Value field on our hostObject instance like this:

 engine.Execute(@"Value = 42;", session);
engine.Execute(@"System.Console.WriteLine(Value);", session);

Notice how the instance's members are implicitly available within the session. We simply access any public member on the instance.

The examples we have been looking at so far are really simple, so let’s round off this introduction by adding some functionality that resembles what an application might do with the scripting API. Imagine we have a Report type like the one listed below.

 // This will act as our host object
public class Report
{
    // Internal state
    private readonly List<int> values = new List<int>();
    private int result;
 
    // Encapsulate the internal data structure
    public void AddValue(int value)
    {
        values.Add(value);
    }
 
    // Allow read-only enumeration of values
    public IEnumerable<int> Values { get { return values; } }
 
    // User code will provide the implementation for these methods
    public Action GetValues;
    public Func<int> CalculateResult;
 
    // This method may be called by both the host application and the user code
    public void PrintResult()
    {
        var get = GetValues;
        if (get != null)
        {
            get();
        }
 
        var calc = CalculateResult;
        if (calc != null)
        {
            result = calc();
        }
 
        Console.WriteLine("The result of the calculation is {0}", result);
    }
}

The class stores an internal list of numbers and a result. It provides an AddValue method to populate the list and a read-only iterator. The result can be printed by calling the PrintResult method. User-supplied code determines how the list is actually populated and how the result is calculated.

The Report class exposes two delegate types, GetValues and CalculateResult for this purpose. When PrintResult is called it invokes the delegates to populate the list of values and calculate the result. Our simple application may look something like this:

 static void Main(string[] args)
{
    var report = new Report();
    var engine = new ScriptEngine(new[] { 
        "System.Core", report.GetType().Assembly.Location });
    var session = Session.Create(report);
 
    engine.Execute(@"using System.Linq;", session);
    engine.Execute(
        @"GetValues = () => { for(int i = 1; i <= 3; i++) AddValue(i); };",
        session);
    engine.Execute(@"CalculateResult = () => Values.Sum();", session);
 
    report.PrintResult();
}

Admittedly this is still a simple example, but it does illustrate how script code can be used to customize our host application.

Notice how we can assign anonymous methods to the delegate members, and the host application can call them. Also, notice how we can access the LINQ extension method Sum in the user supplied code. To enable this we need to let ScriptEngine reference System.Core and import the System.Linq namespace.

That completes the overview of how to use Roslyn to add scripting support to an application. For more information on the Roslyn Scripting API please see the walkthroughs and The Roslyn Project Overview.

About the author

Brian is a Senior SDET at Microsoft working on the C# and VB language services in Roslyn. Before joining Microsoft Brian was a Microsoft MVP for Visual C# for four years. Brian can be found on Twitter (@kodehoved).