You’ve likely heard of Roslyn in Dotnet circles, but have you had a chance to learn what you can do with it? Let’s take a tour of some of the things you might be missing out on.
A compiler as an API
At the heart of it all, Rosyln is a compiler of C#/VB meant to be run and executed in your runtime for various uses:
Code Generation - Build a class inside your app programmatically, compile it while it’s running — then execute it (keep reading for an example below).
Static Analyzers - Write code that inspects other code.
Code Fixes - Write code that fixes other code in the repo to conform to standards.
Roslyn is available in both .NET FW and modern Dotnet via Nuget
Rosyln is the continuation of sorts of an older code generation library from .NET FW (System.CodeDom.Compiler
). With that library you were able to compile C# from a string but it was less capable and now likely not what you wanna use these days.
Rosyln supports modern C# versions where the latter does not. If you’re using modern Dotnet, please use Roslyn.
Seeing is Believing
You may be wondering “why on Earth would I need my app to write code in real-time instead of just writing the code?”. The reason I even learned to use Roslyn was because it turns out, C# has constraints about things. Specifically in my case I needed to dynamically interpolate a string where the template was a piece of dynamically loaded text (e.g. loaded from a file or DB). C# requires the template to be a compile-time constant but I needed it to be variable as I might need to load let’s say an email template at run-time, then interpolate the values into it. Let’s go thru some pseudo-code that won’t work:
//works fine
var bar = 123;
Console.WriteLine($"Let's interpolate...{bar}"); //Let's interpolate...123
Console.WriteLine($"{getExternally()}"); //"Let's interpolate...{bar}", doesn't do the interpolation
//what about REGEX? nah
//what about string.Format?
Console.WriteLine(string.Format(getExternally(), 123)); //won't work as these are positional and only works if a compile-time const
string getExternally()
{
//data we get from external can't be interpolated, it has to be a compile-time const
return "Let's interpolate...{bar}";
}
To reiterate, the problem is that you cannot interpolate a string in C# without a compile-time constant template. This is where we can introduce a Rosyln code-generator to create a compiled template on-the-fly.
Let’s compile code in real-time to overcome this
Before we get into the code, let’s inventory the things we’ll need to do to pull off this magic trick:
Get a string from an external source, this can be any or more of the following:
A database
A text file
A cloud-hosted file (e.g. S3, Azure Blob)
An embedded resources (in the DLL)
Web call
Really from any trusted source
Use C# to write the code for our class to compile (add any ref’s and compiler settings you’d like)
Use Rosyln to compile the code
Execute the code we just compiled within your app via reflection
In the example repo, we use a static string, but you can trade this out for a use-case from the list above. So let’s get moving, we need to setup a class that will write, emit and execute the code. The first thing we need to do is to make sure Rosyln namespaces/DLL’s are available to your app. Make sure you add this Nuget to your project if needed.
Create and compile the code
What we’re really doing in this example is creating an entire assembly, adding references, the whole bit — all in memory. When we’re done, we’ll be able to execute a method that we wrote. The method that we’ll be driving towards is called InterpolateWithModel
and it takes any instance class. The model I chose is just a token one called MyModel
:
//given a template from an external source like this
//"simulated template string with named placeholders name => {model.Name} age => {model.Age}"
//we will do real interpolation, not some simple string replace
var result = codeGenerator.InterpolateWithModel("someTemplateName", new MyModel
{
Name = "Fred",
Age = 35
}); //simulated template string with named placeholders name => Fred age => 35
In my example I’m essentially loading the string template from a simulated external source. I then take that and build a class with a namespace. I add one method that takes one arg that is the same type as the given model:
var codeBuilder = new StringBuilder();
...
codeBuilder.AppendLine($"namespace {namespaceName}");
codeBuilder.AppendLine("{");
codeBuilder.AppendLine($" public class {className}");
codeBuilder.AppendLine(" {");
codeBuilder.AppendLine($" public string {MethodName}({modelType.FullName.Replace("+", ".")} model)");
codeBuilder.AppendLine(" {");
codeBuilder.AppendLine($" return $\"{fileContents}\";");
codeBuilder.AppendLine(" }");
codeBuilder.AppendLine(" }");
codeBuilder.AppendLine("}");
When the example code runs, you’ll get a class that looks like this, but only as a string at this point:
namespace MyStringTemplate_someTemplateName
{
public class MyStringTemplate_someTemplateNameTemplateGenerator
{
public string Interpolate(DotnetDev.Roslyn.MyModel model)
{
return $"simulated template string with named placeholders name => {model.Name} age => {model.Age}";
}
}
}
Notice that the fields use
model.<propertyName>
as I had to give the input arg a name.
Now that we’ve written the code, we need to compile it. To do so, we need a reference to the Roslyn CSharpCompilation
object. We will be able to pass in namespaces, references and other options:
var parsedSyntaxTree = getSyntaxTree(codeBuilder.ToString(), "", CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp13));
var compilation = CSharpCompilation.Create($"{templateName}Template", [parsedSyntaxTree], DefaultReferences, DefaultCompilationOptions);
There’s many options, one of which is the version of C#. You will likely encounter build errors while building this out. Rosyln has the idea of “SyntaxTree’s” which you can read more about here. In order to invoke the compiler we’ll need to do the following:
using (var stream = new MemoryStream())
{
//attempt to generate the byte code
var emitResult = compilation.Emit(stream);
//evaluate the compilation result
if (emitResult.Success)
{
stream.Seek(0, SeekOrigin.Begin);
//read the stream into memory
var assembly = Assembly.Load(stream.ToArray());
return assembly;
}
throw new Exception("Compilation failed");
}
The code above compiles and upon success; loads it into the app domain for use. If it fails, we’ll get an exception.
Ready, aim, fire!
The last thing we need to do is execute our code. It can be executed like any other code loaded into memory. For my purposes, I used Reflection to find and invoke it:
var className = generateClassNameFromNamespace(namespaceName);
var fullyQualifiedName = $"{namespaceName}.{className}";
var instance = assembly.CreateInstance(fullyQualifiedName);
if (instance == null)
{
throw new Exception($"Cannot create an instance of the object => {fullyQualifiedName}");
}
var type = instance.GetType();
var method = type.GetMethod(MethodName);
if (method == null)
{
throw new Exception($"Cannot find method {MethodName}");
}
return method.Invoke(instance, args).ToString();
The output of this class ends up being simulated template string with named placeholders name => Fred age => 35
and is done so with native C# interpolation, not a string replace or REGEX.
But wait, you are creating a security hole!
You’ll want to make sure any input into Rosyln is from a trusted source. Obviously if you take any input from the user to generate code, you risk a big security nightmare. I’d only recommend taking input from static files you control or other trusted sources (a database you control).
Performance?
Creating classes in real-time isn’t new for Dotnet, Roslyn is just an updated and more robust way to do so. You’ll need to cache the assembly so that you’re not compiling templates over and over when they won’t change. Since this uses native C# string interpolation, I’ve noticed it takes mere microseconds to render even moderately sized templates.
Full Example
I’ve got a full working example located here. In the example you’ll see some quality of life enhancements like being able to pass in some namespaces as needed. Roslyn is a power compiler as an API, the possibilities are quite limitless. I hope you enjoyed this, happy coding!