Running a C# REPL in a DNX application with scriptcs

Β· 1334 words Β· 7 minutes to read

One of the cool things that scriptcs allows you to do, is that you can embed it into your application and allow execution of C# scripts. There are even some great resources on that out there, like this awesome post by Mads.

The same applies to the REPL functionality - you don’t have to use scriptcs.exe to access the REPL - you can use the scriptcs Nuget packages to create a REPL inside your app.

And because there aren’t that many resources (if any) on how to host a scriptcs REPL, today I wanted to show you just that. But for a more interesting twist, we’ll do that inside a DNX application.

There are many reasons why DNX is awesome, and why you’d want to use it, but especially because, through the project.json project system, it has a much improved way of referencing and loading dependencies and Nuget packages - and we can leverage that mechanism to feed assemblies into our REPL.

Getting started πŸ”—

At the moment neither Roslyn nor scriptcs supports CoreCLR, so we’ll need to skip that (although things will change in the future). However, our solution will still be cross-platform, because scriptcs works on Mono through it’s Mono.Csharp execution engine.

To begin with, we’ll need to reference the necessary packages, so we’ll throw this into the package.json.

"dependencies": {  
"Microsoft.Dnx.Runtime.Abstractions": "1.0.0-*",  
"ScriptCs.Hosting": "0.15.0",  
"ScriptCs.Engine.Roslyn": "0.15.0",  
"ScriptCs.Engine.Mono": "0.15.0"  
},  

The key package to host scriptcs is ScriptCs.Hosting which will give you most of the stuff that’s needed to build up scriptcs execution pipeline - except for the actual engine to execute the script code. That last piece, the engine, is packaged in ScriptCs.Engine.Roslyn and ScriptCs.Engine.Mono packages - with implementations for Windows and *nix respectively. We’ll use both since we want an x-plat solution here.

Finally, we’ll also use DNX’s ILibraryManager to be able to find out what assemblies should be fed into the REPL session. That interface is part of Microsoft.Dnx.Runtime.Abstractions package.

Building the REPL πŸ”—

Now let’s create an entry point for our DNX application - a Program class. In there, we’ll need to take care of a number of things:

  • inject the ILibraryManager so that we can use it to discover the assemblies in current application context
  • determine whether we are in Mono runtime
  • configure the scriptcs REPL

This code is shown below:

{  
private static readonly bool IsMono = Type.GetType("Mono.Runtime") != null;  
private readonly ILibraryManager _libraryManager;

public Program(ILibraryManager libraryManager)  
{  
_libraryManager = libraryManager;  
}

private static readonly IConsole Console = new ScriptConsole();

public void Main(string[] args)  
{  
var scriptServicesBuilder =  
new ScriptServicesBuilder(Console, new DefaultLogProvider()).Cache(false).Repl(true);

scriptServicesBuilder = IsMono ? scriptServicesBuilder.ScriptEngine<MonoScriptEngine>() : scriptServicesBuilder.ScriptEngine<RoslynScriptEngine>();

var assemblyReslover = new AspNet5AssemblyResolver(_libraryManager);  
((ScriptServicesBuilder)scriptServicesBuilder).Overrides[typeof (IAssemblyResolver)] = assemblyReslover;

var scriptcs = scriptServicesBuilder.Build();

scriptcs.Repl.Initialize(Enumerable.Empty<string>(), Enumerable.Empty<IScriptPack>());  
scriptcs.Repl.AddReferences(assemblyReslover.GetAssemblyPaths(string.Empty).ToArray());

//TODO: now run the REPL!  
}  
}  

A few notes about the snippet above. First of all, we can recognize if we are on Mono by looking for a type called Mono.Runtime. As far as console interaction goes, scriptcs actually uses an abstraction called IConsole - with the default implementation being ScriptConsole, a simple wrapper around System.Console, which is enough for us here.

scriptcs pipeline is configured through a builder called ScriptServicesBuilder. You define there whether you want caching (effectively means saving the executed script code into a DLL for faster re-execution in the future) - which we don’t need, and whether you want REPL, which obviously we do.

Next step is to set a relevant scriptcs engine on the ScriptServicesBuilder - we can use our IsMono flag to toggle between a Roslyn engine (for Windows) and Mono engine (for *nix). Another important thing to do here is to replace the default scriptcs IAssemblyResolver. Out of the box, scriptcs will use the classic Nuget convention - with packages.config file and a packages folder, to discover what assemblies should be available in the context of the REPL. In our case though, we want to use the assemblies discovered already by the DNX runtime to be available in the REPL, so we provide a custom implementation of IAssemblyResolver - AspNet5AssemblyResolver.

The code is quite simple:

public class AspNet5AssemblyResolver : IAssemblyResolver  
{  
private readonly ILibraryManager _libraryManager;

public AspNet5AssemblyResolver(ILibraryManager libraryManager)  
{  
_libraryManager = libraryManager;  
}

public IEnumerable<string> GetAssemblyPaths(string path, bool binariesOnly = false)  
{  
var assemblies = _libraryManager.GetLibraries().SelectMany(x => x.Assemblies).Select(x =>  
{  
try  
{  
return Assembly.Load(x);  
}  
catch (Exception)  
{  
}

return null;  
});

return assemblies.Where(x => !string.IsNullOrEmpty(x?.Location)).Select(x => x.Location);  
}  
}  

AspNet5AssemblyResolver will use ILibraryManager, and find all of the assemblies that are available through it and attempt to load them. It’s a bit brute force approach so we need to try/catch it, but it’s sufficient for demo purposes. For real life scenarios you may want a more elegant way of doing this, i.e. taking the approach of ASP.NET MVC which uses GetReferencingLibraries() method on the ILibraryManager to only pick up libraries that reference MVC core.

Anyway, another thing worth noting here is that we must filter out in memory assemblies here too. It’s a bit of a shame but with the current version of Roslyn/scriptcs creating MetadataReference, which is what is needed to be fed into the REPL, from an in memory assembly, is not possible (or not possible without hacks). This too is going to change one day though, as this feature is coming to Roslyn.

All right, so once we have the AspNet5AssemblyResolver, we can set it in the scriptcs pipeline using the Overrides dictionary - which is simply a scriptcs services graph into which you can inject your own implementations.

At that point we can call Build() and we get an instance of a ScriptServices object. It exposes everything you’ll need for script execution or running the REPL - and indeed, the Repl is a property on that object. A scriptcs REPL needs to be initialized - that’s done with a list of assembly paths and a list of script packs (scriptcs extensions, which we’ll leave out of this post).

scriptcs.Repl.Initialize(assemblyReslover.GetAssemblyPaths(string.Empty), Enumerable.Empty<IScriptPack>());  

The list of assemblies to initialize the REPL can be easily grabbed from our assembly resolver which is exactly what we do. After that, we have everything we need, so we can proceed to executing the code.

As you’d expect from a REPL - it’s an infinite loop (while(true)), that’s waiting for your input and exits only on unhandled exception or on an exit command. At this point, it might be worth mentioning that scriptcs provides a bunch of built-in REPL commands, all of which are available out of the box in our REPL (you can opt out from them if you wish).

The code to run the REPL is very basic and shown below:

public void Main(string[] args)  
{  
//earlier code in the method omitted for brevity

scriptcs.Repl.Initialize(assemblyReslover.GetAssemblyPaths(string.Empty), Enumerable.Empty<IScriptPack>());

try  
{  
while (ExecuteLine(scriptcs.Repl))  
{  
}

Console.WriteLine();  
}  
catch (Exception ex)  
{  
var oldColor = Console.ForegroundColor;  
Console.ForegroundColor = ConsoleColor.Red;  
Console.WriteLine(ex.Message);  
Console.ForegroundColor = oldColor;  
}

}

private static bool ExecuteLine(IRepl repl)  
{  
Console.Write(string.IsNullOrWhiteSpace(repl.Buffer) ? "> " : "* ");

try  
{  
var line = Console.ReadLine();

if (!string.IsNullOrWhiteSpace(line))  
{  
repl.Execute(line);  
}

return true;  
}  
catch  
{  
return false;  
}  
}  

So we always print a “>” caret and use Console.ReadLine method, to invite and read the user input. Then, we execute the input by calling repl.Execute. scriptcs REPL supports multi-line C# - this is done by checking the Buffer property - if it’s not empty, it means the code has not executed yet, but it’s being buffered by the REPL. In that case we can display a different caret ("*").

And that’s it.

Running the REPL πŸ”—

If we now use the standard dnx run command, you’ll see we get dropped into the REPL. What’s even more interesting is that all of the packages (assemblies) referenced via project.json are avilable in the REPL too. You can simply import the needed using statements (in fact, you can even pre-seed the REPL with them) and use any of the types from any of those packages.

Here’s a screenshot from Windows:

Screen Shot 2015-09-15 at 22.37.10

And here’s a screenshot from OS X:

Screen Shot 2015-09-15 at 22.57.50

You can find the source code on Github.

About


Hi! I'm Filip W., a software architect from ZΓΌrich πŸ‡¨πŸ‡­. I like Toronto Maple Leafs πŸ‡¨πŸ‡¦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁒󠁳󠁣󠁴󠁿.

You can find me on Github, on Mastodon and on Bluesky.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP