Monday, November 13, 2006

Executing Dynamic Code in .Net

Dynamically executing code in .Net

By Rick Strahl

http://www.west-wind.com/

Last Update: September 8th, 2002

Code for this article:

http://www.west-wind.com/presentations/dynamicCode/dynamicCode.zip

If you find this article useful, consider making a small donation to show your support for this Web site and its content.

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:

  1. Create or read in the code that is to be executed as a string
  2. 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
  3. Compile the source code into an assembly
  4. Check for errors on compilation
  5. Use the assembly reference to create an instance of the object
  6. Call the specified method on the instance reference returned using Reflection
  7. 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

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"); }

}

Assemblies and Namespaces

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.

What's an Application Domain?

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:

  1. Create a new AppDomain
  2. Dynamically create the dynamic assembly and store on disk
  3. 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.
  4. 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.
  5. 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:

  1. 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.
  2. Create an AppDomain.
  3. Get a reference to IRemoteInterface.
  4. 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 code

CreateInstance

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.

wwScripting and custom code

Why do we need a script parser?

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 wasn’t 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.

No comments:

Post a Comment