Dynamically Executing Code in .NET (Cont.) 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 it to 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 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: 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.Create an AppDomain.Get a reference to IRemoteInterface.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. | & | | What's an Application Domain?
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 assemblies 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. |
| Listing 2: (RemoteLoader.cs): Proxy Interface to access an AppDomain | /// Interface that can be run over the remote AppDomain boundary. public interface IRemoteInterface { object Invoke(string lcMethod,object[] Parameters); }
|
| Listing 3: (RemoteLoader.cs): Proxy Loader class returns an Interface | 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(); } }
|
| 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");
|
|