Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Compile and Run VB.NET Code using the CodeDom

0.00/5 (No votes)
27 Jan 2006 2  
Demonstrates run-time compilation and execution of VB.NET code, using the CodeDom
Sample screenshot

Not Another "Script Runner" Article!

The major differences between this article and others that I have seen presented here are the target audience and the usage scenario. You will notice in the screen shot displayed above that there is not a function declaration. No namespace. No class, or include statements. The target audience for this technology is a diverse collection of support personnel with a minimal exposure to programming. If there was any exposure to programming, it would be in the form of DOS batch files, and maybe VBScript.

These are people who would prefer not to write scripts; no matter how easy it was. But they do often modify existing batch files and configuration files, either while setting up a new system, or fixing a support issue. The design goal was to offer "VBScript" simplicity but with "VB.NET" error handling.

Introduction

This is a small sample demonstration of late compile. Often the CodeDom aspect of the .NET Framework is discussed in relation to the ability to generate code. By using the CodeDom as a glorified collection of code bits, you can Add(new codebit) until your program is complete, then serialize the resultant code out to a file, or compile and run.

Although the code generation abilities are very interesting and powerful, this article is primarily focused on the ability of the CodeDom assembly to compile dynamic code to an assembly that exists in memory. An instance of the assembly is then created, and the method containing our dynamic code is executed.

More than 10 years ago, I wrote a VB6 application that ran VBScript macros. Last year, I was again presented with the need to allow dynamic run time code execution. This time, instead of using VBScript, I decided to use actual VB.NET code. Sorry it took me so long to post...

Out With the Old, In With the New...

The code presented here is a portion of that solution, upgraded to use the 2.0 Framework. During the upgrade, I noticed that Microsoft had marked the ICodeCompiler interface obsolete, directing me to instead use methods directly from the CodeProvider object. I have simply commented out the CreateCompiler() function. Those wishing to use this code in the 1.1 Framework should be able to uncomment the code line instancing and calling the interface.

Dim c As VBCodeProvider = New VBCodeProvider
' Obsolete in 2.0 framework
' Dim icc As ICodeCompiler = c.CreateCompiler

Calling the Compiler at Run Time

Using the cEvalProvider class is as easy as creating a string builder, appending VB.NET code to the string builder, and then passing the string builder to the CompileAndRunCode() function.

Dim oRetVal As Object = CompileAndRunCode(Me.RichTextBox1.Text)
MsgBox(oRetVal)

In the case of the code snippet above, the result looks like this:

Sample Image - VBRunNET2.jpg

The CompileAndRunCode() Function

The CompileAndRunCode() function is a wrapper around the cVBEvalProvider object, which is itself a wrapper around the Microsoft.VisualBasic.VBCodeProvider object, which is a .... well you get the idea. One day, I am going to write the ultimate function: DoIt(), which will wrap anything and do everything, or perhaps it's the other way around: "Somewhere, deep in the Silicon, there is a single tiny function..."

We start out by creating an instance of our VB Eval Provider class:

' Instance our CodeDom wrapper
Dim ep as New cVBEvalProvider

We then call the Eval() method, passing a string containing our VB.NET statements:

' Compile and run, getting the results back as an object
Dim objResult as Object = ep.Eval(VBCodeToExecute)

The VB code provider object returns all the compile time errors in a CompilerResults object, and my wrapper class exposes this collection of errors through the CompilerErrors property. So the next thing to do is check and see if the count of CompilerErrors is zero or not. If we have errors present, then write a few lines of information to the debug output, and exit the function.

If ep.CompilerErrors.Count <> 0 Then

    Diagnostics.Debug.WriteLine("CompileAndRunCode: Compile Error Count = _
        " & ep.CompilerErrors.Count)

    Diagnostics.Debug.WriteLine(ep.CompilerErrors.Item(0))

    Return "ERROR" ' Forget it

End If

Note: This is not really a good way to bubble the error state back to the client, since the function that is compiled and executed in real-time may return the string "ERROR" as a normal occurrence. But this is just a quick and dirty example.

If we have no compile errors, we can expect the return result of our dynamically compiled and executed code to be in the objResult object.

Due to the open ended nature of run-time compilation, I return the results as a weakly typed object, then show an example of converting to a more strongly typed object by using the GetType() method. In my sample code, the assumption is that the return type is a System.String, although more complex types can be handled quite easily.

Dim t As Type = objResult.GetType()
If t.ToString() = "System.String" Then
        sReturn_DataType = t.ToString
        sReturn_Value = Convert.ToString(objResult)
    Else
        ' Some other type of data - not really handled at 
        ' this point. rwd
        'ToDo: Add handlers for other data return types, if needed

        ' Here is an example to handle a dataset...
        'Dim ds As DataSet = DirectCast(objResult, DataSet)
        'DataGrid1.Visible = True
        'DataGrid1.DataSource = ds.Tables(0)
    End If

The cVBEvalProvider Object

The cVBEvalProvider Object Diagram

The cVBEvalProvider object has a simple model. It exposes one property, CompilerErrors which is a collection of compile time errors generated during the Eval() method. The Eval() method is passed a string containing VB.NET code to be executed or "evaluated", and returns a generic Object. The New() method simply instances the internal CompilerErrorCollection member variable.

The real meat of the work occurs in the Eval() method, which creates an instance of the VBCodeProvider object. Although the VBCodeProvider object inherits from System.CodeDom.Compiler.CodeDomProvider, don't bother looking for it inside the CodeDom namespace, it is actually a member of the Microsoft.VisualBasic assembly.

Note: This seems obvious to me, but I am going to mention it anyway. If VB.NET is not your thing, there is a Microsoft.CSharp.CSharpCodeProvider class, which also descends from the System.CodeDom.Compiler.CodeDomProvider object. The first step in switching languages would be to switch these objects. Or I could see an else case that instanced the appropriate run-time compiler, based on some switch, along the lines of WScript.

The Eval() Method

The Eval() method creates an instance of the VBCodeProvider, and variables for the CompilerParameters and CompilerResults. Also of interest, a variable of MethodInfo is created. The MethodInfo object exposes an Invoke(), which performs the actual execution of our code.

Dim oCodeProvider As VBCodeProvider = New VBCodeProvider
' Obsolete in 2.0 framework
' Dim oICCompiler As ICodeCompiler = oCodeProvider.CreateCompiler
Dim oCParams As CompilerParameters = New CompilerParameters
Dim oCResults As CompilerResults
Dim oAssy As System.Reflection.Assembly
Dim oExecInstance As Object = Nothing
Dim oRetObj As Object = Nothing
Dim oMethodInfo As MethodInfo
Dim oType As Type 

As noted earlier, with the 1.1 Framework model, you created an instance of the compiler via the CreateCompiler() method of the VBCodeProvider. The CreateCompiler() method returned an ICodeCompiler. The only call made against the ICodeCompiler object was a call to CompileAssemblyFromSource(). In the 2.0 Framework, you can directly call CompileAssemblyFromSource() from the VBCodeProvider. I have left the code inline and commented it out for those who may wish to use this with the 1.1 Framework.

Compiler Parameters

The CompileAssemblyFromSource() method takes an object of CompilerParameter as the first argument. So our first order of business will be to setup the CompilerParameters that we intend to use.

The ReferencedAssemblies Collection

In our code, we add references to any required assemblies. As an exercise to make the cVBEvalProvider object more flexible, we could have exposed the CompilerParameters as a property, or offered an optional parameter to allow the calling process to pass them to us, using defaults if the caller did not pass anything.

The CompilerOptions Property

The CompilerOptions property is where we can specify additional command line arguments for the compiler. Here we are using the /t:library switch, which tells the compiler the Target output type is a library (*.dll) file. There are four target types, {exe, library, module, winexe}.

I hesitate to include this link, because it seems like Microsoft moves things around every six to twelve months... but here are the Compiler Options for the Visual Basic.NET compiler in the MSDN collection. Your mileage may vary.

The GenerateInMemory Property

Finally, we set the GenerateInMemory property of the CompilerParameters object to true, so that on a successful compilation, the generated assembly will be returned in the CompilerResults.CompiledAssembly property.

Note: As an alternative, an output filename could be specified using the /out:filename command line argument, and the assembly could be loaded/instanced from filename instead of the CompilerResults.CompiledAssembly property.

Building the Code Sandwich

Code sandwich? OK, it's a cheesy attempt to get your attention. Plus I feel like I have overused the word "wrapper" in this article. One of the design decisions that I made was to insulate the end-user from stuff like Imports, Namespaces, Classes and Function declarations. To accomplish this, I insert the dynamic user supplied code into a "Code Sandwich" of goodness.

' Generate the Code Framework
Dim sb As StringBuilder = New StringBuilder("")
sb.Append("Imports System" & vbCrLf) 
sb.Append("Imports System.Xml" & vbCrLf) 
sb.Append("Imports System.Data" & vbCrLf) 
' Build a little wrapper code, with our passed in code in the middle 
sb.Append("Namespace dValuate" & vbCrLf)
sb.Append("Class EvalRunTime " & vbCrLf)
sb.Append("Public Function EvaluateIt() As Object " & vbCrLf)
' Insert our dynamic code
sb.Append(vbCode & vbCrLf) 
sb.Append("End Function " & vbCrLf)
sb.Append("End Class " & vbCrLf)
sb.Append("End Namespace" & vbCrLf)

CompileAssemblyFromSource()

The call to CompileAssemblyFromSource() passes our CompilerParameters object, and our code to be compiled. If errors are encountered, the CompilerResults.Errors property will have a count greater than 0, and can be iterated through.

 ' Compile and get results 
' 2.0 Framework - Method called from Code Provider
oCResults = oCodeProvider.CompileAssemblyFromSource(oCParams, sb.ToString)
' 1.1 Framework - Method called from CodeCompiler Interface
' cr = oICCompiler.CompileAssemblyFromSource (cp, sb.ToString)
' Check for compile time errors 
If oCResults.Errors.Count <> 0 Then
    Me.CompilerErrors = oCResults.Errors
    Throw New Exception("Compile Errors")
Else
    ' No Errors On Compile, so continue to process...
    oAssy = oCResults.CompiledAssembly
    oExecInstance = oAssy.CreateInstance("dValuate.EvalRunTime")
    oType = oExecInstance.GetType
    oMethodInfo = oType.GetMethod("EvaluateIt")
    oRetObj = oMethodInfo.Invoke(oExecInstance, Nothing)
    Return oRetObj
End If

If no errors are encountered, then the assembly produced from our code can be found in the CompiledAssembly property, and manipulated like any other assembly. I create an instance of my wrapper class (Sandwich) by using the CreateInstance("dValuate.EvalRunTime") method.

It's a two-step dance to get the actual method. We have to GetType() the assembly instance, then GetMethod("EvaluateIt") to get the MethodInfo object. Once we have the MethodInfo object, we can call the Invoke() method to actually "run" the code. Return values are of type Object, which is then bubbled up to the calling process.

Final Thoughts

Comparing this solution to the one that I wrote using VBScript raises the obvious points.

Compile time error handling is one of the strengths of this implementation. In addition, I believe that the runtime error reporting would also be greatly improved.

In general, the error handling and reporting of the .NET languages is far superior to earlier efforts from Microsoft. Anyone who has had a user point to a COM "-214xxxx" error dialog box knows how weak error reporting can make diagnosing sporadic run-time errors next to impossible. Since I live in the Dallas area, which uses the 214 area code, I have had users (more than once) tell me that there was an error "with a phone number on it." and ask me if they were supposed to call the number.

Revision History

Date Author Comment
1-27-2006 rwd Original
1-30-2006 rwd Updated per reader comment. Added a RichTextBox control to the main form to enter the dynamic code. Also added a design position statement after looking at similar articles.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here