Login | Register   
LinkedIn
Google+
Twitter
RSS Feed
Download our iPhone app
TODAY'S HEADLINES  |   ARTICLE ARCHIVE  |   FORUMS  |   TIP BANK
Browse DevX
Sign up for e-mail newsletters from DevX


advertisement
 

Dynamically Executing Code in .NET : Page 2

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.


advertisement
Understanding How .NET Loads Code
Before I dive into the dynamic code execution class 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.

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 AppDomain cached and compiled so only the first access to it 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 of it) into the template. The documents are merged on the fly and only on an as needed basis. This system can have thousands of entries and almost every page has to be compiled 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. The process consumes 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 application's AppDomain.

So what do you do? Unfortunately there's no simple answer—only a convoluted one. The answer is to create a new application domain and load your dynamic assemblies into that. You can have a choice of loading into this AppDomain, running your code, and unloading it, or alternately you can run all of your dynamic code into the new domain and kill it later or when it reaches a certain number of executions or other metric. Unfortunately this process is not trivial and requires that you use an intermediary proxy object that can invoke a method in a remote AppDomain 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 it to 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 is exactly what we want to avoid. By using a proxy that only publishes an Interface your code load 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.

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:

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 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:

  1. Compile your 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 AppDomain, compiles the code, runs it, and unloads the AppDomain is shown in Listing 4. Revisions from the previous version are highlighted.

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:

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 = oRemote.Invoke("DynamicCode",null);

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.



Comment and Contribute

 

 

 

 

 


(Maximum characters: 1200). You have 1200 characters left.

 

 

Sitemap
Thanks for your registration, follow us on our social networks to keep up-to-date