White Papers Home | White Papers | Message Board | Search | Products | Purchase | News | |
Dynamically executing code in .Net
By Rick Strahl
Last Update: September 8th, 2002
Code for this article:
http://www.west-wind.com/presentations/dynamicCode/dynamicCode.zip
Dynamic code execution is a powerful feature that allows applications to be extended with code that is not compiled into the application. Users can customize applications and developers can dynamically update code easily. In this article, Rick takes a look what it takes to execute code dynamically with the .Net framework and introduces a class that simplifies the tasks by wrapping the details of the process in an easy to use interface that only requires a few lines of code.
I come from an xBase background using Visual FoxPro for many years. One of the nice features of xBase has always been the ability to dynamically execute code in applications. In the past, developers have often snubbed me and xBase in general for this capability to use 'macros' (as they would say with disdain), partially because in the early days of xBase macros where used for a number of kludges required to make the language work correctly. Its use often resulted in highly unreadable code or worse spaghetti code that was difficult to debug and was very slow. However, in recent years use of dynamic code has found a much more widespread audience as mechanisms for building user extensions and providing custom user defined interfaces and templates for representing data. I can hardly imagine an application where there's not a good use to be made of code stored externally either for presentation purpose or for extending the application by end users.
Dynamic code execution is a very powerful thing not to be underestimated! With it you can perform a number of decisions at runtime about which code to run and most importantly allow users to customize and extend existing applications via custom code. Dynamic code execution is also very important for things like templating of code into things like HTML, merge documents (mail merge) or custom document solutions. For example, a scripting engine front end like ASP or ASP.Net, actually rely on the ability to dynamically read code from disk and execute it on the fly. Dynamic code execution is also a feature that is notoriously absent from compiled languages and required specialty tools like the Microsoft Scripting ActiveX control to execute code.
The good news is that .Net provides full control over dynamic code execution natively via the .Net SDK classes. The bad news is that the process is not nearly as trivial as it was in Visual FoxPro (where Evaluate() and ExecScript() are single commands that perform most dynamic code tasks). It requires a fair amount of code to accomplish something similar and it requires dealingl with some architectural issues of the way .Net loads code into applications. But in exchange .Net provides a lot of flexibility in using dynamic code with full control over the entire process including compilation, error reporting and loading of objects and controlling the environment of dynamic code.
In this article, I'll introduce what it takes to dynamically compile and execute code as well as discussing some of the more intricate requirements that most applications will need to consider when running lots of code or constantly changing code dynamically. I'll also introduce a class that simplifies the process of executing code to a few lines and then demonstrate that class by building the beginnings of a free standing ASP like script parser that you can plug into your own desktop applications.
Compiling code on the fly
.Net provides powerful access to the IL code generation process through the System.CodeDom.Compiler and Microsoft.CSharp and Microsoft.VisualBasic namespaces. In these namespaces you find the tools that allow you to compile an assembly either to disk or into memory. You also need the Reflection namespace as it contains the tools to invoke an object and its methods once you've compiled the object.
In the following example, I demonstrate how to execute an arbitrary block of code. The code is free standing and has no dependencies. The process to execute this code dynamically involves the following steps:
- Create or read in the code that is to be executed as a string
- Wrap the code into fully functional assembly source code, which includes namespace references (using commands), a namespace and a class that is to be invoked
- Compile the source code into an assembly
- Check for errors on compilation
- Use the assembly reference to create an instance of the object
- Call the specified method on the instance reference returned using Reflection
- Handle any return value from the method call by casting into the proper type
Figure 1 This sample form demonstrates how to execute code from the top text box dynamically.
The example shown in Listing 1 demonstrates the code to perform these steps. Figure 1 shows an example of the form that utilizes this code. Please note that there's only minimal error handling provided in most code snippets for brevity's sake.
Listing 1: (basicExecution.cs) Basic code to execute code on the fly
using System.Reflection;
using System.CodeDom.Compiler;
using Microsoft.CSharp; // and/or Microsoft.VisualBasic;
private void button1_Click(object sender, System.EventArgs e)
{
// *** Example form input has code in a text box
string lcCode = this.txtCode.Text;
ICodeCompiler loCompiler = new CSharpCodeProvider().CreateCompiler();
CompilerParameters loParameters = new CompilerParameters();
// *** Start by adding any referenced assemblies
loParameters.ReferencedAssemblies.Add("System.dll");
loParameters.ReferencedAssemblies.Add("System.Windows.Forms.dll");
// *** Must create a fully functional assembly as a string
lcCode = @"using System;
using System.IO;
using System.Windows.Forms;
namespace MyNamespace {
public class MyClass {
public object DynamicCode(params object[] Parameters) {
" + lcCode +
"} } }";
// *** Load the resulting assembly into memory
loParameters.GenerateInMemory = false;
// *** Now compile the whole thing
CompilerResults loCompiled =
loCompiler.CompileAssemblyFromSource(loParameters,lcCode);
if (loCompiled.Errors.HasErrors) {
string lcErrorMsg = "";
lcErrorMsg = loCompiled.Errors.Count.ToString() + " Errors:";
for (int x=0;x<loCompiled.Errors.Count;x++)
lcErrorMsg = lcErrorMsg + "\r\nLine: " +
loCompiled.Errors[x].Line.ToString() + " - " +
loCompiled.Errors[x].ErrorText;
MessageBox.Show(lcErrorMsg + "\r\n\r\n" + lcCode,
"Compiler Demo");
return;
}
Assembly loAssembly = loCompiled.CompiledAssembly;
// *** Retrieve an obj ref generic type only
object loObject = loAssembly.CreateInstance("MyNamespace.MyClass");
if (loObject == null) {
MessageBox.Show("Couldn't load class.");
return;
}
object[] loCodeParms = new object[1];
loCodeParms[0] = "West Wind Technologies";
try {
object loResult = loObject.GetType().InvokeMember(
"DynamicCode",BindingFlags.InvokeMethod,
null,loObject,loCodeParms);
DateTime ltNow = (DateTime) loResult;
MessageBox.Show("Method Call Result:\r\n\r\n" +
loResult.ToString(),"Compiler Demo");
}
catch(Exception loError) {
MessageBox.Show(loError.Message,"Compiler Demo"); }
}
In order for code to compile properly it needs both a physical reference to an assembly and any namespace references in the source code. Both are required for the compiler to be able to properly identify and validate your types in the compiled code. You need to provide physical assemblies via the CompilerParameters.ReferencedAssemblies propety. It points to a physical DLL or EXE that contains the types you want to include in your code. You also have to add the appropriate namespace imports to your source code which means using (CSharp) or import (VB) commands for each namespace used. Remember that you may have more than one namespace you need to include for a single physical assembly. This can become cumbersome if you want build generic code as the namespace directives tend to be placed at the top of the document. The wwScripting class handles this with an AddAssembly and AddNamespace methods that handle adding the appropriate assemblies and delays creating of the assembly source code until the final compilation.
The code begins by creating various objects that will be required for compilation. It then uses the CompilerParameters object to add any assembly references that will be required during compilation. These are the physical DLLs that are required and are the equivalent of what you add in the VS.Net project References section. Note that it's very important that every reference is included or you will get compiler errors. As we'll see this is one of the more tricky parts about dynamic code compilation as this step must occur in your application code. Here the Windows Forms assemblies are included to allow using the MessageBox object to display output.
The next step is to generate the complete sourcecode for an assembly. This example makes a few assumptions about the code in that it presets the method parameter and return value signature as:
public object DynamicCode(parms object[] Parameters);
So a block of code MUST return a value of type object or null. It can also accept any number of parameters which can be referenced via the parameters collection. A simple example of a string to execute might be.
string cName = "Rick";
MessageBox.Show("Hello World" + cName);
return (object) DateTime.Now;
If you wanted to access parameters dynamically instead you might do:
string cName = (string) Parameters[0];
Note that you should cast parameters explicitly to the specific type since the object parameter is generic. You can also return any value as long as you cast it to an object type.
This code is now fixed up into an assembly by adding namespace, class and method headers. The final generated code that gets compiled looks like this:
using System.IO;
using System;
using System.Windows.Forms;
namespace MyNamespace {
public class MyClass {
public object DynamicCode(parms object[] Parameters) {
string cName = "Rick";
MessageBox.Show("Hello World" + cName);
return (object) DateTime.Now;
}
}
}
This code can now be compiled into an assembly by using the CompileAssemblyFromSource() method of the CodeCompiler. The CompilerResults object receives information about the result: Error info if the compilation failed can be retrieved via the HasErrors property and then the Error collection which contains detailed information about each error that occurred during compilation. If there were no errors you get a reference to the Assembly in CompiledAssembly property from which you can call CreateInstance() to get a live instance of the MyClass class.
This is where Reflection comes in: Because we've basically created a .Net type on the fly the object reference and all method access must occur dynamically rather than direct referencing. This is because the compiler has no idea of the type at compile time, but must delay creation and type info until runtime. So when we call CreateInstance an object of type object is returned and we have to use Reflection and InvokeMember to call a method on the object indirectly.
The actual call to the object method then proceeds and returns a reference to a generic object type (much like a variant). This type can contain data of any type and I suggest that you immediately cast the return type to an explicit type if possible. Notice also the error handling around the InvokeMember call this is fairly crucial as it protects the calling application from any runtime errors that occur in the dynamic code.
I've demonstrated this technique by using CSharp as the dynamic code language here. You can also use VisualBasic by using the Microsoft.VisualBasic namespace and using the VBCodeProvider class instead to instantiate the loCompiler object. Of course, you'd have to change the assembly source code to VB syntax in addition to the actual dynamic code I showed here. The class I'll present later provides the ability to both execute C# and VB code by setting a language property.
As I mentioned at the start of this article - .Net provides a lot of functionality and full control over the compile and execution process but this is a lot of code to have to integrate into an application each time you want to execute dynamic code. To make life easier I've created a class that simplifies the process considerably and aids in handling errors and debugging the code should errors occur.
Understanding how .Net loads code
Before I dive into the class however I need to discuss the important subject of Application Domains and how they behave when assemblies are loaded. Application Domains are the highest level isolated instances of the .Net runtime that host application code and data. Assemblies get loaded into a specific application domain and execute and use resources in it.
With application domains .Net provides a hosting container inside of a running process that isolates code and data into separate sub applications or domains. In simplistic terms you can think of an Application Domain as a Process within a process that provides the ability to isolate different sets of code. Code and data available in one application domain within a single process is not directly accessible from another application domain. You can however access it through special code designed to instantiate and access types in other domains.
Application domains are useful for providing specific context to an application component. They are also necessary, as described in this article, if you need to unload assemblies at any point. Since an assembly loaded into an AppDomain can never be unloaded from within the domain the only way to release them is to unload the entire domain.
Accessing Code and Data in a different domain requires the use the .Net Remoting framework which provides mechanisms using proxies and stubs that are very similar to the way DCOM operates. These mechanisms allow access to AppDomains in the same process, in a different process and across machine and network boundaries.
When you normally run a .Net application, .Net simply loads each assembly on your references list into the application's primary Application Domain (see sidebar 'What's an Application Domain?'). No problem there you want all code to load into this domain and stay loaded there. So if there's code that dynamically uses the JIT compiler to compile code the code will remain in the app domain cached and compiled so only first access is relatively slow.
So far, so good. But here's the rub in our dynamic code execution scheme: Application Domains load assemblies, but they cannot unload them! If you're only loading a handful of assemblies this won't be a problem, but often times when you run dynamic code it's quite possible that you will create a lot of snippets that need to run and compile independently then essentially throw them away. For example, I have a Desktop application that uses templates on disk to hold HTML mixed with .Net code. The application merges the content of a database record (actually an object view it) into the template. The documents are merged on the fly and only on an as needed basis as they are frequently updated. This system can have thousands of entries and pretty much each of these pages has to compile separately.
If you run the demo above in a loop for 10 20 times you will notice that memory usage increases with each instance of creating and releasing an assembly by a few K each time depending on the size of the assembly and its related referenced assemblies. Once loaded none of that space can be unloaded again if the assembly is loaded into the current main application's AppDomain.
So what to do? Unfortunately there's no simple answer - only a convoluted one. The answer is to create a new Application Domain and load our dynamic assemblies into that. We can either load into this AppDomain, run our code and unload it, or alternately run all of our dynamic code into the new domain and kill it later or when it reaches a certain number of executions or other metric. This process unfortunately is not trivial and requires the use of an intermediary proxy object that can be used to invoke a method in a remote app domain without referencing the object in the local application domain in any way (which again would lock the assembly into the local AppDomain). The process here is essentially the same as invoking a remote object over the network along with all the same complications.
Creating code in alternate AppDomains
Loading an assembly and creating a class instance from it in a different application domain involves the following steps:
- Create a new AppDomain
- Dynamically create the dynamic assembly and store on disk
- Create a separate assembly that acts as an object factory and returns an Interface rather than a physical object reference. This assembly can be generic and is reusable but must be a separate DLL from the rest of the application.
- Create an object reference using AppDomain::CreateInstance and then call a method to return the remote interface. Note the important point here is that an interface not an object reference is returned.
- Use the Interface to call into the remote object indirectly using a custom method that performs the passthrough calls to the remote object.
The whole point of this convoluted exercise is to load the object into another AppDomain and access it without using any of the object's type information. Accessing type information via Reflection forces an assembly to load into the local AppDomain and this exactly what we want to avoid. By using a proxy that only publishes an interface and thus loads only a single assembly that publishes this generic interface.
For the dynamic code execution class I'm going to create a very simple interface (shown in Listing 2) that can simply Invoke a method of the object.
Listing 2 (RemoteLoader.cs): Proxy interface used to access for AppDomain loading
/// Interface that can be run over the remote AppDomain boundary.
public interface IRemoteInterface
{ object Invoke(string lcMethod,object[] Parameters); }
This interface is then used to make pass through calls on the methods of the dynamic object. The code we now use to generate the full assembly looks like this:
using System.IO;
using System;
using System.Windows.Forms;
namespace MyNamespace {
public class MyClass:MarshalByRefObject,IRemoteInterface {
public object Invoke(string lcMethod,object[] Parameters) {
return this.GetType.InvokeMember(lcMethod,
BindingFlags.InvokeMethod,null,this,Parameters);
}
public object DynamicCode(parms object[] Parameters) {
string cName = "Rick";
MessageBox.Show("Hello World" + cName);
return (object) DateTime.Now;
} } }
By doing this we're deferring the type determination via Reflection into the class itself. Note that the class must also derive from MarshalRefObject which provides the access to data across application domain boundaries (and .Net remoting boundaries) using proxies.
In addition to the interface we'll also need a proxy loader object that acts as an Interface factory: It creates an instance reference to the remote object by returning only an interface to the client. Listing 3 shows the code for this single method class that returns an interface pointer against which we can call the Invoke method across domain boundaries without requiring to have a local reference to the type information.
Listing 3 (RemoteLoader.cs): Proxy loader class that returns an interface ref
using System;
using System.Reflection;
public class RemoteLoaderFactory : MarshalByRefObject
{
private const BindingFlags bfi =
BindingFlags.Instance | BindingFlags.Public |
BindingFlags.CreateInstance;
public IRemoteInterface Create(string assemblyFile,string typeName,
object[] constructArgs ) {
return (IRemoteInterface) Activator.CreateInstanceFrom(
assemblyFile, typeName, false, bfi, null, constructArgs,
null, null, null ).Unwrap();
} }
This class and the IRemoteInterface should be compiled into a separate, lightweight DLL so it can be accessed by the dynamic code for the interface. Both the client code and the dynamic code must link to the RemoteLoader.dll as both need access to IRemoteInterface.
To use all of this in our client code we need to do the following:
- Compile our DLL to disk you can't load the assembly from memory into the other appdomain unless you run the entire compilation process in the other appdomain.
- Create an AppDomain.
- Get a reference to IRemoteInterface.
- Call the Invoke method to make the remote method call.
The revised code that loads an app domain, compiles the code, runs it and unloads the appdomain is shown in Listing 4. Revisions from the previous version are highlighted in blue.
Listing 4 (BasicExecution.cs): Running code dynamically in an AppDomain
using System.Reflection;
using System.CodeDom.Compiler;
using Microsoft.CSharp;
using System.Reflection;
using Westwind.RemoteLoader; // add reference too!
string lcCode = this.txtCode.Text;
// ** Create an AppDomain
AppDomainSetup loSetup = new AppDomainSetup();
loSetup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;
AppDomain loAppDomain = AppDomain.CreateDomain("MyAppDomain",null,loSetup);
// *** Must create a fully functional assembly code
lcCode = @"using System;
using System.IO;
using System.Windows.Forms;
using System.Reflection;
using Westwind.RemoteLoader;
namespace MyNamespace {
public class MyClass : MarshalByRefObject,IRemoteInterface {
public object Invoke(string lcMethod,object[] Parameters) {
return this.GetType().InvokeMember(lcMethod,
BindingFlags.InvokeMethod,null,this,Parameters);
}
public object DynamicCode(params object[] Parameters) {
" + lcCode +
"} } }";
ICodeCompiler loCompiler = new CSharpCodeProvider().CreateCompiler();
CompilerParameters loParameters = new CompilerParameters();
// *** Start by adding any referenced assemblies
loParameters.ReferencedAssemblies.Add("System.dll");
loParameters.ReferencedAssemblies.Add("System.Windows.Forms.dll");
// *** Important that this gets loaded or the interface won't work!
loParameters.ReferencedAssemblies.Add("Remoteloader.dll");
// *** Load the resulting assembly into memory
loParameters.GenerateInMemory = false;
loParameters.OutputAssembly = "MyNamespace.dll";
// *** Now compile the whole thing
CompilerResults loCompiled =
loCompiler.CompileAssemblyFromSource(loParameters,lcCode);
if (loCompiled.Errors.HasErrors) {
...
return;
}
this.txtAssemblyCode.Text = lcCode;
// create the factory class in the secondary app-domain
RemoteLoaderFactory factory =
(RemoteLoaderFactory) loAppDomain.CreateInstance( "RemoteLoader",
"Westwind.RemoteLoader.RemoteLoaderFactory" ).Unwrap();
// with help of factory, create a real 'LiveClass' instance
object loObject = factory.Create("mynamespace.dll",
"MyNamespace.MyClass", null );
// *** Cast object to remote interface, avoid loading type info
IRemoteInterface loRemote = (IRemoteInterface) loObject;
if (loObject == null) {
MessageBox.Show("Couldn't load class.");
return;
}
object[] loCodeParms = new object[1];
loCodeParms[0] = "West Wind Technologies";
try {
// *** Indirectly call the remote interface
object loResult = loRemote.Invoke("DynamicCode",loCodeParms);
DateTime ltNow = (DateTime) loResult;
MessageBox.Show("Method Call Result:\r\n\"+loResult.ToString())
}
catch(Exception loError)
{ MessageBox.Show(loError.Message,"Compiler Demo"); }
loRemote = null;
AppDomain.Unload(loAppDomain);
loAppDomain = null;
// *** Delete the generated code DLL when done
File.Delete("mynamespace.dll");
The key differences are the loading of the AppDomain, and how the actual reference to the remote object is retrieved. The critical code that performs the difficult tasks is summarized in:
RemoteLoaderFactory factory =
(RemoteLoaderFactory) loAppDomain.CreateInstance( "RemoteLoader",
"Westwind.RemoteLoader.RemoteLoaderFactory" ).Unwrap();
// *** create interface reference from assembly
object loObject = factory.Create( "mynamespace.dll",
"MyNamespace.MyClass", null );
// *** Cast object to remote interface, to avoid loading type info
IRemoteInterface loRemote = (IRemoteInterface) loObject;
// *** Call the DynamicCode method with no parms
object loResult = loRemote.Invoke("DynamicCode",null);
This code gets a reference to a proxy. RemoteLoader actually loads the object in the remote appdomain and passes back the interface pointer. The interface then talks to the remote appdomain proxy to pass and retrieve the actual data. Because we do have the interface defined locally (through the DLL reference) we can simply call the Invoke() method published by the interface directly.
As you might expect all of this clowning around with creating an AppDomain, loading assemblies into, making remote calls and finally shutting the domain down again causes some overhead. Operation of this mechanism compared to running an assembly in process is noticeably slower.
However, you can optimize this a little by creating an application domain only once and then loading multiple assemblies into it. Alternately you can create one large assembly with many methods that are to be called and simply hang on to the application domain as long as needed. Still even without creating and deleting the domain operation is slower because of the proxy/remoting overhead.
Making life easier with wwScripting
There's a lot of power in all of that code it shows how much flexibility there is in the .Net framework, but you certainly wouldn't want to put all of that code into your app each time you need to execute dynamically. It's reasonably easy to abstract all of this code into a class. You can find the code to such a class in the wwScript.cs source file and in the Westwind.Tools.Scripting namespace with the wwScripting class.
The class provides the following features:
- Transparent execution of C# and CSharp code
- Execution in the current AppDomain or via external AppDomains for shutdowns
- Error handling
- High level and low level methods
With the class running dynamic code gets a bit easier as shown in Listing 5.
Listing 5 (wwScriptingForm.cs): Using the wwScripting class to execute code dynamically
using Westwind.Tools.Scripting;
// *** Specify language: CSharp or VB
wwScripting loScript = new wwScripting("CSharp");
// *** get the source code from a text box
string lcCode = this.txtCode.Text;
// *** Optionally save the generated code for review
loScript.lSaveSourceCode = true;
// *** add any assemblies and namespaces required
loScript.AddAssembly("system.windows.forms.dll",
"System.Windows.Forms");
loScript.AddNamespace("System.IO");
// *** Execute the actual code with 3 parameters
lcResult = (string) loScript.ExecuteCode(lcCode,
"rick strahl",(int) x,(decimal) 10 );
if (loScript.bError)
MessageBox.Show(loScript.cErrorMsg + "\r\n\r\n" +
loScript.cSourceCode);
else
MessageBox.Show(lcResult);
loScript.Dispose() // *** force release of resources
If you want to load the code into a different AppDomain call the CreateAppDomain("Name") method before the ExecuteCode() method call.
The class also includes several methods for executing code. For example, ExecuteMethod() allows you to provide a full method including the signature defining parameters and return values. This makes it possible to create properly typed parameters and return values. For example take a code snippet like this:
public string Test(string lcName, int x) {
string cHello;
cHello = lcName;
MessageBox.Show(cHello,"Compiler Demo");
return DateTime.Now.ToString();
}
which you can then run with this code:
string lcResult = (string) loScript.ExecuteMethod(lcCode,"Test","rick strahl",x);
Notice that you can access the parameters directly by name in the dynamic code snippet. It's a little cleaner if you pass parameter and return values this way. You can also pass multiple methods as a string:
public string Test(string lcName, int x) {
string cHello;
cHello = lcName;
MessageBox.Show(cHello,"Compiler Demo");
return DateTime.Now.ToString();
}
public string Test2(string lcName, int x) {
return Test(lcName,x);
}
You can then call the two methods like this:
string lcResult = (string) loScript.ExecuteMethod(
lcCode,"Test","rick strahl",(int) x);
lcResult = (string) loScript.CallMethod(loScript.oObjRef,
"Test2","rick strahl",(int) x);
Note that making the second call is rather more efficient because the object already exists and is loaded. No recompilation or regeneration occurs on this second call.
CallMethod() is one of the lower level methods of the class. With it you can perform each step of the compile process individually. A number of other low level methods are available:
Low Level Method
Function
Parameters
CompileAssembly
Compiles an assembly and hold an internal pointer to the assembly object (only if locally loaded app domains are handled from disk).
lcSource
Source codeCreateInstance
Creates an instance of the compiled code either in the local or a remote AppDomain. Sets the oObjRef property with the reference to the object or interface.
None
Uses internal references to the Assembly or the name of the DLL file to load into an AppDomain.CallMethod
Executes a method by name using the oObjRef pointer. Knows about local or remote AppDomain.
lcMethod
The method to call.
Parameters()
A variable list of parameters from 0 to n.CreateAppDomain
Creates an AppDomain and forces CreateInstance and CallMethod to use that domain to load and execute code in.
lcAppDomainName
Name of the domain
Dispose
Cleans up and releases references.
None
Property
Function
bError
Error Flag that should be checked after making calls before using any results.
cErrorMsg
Contains error information either after compiling or running code.
lSaveSourceCode
Determines whether the code that is finally compiled is saved. Full assembly source code.
cSourceCode
Set before compilation if lSaveSourceCode is true.
oObjRef
After a successful method execution (or after calling CreateInstance) this property contains an instance of the dynamic object.
cAssemblyNamespace
Name of the namespace that the code is generated into. This is used to generate the assembly and then used again when the class is instantiated to reference the type.
cClassname
Same as cAssemblyNamespace
lDefaultAssemblies
Determines if certain assemblies and namespaces are loaded by default. Loads System, System.IO, System.Reflection.
The provided wwScripting class provides a very high level wrapper around dynamic code execution specifically to execute a code snippet or a single method. But if you choose you can also add multiple methods or create an entire assembly's source code manually and then let wwScripting handle compilation and passing a reference to you. You can then execute code within it including the ability to easily call multiple methods and set properties. The following slightly more low level code (minus error handling) demonstrates how to compile code, create an instance of it and call a couple of methods:
loScript = new wwScripting("CSharp")
// loScript.CreateAppDomain("MyAppDomain")
// Add any assemblies referenced
loScript.AddAssembly("System.Net");
loScript.CompileAssembly(lcAssemblySource)
object loObjRef = loScript.CreateInstance();
// *** Call the method with parameters
object loResult =
loScript.CallMethod(loObjRef,"Helloworld",
"Parm1",2)
string lcResult = (string)
loScript.CallMethod(loObjRef,"SecondMeth",
"Parm1",2)
loScript.Dispose()
This approach might be used to compile lots of code snippets together in a single class. This might work well in scenarios where code gets precompiled at startup of an application or on first execution. The key then would be to find a good strategy for naming methods of the class.
It also works to easily build a .Net script engine. You can create a simple Exe that takes a file name as input and automatically compiles and runs these files from disk as is without precompilation. Control the OS with this for example.
After all .Net includes the powerful ASP.Net parser. In fact, behind the scenes ASP.Net likely uses similar technology to parse and create code on the fly from ASP.Net template pages. Unfortunately the parser that ASP.Net uses is available only for Web apps not generically for general applications.
In my own applications I use template processing extensively, mostly for HTML generation or other output generation. Code generation as well. They also come in very handy in document management systems where fields from objects or databases need to be merged with common, frequently changing templates. For example, have an HTML Help generation application that uses this approach to manage each topic which is generated by merging the content of a help topic into an HTML template. The merged document can then be previewed or stored to disk for Help file compilation. I've also used templates frequently to generate code from Wizard input.
Building an ASP like script parser
To show you how useful dynamic code execution is and how little code it takes to build powerful functionality, I've included another class wwASPScripting and a small sample app that demonstrates it with the source code. It's basically a simple ASP template parser you can use in your own non-Web applications. In .Net we already have ASP.Net which is very powerful at parsing content from templates into HTML or any other output. It works well for the Web, but unfortunately the scripting engine is completely tied to the HTTP engine and so you can't use this same kind of templating in your own non-Web code.
While ASP.Net is extremely powerful and easy use it only works with Web
However, it's not too difficult to build a basic parser that can handle this task. Take a look at Figure 2, which shows both the generated C# code and the output.
Figure 2 The wwASPScripting class in conjunction with the wwScripting class can run C# based script code that works with basic ASP syntax.
If you look closer at Figure 2 you can see that what happens behind the scenes. The HTML template is turned into C# source code. The parser simply runs through the page finding all of the <% tags and inserts the appropriate Response.Write() or Response.oSb.Append() commands. Non tagged text is expanded into strings delimited with quotes. As a special case the <%@ %> directive handles Assembly and Import keyword to allow importing namespaces and assembly files for linking. To include assemblies and namespaces you can use directives like this:
<%@ Assembly name="System.Windows.Forms.dll"%>
<%@ Import namespace="System.Windows.Forms"%>
I put a separate class wwASPScripting together to handle the parsing of a string into C# code. it's only a demo and provides rudimentary functionality a first stab. This parser also only handles C# code at this time as VB code would require generating code quite differently and my VB skills lack a bit in that department.
The code to accomplish parsing of a template page then looks like this:
Listing 6 (wwAspscriptingForm.cs): Executing a C#/ASP template
// *** Use the script object to parse the template
// *** into runnable code
wwASPScripting oASP = new wwASPScripting();
string lcCode = oASP.ParseScript(this.txtCode.Text);
// *** Access the built-in Script processor will have
// *** Namespaces loaded from <%@ %> directives
wwScripting loScript = oASP.oScript;
loScript.lSaveSourceCode = true;
// loScript.CreateAppDomain("wwScriptDomain");
string lcResult = (string) loScript.ExecuteCode(lcCode);
if (loScript.bError)
MessageBox.Show(loScript.cErrorMsg + "\r\n\r\n" + loScript.cSourceCode);
else
{
MessageBox.Show(lcResult,"Script Output");
MessageBox.Show(loScript.cSourceCode,"Generated Assembly Source Code");
}
loScript.Dispose();
The key and new feature of this code is the ParseScript method which basically turns the ASP style code seen in Figure 2 into runnable C# code which is then passed to the wwScripting class to dynamically execute.
The wwASPScripting class is only a first shot and doesn't do much else than parse. But it has a private implementation of a Response object that is used to write output into the output stream which natively uses a string build. The ParseScript method is rather short and you can review the source code of how the code conversion is performed in the wwAspScripting.cs source file included with the downloadable code.
You're so dynamic!
It is interesting how .Net allows you to run dynamic code essentially it provides you all the tools that a compiler uses to generate an executable. If you want to get even more low level you can use the System.Reflection.Emit namespace to generate IL level code directly. What is amazing is how little overall code this mechanism requires even if coming up with that code wasnt quite so trivial digging through the .Net docs (and help from several people on the newsgroups especially!). It's also interesting to see how to apply this technology and build a custom script parser with even less code. The process is relatively easy and straightforward especially once the wrapper classes can be utilized. Well, easy may be overstated. This whole exercise requires deployment of two DLLs in your apps the wwScripting dll that holds both the code execution and scripting classes as well as the remote loader DLL required to handle the AppDomain proxy interface. But regardless I hope these classes and this discussion have helped you understand how you can run dynamic code in .Net. I know I've learned a lot about how .Net works under the covers and I hope this article and the provided helper classes are useful to you in extending your applications with dynamic code. I know I couldn't live without this capability in my applications
As usual if you have any questions or comments, please comment on the Code Message board at:
http://www.west-wind.com/wwthreads/default.asp?Forum=Code+Magazine. Source code for this article is available at: http://www.west-wind.com/presentations/dynamicCode/dynamicCode.zip.
Rick Strahl is president of West Wind Technologies on Maui, Hawaii. The company specializes in Web and distributed application development and tools with focus on Windows 2000, ISAPI, Visual FoxPro, .NET and Visual Studio. Rick is author of West Wind Web Connection, a powerful and widely used Web application framework for Visual FoxPro and West Wind HTML Help Builder. He's also a Microsoft Most Valuable Professional, and a frequent contributor to magazines and books. He is co-publisher and co-editor of CoDe magazine, and his book, "Internet Applications with Visual FoxPro 6.0", is published by Hentzenwerke Publishing. For more information please visit: http://www.west-wind.com/.
White Papers Home | White Papers | Message Board | Search | Products | Purchase | News | |