About once a year, I face a request or a situation where an ideal solution would be to have .Net code in a file or in a database and being able to able to load it, compile it, and of course, execute it.
This feature is called dynamic code (or compile on the fly). My demo here shows how to load and run VB and C# code but this very same mechanism works with just every other language recognize by .Net.
My friend François found a way of doing it and I have to admit that it is much simpler than what I expected.
Downloadable demo
This month demo has been created with Visual Studio 2010 but since it works since the .Net Framework 2.0, you can reuse this code starting in Visual Studio 2005.
The solution contains 2 projects: one in VB and one in C#.
Figure 1: The running demo application
These projects have no special references. They only use what is normally referenced when you create a plain old Windows Forms application.
The purpose of my demo
My demo is simply a snippet of dynamic code to calculate the taxes which is something that happens regularly in my country.
The calculation will be done in dynamic code. Well not really dynamic in my sample because the “dynamic” part is hardcoded into the application but this code snippet can come from anywhere (database, file, web service call…).
The dynamic code
Because it is impossible to trace into the dynamic code, what I like to do is to create a dummy application just to test my snippet of code. When it works as I want, I then add it to a file or database for the real application to load it.
Here is the method in my application that returns the string of code that will be loaded and compiled during the execution of my application:
Public Function VBCodeToCompile() As String Dim strCode As New System.Text.StringBuilder(5000) With strCode .AppendLine("Public Class MainClass") .AppendLine(" Public Shared Function CalcSalesTaxes(ByVal pState As String, ByVal pAmount As Decimal) As Decimal ") .AppendLine(" Dim decRate As Decimal ") .AppendLine(" Select Case pState ") .AppendLine(" Case ""AB"" ") .AppendLine(" decRate = 0.05D ") .AppendLine(" Case ""BC"", ""MB"" ") .AppendLine(" decRate = 0.12D ") .AppendLine(" Case ""NB"", ""NF"", ""ON"" ") .AppendLine(" decRate = 0.13D ") .AppendLine(" Case ""NS"" ") .AppendLine(" decRate = 0.15D ") .AppendLine(" Case ""PEI"" ") .AppendLine(" decRate = 0.155D ") .AppendLine(" Case ""QC"" ") .AppendLine(" decRate = 0.13925D ") .AppendLine(" Case ""SK"" ") .AppendLine(" decRate = 0.1D ") .AppendLine(" Case Else ") .AppendLine(" decRate = 0D ") .AppendLine(" End Select ") .AppendLine(" Return pAmount * decRate ") .AppendLine(" End Function ") .AppendLine("End Class") End With Return strCode.ToString End Function
The same method in the C# application reads like this:
public string CSharpCodeToCompile() { System.Text.StringBuilder strCode = new System.Text.StringBuilder(5000); { strCode.AppendLine("namespace DynamicNS "); strCode.AppendLine("{ "); strCode.AppendLine("public class cTest "); strCode.AppendLine("{ "); strCode.AppendLine(" public static decimal CalcSalesTaxes(string pState, decimal pAmount) "); strCode.AppendLine(" { "); strCode.AppendLine(" decimal decRate = default(decimal); "); strCode.AppendLine(" switch (pState) "); strCode.AppendLine(" { "); strCode.AppendLine(" case \"AB\": "); strCode.AppendLine(" decRate = 0.05m; "); strCode.AppendLine(" break; "); strCode.AppendLine(" case \"BC\": "); strCode.AppendLine(" case \"MB\": "); strCode.AppendLine(" decRate = 0.12m; "); strCode.AppendLine(" break; "); strCode.AppendLine(" case \"NB\": "); strCode.AppendLine(" case \"NF\": "); strCode.AppendLine(" case \"ON\": "); strCode.AppendLine(" decRate = 0.13m; "); strCode.AppendLine(" break; "); strCode.AppendLine(" case \"NS\": "); strCode.AppendLine(" decRate = 0.15m; "); strCode.AppendLine(" break; "); strCode.AppendLine(" case \"PEI\": "); strCode.AppendLine(" decRate = 0.155m; "); strCode.AppendLine(" break; "); strCode.AppendLine(" case \"QC\": "); strCode.AppendLine(" decRate = 0.13925m; "); strCode.AppendLine(" break; "); strCode.AppendLine(" case \"SK\": "); strCode.AppendLine(" decRate = 0.1m; "); strCode.AppendLine(" break; "); strCode.AppendLine(" default: "); strCode.AppendLine(" decRate = 0m; "); strCode.AppendLine(" break; "); strCode.AppendLine(" } "); strCode.AppendLine(" return pAmount * decRate; "); strCode.AppendLine(" } "); strCode.AppendLine("} "); strCode.AppendLine("} "); } return strCode.ToString(); }
You will notice here that the C# version requires a namespace definition (this namespace is automatic in VB).
I would like to say here that the language in which the snippet is compiled can be different then the language used to create the application.
As you can see, this method has 2 arguments which we will fill with the values entered by the user. The method also returns a decimal value which is the calculated amount of taxes.
How to dynamically compile code
Here, I will give you to main sections required to compile dynamic code.
First you need to create a compiler provider for the specific language you want:
Dim objCompiler As CodeDomProvider = CodeDomProvider.CreateProvider("VisualBasic")
This previous line is used to instantiate a VB compiler. Replacing “VisualBasic” with “CSharp” would let your application to load and compile C# code. Again, the language in which the snippet is compiled can be different then the language used to create the application.
Now that we have a compiler instance, we can provide some code and try to generate an assembly from it:
Dim objCompilerResults As CompilerResults = objCompiler.CompileAssemblyFromSource(objCompilerParams, VBCodeToCompile)
In this line, VBCodeToCompile is the method that loads the dynamic snippet of code and returns it as a string.
After we compiled the snippet of code, we need to check for any compilation errors:
If (objCompilerResults.Errors.Count > 0) Then
If any errors are detected, the assembly won’t be available to be executed.
If we don’t have any errors, we can now try to reach the method we just compiled. Because my example is so simple, it contains a single module we can access like this:
Dim mods As [Module]() = objCompilerResults.CompiledAssembly.GetModules(False)
Now that we have the module (which is a class), we can loop through the types it expose to find the method in which we are interested:
For Each type As Type In mods(0).GetTypes Dim mi As MethodInfo = type.GetMethod("CalcSalesTaxes", BindingFlags.Public Or BindingFlags.Static) If Not (mi Is Nothing) Then
Once we have a reference to that method, we need to create the parameters (state and amount) to pass to the method:
'Provide values to method arguments Dim parms(1) As Object parms(0) = pState parms(1) = pAmount
And we are finally ready to call (invoke) that method:
'Invoke the method and retreive the return value Return Convert.ToDecimal(mi.Invoke(Nothing, parms))
And because the method is a function, the return is casted into a decimal to return it to the main caller.
Here is the complete code of this method:
Private Function CalcSalesTaxesDynamic(ByVal pState As String, ByVal pAmount As Decimal) As Decimal 'Declare a compiler Dim objCompiler As CodeDomProvider = CodeDomProvider.CreateProvider("VisualBasic") 'Set the compiler parameters Dim objCompilerParams As New CompilerParameters() objCompilerParams.CompilerOptions = "/target:library /optimize" objCompilerParams.GenerateExecutable = False objCompilerParams.GenerateInMemory = True objCompilerParams.IncludeDebugInformation = False objCompilerParams.ReferencedAssemblies.Add("mscorlib.dll") objCompilerParams.ReferencedAssemblies.Add("System.dll") 'Compile Assembly Dim objCompilerResults As CompilerResults = objCompiler.CompileAssemblyFromSource(objCompilerParams, VBCodeToCompile) 'Do we have any compiler errors? If (objCompilerResults.Errors.Count > 0) Then For Each err As CompilerError In objCompilerResults.Errors MessageBox.Show(err.ErrorText) Next Return Nothing End If 'Get all the modules part of the assembly Dim mods As [Module]() = objCompilerResults.CompiledAssembly.GetModules(False) 'Go through all the types returned by the assembly For Each type As Type In mods(0).GetTypes Dim mi As MethodInfo = type.GetMethod("CalcSalesTaxes", BindingFlags.Public Or BindingFlags.Static) If Not (mi Is Nothing) Then 'Provide values to method arguments Dim parms(1) As Object parms(0) = pState parms(1) = pAmount 'Invoke the method and retreive the return value Return Convert.ToDecimal(mi.Invoke(Nothing, parms)) End If Next Return Nothing End Function
The only thing missing is a little method to get the inputs from the user, call the main method and display the result. Only 3 lines of code are required:
Dim decAmount As Decimal Decimal.TryParse(txtAmount.Text, decAmount) lblResult.Text = CalcSalesTaxesDynamic(txtProvince.Text, decAmount).ToString("C")
Loading dynamic code from a file
Instead of loading your code from a string like I do here using CompileAssemblyFromSource, there is another method called CompileAssemblyFromFile which will load the code straight from a file.
Conclusion
Of course there are some overhead in loading, compiling and executing code like this but this overhead is often required.