This is part two of the series - if you haven’t read part one check it out before proceeding.
Last time we did some cool stuff with using C# script files to instantiate a working Web API server - by executing them with RCSI and C# interactive window.
In part two, let’s use Roslyn to build our own scripting application (custom C# console), which will act as an input window for the user; user will be able type C# code that’s supposed to be executed and run it (something that services such as Compilify offer). We will expose Web API assemblies in that context, allowing the user to type in the code required for the Web API server to be run.
More after the jump.
Some Roslyn scripting basics π
In order to achieve our goal, we need to get familiar with a couple of Roslyn concepts. Roslyn.Scripting.ScriptEngine class gives us a context in which we can parse and execute code submitted by the user. However, each bit/snippet of code is treated separately (as a separate compilation), so results of one statement don’t carry over to another. This is solved through using Roslyn.Scripting.Session, as it gives us a cumulative context.
There is also a very interesting notion of a host object is the object which can be used to create the session, and the properties of the host object get exposed to the session consumer. This, however, is beyond the scope of this article, as we will not be needing this feature.
The wrapper π
Let’s go ahead and build our application. Create a new console application, .NET 4.5. We should pull two packages - WebAPI self host and Roslyn.Services.CSharp - from Nuget.
First we need (well, we don’t really need, but I will use it for code cleanness) a class to act as our scripting wrapper:
public class ScriptingWrapper
{
public ScriptEngine Engine { get; private set; }
public Session Session { get; private set; }
public ScriptingWrapper()
{
Engine = new ScriptEngine();
Engine.AddReference("System");
Engine.AddReference("System.Threading.Tasks");
Engine.AddReference(@"C:Program Files (x86)Microsoft ASP.NETASP.NET MVC 4AssembliesSystem.Net.Http.dll");
Engine.AddReference(@"C:Program Files (x86)Microsoft ASP.NETASP.NET MVC 4AssembliesSystem.Net.Http.Formatting.dll");
Engine.AddReference(@"C:Program Files (x86)Microsoft ASP.NETASP.NET MVC 4AssembliesSystem.Web.Http.dll");
Engine.AddReference(@"C:Program Files (x86)Microsoft ASP.NETASP.NET MVC 4AssembliesSystem.Web.Http.SelfHost.dll");
Engine.AddReference(@"C:Program Files (x86)Microsoft ASP.NETASP.NET MVC 4PackagesNewtonsoft.Json.4.5.6libnet40Newtonsoft.Json.dll");
Engine.AddReference(@"c:UsersFilipdocumentsvisual studio 2012ProjectsRoslyn.WebApipackagesRoslyn.Compilers.Common.1.2.20906.2libnet45Roslyn.Compilers.dll");
Engine.AddReference(@"c:usersfilipdocumentsvisual studio 2012ProjectsRoslyn.WebApipackagesRoslyn.Compilers.CSharp.1.2.20906.2libnet45Roslyn.Compilers.CSharp.dll");
Engine.AddReference(this.GetType().Assembly.Location);
Session = Engine.CreateSession();
}
}
What we have done here is programmatically created a scripting environment. If you recall the previous post, we used RCSI or C# Interactive window to execute our code - this time, we do this programmatically instead, leveraging on the ScriptEngine and the Session objects we just created.
Also, in the script files in the last tutorial, we used the #r syntax to reference external assemblies; similarly, in this case, we reference the assemblies of Web API and Roslyn Compiler Services - to expose to the user of our scripting application. This allows our user to type in using statements to pull the namespaces they need into their session context.
Notice we also add a reference to the current assembly.
The Program π
The program itself is quite simple:
public class Program
{
static void Main(string[] args)
{
var script = new ScriptingWrapper();
var code = string.Empty;
while (!string.Equals(code,"q!"))
{
if (!string.IsNullOrEmpty(code))
try
{
object result = null;
result = script.Session.Execute(code);
if (result != null)
Console.WriteLine(result.ToString());
}
catch (Exception e)
{
Console.WriteLine("Damn! " + e.Message);
}
}
Console.Write("> ");
code = Console.ReadLine();
}
}
}
What happens here:
- we instantiate our ScriptingWrapper - allowing our user to have a coding session available to him
- we grab the user input - until the user types in the VI-style “q!” - the session will continue
- we execute the user input using the Session - we also cacth errors and show them if something is wrong
What we can do now is run the application (remember, to use Self Host API elevated privileges are needed) - I will use PowerShell for better screenshots, but it’s a regular Console Application. In the application we can simply type all the necessary code to run the server. Just type the regular C# code, and hit enter after every line.
Of course this currently has no controllers so it’s not very usable - but the server is up and running (we can ping it, or navigate to it in the browser) - and we didn’t compile any code, or didn’t even use the script files as we did in the last example. We simply allowed the user to type in some C# code as strings in Console window!
Adding support for external assemblies π
In order to solve the problem of the controllers, which, if you recall from part 1, have to be packaged in a separate assembly, we need to add support for parsing external files.
First let’s create a code that would package the controllers into an assembly. Let’s add a new file webapiControllersScript.csx:
using System.IO;
using System.Reflection;
using Roslyn.Compilers.CSharp;
using Roslyn.Compilers;
var tree = SyntaxTree.ParseFile(@"c:UsersFilipDocumentsVisual Studio 2012ProjectsRoslyn.WebApiRoslyn.WebApiwebapiScriptControllers.txt");
var compiledCode = Compilation.Create(
"controllers.dll",
options: new CompilationOptions(outputKind: OutputKind.DynamicallyLinkedLibrary),
syntaxTrees: new[] { tree },
references: new[] { new MetadataFileReference(typeof(object).Assembly.Location), new MetadataFileReference(@"C:Program Files (x86)Microsoft ASP.NETASP.NET MVC 4AssembliesSystem.Web.Http.dll") });
Assembly assembly;
using (var stream = new MemoryStream())
{
EmitResult compileResult = compiledCode.Emit(stream);
assembly = Assembly.Load(stream.GetBuffer());
}
This is almost the same code as we used in the previous post of this series. We don’t need any #r references since our ScriptEngine provides them already. We simply tell Roslyn - “our controllers are in this text file webapiScriptControllers.txt, create a dynamic assembly out of them”. Notice that the controllers file is exactly the same as in the previous post. Please go back to that post in case you want to have a second look.
Now, let’s add a support to execute script files, rather than just manually typed code to our scripting console application. This is super simple - the only change happens inside the try block.
try
{
object result = null;
if (code.StartsWith("file"))
script.Session.ExecuteFile(code.Substring(code.IndexOf(" ")));
else
result = script.Session.Execute(code);
if (result != null)
Console.WriteLine(result.ToString());
}
So if the user types “file” and then space and a path to the file, that script file will be executed.
Let’s test it. This time, let’s do everything exactly the same way as we did, but before starting the server, let’s execute the script which will dynamically create a controllers assembly.
If we now go the browser - magically, the controllers are working just fine.
Bonus π
Since our scripting application can now execute script files as well - there is no reason why we couldn’t exacute the big fat Web API script we created in previous post. The only caveat is that since we pull all the required references in our ScriptEngine already, we need to remove all the #r references from the script (otherwise we will get double references to the same DLLs which will cause an error).
If we execute it:
The Web API server will run just fine as well.
Summary π
Roslyn gives us developers really tremendous capabilites if we want to dynamically execute code. It’s really mind blowing that not only we can script sophisticated things like Web API servers (which we have seen in part 1 of this series), but actually very easily build an environment which takes nothing more than user input as string, and in runtime convert that into a fully fledged .NET solutions such as the self host API server demoed in this post!
Hope you enjoyed this little two part series!