Introduction to Scripting with the .NET Compiler Platform (Roslyn)

So easy a caveman can do it.

Published on Wednesday, February 18, 2015

Scripting support in the .NET Compiler Platform (formerly known as Roslyn) has been a long time coming. It was originally introduced more than a year ago and then removed while the team considered what the ideal API should look like. It was recently reintroduced into the master source branch on GitHub, though as of this blog post it still isn't available on the nightly MyGet feed or on NuGet. In this post I will explain how to obtain and build the new scripting bits (including on a system without Visual Studio 2015 - it can actually be built using only Visual Studio 2013), introduce some of the scripting functionality, and show some scenarios where this might be helpful in your own applications. I also want to caveat this post by saying that it may go out of date quickly. The .NET Compiler Platform is under heavy development and it is changing frequently, including the public API. While I wouldn't expect any sweeping changes in the scripting support at this point, many of the details are subject to change.

Obtaining and Building

Since the scripting support isn't available in one of the binary distribution channels like the nightly MyGet feed or on NuGet, you'll need to obtain and build the .NET Compiler Platform from source in order to use them. Luckily the development team has been working hard to ensure this part is straightforward. The first step is to clone the repository from GitHub using the following command (or your favorite Git GUI): git clone https://github.com/dotnet/roslyn.git.

The readme file says that to build the libraries you'll need Visual Studio 2015, but that's not actually true. You can build the .NET Compiler Platform just fine with only Visual Studio 2013 using the special Roslyn2013.sln solution file they've provided. In fact, it's just a simple two-command process. Make sure to run these commands from a Visual Studio command prompt. First you have to restore the NuGet packages and then build the solution. You might get some warnings about missing FxCop, but those are safe to ignore. From the \src directory in the repository run:

powershell .nuget\NuGetRestore.ps1
msbuild Roslyn2013.sln

This will compile a bunch of libraries to the \Binaries\Debug directory. You don't need all of them for scripting. Assuming you want to script in C#, the libraries you need are:

  • Microsoft.CodeAnalysis
  • Microsoft.CodeAnalysis.CSharp
  • Microsoft.CodeAnalysis.Desktop
  • Microsoft.CodeAnalysis.CSharp.Desktop
  • Microsoft.CodeAnalysis.Scripting
  • Microsoft.CodeAnalysis.Scripting.CSharp
  • System.Collections.Immutable
  • System.Reflection.Metadata

I also ran into a problem with VBCSCompiler.exe crashing during the build, which appears like it might be related to this issue. In any case, upgrading to the latest .NET 4.5.2 runtime resolved my problem. Of course, if you have Visual Studio 2015 installed you can also try building the other Roslyn.sln solution.

Your First Script

Writing a simple script in the .NET Compiler Platform is really easy. For the remainder of this post I will discuss scripting in C#. There is a nearly identical API for scripting in Visual Basic if that's your thing. The CSharpScript class has a bunch of static methods that provide a good entry point to the API. Perhaps the most straightforward of these is CSharpScript.Eval() which will evaluate C# statements and return a result. For example:

var value = CSharpScript.Eval("1 + 2");
Console.Write(value); // 3

This returns an int with a value of 3. See, easy right? If you want more control, there's also CSharpScript.Create() which returns a CSharpScript object suitable for further manipulation before evaluation and CSharpScript.Run() which evaluates the script and returns a ScriptState object with the return value and other state information useful for REPL scenarios.

Getting Variables

As you saw above, it's easy to get the return value from the script using the CSharpScript.Eval() method. But what about other variables that get created during evaluation? We can get those as well by using the ScriptState object you get back from calling CSharpScript.Run(). It contains a member called Variables (of type ScriptVariables) that enumerates ScriptVariable objects with the name, type, and value for each variable the script created. For example:

ScriptState state = CSharpScript.Run("int c = 1 + 2;");
var value = state.Variables["c"].Value;
Console.Write(value); // 3

References and Namespaces

If you want to do anything reasonably advanced, you'll probably need to include references and import namespaces for additional assemblies. Thankfully this is also really easy. There are a number of ways to do it, but the easiest is to use a ScriptOptions object which is accepted by any of the three CSharpScript static methods. The default ScriptOptions includes the System assembly and namespace and will search for additional assemblies in the current runtime directory. To modify this, start with ScriptOptions.Default and use it's fluent interface to setup additional references and namespaces (you can also create your own ScriptOptions if you don't want the default System assembly and namespace). Use ScriptOptions.AddReferences() to add references and ScriptOptions.AddNamespaces() to add namespaces (there are also several variations on these methods). ScriptOptions.AddReferences() accepts a variety of different ways of referring to assemblies, including the .NET Compiler Platform type MetadataReference if you're used to using that from the other portions of the platform. Here is an example of including System.IO support in a script:

ScriptOptions options = ScriptOptions.Default
	.AddReferences(Assembly.GetAssembly(typeof(Path)))
	.AddNamespaces("System.IO");
var value = CSharpScript.Eval(@"Path.Combine(""A"", ""B"")", options);
Console.Write(value); // A\B

Dynamic Support

Getting support for dynamic in your script can be a challenge if only because it's not obvious which assemblies need to be referenced to support it. The answer is that you need System.Core and Microsoft.CSharp. That's all that's strictly needed, but if you also want support for ExpandoObject you'll need an extra reference and namespace support for System.Dynamic. Here is an example script with dynamic support (note that there's no magic to the types I pass to Assembly.GetAssembly(); these just happen to be types I know are defined in the required assemblies). Note also that I had to use CSharpScript.Run() since you can't directly return values from a script so I had to store my value in a variable and get it from the ScriptState object as we saw earlier.

ScriptOptions options = ScriptOptions.Default
	.AddReferences(
		Assembly.GetAssembly(typeof(System.Dynamic.DynamicObject)),  // System.Code
		Assembly.GetAssembly(typeof(Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo)),  // Microsoft.CSharp
		Assembly.GetAssembly(typeof(System.Dynamic.ExpandoObject)))  // System.Dynamic
	.AddNamespaces("System.Dynamic");
ScriptState state = CSharpScript.Run(@"
	dynamic dyn = new ExpandoObject();
	dyn.Five = 5;
	var value = dyn.Five;", options);
Console.Write(state.Variables["value"].Value); // 5

Setting Globals

We've seen a number of ways of getting data out of the script, but what about getting data into the script? This is one of my favorite features of the scripting API because it's so easy to use. To set the data available to the script, you just have to pass an arbitrary object to one of the three CSharpScript static methods. The members of this object will then be available to your script as globals. For example:

// Defined elsewhere
public class Globals
{
	public int X;
	public int Y;
}

var value = CSharpScript.Eval("X + Y", new Globals { X = 1, Y = 2 });
Console.Write(value); // 3

Note that the scripting engine respects protection levels so it's not possible to directly pass an anonymous object to the script because anonymous objects are usually scoped to the method in which they appear.

Creating a Delegate

Finally you may want to compile the script, but store it for later reuse. Thankfully, there is also an easy way to create a delegate from any method in your script by calling ScriptState.CreateDelegate().

ScriptState state = CSharpScript.Run("int Times(int x) { return x * x; }");
var fn = state.CreateDelegate>("Times");
var value = fn(5);
Console.Write(value);  // 25

Conclusions

By now you've hopefully thought of some use cases for the new scripting capability. My personal favorite at the moment is using this to drive configuration files. Instead of configuring your application using XML or JSON, why not set up a scripting environment and let your users (or you) write code to configure the application. If this interests you, I should also mention ConfigR which does exactly this while abstracting away many of the details. Any post on C# scripting would also be incomplete without a mention of scriptcs which provides a REPL for you to use on the command line for executing your own script files.

There's also a lot more to the .NET Compiler Platform scripting support than what I've discussed here. It's even possible to compile your script to a syntax tree and then use the rest of the .NET Compiler Platform capabilities to explore, analyze, and manipulate the script. A good place to continue exploring is the enhanced Roslyn source view site.

comments powered by Disqus