Script execution with CodeDOM tutorial

shaul_ahuva
11-15-2005, 11:11 PM
I thought this might make a good tutorial - I spent the better part of a day searching msdn and various blogs while I was trying to resolve problems with compiling code and loading/unloading assemblies.

-----

Please note: This code was created with VS 2005 Beta 2.

The concept of dynamically compiling code can be daunting, but hopefully you will find that it's actually quite simple with this short tutorial that explains how to load, compile, execute, and re-build (if necessary) code contained within unrelated vb files.

In order for code to compile, it must be located within a class. Therefore, the first step is to define a generic class that can execute the code. The easiest solution I have found to do this is to create a template and store it as a resource to be pulled out of the assembly's manifest at runtime and updated.

First we will define an interface for this template class. Defining an interface allows us to execute the code without using Reflection.


''' <summary>
''' Defines classes created from script files.
''' </summary>
''' <remarks></remarks>
Public Interface IScriptWrapper
''' <summary>
''' When implemented, executes the code in the script file.
''' </summary>
''' <remarks></remarks>
Sub Execute()

''' <summary>
''' When implemented, returns the name of the script.
''' </summary>
''' <value></value>
''' <remarks></remarks>
ReadOnly Property Name() As String
End Interface

Next we can create the template that will be modified at runtime:


Imports System
Imports System.Windows.Forms

Namespace Scripts
Public Class {0}
Implements CSEE.Common.IScriptWrapper

Public Sub Execute() Implements CSEE.Common.IScriptWrapper.Execute
{1}
End Sub

Public ReadOnly Property Name() As String Implements CSEE.Common.IScriptWrapper.Name
Get
Return "{0}"
End Get
End Property
End Class
End Namespace

The above template will allow us to "plug in" code loaded from the script files into the Execute method.

Before we continue, a sidenote on the AppDomain class is needed. Every .NET application runs within the context of an AppDomain. A single AppDomain can have an unlimited number of referenced assemblies, and most .NET applications utilize a single AppDomain. There are many reasons why secondary AppDomains can be useful, but our main reason for this tutorial is fairly simple: once an assembly is loaded in an AppDomain it cannot be unloaded. The only way to unload an assembly is to unload the AppDomain in which it is referenced. This presents a problem: how do we call a method without getting a reference to the type the method is implemented on?

Enter the ScriptManager class. The ScriptManager class takes the responsibility of loading, compiling and executing scripts out of the main AppDomain and allows us to place that responsibility within the secondary AppDomain created for our dynamic assembly.

There are two main methods of the ScriptManager class: LoadScripts and Execute.

The general workflow of LoadScripts is:

1) Load the script files from the specified directory.
2) Insert the code from each script file into the template class, and store the combined code as a code source.
3) Using the VBCodeProvider class, compile the code sources into an assembly.
4) Create and store instances of the script classes for future use.


''' <summary>
''' Loads and compiles the scripts.
''' </summary>
''' <param name="scriptFolder">The folder containing the script (vb) files.</param>
''' <remarks></remarks>
Public Sub LoadScripts(ByVal scriptFolder As String)
mScripts = New List(Of IScriptWrapper)

Dim compiler As VBCodeProvider = New VBCodeProvider()
Dim files() As FileInfo = New DirectoryInfo(scriptFolder).GetFiles("*.vb")
Dim names As New List(Of String)

'Add the assembly references we'll need,
'specify the assembly name and indicate we
'want debug symbols generated.
Dim parameters As New CompilerParameters(New String() _
{"System.dll", _
"System.Windows.Forms.dll", _
"Microsoft.VisualBasic.dll", _
"Common.dll"}, "Scripts.dll", True)

Dim results As CompilerResults
Dim sources(files.Length - 1) As String
Dim template As String = My.Resources.Templates.ScriptTemplate

RaiseEvent ScriptEvent(String.Format("Processing scripts in ""{0}""...", scriptFolder))

For i As Integer = 0 To files.Length - 1
RaiseEvent ScriptEvent(String.Format("Processing ""{0}""...", files(i).FullName))

Dim f As FileInfo = files(i)
Dim r As New StreamReader(f.OpenRead())
Dim source As String = r.ReadToEnd()
Dim scriptName As String = f.Name.Substring(0, f.Name.IndexOf(".vb"))

r.Close()

'Replace the placeholders with the script name and code.
sources(i) = String.Format(template, scriptName, source)
names.Add(scriptName)
Next

RaiseEvent ScriptEvent("Compiling ""Scripts.dll""...")

'Compile the assembly.
parameters.MainClass = "Scripts"
parameters.OutputAssembly = "Scripts"
results = compiler.CompileAssemblyFromSource(parameters, sources)

If results.Errors.Count > 0 Then
RaiseEvent ScriptEvent("Errors occurred during compilation:")
For Each e As CompilerError In results.Errors
RaiseEvent ScriptEvent(String.Format("Line ""{0}"": {1}", e.Line.ToString(), e.ErrorText))
Next
ElseIf results.NativeCompilerReturnValue Then
RaiseEvent ScriptEvent("Errors occurred during compilation.")
For Each s As String In results.Output
RaiseEvent ScriptEvent(s)
Next
Else
'Create an instance of each script class.
For Each n As String In names
mScripts.Add(DirectCast(AppDomain.CurrentDomain.CreateInstanceAndUnwra p("Scripts", "Scripts." & n), _
IScriptWrapper))
Next

RaiseEvent ScriptEvent("""Scripts.dll"" compiled successfully.")
End If
End Sub


The Execute method is fairly simple as there is only one thing to do...execute the scripts :)


''' <summary>
''' Executes the scripts.
''' </summary>
''' <remarks></remarks>
Public Sub ExecuteScripts()
For Each w As IScriptWrapper In mScripts
RaiseEvent ScriptEvent(String.Format("Executing script ""{0}""...", w.Name))

Try
w.Execute()
Catch ex As Exception
RaiseEvent ScriptEvent(String.Format("An exception of type ""{0}"" occurred while executing script ""{1}"".", _
ex.GetType().ToString(), w.Name))
End Try
Next
End Sub

Now that the infrastructure is in place, we need to add a simple application to create and execute scripts in a second AppDomain. First, we will create the AppDomain and specify that we want the assemblies to be shadow copied. This option will allow us to rebuild the assembly without restarting the application.


mScripts = AppDomain.CreateDomain("Scripts", _
AppDomain.CurrentDomain.Evidence, _
AppDomain.CurrentDomain.BaseDirectory, _
AppDomain.CurrentDomain.RelativeSearchPath, True)

Then we need to create an instance of the ScriptManager class in the mScripts AppDomain.

*** NOTE *** This is critical if you plan on rebuilding the assembly. If the main AppDomain EVER gets the type information for any types within the scripts assembly, an assembly reference will be added to the main AppDomain and we will be unable to unload the scripts assembly.


mManager = DirectCast(mScripts.CreateInstanceAndUnwrap("Common", _
"CSEE.Common.ScriptManager"), ScriptManager)

The ScriptManager class inherits from MarshalByRef, which essentially allows the primary AppDomain to work with a proxy of the actual ScriptManager instance in the second AppDomain.

The only things left to do are to load and compile the scripts, which are done quite easily:


mManager.LoadScripts(lblRepository.Text)
mManager.ExecuteScripts()

I hope that this tutorial and the accompanying code will help some of you out there - I know I spent a good bit of time troubleshooting various problems with compiling code on the fly and loading/unloading assemblies.

EZ Archive Ads Plugin for vBulletin Copyright 2006 Computer Help Forum