Adding C# scripting to your development arsenal - Part 1
Guest Post by Filip W
Welcome to our weekly MVP series where we discuss some of the most useful solutions, tips & tricks and how to’s on integrating OSS & Microsoft technologies. In case you have any questions, don’t hesitate and drop us a comment below or reach out to us via Twitter and Facebook! Enjoy!
Through the new Roslyn compiler, scripting has also finally come to C#. While the sound of such concept - after all we are talking about scripting for a compiled, object oriented (class based) programming language - might sound counter intuitive, just bear with me for a moment, as we'll try to make sense of it in this article.
C# scripting was introduced into .NET community together with the Roslyn CTP back in October 2011. The primary idea behind C# scripting was to allow for code to be dynamically evaluated at runtime. While there have been other technologies allowing that in the past (Reflection.Emit, CodeDOM etc.), Roslyn took this concept to new heights by introducing scripting - using not the regular strict C#, but a relaxed version of C# language semantics.
As Roslyn matured, in one of the later preview releases, the scripting bits were actually pulled from Roslyn altogether, as the Microsoft Language Team opted for a redesign of the scripting APIs. As a result, scripting didn't make it into the stable Roslyn 1.0.0 release (which was also the version that shipped with Visual Studio 2015). However, since then, scripting has made a return to Roslyn and is currently available on Nuget, in 1.1.0 version of Roslyn. It is also part of Visual Studio 2015 Update 1 - the stable version of which was released yesterday (30 November 2015).
Aside from the official Roslyn packages from Microsoft, the C# community has been enjoying a community-driven way of scripting with C# via a very popular scriptcs open source project for a few years now. scriptcs was built around the initial Roslyn CTP with the goal to provide excellent, rich experience for C# scripting - further enhancing the capabilities of Roslyn. It even introduced support for cross platform - Linux, OS X - C# scripting using Mono.CSharp. This is particularly useful, because Roslyn, while it has an ultimate goal of supporting x-plat scripting, even at this day, with the latest 1.1.0, only works on Windows.
What does it all mean in practice?
Well, C# scripting allows you to write C# code you know and love, but in a way that's much friendlier for non-Visual Studio usage.
Because scripted C# is characterized by looser syntax requirements, the overall experience can be really low-ceremony. Below are a few things to remember:
- the entry point to your script is the first line of your script (no mandatory Main method)
- any code allowed from the first line (no top level mandatory Program class)
- global functions allowed
- no namespaces
- no project or solution file
- your script can be entirely self-contained
- using statements and references can be imported implicitly by the hosting application (responsible for invoking the script)
Think about it for a second - because it is a very enticing idea - being able to author C# in any text editor (no need for Visual Studio), without having to worry about the heavy structures of solution and project files, build tasks, or traditional entry point constructs like "static void Main", holds tremendous power.
To get started with C# scripting, you can either install the Roslyn scripting package from Nuget or opt to use scriptcs instead.
Getting started with Roslyn scripting
You can grab the C# scripting package using the following Nuget installation command (there is also a corresponding one for Visual Basic scripting, Microsoft.CodeAnalysis.Scripting.VisualBasic):
This will add the Roslyn scripting libraries to your project and allow you to execute C# scripts from your application./p>
The simplest thing you can do is to use the static EvaluateAsync method of CSharpScript class. It allows you to quickly compile and invoke C# scripted code. For example:
Of course your script code can be more elaborate - as mentioned earlier, you can define classes, loose methods and global variables. Moreover, your C# script can actually return a value to the hosting application if needed. The last line of your script, without a semicolon, is an instruction for Roslyn to evaluate an expression on that last line and return it. For example:
In the above snippet, the last line is a call to the Add method (notice the lack of semicolon) - this means that this statement will be evaluated and the value returned to the host application, which conveniently captures it into result variable. This is a very easy way of marshaling the data back from the script.
If you want to do the opposite - that is, pass data into your script, you can do so using an instance of a so called globals object. This concept, also known as a host object, will expose all of the public members of the host class as ambient global properties and methods, that can simply be accessed anywhere in the script. Host object can be any type you wish, but it must itself be declared as public.
In the snippet above, an instance of ScriptHost is used as host object, and therefore its property Number can be used inside the script to read the data (number 5) being passed in.
Finally, in a more advanced scenario, you can also pre-seed your script context with assembly references (this way your script will have access to types defined there) and using statements (this way your script will automatically have access to types from those namespaces without having to manually import them, of course as long as the relevant assembly is referenced too).
This is a great way to simplify your scripting experience. The following snippet adds a reference to System.Xml DLL from the GAC and imports the using statements for System.Xml in order to process an XML file (obviously that file has to exist on your machine in the first place - I just used a generic test file I had). This is controlled by the ScriptOptions object.
Notice that in the above example, both the XmlDocument and Console classes are available, because the relevant namespaces have been imported into the script context. Additionally, the XmlDocument is only resolved correctly, because a reference to its assembly (the aforementioned System.Xml DLL) was done.
Finally, there are two new extra feature worth mentioning, and these are new compiler directive, #load and #r. They are only allowed to be used in C# script code (would not work with "classic" C# syntax), and allow you to reference a script from another script ( #load) and import an assembly reference from GAC or from a path ( #r).
This is particularly useful for code sharing between files. Let's take our earlier example and extend it with a #load and #r sample:
The above snippet is similar to the previous one, but we introduced a few changes. First of all, System.Xml is no longer referenced from the host application level, via ScriptOptions. Instead, the script itself is asking for the DLL to be referenced. The end result is the same as earlier, meaning that the assembly is available to be used in the script, but the responsibility of making that decision has been shifted.
Moreover, we actually replaced the path to the XML report file with a variable reportPath - and you might be wondering where is it coming from? Well, since at the second line, we load a C# script file from c:\test\setup.csx, that's a good bet to look. And indeed, whatever you declare in the #loaded script (variables, methods, classes) - all of that will be available in the other script. In our case, the setup.csx happened to be a one line CSX file:
By the way, CSX is the C# script file extension convention.
In case you are wondering how all of this works under the hood, Roslyn will create a so called submission from your script code. A submission is an in memory assembly containing the types generated around your script code, which can be identified among the assemblies in the current AppDomain by a ℛ prefix in the name.
The precise implementation details are not important here (though, for example, scriptcs heavily relies on understanding in detail how Roslyn works to provide its extra features), but it's important to know that submissions can be chained together. When they are chained, variables, methods or classes defined in an earlier submission are available to use in subsequent submissions, creating a feature of a C# REPL (read-evaluate-print loop). The following example can illustrate how a super simple C# REPL could be built in a few lines of C# code:
In this very basic example, we enter into a forever loop, and create a null ScriptState variable. We then wait for user input. To initialize a C# REPL, we call CSharpScript.RunAsync and pass in user's input, which results in the user code being invoked and the script state being populated from this first submission. On subsequent runs, we call ContinueWithAsync method on the ScriptState itself, which will effectively result in new submissions being chained after the original one.
Here's a sample output from the above snippet:
* using System; * var msg = "Hello"; * Console.WriteLine(msg); Hello *
Now, this is all excellent and very exciting - but the biggest productivity gains from C# scripting probably do not involve calling C# script code from within your application.
Instead, what is likely of most value for us developers, is the ability to write that scripted C# as standalone CSX files, and rely on a stable, established script runner to run them, just like it's the case with all scripting languages. Sure, you could write such a runner yourself, but that would be an unnecessary overkill.
What you can do, is one of two things:
- install scriptcs, which has been the go-to community driven script runner for quite a while now (scriptcs installation instructions)
- install Visual Studio 2015 Update 1, which ships with CSI, a command line script runner which can be accessed from Visual Studio Developer Prompt (it's also located under C:\Program Files (x86)\MSBuild\14.0\Bin if you need to access the EXE directly)
They are both very powerful, and aside from being able to execute scripts, they also ship with built-in REPLs.
We'll look into using both in the next post of this series!