September 21, 2006

dotNET AppDomain and Remote Computing (Remoting)

AppDomain

If for some reason you find that you're application cannot load an Type/Assembly at runtime it's usually because assembly lives somewhere different at runtime. For instance the host could do some copying to temporary locations at runtime, this can sometimes cause the references between assemblies to breakdown. So how do you tell the runtime to look somwhere else for the assembly well that's where the AppDomain comes into play.


AppDomain refers to the namespace, space allocated in .NET memory/CLR in which your application is allowed to run.
It's a security thing which prevents applications from cross communicating with each other.

Reasons why you'd want to access the AppDomain can include:
Need to manage the AppDomain i.e. delete or add resources from outside the applications namespace. If you wanted to ensure that certain assemblies for example were to be unloaded from another AppDomain at a certain point, you'd have to setup a new AppDomain and somehow grant it access to the other AppDomain.

AppDomain's have certain properties which are vital to setting them up. A major problem I encountered was forcing an Application to include Assemblies in different path from the one in which it was loaded. This had to be forced by the AppDomain.applicationBase, this was all done with a helper class AppSetup, which allows you to set various properties of an AppDomain before the AppDomain's creation. You must set these properties before creating the AppDomain.


AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
AppDomainSetup appSetup = new AppDomainSetup();
appSetup.ApplicationBase = "file:///C:\\project\\export\\bin;
appSetup.PrivateBinPath = AppDomain.CurrentDomain.BaseDirectory+";C:\\Program Files\\Microsoft Visual Studio 8\\Common7\\IDE\\" ;

AppDomain ad = AppDomain.CreateDomain("Compiling", null, appSetup);

At this stage you have a new AppDomain which will first search the ApplicationBase for Assemblies. The ResolveEventHandler that I register is the actual method that is used to locate assemblies so in you EventHandler you should add code to locate Assemblies in certain paths

private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
//Look for assemblies in the current VS project location
string projectDir = Path.GetDirectoryName(BuildTask._presentProjectLocation);
string shortAssemblyName = args.Name.Substring(0, args.Name.IndexOf(','));
string fileName = Path.Combine(projectDir, shortAssemblyName + ".dll");
if (File.Exists(fileName))
{
Assembly result = Assembly.LoadFrom(fileName);
return result;
}
fileName = Path.Combine(projectDir+"\\bin\\"+_mode+"\\", shortAssemblyName + ".dll");
if (File.Exists(fileName))
{
Assembly result = Assembly.LoadFrom(fileName);
return result;
}
//Try the Galaxy export directory
fileName = Path.Combine("C:\\project\\export\\bin\\" + _mode + "\\Tools\\", shortAssemblyName + ".dll");
if (File.Exists(fileName))
{
Assembly result = Assembly.LoadFrom(fileName);
return result;
}
fileName = Path.Combine("C:\\Program Files\\Microsoft Visual Studio 8\\Common7\\IDE\\", shortAssemblyName + ".dll");
if (File.Exists(fileName))
{
Assembly result = Assembly.LoadFrom(fileName);
return result;
}

else
return Assembly.GetExecutingAssembly().FullName == args.Name ? Assembly.GetExecutingAssembly() : null;

}

Calling a remote method from a Visual Studio MSBuild BuildTask

It can be tricky to get this right. We came across this when trying to add a BuildTask.

The problem we had was that we were compiling files with our own compiler and we needed to load and unload files at certain times, the way to implement this was through an AppDomain. This temporary AppDomain was required because the output fiiles are first compiled into a dll in the current AppDomain and it cannot be released until the processing of the results are finished. We wanted to access the info in the dll before it was released and so we created a temporary new AppDomain .

Because of this we had to introduce Remote Computing, this is required for one AppDomain to communicate with another. The implementation takes 2 parts, first the class that will be used must inherit from MarshalByRefObject, in our case the Configuration.Compiler class.

To create a remote instance use one of the AppDomain. CreateInstance.... methods. Depending on the location of the assembly containing the MarshalByRefObject class. In our case the Compiler class was in the Configuration.Compiler.dll, which was signed. This meant we could instantiate the object using

Configuration.CompileProxy.Compiler compiler = (Configuration.CompileProxy.Compiler) ad.CreateInstanceAndUnwrap ("Configuration.CompileProxy, Version=1.0.0.0, Culture=neutral, PublicKeyToken=1f77dad97b7dc0c2", "Configuration.CompileProxy.Compiler");

But there were problems. This worked fine when the assembly was located in the GAC or in the Visual Studio installation (along with all of it's dependancies),

C:\Program Files\Microsoft Visual Studio 8\Common7\IDE\PrivateAssemblies.

I still haven't found a way to instantiate this with the assembly outside of these locations.

  • Common problems were:
    • Casting the resultant Object from CreateInstanceFromAndUnWrap.. to Type Configuration.CompileProxy.Compiler. Still haven't the resoltion for this, it only happens when I use CreateInstanceFromAndUnwrap or CreateInstanceFrom and then use UnWrap().
    • When CreateInstanceAndUnWrap() is used it creates the Object without issue only if the Configuration.CompileProxy.dll is in the GAC or in the VS installation. I have no soluution for this either.
    • There is an Event which can be handled to resolve the paths to assemblies,

    • ad.AssemblyResolve += new ResolveEventHandler(BuildTask.CurrentDomain_AssemblyResolve);
    • I tried to use this but found that it broke the initial AppDomain too.

I found the solution. The problem is that it is the original AppDomain that is used to resolve Assemblies. The problem was with the UnWrap(), what appears to happen is the UnWrap() tries to match the assembly name just loaded through CreateInstance or CreateInstanceFrom with it's full name, something like

"Configuration, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null".

If it can't match it bails out. The solution is to add an EventHandler for the AssemblyResolve Event match the full name with the actual name of an assembly and it's path (I had tried this earlier but it caused an infinite loop, another call I had made must have been causing this!!). The EventHandler is subscribed to the Event of the original AppDomain . Also you should set yourAppDomain's ApplicationBase to the location of your assemblies, and then use CreateInstanceAndUndWrap().

Once you have the object instance you can invoke your method using the ordinary call.

bool result = compiler.Compile(confFileStream, confFileName, out messages);

Also note that there is a tool available in the .NET Frameword for debugging assembly binding and resolving, it's called the Fusion Logger, Fuslogvw.exe, it allows you to view what the CLR is doing at runtime with the assemblies.

To run fuslogvw.exe you must install the .NET SDK and then run it from here
C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0>Fuslogvw.exe

Fuselogvs.exe CodeProject Tool to debug AppDomain Assemblies
AppDomain Class (msdn)

Suzanne Cooks .NET CLR Notes - Executing Code in Another AppDomain

Suzanne Cooks .NET CLR Notes - Choosing a Binding Context

AppDomain.CreateInstanceAndUnwrap(..) vs AppDomain.CreateInstanceFrom

Why does AppDomain.CreateInstanceAndUnwrap(..) work and AppDomain.CreateInstanceFrom(...).UnWrap doesn't?

Remote Computing and Marshaling (MarshalByRefObject)
Yariv Hammers - Calling Remote Objects - Basic Concepts.
Yariv Hammers - Create a Simple Client/Server App with Remoting..

I've mentioned in the AppDomain about marshalling and inheriting your class from MarshalByRefObject, now I'll describe what this actually means and does.

Marshaling
This is the related to the Serializing and De-Serializing of objects, this term is particular to Remote Computing. As I've described in another post Serializing of objects is creating a stream version of your objects data.
In Remote computing objects can be passed from one application to another by serializing the object on the sender side and de-serializing on the receiver side, Marshaling and Demarshaling.

In .NET this is achieved by making your class inherit from MarshalByRefObject. Now when you pass objects from your class to another AppDomain they will be marshalled before passing. The other AppDomain will then Demarshal the parameters when received.

No comments: