The code is available on GitHub
Executing code dynamically without precompiling is a powerful feature that can enhance the capabilities of an application. In this article we explore how we can execute C# and Visual Basic code dynamically at runtime, without the need for precompilation. We will dive into the steps involved in the dynamic compilation process, including referencing external DLLs, adding imports, and handling potential errors. By the end of this article, you will have a full understanding of dynamic code execution with a fully functional library that takes code as input, executes it dynamically, and returns the desired result.
What you will learn
Producing an Assembly from a Code Snippet
Learn how to generate an Assembly from a code snippet in either C# or Visual Basic. Whether you have a class or just a method body, we will walk you through the process of dynamically compiling the code at runtime.
Executing the Produced Assembly using Reflection
Learn how to utilize reflection to execute the dynamically produced Assembly.
Obtaining Compile and Runtime Errors
Learn how to obtain compile-time or runtime errors in C# and Visual Basic scripts.
Leveraging the Roslyn Compiler
We will explain how you can leverage the Roslyn compiler to execute C# and Visual Basic code dynamically.
Designing a Flexible Library for dynamic code execution
How to properly design a library that encapsulates the complexity of Roslyn and allow for the user to easily execute scripts at runtime. The library will be designed with respect to easily be extended to include other language implementations, like python or Java.
- What you will learn
- The basic concepts of Dynamic code execution
- Building the Core of the Dynamic Script Execution Library
- Usage Examples
The basic concepts of Dynamic code execution
In order to execute code dynamically we need an appropriate compiler for the language we want to execute. The process of executing code dynamically typically consists of two distinct phases:
Evaluating the Code.
The first phase involves parsing the script as text and generating an executable result. The result might be different and depends on the language we try to execute. For instance, executing C# or Visual Basic code will produce an assembly DLL file, while Python code will generate a different type of output.
To successfully generate an executable, the compiler requires all the necessary components. This includes ensuring that the script has the appropriate imports and references to external libraries.
In the image below, all necessary components for executing .NET code are depicted:
In the following list we describe further the parameters needed for compiling our script:
- References – A list of all DLL file locations to include in the executable. For example, if your script relies on the Newtonsoft.Json.dll library, you would include its location here.
- Script – This parameter represents the .NET code, either in C# or Visual Basic. Our implementation offers two modes: ClassTemplate and MethodTemplate. In ClassTemplate mode, you provide the entire class code, which includes the method you wish to call, along with other methods and any necessary using/imports statements. In MethodTemplate mode, you only provide the body of the method you want to execute, without its signature. Our library automatically generates the signature and the rest of the file based on the parameters you set.
- Imports – A list of namespace names to be included in the generated class. In ClassTemplate mode, there is no need to add items to this list since the entire class file already contains all the required using/imports statements.
- AssemblyName – The name of the resulting assembly that will be generated.
- Parameters – In MethodTemplate mode, you can provide inputs and outputs for the generated method. Those parameters are necessary for generating the method’s signature.
Executing the Produced Executable File
Once the executable file is generated, we can proceed to execute it by calling the desired method and providing any necessary arguments. The parameters required to distinguish which method to call are as follows:
- Namespace name (optional ) – Specify the namespace where the target method is located.
- Class name (optional) – Provide the name of the class where the target method is located.
- Method name (optional) – Specify the name of the method you want to call within the class.
However, in the case of MethodTemplate mode, you don’t need to provide any of the mentioned arguments. Since the library generates the namespace, class, and method itself, it already knows which method to call.
- Method Arguments (optional) – If the method requires any arguments, you can provide them in a key-value format. The key represents the name of the method parameter, while the value corresponds to the argument to be passed.
Building the Core of the Dynamic Script Execution Library
Our library will consists of a main interface that all different language implementations will implement. In addition, the library will contain some common models that represent the necessary information for constructing and executing the executable code. In this article, we focus on implementing a library capable of executing .NET code dynamically.
Designing the Main Interface
First, we need an interface with two methods. One for Evaluating the script and another that executes it. We create an interface called IDynamicScriptController that contains those two methods, as shown below:
public interface IDynamicScriptController<T, K>
where T : IDynamicScriptParameter
where K : CallArguments
{
EvaluationResult Evaluate(T t);
ExecutionResult Execute(K callingArgs = null, List<ParameterArgument> methodArgs = null);
}
To ensure flexibility and allow for the integration of different programming languages beyond C# and Visual Basic, we utilize generics for allowing different language implementations to provide their own concrete classes. However, the T and K implementations must extend from IDynamicScriptParameter and CallArguments respectively.
The IDynamicScriptParameter represents the model that contains all necessary information for generating the executable. This interface has only a Script property because it is the most common property regardless the language implementation.
public interface IDynamicScriptParameter
{
string Script { get; }
}
Similarly, the CallArguments class is used in order to identify which method to call in the generated executable. Its base implementation includes only the method name since it is a common requirement across different implementations.
public class CallArguments
{
public string MethodName { get; }
public CallArguments(string methodName = null)
{
MethodName = methodName;
}
}
Designing the Models for the .NET Implementation
In .NET, we create the DotNetDynamicScriptParameter class that contains the necessary arguments for building the executable code. This class includes properties such as Script (the code itself), AssemblyName (optional), Imports (list of namespaces to include), References (list of external DLL references), and Parameters (list of method parameters).
public class DotNetDynamicScriptParameter : IDynamicScriptParameter
{
public string Script { get; }
public string AssemblyName { get; }
public List<string> Imports { get; }
public List<string> References { get; }
public List<ParameterDefinition> Parameters { get; }
public DotNetDynamicScriptParameter(string script, string assemblyName = null, List<string> imports = null, List<string> references = null, List<ParameterDefinition> parameters = null)
{
Script = script;
AssemblyName = assemblyName ?? "GeneratedAssembly";
Imports = imports ?? new List<string>();
References = references ?? new List<string>();
Parameters = parameters ?? new List<ParameterDefinition>();
}
}
Additionally, we create the DotNetCallArguments class for specifying the Namespace, Class, and Method names to identify the desired method to call. The InstanceSignature property concatenates the Namespace and Class names to uniquely identify the instance to create from the generated assembly. In case we don’t provide a namespace, only the class name is returned.
public class DotNetCallArguments : CallArguments
{
public string NamespaceName { get; }
public string ClassName { get; }
public DotNetCallArguments(string namespaceName = null,
string className = null,
string methodName = null) : base(methodName ?? "Main")
{
NamespaceName = namespaceName;
ClassName = className;
}
public string InstanceSignature
{
get
{
if (!string.IsNullOrWhiteSpace(NamespaceName) && !string.IsNullOrWhiteSpace(ClassName))
return $"{NamespaceName}.{ClassName}";
if (string.IsNullOrWhiteSpace(NamespaceName) && !string.IsNullOrWhiteSpace(ClassName))
return ClassName;
return string.Empty;
}
}
}
When evaluating the script in MethodTemplate mode, the namespace, class, and method names are automatically generated by the library.
In the class diagram below, we present an overview of all the core components we have developed so far for the dynamic script execution library.
Up until now, we have described the general algorithm and Core components of executing code dynamically. In the next sections, we delve into the specifics of the .NET implementation and explore how to execute C# and Visual Basic code dynamically using the Roslyn compiler.
Evaluate/Compile .NET Code Using Roslyn
To compile a .NET script, we make use of the .NET Compiler Platform SDK, which generates an executable from source code. In order to use the Roslyn and the code analysis functionality we import the Microsoft.CodeAnalysis.Common NuGet package.
In this section, we focus on the .NET implementation of the IDynamicScriptController, which includes the Evaluate and Execute methods. First, let’s look at the implementation of the Evaluate method, which generates an Assembly from a script.
The process of generating an Assembly from C# or Visual Basic code can be summarized in the following steps:
1. Provide the script.
There are two scenarios for providing the script. In the first scenario, the user can provide the entire script, including imports, namespace, class, and the method to call. In the second scenario, only the method body is provided, and our library constructs the necessary class structure to create a valid script for the Roslyn compiler.
To handle these scenarios, we introduce a CodeTemplate abstract component. This component is responsible to return the entire script code depending on its implementation. Here is the base CodeTemplate class:
public abstract class CodeTemplate
{
protected string _generatedCode;
public virtual string GeneratedCodeNamespaceName => "MyNamespace";
public virtual string GeneratedCodeClassName => "MyClass";
public virtual string GeneratedCodeMethodName => "Run";
public abstract CodeTemplateResult CreateInstance(DotNetCallArguments instanceArgs, Assembly assembly);
// Depending on parameters we construct the script accordingly.
public string Build(DotNetDynamicScriptParameter scriptParameters)
{
var _generatedCode = GetCodeTemplate();
scriptParameters.Imports.AddRange(DefaultImports);
BuildImports(scriptParameters.Imports, ref _generatedCode);
BuildMethodParameters(scriptParameters, ref _generatedCode);
BuildBody(scriptParameters.Script, ref _generatedCode);
return _generatedCode;
}
public virtual string GetInstanceSignature() => $"{GeneratedCodeNamespaceName}.{GeneratedCodeClassName}";
public virtual string GetMethodName() => GeneratedCodeMethodName;
protected abstract List<string> DefaultImports { get; }
// Gets the actual line the errors are referring.
public abstract int GetCodeLineOffset(string code);
// Returns the template of the code depending on implementation.
protected abstract string GetCodeTemplate();
protected abstract void BuildMethodParameters(DotNetDynamicScriptParameter p, ref string code);
protected abstract void BuildImports(List<string> imports, ref string code);
// Replace the method body with the provided script.
private void BuildBody(string methodBody, ref string code)
{
code = code.Replace("{code}", methodBody);
}
}
The CodeTemplate component has two main implementations: MethodBodyCodeTemplate and ClassCodeTemplate.
The MethodBodyCodeTemplate constructs the entire script from just the method body and it is language-agnostic. It also has two implementations, the CSharpMethodBodyCodeTemplate for C# and VisualBasicMethodBodyCodeTemplate for Visual Basic.
On the other hand, the ClassCodeTemplate supports the case where the user provides the entire class as the script input. It is also language-agnostic.
The MethodBodyCodeTemplate and ClassCodeTemplate implementations are shown below:
// Use this implementation in case you want to have a uniform algorithm
// of constructing the entire script from only the method body to call.
// The component is language agnostic.
public abstract class MethodBodyCodeTemplate : CodeTemplate
{
protected override void BuildMethodParameters(DotNetDynamicScriptParameter operationParams, ref string code)
{
var methodParameters = string.Empty;
if (operationParams != null && operationParams.Parameters != null && operationParams.Parameters.Any())
methodParameters = DoBuildMethodParameters(operationParams.Parameters);
code = code.Replace("{methodParameters}", methodParameters);
}
// The signature of the method is different depending on language implementation.
protected abstract string DoBuildMethodParameters(List<ParameterDefinition> parameterDefinitions);
// In the execution phase, we retrieve the instance from the parameters
// located in the CodeTemplate abstract component.
public override CodeTemplateResult CreateInstance(DotNetCallArguments instanceArgs, Assembly assembly)
{
var instance = assembly.CreateInstance(GetInstanceSignature());
var method = instance.GetType().GetMethod(GetMethodName());
return new CodeTemplateResult(instance, method);
}
}
// Use this implementation in case you want to have a uniform algorithm
// of supporting the case where the user provides the entire class as script input.
// The component is language agnostic.
public class ClassCodeTemplate : CodeTemplate
{
public override int GetCodeLineOffset(string code) => 0;
// The script itself is the template.
protected override string GetCodeTemplate() => "{code}";
// No need to provide default imports. All imports are included in the script.
protected override List<string> DefaultImports => new List<string>();
// All imports are already in the script. So an empty implementation is provided.
protected override void BuildImports(List<string> imports, ref string code) { }
protected override void BuildMethodParameters(DotNetDynamicScriptParameter p, ref string code) { }
// In execution phase the user must provide
// the namespace, class and method to call.
public override CodeTemplateResult CreateInstance(DotNetCallArguments args, Assembly assembly)
{
object instance = null;
if(string.IsNullOrEmpty(args.InstanceSignature))
{
var type = assembly.GetExportedTypes().FirstOrDefault(x => x.GetMethod(args.MethodName) != null);
instance = Activator.CreateInstance(type);
}else
instance = assembly.CreateInstance(args.InstanceSignature);
var method = instance.GetType().GetMethod(args.MethodName ?? GetMethodName());
return new CodeTemplateResult(instance, method);
}
}
Also, the C# implementation of MethodBodyCodeTemplate is shown below:
public class CSharpMethodBodyCodeTemplate : MethodBodyCodeTemplate
{
// This template is used for constructing the whole class file.
protected override string GetCodeTemplate()
=> @$"{{usings}}
namespace {GeneratedCodeNamespaceName} {{
public class {GeneratedCodeClassName} {{
public void {GeneratedCodeMethodName}({{methodParameters}}) {{
{{code}}
}}
}}
}}";
// In case of errors we should provide the line numbers with respect only
// with the method body not the whole class.
public override int GetCodeLineOffset(string code) =>
code.Substring(0, code.IndexOf($"public void {GeneratedCodeMethodName}"))
.Count(x => x == '\n');
protected override List<string> DefaultImports => new List<string>()
{
"System",
"System.Linq"
};
// Depending on parameter definitions the user set, we construct the proper
// signature of the method to call.
protected override string DoBuildMethodParameters(List<ParameterDefinition> parameterDefinitions)
{
var methodParams = string.Join(", ", parameterDefinitions.Select(x => $"{(x.Direction == ParameterDirection.Output || x.Direction == ParameterDirection.InputOutput ? "ref " : string.Empty)}{MapToCSharpType(x.Type)} {x.Key}"));
return methodParams;
}
protected override void BuildImports(List<string> imports, ref string code)
{
code = code.Replace("{usings}", string.Join(Environment.NewLine, imports.Select(x => $"using {x};")));
}
private string MapToCSharpType(ParameterDefinitionType type)
{
return type switch
{
ParameterDefinitionType.Bool => "bool",
ParameterDefinitionType.Int => "int",
ParameterDefinitionType.Datetime => "DateTime",
ParameterDefinitionType.Datatable => "DataTable",
ParameterDefinitionType.Double => "double",
ParameterDefinitionType.Decimal => "decimal",
ParameterDefinitionType.Dynamic => "dynamic",
ParameterDefinitionType.Long => "long",
ParameterDefinitionType.String => "string",
ParameterDefinitionType.List => "dynamic",
_ => throw new ArgumentOutOfRangeException(nameof(type)),
};
}
}
Similarly, The Visual Basic implementation of the MethodBodyCodeTemplate is shown below:
public class VisualBasicMethodBodyCodeTemplate : MethodBodyCodeTemplate
{
protected override List<string> DefaultImports => new List<string>()
{
"System",
"System.IO",
"System.Collections.Generic",
"System.Linq"
};
protected override string GetCodeTemplate()
=> @$"{{imports}}
Namespace {GeneratedCodeNamespaceName}
Public Class [{GeneratedCodeClassName}]
Public Sub {GeneratedCodeMethodName}({{methodParameters}})
{{code}}
End Sub
End Class
End Namespace";
public override int GetCodeLineOffset(string code) =>
code.Substring(0, code.IndexOf($"Public Sub {GeneratedCodeMethodName}"))
.Count(x => x == '\n');
protected override string DoBuildMethodParameters(List<ParameterDefinition> parameterDefinitions)
{
var methodParams = string.Join(", ", parameterDefinitions.Select(x => $"{(x.Direction == ParameterDirection.Output || x.Direction == ParameterDirection.InputOutput ? "ByRef" : string.Empty)} {x.Key} As {MapToVBType(x.Type)}"));
return methodParams;
}
private string MapToVBType(ParameterDefinitionType type)
{
return type switch
{
ParameterDefinitionType.Bool => "Boolean",
ParameterDefinitionType.Int => "Integer",
ParameterDefinitionType.Datetime => "DateTime",
ParameterDefinitionType.Datatable => "DataTable",
ParameterDefinitionType.Double => "Double",
ParameterDefinitionType.Decimal => "Decimal",
ParameterDefinitionType.Long => "Long",
ParameterDefinitionType.String => "String",
ParameterDefinitionType.Dynamic => "Object",
ParameterDefinitionType.List => "Object",
_ => throw new ArgumentOutOfRangeException(nameof(type)),
};
}
protected override void BuildImports(List<string> imports, ref string code)
{
code = code.Replace("{imports}", string.Join(Environment.NewLine, imports.Select(x => $"Imports {x}")));
}
}
The CodeTemplate component handles the construction of the script, including imports, method parameters, and the method body, depending on the provided parameters.
Another key point is how we construct the method signature. In case a parameter is defined as Output or InputOutput, we make use of the ref keyword in order to be able to get the modified value after the method execution.
An anatomy of the generated method is shown below:
The class diagram illustrates the hierarchy of the CodeTemplate component and its implementations.
The final result is a string containing the complete code to be passed to the Roslyn compiler.
2. Create a SyntaxTree from the script.
To create a SyntaxTree from the script, we use the CSharpSyntaxTree.ParseText() method for C# code and VisualBasicSyntaxTree.ParseText() method for Visual Basic code. These methods parse the code into a structured representation called a SyntaxTree.
A SyntaxTree represents a single document of code, like a .cs file or an assemblyInfo file. It contains information about the syntax and semantics of the code.
Here’s an example of creating a SyntaxTree using C# code:
string code = @"using System;
namespace Test
{
public class TestClass
{
public void Run() {
System.Console.WriteLine(""Hello world"");
}
}
}";
SyntaxTree tree = CSharpSyntaxTree.ParseText(code);
In our case, we have an abstract method called GetSyntaxTree() that retrieves the SyntaxTree for a given code.
In the Evaluate method of our .NET code execution, we call the GetSyntaxTree method to obtain the SyntaxTree for the provided code.
public EvaluationResult Evaluate(DotNetDynamicScriptParameter p)
{
_operationParams = p;
var code = _codeTemplate.Build(p);
var syntaxTree = GetSyntaxTree(code);
..
}
protected abstract SyntaxTree GetSyntaxTree(string code);
Each implementation (C# and Visual Basic) of the GetSyntaxTree method handles the parsing differently based on the programming language.
For example, in the CSharpDynamicScriptController class:
protected override SyntaxTree GetSyntaxTree(string code)
{
// in this point we can set the language version of the script.
var options = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp5);
return CSharpSyntaxTree.ParseText(code, options);
}
And in the VisualBasicDynamicScriptController class:
protected override SyntaxTree GetSyntaxTree(string code)
{
var options = VisualBasicParseOptions.Default.WithLanguageVersion(LanguageVersion.VisualBasic16_9);
return VisualBasicSyntaxTree.ParseText(code, options);
}
3. Create a Compilation object
The Compilation is an immutable representation of a single invocation of the compiler. This step allows us to add SyntaxTrees and references to external DLLs. There are two implementations of the abstract Compilation object, CSharpCompilation for C# and VisualBasicCompilation for Visual Basic.
In the common DotNetDynamicScriptController, we add all the references that both languages require, along with the SyntaxTree of the provided script. We also allow subclasses to add any additional references they need.
Below we show the creation of the Compilation object inside the Evaluation method:
//.. Previous code in the Evaluate method
var compilation = GetCompilationForAssembly(p.AssemblyName)
.WithOptions(GetOptions())
// Each syntax tree corresponds to a "file" to be included in the compilation.
.AddSyntaxTrees(syntaxTree);
compilation = AddDefaultReferences(rootPath, compilation);
// Add additional references
compilation = AddReferences(rootPath, compilation); // This method can be extended in C# and Visual Basic subclasses
// Add references from the parameters
foreach (var reference in p.References)
{
compilation = compilation.AddReferences(MetadataReference.CreateFromFile(reference));
}
...
The default references are included to ensure that even simple C# code can be executed without any other references or configurations provided by the client. The AddDefaultReferences method adds these default references:
private Compilation AddDefaultReferences(string rootPath, Compilation compilation)
{
return compilation.AddReferences(MetadataReference.CreateFromFile(typeof(int).Assembly.Location))
.AddReferences(MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location))
.AddReferences(MetadataReference.CreateFromFile(typeof(DataTable).Assembly.Location))
.AddReferences(MetadataReference.CreateFromFile(typeof(Object).Assembly.Location))
.AddReferences(MetadataReference.CreateFromFile(typeof(AssemblyTitleAttribute).Assembly.Location))
.AddReferences(MetadataReference.CreateFromFile(Path.Combine(rootPath, "System.dll")))
.AddReferences(MetadataReference.CreateFromFile(Path.Combine(rootPath, "netstandard.dll")))
.AddReferences(MetadataReference.CreateFromFile(Path.Combine(rootPath, "System.Runtime.dll")));
}
All default references are common for both C# and Visual Basic. After adding the default references, the implementations for C# and Visual Basic can extend and add their own references. The CSharpDotNetDynamicScriptController extends that method and adds an additional reference to Microsoft.CSharp.dll as shown below:
protected override Compilation GetCompilationForAssembly(string assemblyName)
=> CSharpCompilation.Create(assemblyName);
protected override CompilationOptions GetOptions()
// We want to produce a DLL library, so the DynamicallyLinkedLibrary option is used.
=> new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary);
protected override Compilation AddReferences(string rootPath, Compilation compilation)
{
return compilation.AddReferences(MetadataReference.CreateFromFile(Path.Combine(rootPath, "Microsoft.CSharp.dll")));
}
Similarly, the Visual Basic has no additional libraries to add, so it doesn’t override the AddReferences method. However, the other methods are implemented as shown below:
protected override Compilation GetCompilationForAssembly(string assemblyName)
=> VisualBasicCompilation.Create(assemblyName);
protected override CompilationOptions GetOptions()
=> new VisualBasicCompilationOptions(OutputKind.DynamicallyLinkedLibrary);
Finally we add the references from the References array using the MetadataReference.CreateFromFile.
4. Get errors from Diagnostics
After compilation, we need to check if the compilation was completed successfully without any errors. To obtain detailed error information, we can use the GetDiagnostics function of the Compilation object.
Here’s how to retrieve errors from the diagnostics:
var diagnostics = compilation.GetDiagnostics();
var errors = diagnostics.Where(x => x.Severity == DiagnosticSeverity.Error);
// Handle errors.
The GetDiagnostics method returns a collection of diagnostics, which includes information about warnings, errors, and other issues found during the compilation process. By filtering the diagnostics based on severity, we can specifically obtain the errors.
5. Generating the executable
The final step in the first phase is to generate the Assembly that contains all the provided information. This Assembly will be used in the next phase to execute the code.
To generate the assembly, we use a memory stream to write the assembly bytes, and the Emit function of the Compilation object:
using (var ms = new MemoryStream())
{
var result = compilation.Emit(ms, pdbStream: null);
if (!result.Success)
{
int lineOffset = _codeTemplate.GetCodeLineOffset(code);
return EvaluationResult.WithErrors(result.Diagnostics.Where(x => x.Severity == DiagnosticSeverity.Error).Select(x => GetDynamicScriptError(x, lineOffset)));
}
var assemblyBytes = ms.ToArray();
_assembly = Assembly.Load(assemblyBytes);
}
If the call to the Emit function is successful (result.Success), we convert the memory stream to an array of assembly bytes. We then load the assembly using Assembly.Load and store it in the _assembly variable for later use.
The complete code of the Evaluation method is shown below:
public EvaluationResult Evaluate(DotNetDynamicScriptParameter p)
{
_operationParams = p;
var code = _codeTemplate.Build(p);
var syntaxTree = GetSyntaxTree(code);
var rootPath = Path.GetDirectoryName(typeof(object).Assembly.Location) + Path.DirectorySeparatorChar;
var compilation = GetCompilationForAssembly(p.AssemblyName)
.WithOptions(GetOptions())
.AddSyntaxTrees(syntaxTree);
compilation = AddDefaultReferences(rootPath, compilation);
compilation = AddReferences(rootPath, compilation);
foreach (var reference in p.References)
{
compilation = compilation.AddReferences(MetadataReference.CreateFromFile(reference));
}
var diagnostics = compilation.GetDiagnostics();
var errors = diagnostics.Where(x => x.Severity == DiagnosticSeverity.Error);
if (errors.Any())
{
int lineOffset = _codeTemplate.GetCodeLineOffset(code);
return EvaluationResult.WithErrors(errors.Select(x => GetDynamicScriptError(x, lineOffset)));
}
using (var ms = new MemoryStream())
{
string resourcesFileLocation = Path.Combine(Path.GetDirectoryName(this.GetType().Assembly.Location), "resources.resx");
var result = compilation.Emit(ms, pdbStream: null);
if (!result.Success)
{
int lineOffset = _codeTemplate.GetCodeLineOffset(code);
return EvaluationResult.WithErrors(result.Diagnostics.Where(x => x.Severity == DiagnosticSeverity.Error).Select(x => GetDynamicScriptError(x, lineOffset)));
}
var assemblyBytes = ms.ToArray();
_assembly = Assembly.Load(assemblyBytes);
}
return EvaluationResult.Ok();
}
Execution Phase – Executing the produced executable file
After successfully producing the executable file, we can dynamically call any function inside the assembly and pass the necessary arguments using reflection.
The execution algorithm consists of the following steps:
1. Create the Instance to Execute using Reflection
In this step, we call the CreateInstance method of the CodeTemplate, which returns the correct instance depending on its implementation (ClassTemplate or MethodTemplate
var instanceResult = _codeTemplate.CreateInstance(callArgs ?? new DotNetCallArguments(), _assembly);
The instanceResult contains the MethodInfo and the instance to call. The CodeTemplateResult is shown below:
public class CodeTemplateResult
{
public object Instance { get; }
public MethodInfo Method { get; }
public CodeTemplateResult(object instance, MethodInfo method)
{
Instance = instance;
Method = method;
}
}
2. Parse the Arguments for the Method to Call
Next, we parse the arguments given by the user and sort them with respect to the parameters in the method we are going to call. If an argument is not provided, we add null to the array.
var arguments = ParseMethodArguments(methodArgs, instanceResult.Method);
private object[] ParseMethodArguments(List<ParameterArgument> methodArgs, MethodInfo method)
{
List<object> arguments = new List<object>();
foreach(var parameter in method.GetParameters())
{
var arg = methodArgs.FirstOrDefault(x => x.Key == parameter.Name);
arguments.Add(arg?.Value); // add null in case the argument is not provided.
}
return arguments.ToArray();
}
3. Invoke the Method
Using reflection, we invoke the method by passing the argument array.
var methodResult = instanceResult.Method.Invoke(instanceResult.Instance, arguments);
4. Get the Results from the Execution.
We collect both the result from a return statement inside the method and the modified values from the ref parameters.
var res = ExecutionResult.Ok();
if(instanceResult.Method.ReturnType != typeof(void))
res.Add(new MethodReturnValue(methodResult));
foreach (var outParam in OutputParameters)
{
var entry = methodArgs.FirstOrDefault(x => x.Key == outParam.Key);
var index = _operationParams.Parameters.IndexOf(outParam);
res.Add(new ExecutionResultEntry(outParam.Key, arguments[index]));
}
The ExecutionResult is a result object that contains the execution outcome, including the method return value and any modified values from the ref parameters.
The complete implementation of the Execute method is as follows:
public ExecutionResult Execute(DotNetCallArguments callArgs = null, List<ParameterArgument> methodArgs = null)
{
try
{
var instanceResult = _codeTemplate.CreateInstance(callArgs ?? new DotNetCallArguments(), _assembly);
var arguments = ParseMethodArguments(methodArgs, instanceResult.Method);
var methodResult = instanceResult.Method.Invoke(instanceResult.Instance, arguments);
var res = ExecutionResult.Ok();
if(instanceResult.Method.ReturnType != typeof(void))
res.Add(new MethodReturnValue(methodResult));
foreach (var outParam in OutputParameters)
{
var entry = methodArgs.FirstOrDefault(x => x.Key == outParam.Key);
var index = _operationParams.Parameters.IndexOf(outParam);
res.Add(new ExecutionResultEntry(outParam.Key, arguments[index]));
}
return res;
}
catch (TargetInvocationException tie)
{
return ExecutionResult.WithError(tie);
}
catch (ArgumentException ae)
{
return ExecutionResult.WithError(ae);
}
}
Usage Examples
Plenty of examples are found in test cases on Github
These examples demonstrate the usage of the dynamic script library in different scenarios.
Example1: Execute a C# code containing a whole class and get the result
var controller = new CSharpDynamicScriptController(new ClassCodeTemplate());
controller.Evaluate(new DotNetDynamicScriptParameter(@"using System;
namespace Test
{
public class TestClass
{
public int Run() {
return 1;
}
}
}"));
var executionResult = controller.Execute(
new DotNetCallArguments(namespaceName: "Test", className: "TestClass", methodName: "Run"),
new List<ParameterArgument>() { });
Console.WriteLine(executionResult.ReturnValue);
This example compiles and executes a C# code containing a class TestClass with a method Run that returns an integer. It uses the CSharpDynamicScriptController with the ClassCodeTemplate to evaluate and execute the code. The result of the execution is obtained and printed.
Example 2: Execute C# code by providing only the method body, parameters, and an external reference
var path = Path.Combine(Directory.GetCurrentDirectory(), "test.txt");
File.WriteAllText(path, "contents");
var controller = new CSharpDynamicScriptController(new CSharpMethodBodyCodeTemplate());
controller.Evaluate(new DotNetDynamicScriptParameter($@"
var content = System.IO.File.ReadAllText(path);
length = content.Length;
result = JsonConvert.SerializeObject(new {{ content, length }});
", parameters: new List<ParameterDefinition>()
{
new("path", ParameterDefinitionType.String, ParameterDirection.Input),
new("length", ParameterDefinitionType.Int, ParameterDirection.Output),
new("result", ParameterDefinitionType.String, ParameterDirection.Output)
}, imports: new List<string>() { "Newtonsoft.Json" }, references: new List<string>() { NewtonsoftLocation }));
var executionResult = controller.Execute(methodArgs: new List<ParameterArgument>() {
new("path", path),
new("length"),
new("result")
});
File.Delete(path);
Assert.AreEqual(executionResult["length"], "contents".Length);
Assert.AreEqual(executionResult["result"], @"{""content"":""contents"",""length"":8}");
In this example, C# code is executed by providing the method body, parameters, and an external reference to Newtonsoft.Json. The code reads the contents of a file, calculates its length, and serializes the result using Newtonsoft.Json. The CSharpDynamicScriptController with the CSharpMethodBodyCodeTemplate is used.
Example 3: Execute Visual Basic code by providing the method body and a Datetime InputOutput argument
var controller = new VisualBasicDynamicScriptController(new VisualBasicMethodBodyCodeTemplate());
controller.Evaluate(new DotNetDynamicScriptParameter($@"
ddate = ddate.AddDays(10)
", parameters: new List<ParameterDefinition>()
{
new ParameterDefinition("ddate", ParameterDefinitionType.Datetime, ParameterDirection.InputOutput)
}));
var executionResult = controller.Execute(methodArgs: new List<ParameterArgument>() {
new ParameterArgument("ddate", new DateTime(2020, 2, 2))
});
Assert.AreEqual(executionResult.GetValue<DateTime>("ddate").Day, 12);
In this example, Visual Basic code is executed by providing the method body and a DateTime InputOutput argument. The code adds 10 days to the provided DateTime value. The VisualBasicDynamicScriptController with the VisualBasicMethodBodyCodeTemplate is used.
These examples showcase different scenarios of using the dynamic script library to evaluate and execute C# and Visual Basic code with various input and output parameters.
5 Comments
This is some epic work you’ve done. Any chance you can open the source? Currently github returns 404
I am sure many people would appreciate it.
I am glad you find it helpful! Thanks for the feedback, I forgot to make the repo public now it should be accessible.
Hello! Awesome work! This is truly amazing & an awesome blog post. Very technical and easy to follow, but there is no talking about performance and no benchmarks.
– How does it perform vs the regular way? Not only speed & memory, but also in terms of clean up / garbage collection, etc.
– As of now it just “stores” the assembly created from the memory stream during runtime. Could we just store it to the filesystem?
– What would be the right way to use this e.x. in a dotnet runtime with AssemblyLoadContext to load it separately?
Hi Dimitris, I am interested in using your code in a project for my company. What type of license does your code require to use?
Regards,
James
Hi James,
Thank you for finding the article useful.
Of course you can use it. I will update the license but don’t worry about it.