|
Dynamically Executing Code in .NET
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.
Learn what it takes to execute code dynamically with the .NET Framework and
create a class that simplifies these tasks by wrapping the details of the
process in an easy-to-use interface that requires only a few lines of code.
by Rick Strahl xecuting code dynamically at runtime is a
powerful tool to allow users to customize applications after deployment. .NET
provides all the tools that make it possible to build code on the fly, compile
it and run it dynamically.
The code begins by creating various objects that are required for compilation. It then uses the CompilerParameters object to add any assembly references 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. This is one of the 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 source code for an assembly. This example makes a few assumptions about the code in that it presets the method parameter and return value signature as:
So a block of code MUST return a value of type object or null. It can also
accept any number of parameters that can be referenced via the Parameters
collection. A simple example of a string to execute might be.
If you wanted to access parameters dynamically instead you might do this:
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:
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. You can retrieve compile errors via the HasErrors
property and Error collection. If there were no errors you get a reference to
the Assembly in CompiledAssembly property from which
you can call CreateInstance() to create 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 through direct referencing. The compiler has no idea of the type at compile time, but must delay creation and type info until runtime. So when I called CreateInstance an object of type Object is returned and I 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 Visual C# .NET as the dynamic code language here. You can also use Visual Basic 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 show here. The class I'll present later provides the ability to execute both 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 control over the compile and execution process. However, 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
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. This Interface is then used to make passthrough calls on the methods of the dynamic object. The code to generate the full assembly looks like this:
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 I'll show you how to create 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 that you have a local reference to the type information. 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 your client code you need to do the following:
The key differences are loading the AppDomain and how you retrieve the actual reference to the remote object. The critical code that performs the difficult tasks is summarized in:
This code retrieves a reference to a proxy. RemoteLoader 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 the Interface is defined locally (through the DLL reference), simply
call the Invoke() method published by the Interface
directly.Creating an AppDomain, loading assemblies into it, making remote calls, and finally shutting the domain down does incur 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 to call 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 application each time you need to execute code dynamically. It's reasonably easy to abstract all of this code into a class. You can find the code in the wwScript.cs source file and in the Westwind.Tools.Scripting namespace with the wwScripting class. The class provides the following features:
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:
You can then run with this code:
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:
You can then call the two methods like this:
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 (see Table 1 and Table 2). Table 1: Low-level methods of the wwScripting object.
Table 2: Low-level properties of the wwScripting object.
Building an ASP-like Script Parser
It isn't 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. If you look closer at Figure 2 you can see 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 keywords to allow importing namespaces and assembly files for linking. To include assemblies and namespaces you can use directives like this:
I put together a separate class, wwASPScripting, to handle parsing strings
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.Figure 6 shows the code to accomplish parsing a template page. The key and new feature of this code is the ParseScript method that basically turns the ASP-style code seen in Figure 2 into runnable C# code that is then passed to the wwScripting class to dynamically execute. The wwASPScripting class is only a first shot and doesn't do more than parse. It has a private implementation of a Response object that is used to write output into the output stream. The wwASPScripting class natively uses a string builder to allow output to be sent to a string or stream. 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. I am amazed 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 various newsgroups!). 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 once you can use the wrapper classes. Well, easy may be a little overstated. This whole exercise requires deployment of two DLLs in your applications—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. I hope these classes and this discussion help you understand how to run dynamic code in .NET. I 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 cannot live without this capability in my applications. Get the Code from this Article |
Copyright 2002 - 2003 XOCOMP, llc All materials and notes are the intellectual property of XOCOMP with expressed permission for use by PROTOCOL. |