The article explains in detail how to dynamically compile and assemble various code snippets into a dynamic assembly. It also describes loading and using the dynamic assembly within the application.
Introduction
MS Roslyn is a great tool, though pretty new and not well documented with few examples available on the internet. It is to fill this documentation/sample gap that I decided to write this article.
Over the past year, I worked on several projects that involved dynamic code generation, compilation and creating dynamic assemblies using MS Roslyn compiler as a service platform. To create a dynamic assembly, I would compile each of the individual components separately into Net Modules and then combine them into the assembly (in-memory DLL). This approach was adopted in order to avoid the costly recompilation of components that have not been modified.
Having recently spent several hours trying to answer a github question on why loading a module into a dynamic DLL produces an error github question on why trying to load a module produces an error, I decided to publish the solution on the CodeProject so that others did not have to go through the same pain in the future.
The Code
The RoslynAssembly
solution was created as a simple "Console App" project using VS 2017. Then I added a single NuGet package Microsoft.CodeAnalysis.CSharp
to it. Since there is a dependency on a NuGet package - it will show that it misses some references in your Visual Studio, but once you compile it (provided you have a working internet connection), the NuGet packages should be downloaded and installed and all the references should be filled.
The code consists of only one class Program
within single file Program.cs.
Here is the code for the sample:
public static class Program
{
public static void Main()
{
try
{
var classAString =
@"public class A
{
public static string Print()
{
return ""Hello "";
}
}";
var classBString =
@"public class B : A
{
public static string Print()
{
return ""World!"";
}
}";
var mainProgramString =
@"public class Program
{
public static void Main()
{
System.Console.Write(A.Print());
System.Console.WriteLine(B.Print());
}
}";
#region class A compilation into A.netmodule
var compilationA =
CreateCompilationWithMscorlib
(
"A",
classAString,
compilerOptions: new CSharpCompilationOptions(OutputKind.NetModule)
);
byte[] compilationAResult = compilationA.EmitToArray();
MetadataReference referenceA =
ModuleMetadata
.CreateFromImage(compilationAResult)
.GetReference(display: "A.netmodule");
#endregion class A compilation into A.netmodule
#region class B compilation into B.netmodule
var compilationB =
CreateCompilationWithMscorlib
(
"B",
classBString,
compilerOptions: new CSharpCompilationOptions(OutputKind.NetModule),
references: new[] { referenceA }
);
byte[] compilationBResult = compilationB.EmitToArray();
MetadataReference referenceB =
ModuleMetadata
.CreateFromImage(compilationBResult)
.GetReference(display: "B.netmodule");
#endregion class B compilation into B.netmodule
#region main program compilation into the assembly
var mainCompilation =
CreateCompilationWithMscorlib
(
"program",
mainProgramString,
compilerOptions: new CSharpCompilationOptions
(OutputKind.ConsoleApplication),
references: new[] { referenceA, referenceB }
);
byte[] result = mainCompilation.EmitToArray();
Assembly assembly = Assembly.Load(result);
#endregion main program compilation into the assembly
assembly.LoadModule("A.netmodule", compilationAResult);
assembly.LoadModule("B.netmodule", compilationBResult);
#region Test the program
Type programType = assembly.GetType("Program");
MethodInfo method = programType.GetMethod("Main");
method.Invoke(null, null);
#endregion Test the program
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
private static CSharpCompilation CreateCompilationWithMscorlib
(
string assemblyOrModuleName,
string code,
CSharpCompilationOptions compilerOptions = null,
IEnumerable<MetadataReference> references = null)
{
SyntaxTree syntaxTree = SyntaxFactory.ParseSyntaxTree(code, null, "");
MetadataReference mscoreLibReference =
AssemblyMetadata
.CreateFromFile(typeof(string).Assembly.Location)
.GetReference();
IEnumerable<MetadataReference> allReferences =
new MetadataReference[] { mscoreLibReference };
if (references != null)
{
allReferences = allReferences.Concat(references);
}
CSharpCompilation compilation = CSharpCompilation.Create
(
assemblyOrModuleName,
new[] { syntaxTree },
options: compilerOptions,
references: allReferences
);
return compilation;
}
private static byte[] EmitToArray
(
this Compilation compilation
)
{
using (var stream = new MemoryStream())
{
var emitResult = compilation.Emit(stream);
if (!emitResult.Success)
{
Diagnostic firstError =
emitResult
.Diagnostics
.FirstOrDefault
(
diagnostic =>
diagnostic.Severity == DiagnosticSeverity.Error
);
throw new Exception(firstError?.GetMessage());
}
return stream.ToArray();
}
}
}
Code Description
Main Program Explained
The code demonstrates how to compile and assemble three classes A
, B
and Program
. A
and B
are compiled into net modules. Class Program
is compiled into the runnable assembly. We load the A.netmodule
and B.netmodule
into the main assembly and then test it by running a static
method Program.Main()
which calls static
methods both classes A
and B
.
Here is the code for A
class:
var classAString =
@"public class A
{
public static string Print()
{
return ""Hello "";
}
}";
Its static
method A.Print()
prints "Hello
" string
.
Here is B
class code:
var classBString =
@"public class B : A
{
public static string Print()
{
return ""World!"";
}
}";
Its static
method B.Print()
prints string
"World!
" Note that in order to spice it up, I made class B
extend class A
. This will require passing a reference to A
during B
compilation (as shown below).
Here is the main Program
class:
var mainProgramString =
@"public class Program
{
public static void Main()
{
System.Console.Write(A.Print());
System.Console.WriteLine(B.Print());
}
}";
Here is how we create a A.netmodule
and a reference to it.
-
Create Roslyn Compilation
object for A.netmodule
:
var compilationA =
CreateCompilationWithMscorlib
(
"A",
classAString,
compilerOptions: new CSharpCompilationOptions(OutputKind.NetModule)
);
Method CreateCompilationWithMscorlib
is a utility method that creates the compilation and it will be discussed below. -
Emit the compilation to a byte array:
byte[] compilationAResult = compilationA.EmitToArray();
This array is a binary representation of the module code. EmitToArray
is another utility function that will be discussed below in detail. -
Create a reference to A.netmodule
to be used for creating B.netmodule
(since class B
depends on class A
) and also for creating the main program code (since it also depends on A
).
MetadataReference referenceA =
ModuleMetadata
.CreateFromImage(compilationAResult)
.GetReference(display: "A.netmodule");
Very Important Note: Every time the compilation results are emitted (in our case, it happens within EmitToArray()
method), the resulting byte code changes slightly, probably because of a time stamp. Because of that, it is important to have the reference and Module code produced from the same Emit result. Otherwise, if you have different Emit(...)
s for the module code and for the reference, trying to load the module into the assembly will result in a hash mismatch exception, since the hash or the reference used to build the assembly and the hash of the module code will be different. This is what took me several hours to figure out and this is the primary reason I am writing this article.
Creating the B.netmodule
and its reference is almost the same as that of A.netmodule
, with the exception that we need to pass a reference to A.netmodule
to the method CreateCompilationWithMscorlib(...)
(since class B
depends on class A
).
Here is how we create the main assembly:
-
Create the Roslyn Compilation
object for the main assembly:
var mainCompilation =
CreateCompilationWithMscorlib
(
"program",
mainProgramString,
compilerOptions: new CSharpCompilationOptions(OutputKind.ConsoleApplication),
references: new[] { referenceA, referenceB }
);
note that we pass <code>OutputKind.ConsoleApplication</code> option since it is an
assembly and not a net module.
-
Emit the compilation result into a byte array:
byte[] result = mainCompilation.EmitToArray();
-
Load the assembly into the domain:
Assembly assembly = Assembly.Load(result));
-
Load the two modules into the assembly:
assembly.LoadModule("A.netmodule", compilationAResult);
assembly.LoadModule("B.netmodule", compilationBResult);
Note: It is at this stage that an exception will be thrown if the hashes of the reference and module code mismatch.
Finally, here is the code that tests the functionality of the assembly with the Net Modules:
-
Get the C# type Program
from the assembly:
Type programType = assembly.GetType("Program");
-
Get the MethodInfo
for the static
method Program.Main()
from the type:
MethodInfo method = programType.GetMethod("Main");
-
Invoke static
method Program.Main()
:
method.Invoke(null, null);
The outcome of this program should be "Hello World!
" printed on the console.
Utility Methods
There are two simple static
utility methods:
CreateCompilationWithMscorelib(...)
- creates roslyn Compilation
EmitToArray(...)
- emits a byte array representing the .NET code of the compilation.
CreateCompilationWithMscorelib(...) Method
The purpose of the method is to create a Roslyn Compilation
object adding to it the reference to mscore
library containing the basic .NET functionality. On top of that, it can also add references to the modules passed as its last argument 'references' (if needed).
The method takes the following arguments:
string assemblyOrModuleName
- name of the resulting assembly or the module string code
- a string
containing the code to compile CSharpCompilationOptions compilerOptions
- should contain new CSharpCompilationOptions(OutputKind.NetModule)
for modules or new CSharpCompilationOptions(OutputKind.ConsoleApplication)
for the applications IEnumerable<MetadataReference> references
- the extra references to be added after the reference to mscore
library
First, it parses the code into the syntax tree (Roslyn syntax tree converts the string
code objects reflecting C# syntax in preparation for the compilation):
SyntaxTree syntaxTree = SyntaxFactory.ParseSyntaxTree(code, null, "");
We build the allReferences
collection by concatenation the reference to mscore
library and the references passed to the method:
MetadataReference mscoreLibReference =
AssemblyMetadata
.CreateFromFile(typeof(string).Assembly.Location)
.GetReference();
IEnumerable allReferences =
new MetadataReference[] { mscoreLibReference };
if (references != null)
{
allReferences = allReferences.Concat(references);
}
Finally, we build and return the Roslyn Compilation
object via CSharpCompilation.Create(...)
method:
CSharpCompilation compilation = CSharpCompilation.Create
(
assemblyOrModuleName,
new[] { syntaxTree },
options: compilerOptions,
references: allReferences
);
return compilation;
EmitToArray(...) Method
The purpose of EmitToArray(...)
method is to emit the byte code (the real compilation actually occurs at this stage), to check for errors (and throw an exception if emission is not successful) and to return the byte array of .NET code.
It takes only one argument - "compilation" of Roslyn Compilation
type.
First, we create the MemoryStream
to accommodate the byte array. Then, we emit the compilation result into the stream
:
using (var stream = new MemoryStream())
{
var emitResult = compilation.Emit(stream);
Then, we test the compilation result for errors and throw an exception containing the first error message (if errors are found):
if (!emitResult.Success)
{
Diagnostic firstError =
emitResult
.Diagnostics
.FirstOrDefault
(
diagnostic =>
diagnostic.Severity == DiagnosticSeverity.Error
);
throw new Exception(firstError?.GetMessage());
}
Finally (if there are no errors), we return the byte array from the stream
:
return stream.ToArray();
Summary
Roslyn is a very powerful and underused framework whose full power is not quite realized by most of the companies due to the lack of documentation and samples.
In this article, I explain how to compile and assemble dynamically generated code into an executable dynamic assembly at run time using Roslyn.
I try to explain each stage of compilation and assembly also mentioning the possible traps in detail so that the readers of this article will not have to spend as much time on trying to make things work as I did:).
History
- 13th November, 2017: Added link to GITHUB