Every time I need to load .NET assemblies from a non-default path in an application I end up spending quite a bit of time trying to get it right. Today was no different: I had a situation where I needed to create a type in a seperate AppDomain based on a dynamically generated assembly that might live in a directory other than the application’s base path. This is sort of like a perfect storm of combinations that are problematic for Assembly loading: Loading across AppDomains and loading an assembly out of a non-default path. Although the behavior I eventually found is fairly straightforward, there are a lot of similar combinations that I ended up with that didn’t work before I found the working solution.
My scenario for this is for a code generator for a FoxPro tool that I’m building for a customer. Basically it’s a Web Service client generator for VFP that creates a .NET Web Service proxy and then generates code for a matching FoxPro proxy to call the .NET proxy. For .NET folks this sounds pretty clumsy, but for Fox developers this is quite useful because normally creating the .NET proxy and then creating the calls to the proxy from FoxPro take a bit of work and setup, so I decided to build a tool to automate most of this process.
All this worked well right out of the chute, except for one thing that is usually a problem in scenarios where code is dynamically generated – in this case by WSDL.exe. The problem is that the assembly can be generated in any location the user chooses and the user might generate the assembly multiple times and generate the proxy multiple times. This of course would fail on the second attempt as the assembly would already be loaded. So the code somehow needs to be able to unload the assembly from memory when the proxy’s been built.
In order to do that a new AppDomain that can host the assembly is required. By default any assembly loaded into a .NET AppDomain stays loaded and locked in that AppDomain for the lifetime of that AppDomain. Most of the time that AppDomain is the default AppDomain that is created when the application starts and any assembly loaded into it stays loaded until shut down. The assembly DLL on disk is locked while it’s in memory so the file can’t be replaced as would need to happen for any subsequent WSDL.exe generations.
Using a separate AppDomain allows you to get around this limitation. By using cross AppDomain remoting you can instantiate a type in another AppDomain execute any methods on it and then unload the AppDomain and so unlock the assembly and remove any references from Memory. This is quite a common scenario for plug-ins and anything else that loads dynamic code that might change at runtime during the lifetime of the main application.
The other complication is loading the assembly that will be loaded in the alternate AppDomain out of any path the user specifies. Normally when loading in the current AppDomain this process is reasonably straight forward by using Assembly.LoadFrom() to load assemblies out of arbitrary paths. Note that strongly typed assemblies cannot be loaded from non-application paths – this only works for unsigned assemblies.
Long story short I had a number of false starts on this loading up the WSDL.exe generated assembly from an arbitrary path into a new AppDomain. In the end I wound up with a working factory class that looks like this:
[ClassInterface(ClassInterfaceType.AutoDual)]
public class WsdlClassParserFactory : MarshalByRefObject
{
public AppDomain LocalAppDomain = null;
public string ErrorMessage = string.Empty;
/// <summary>
/// Creates a new instance of the WsdlParser in a new AppDomain
/// </summary>
/// <returns></returns>
public WsdlClassParser CreateWsdlClassParser()
{
this.CreateAppDomain(null);
string AssemblyPath = Assembly.GetExecutingAssembly().Location;
WsdlClassParser parser = null;
try
{
parser = (WsdlClassParser) this.LocalAppDomain.CreateInstanceFrom(AssemblyPath,
typeof(Westwind.WebServices.WsdlClassParser).FullName).Unwrap() ;
}
catch (Exception ex)
{
this.ErrorMessage = ex.Message;
}
return parser;
}
public bool CreateAppDomain(string appDomain)
{
if (string.IsNullOrEmpty(appDomain))
appDomain = "wsdlparser" + Guid.NewGuid().ToString().GetHashCode().ToString("x");
AppDomainSetup domainSetup = new AppDomainSetup();
domainSetup.ApplicationName = appDomain;
// *** Point at current directory
domainSetup.ApplicationBase = Environment.CurrentDirectory; // AppDomain.CurrentDomain.BaseDirectory;
this.LocalAppDomain = AppDomain.CreateDomain(appDomain, null, domainSetup);
// *** Need a custom resolver so we can load assembly from non current path
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
return true;
}
Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
try
{
Assembly assembly = System.Reflection.Assembly.Load(args.Name);
if (assembly != null)
return assembly;
}
catch { // ignore load error }
// *** Try to load by filename - split out the filename of the full assembly name
// *** and append the base path of the original assembly (ie. look in the same dir)
// *** NOTE: this doesn't account for special search paths but then that never
// worked before either.
string[] Parts = args.Name.Split(',');
string File = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + "\\" + Parts[0].Trim() + ".dll";
return System.Reflection.Assembly.LoadFrom(File);
}
/// <summary>
///
/// </summary>
public void Unload()
{
if (this.LocalAppDomain != null)
{
AppDomain.Unload(this.LocalAppDomain);
this.LocalAppDomain = null;
}
}
}
To use the factory create an actual instance to work with then looks like this:
WsdlClassParserFactory factory = new WsdlClassParserFactory();
WsdlClassParser parser = factory.CreateWsdlClassParser();
... do work with the parser
factory.Unload();
where the Unload() allows for unloading the AppDomain and releasing the assembly with it. This is quite a bit of code just for instantiating a type in separate AppDomain, but the factory is useful in that it makes it easy to load the type and unload it easily later. The class instance is required in order to hold on to the to AppDomain reference so it can be unloaded.
Loading an AppDomain is straight forward. The biggest headache is properly configuring the ApplicationBase path and related directories. For my particular application I point the base path to the current directory which in case of this application may not be the same directory the main executable started in – IOW, a non-default path. In my case I want to generate and load the assembly in this path and so the base path settings makes the AppDomain look in the base path (plus any PrivateBinPath subdirs of those were set).
The trick to getting the assembly to load from an arbitrary directory is to use AppDomain.CreateInstanceFrom() which expects a file location. When assemblies or AppDomains ask for paths I’m never sure what the heck is being asked for: A fully qualified path, a CodeBase path or just an assembly name (without the .dll) which is searched for in the app’s bin path. Here the fully qualified OS path is required for the assembly as can be retrieved by Assembly.Location. You would think that this would be enough – and it is if you are using Assembly.LoadFrom() in an application. However, when running cross AppDomains, the assembly still failed to load from the external path when I didn’t provide a custom assembly resolver. I’d get the following error:
Unable to cast transparent proxy to type ‘Westwind.WebServices.WsdlParser’
Oddly AppDomain.CreateInstanceFrom() doesn’t fail and returns an object, but when casting the object to the actual type representation the above error pops up. I would have expected a failure to be more spectacular – an exception would have been nice but apparently SOME sort of Transparent Proxy is returned. Apparently not the right one though and I have no idea what this pseudo proxy could be – not a whole lot of debugging info on a Transparent Proxy object.
So, the trick to resolving this issue is to provide an assembly resolver:
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
and implement custom logic in the resolver to retrieve the assembly explicitly. The handler first tries a plain Load with the assembly name that .NET passes in. Oddly in my case that just works because the assembly is already loaded since it was the one launching the new AppDomain (the launcher is automatically loaded into the new AppDomain). Which makes me really wonder why the hell the AppDomain can’t find the type without an assembly resolver.
Either way, it works with the simple resolver. In other situations when an assembly has not yet been loaded it might also be useful to have a fallback using LoadFrom() and specify the directory in some shape. Here the folder is based on the current assembly so assemblies are searched for in the same folder as the loader’s assembly.
You’d think the process of assembly loading in general, and loading assemblies across AppDomains should be easier, since loading additional types out of the same assembly is a common scenario – having to resort to an explicit assembly resolver seems like overkill especially given that the resolver isn’t doing anything other than forwarding to Assembly.Load().
I’ve hit this snag one too many times in the past and even so this little exercise cost me a couple hours of poking around and experimenting today until I found the right combination. Hopefully this will be useful to some of you the next time you need to create a cross AppDomain proxy.
Other Posts you might also like