(Print this page)

Compiling code on the fly
Published date: Monday, July 25, 2011
On: Moer and Éric Moreau's web site

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.


(Print this page)