Implementing custom #load behavior in Roslyn scripting

#load directives in C# scripts are intended to allow you to reference a C# script source file from another C# script. As an author of a host application, in which the Roslyn scripting would be embedded, it’s up to you to define how #load should behave.

Let’s have a look at the process of doing that.

Background

C# scripting, since its early beginnings (been trying to find the old white paper by Bill Chiles where it was mentioned, but I seem to have displaced it), contained the concept of #load preprocessor directives.

In the early Roslyn previews (2011, 2012 and so on), Roslyn itself didn’t support for it though, instead in the scriptcs project, we implemented our own version of this, where we’d recurse through reference files, resolve their references and compile them into one compilation unit so that your script can be run successfully.

This changed with the stable release of Roslyn and for a while now, Roslyn has had built-in support for #load, which you can actually customize in terms of its behavior. This is really important feature, because while scriptcs traditionally allowed you to define and implement custom preprocesssor directives (i.e. #gist, #sql and so on), Roslyn wants you to use #load to cover all of these cases.

Source resolvers

In order to introduce your custom #load logic, you will have to implement an abstract class SourceReferenceResolver. It defines the necessary methods Roslyn will call when trying to resolve the #load directives it encounters in script code.

The class is shown below (mainly just for reference):

Now, for most use cases you’d only need to deal with SourceFileResolver, which is the default implementation, and simply allows you to look for the #loaded files in local files system.

Consider the following code:

Now, the simplest possible program to execute these scripts (and look at diagnostics) is as follows:

If you try to execute these scripts with a Roslyn scripting engine, you will get the following error:

When you set up the execution of your scripts, and you do not specify any resolver to be used, Roslyn will fallback to ScriptFileResolver – but the caveat is that by default the search paths are empty, and the base directory is set to null, so it you won’t be able to reference any files anyway.

So in order to run your code, you have to modify the ScriptOptions accordingly:

In this case, we are in a .NET Core application, so we use AppContext.BaseDirectory to indicate about the current base directory. In a traditional .NET app, you’d probably look at AppDomain.CurrentDomain.BaseDirectory or wherever your #loaded files are located.

Now, this is fine, and if all you need is tweak how Roslyn is looking at the file system, instead of implementing the base abstract class directly, you can also choose to extend SourceFileResolver which might save you some keystrokes.

Creating a custom remote resolver

Let’s imagine the following use case – we’d like to simultaneously support:
#load from a local file, relative to my host’s (application running the script) application directory
#load from a remote file, over HTTP

At the moment Roslyn allows you to configure only one source resolver at a time, meaning if you want to be able to load from multiple source at once (say remote over HTTP and local file system), your resolver would have to do it all.

To do that, we’d have to implement a “hybrid”/”composite” resolver which will cater for both scenarios. Because of that, the extensibility point at the moment is not the most convenient. Another issue with it, is that it does assume all of the operations are synchronous, which is going to be a little constraining to us with network requests.

Nevertheless, the implementation of our custom RemoteFileResolver is shown below. The idea is to verify the type of path Roslyn will be requesting from us – and that is based on what the scripting engine finds in the #load directive – and yield processing the built in SourceFileResolver or use the logic that will be able to fetch the remote file.

The code is a bit long, but that’s mainly due to the fact that we have to implement GetHashCode and Equals.

So in short, if we can determine a valid http or https based URL, we will try to fetch from it remotely using HttpClient. Then we can store the result for later use in a local dictionary, so that later on, when Roslyn asks us for the Stream representing a given “file” (since our file is not physically a local file anymore, but an in memory representation of a remote file) we can just expose that saved string as a Stream.

This works reasonably well, so we can now update our script to reference some remote code. I have created a gist with some silly code here. It will simply spit out a hello from a remote file.

Now, our script can look like this:

We should now plug in our custom resolver, everything else stays the same:

And if I execute our program, we get the expected output:

Screenshot 2016-06-17 09.35.21

Please note that all this sample code is built on .NET Core and compiled as netcoreapp1.0. You can find the full source code at Github.

Be Sociable, Share!