Tallan Blog

Tallan’s Experts Share Their Knowledge on Technology, Trends and Solutions to Business Challenges

Compiling and Running a DLL in .NET Core 2 Using Roslyn and Reflection

During a recent client engagement, my team and I were given an unusual task: give the user the ability to write, compile, and run Visual Basic .NET code in a web app environment. This presented us with a great learning opportunity since no one on the team had experienced anything like this before. Our first choice was to use the System.CodeDom namespace to compile the source code and generate a dll to be run on-demand in other components of our web app. However, we quickly discovered that while CodeDom is available for .NET Core 2 (after installing it via NuGet), calls to the CompileAssembly methods would throw a System.PlatformNotSupportedException. We knew that we would need to change our approach. This led us on a path to Roslyn. Roslyn is a .NET compiler framework written in .NET. It contains code analysis tools that we used to compile a dll from user code.

Compile

The first step in the compilation process is creating a syntax tree object out of the source code string passed from the front end. In essence, the syntax tree object describes the code using a set of objects that represent various parts of the program. In this case, using the Microsoft.CodeAnalysis namespace, the VisualBasic.VisualBasicSyntaxTree object generated here will contain instances of the following classes:

  • SyntaxNode, which represent declarations, statements, clauses, and expressions.
  • SyntaxToken, which represent tokens, such as keywords or operators.
  • SyntaxTrivia, which represent trivia in the code, or everything that is unnecessary for the code to compile, such as whitespace and comments.

These are known as full-fidelity trees, meaning that the full code is maintained in the tree. Running a .toString() on the tree will yield the exact same source code as the input.

var syntaxTree = VisualBasicSyntaxTree.ParseText(sourceCodeModel.SourceCode);

List references = new List();
foreach (var ref in ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")).Split(Path.PathSeparator))
     {
          references.Add(MetadataReference.CreateFromFile(ref));
     }

The next step in preparing the compilation object is gathering the necessary references. In this case, we chose to include all trusted platform assemblies defined by the .Net Core runtime. Typically, if you know the contents of the code you are compiling, you would only add references to the libraries you know are necessary. Here, however, we are casting a wide net and adding all runtime assemblies from trusted locations, since we don’t know what the user will need.

var compilation = VisualBasicCompilation.Create($"{sourceCodeModel.FileName}.dll",
     syntaxTrees: new[] { syntaxTree },
     references: references,
     options: new VisualBasicCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

With the reference list populated and the syntax tree instantiated, it is now time to create the Visual Basic Compilation object. Other than the references and syntax tree, we will pass an assembly name and a set of compilation options into the method to create a VisualBasicCompilation instance. Note that we want the output to be in the form of a dll.

List message = new List();
using (var ms = new MemoryStream())
{
     EmitResult result = compilation.Emit(ms);

Using a memory stream, we emit the results of the compilation to an EmitResults object. This object contains a flag indicating the success of the compilation, as well as an object with diagnostics information. This diagnostics object contains a list of all the diagnostics associated with compilations. This include parse errors, declaration errors, compilation errors, and emitting errors.

if (!result.Success)
  {
     IEnumerable failures = result.Diagnostics.Where(diagnostic =>
          diagnostic.IsWarningAsError ||
          diagnostic.Severity == DiagnosticSeverity.Error);

     foreach (Diagnostic diagnostic in failures.OrderBy(o=>o.Location.GetLineSpan().StartLinePosition.Line))
     {
          message.Add($"({diagnostic.Location.GetLineSpan().StartLinePosition.Line}) {diagnostic.Id}: {diagnostic.GetMessage()}");

     }
}

For our purposes, if the compilation fails, we construct a string array of all warnings and errors written to the diagnostics object. Each string will contain the line number of the issue, the id of the error, and the error message. We will then return our wrapper object, with the messages ordered by line number.

else
  {
     if (saveDll)
     {
          ms.Seek(0, SeekOrigin.Begin);
          byte[] byteAssembly = ms.ToArray();

          assemblyMetadataId = _assemblyRepo.SaveDll(byteAssembly, sourceCodeModel);
          message.Add("Save successful");
     }
     else
     {
          message.Add("Compilation successful");
     }
}

If the compilation is successful, we want to save the dll to the database. Our database table contains a varbinary(max) column to store the assembly data. We use the memory stream object that the compilation results were emitted to create a byte array with the .ToArray() function. This object is then sent to the repository layer of our application to be written to the database.

Load and Run the Assembly

We now have a dll saved to our database containing code written by a user, which we know was successfully compiled. The next step is pulling that compiled dll out of the database, and using reflection to execute it.
What is reflection? Reflection gives the ability to inspect assembly metadata at runtime. In .NET, you will use the System.Reflection library to create an assembly object, which is a reusable, versionable, and self-describing building block of an application. You can discover an assembly’s type, and create an instance of it. From there, we can access the methods associated with that type, and select one as an entry point to invoke. Once we have a reference to the type and an entry point method of the assembly, we can use that information to execute that method.

public object RunDll(RunCodeRequest request)
{
     byte[] byteAssembly = _assemblyRepo.GetDllById(request.AssemblyMetadataId);
     Assembly assembly = Assembly.Load(byteAssembly);
     Type exampleClassType = assembly.GetType("ExampleClass");
     var instance = Activator.CreateInstance(exampleClassType);
     MethodInfo EntryPoint = exampleClassType.GetMethod("EntryPoint");
     var result = EntryPoint.Invoke(instance, null);
     return result;
}

The first step is to pull the assembly data from the database through the application’s repository layer. Using this data, which is returned as a byte array, we can instantiate an assembly object which will be used to execute the code. Before we compiled the user code, we wrapped it in a class called ExampleClass, and a method called EntryPoint, so we know the name of the class and method we are looking for within the assembly. Now that we have a representation of the assembly, and we know its type, we can use the Activator class from System.Runtime, to create an instance of type ExampleClass.
Using System.Reflection.MethodInfo, we can create a reference to the method “EntryPoint” within the class, and invoke that method with the Invoke() function. This is where you would add in any required parameters, but in our example case, there are none. After invoking the method, we have access to its return value, which we can pass along to the rest of our application as needed.

Although this example only scratched the surface of what is possible with the Roslyn compiler and reflection in C#, you can see that they provide powerful tools to compile and reference external assemblies at runtime.


Learn more about Tallan or see us in person at one of our many Events!

Share this post:

No comments

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

\\\