This is not the first time I've run into this wonderful error while creating new AppDomains in .NET and then trying to load types and access them across App Domains.
In almost all cases the problem I've run into with this error the problem comes from the two AppDomains involved loading different copies of the same type. Unless the types match exactly and come exactly from the same assembly the typecast will fail. The most common scenario is that the types are loaded from different assemblies - as unlikely as that sounds.
An Example of Failure
To give some context, I'm working on some old code in Html Help Builder that creates a new AppDomain in order to parse assembly information for documentation purposes. I create a new AppDomain in order to load up an assembly process it and then immediately unload it along with the AppDomain. The AppDomain allows for unloading that otherwise wouldn't be possible as well as isolating my code from the assembly that's being loaded.
The process to accomplish this is fairly established and I use it for lots of applications that use add-in like functionality - basically anywhere where code needs to be isolated and have the ability to be unloaded. My pattern for this is:
- Create a new AppDomain
- Load a Factory Class into the AppDomain
- Use the Factory Class to load additional types from the remote domain
Here's the relevant code from my TypeParserFactory that creates a domain and then loads a specific type - TypeParser - that is accessed cross-AppDomain in the parent domain:
public class TypeParserFactory : System.MarshalByRefObject,IDisposable
{
…/// <summary>
/// TypeParser Factory method that loads the TypeParser
/// object into a new AppDomain so it can be unloaded.
/// Creates AppDomain and creates type.
/// </summary>
/// <returns></returns>
public TypeParser CreateTypeParser()
{
if (!CreateAppDomain(null))
return null;
/// Create the instance inside of the new AppDomain
/// Note: remote domain uses local EXE's AppBasePath!!!
TypeParser parser = null;
try
{
Assembly assembly = Assembly.GetExecutingAssembly();
string assemblyPath = Assembly.GetExecutingAssembly().Location;
parser = (TypeParser) this.LocalAppDomain.CreateInstanceFrom(assemblyPath,
typeof(TypeParser).FullName).Unwrap();
}
catch (Exception ex)
{
this.ErrorMessage = ex.GetBaseException().Message;
return null;
}
return parser;
}
private bool CreateAppDomain(string lcAppDomain)
{
if (lcAppDomain == null)
lcAppDomain = "wwReflection" + Guid.NewGuid().ToString().GetHashCode().ToString("x");
AppDomainSetup setup = new AppDomainSetup();
// *** Point at current directory
setup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;
//setup.PrivateBinPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "bin");
this.LocalAppDomain = AppDomain.CreateDomain(lcAppDomain,null,setup);
// Need a custom resolver so we can load assembly from non current path
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
return true;
}
…
}
Note that the classes must be either [Serializable] (by value) or inherit from MarshalByRefObject in order to be accessible remotely. Here I need to call methods on the remote object so all classes are MarshalByRefObject.
The specific problem code is the loading up a new type which points at an assembly that visible both in the current domain and the remote domain and then instantiates a type from it. This is the code in question:
Assembly assembly = Assembly.GetExecutingAssembly();
string assemblyPath = Assembly.GetExecutingAssembly().Location;
parser = (TypeParser) this.LocalAppDomain.CreateInstanceFrom(assemblyPath,
typeof(TypeParser).FullName).Unwrap();
The last line of code is what blows up with the Unable to cast transparent proxy to type <type> error. Without the cast the code actually returns a TransparentProxy instance, but the cast is what blows up. In other words I AM in fact getting a TypeParser instance back but it can't be cast to the TypeParser type that is loaded in the current AppDomain.
Finding the Problem
To see what's going on I tried using the .NET 4.0 dynamic type on the result and lo and behold it worked with dynamic - the value returned is actually a TypeParser instance:
Assembly assembly = Assembly.GetExecutingAssembly();
string assemblyPath = Assembly.GetExecutingAssembly().Location;
object objparser = this.LocalAppDomain.CreateInstanceFrom(assemblyPath,
typeof(TypeParser).FullName).Unwrap();
// dynamic works
dynamic dynParser = objparser;
string info = dynParser.GetVersionInfo(); // method call works
// casting fails
parser = (TypeParser)objparser;
So clearly a TypeParser type is coming back, but nevertheless it's not the right one. Hmmm… mysterious.
Another couple of tries reveal the problem however:
// works
dynamic dynParser = objparser;
string info = dynParser.GetVersionInfo(); // method call works
// c:\wwapps\wwhelp\wwReflection20.dll (Current Execution Folder)
string info3 = typeof(TypeParser).Assembly.CodeBase;
// c:\program files\vfp9\wwReflection20.dll (my COM client EXE's folder)
string info4 = dynParser.GetType().Assembly.CodeBase;
// fails
parser = (TypeParser)objparser;
As you can see the second value is coming from a totally different assembly. Note that this is even though I EXPLICITLY SPECIFIED an assembly path to load the assembly from! Instead .NET decided to load the assembly from the original ApplicationBase folder. Ouch!
How I actually tracked this down was a little more tedious: I added a method like this to both the factory and the instance types and then compared notes:
public string GetVersionInfo()
{
return ".NET Version: " + Environment.Version.ToString() + "\r\n" +
"wwReflection Assembly: " + typeof(TypeParserFactory).Assembly.CodeBase.Replace("file:///", "").Replace("/", "\\") + "\r\n" +
"Assembly Cur Dir: " + Directory.GetCurrentDirectory() + "\r\n" +
"ApplicationBase: " + AppDomain.CurrentDomain.SetupInformation.ApplicationBase + "\r\n" +
"App Domain: " + AppDomain.CurrentDomain.FriendlyName + "\r\n";
}
For the factory I got:
.NET Version: 4.0.30319.239
wwReflection Assembly: c:\wwapps\wwhelp\bin\wwreflection20.dll
Assembly Cur Dir: c:\wwapps\wwhelp
ApplicationBase: C:\Programs\vfp9\
App Domain: wwReflection534cfa1f
For the instance type I got:
.NET Version: 4.0.30319.239
wwReflection Assembly: C:\\Programs\\vfp9\wwreflection20.dll
Assembly Cur Dir: c:\\wwapps\\wwhelp
ApplicationBase: C:\\Programs\\vfp9\
App Domain: wwDotNetBridge_56006605
which clearly shows the problem. You can see that both are loading from different appDomains but the each is loading the assembly from a different location.
Probably a better solution yet (for ANY kind of assembly loading problem) is to use the .NET Fusion Log Viewer to trace assembly loads.The Fusion viewer will show a load trace for each assembly loaded and where it's looking to find it. Here's what the viewer looks like:
The last trace above that I found for the second wwReflection20 load (the one that is wonky) looks like this:
*** Assembly Binder Log Entry (1/13/2012 @ 3:06:49 AM) ***
The operation was successful.
Bind result: hr = 0x0. The operation completed successfully.
Assembly manager loaded from: C:\Windows\Microsoft.NET\Framework\V4.0.30319\clr.dll
Running under executable c:\programs\vfp9\vfp9.exe
--- A detailed error log follows.
=== Pre-bind state information ===
LOG: User = Ras\ricks
LOG: DisplayName = wwReflection20, Version=4.61.0.0, Culture=neutral, PublicKeyToken=null
(Fully-specified)
LOG: Appbase = file:///C:/Programs/vfp9/
LOG: Initial PrivatePath = NULL
LOG: Dynamic Base = NULL
LOG: Cache Base = NULL
LOG: AppName = vfp9.exe
Calling assembly : (Unknown).
===
LOG: This bind starts in default load context.
LOG: Using application configuration file: C:\Programs\vfp9\vfp9.exe.Config
LOG: Using host configuration file:
LOG: Using machine configuration file from C:\Windows\Microsoft.NET\Framework\V4.0.30319\config\machine.config.
LOG: Policy not being applied to reference at this time (private, custom, partial, or location-based assembly bind).
LOG: Attempting download of new URL file:///C:/Programs/vfp9/wwReflection20.DLL.
LOG: Assembly download was successful. Attempting setup of file: C:\Programs\vfp9\wwReflection20.dll
LOG: Entering run-from-source setup phase.
LOG: Assembly Name is: wwReflection20, Version=4.61.0.0, Culture=neutral, PublicKeyToken=null
LOG: Binding succeeds. Returns assembly from C:\Programs\vfp9\wwReflection20.dll.
LOG: Assembly is loaded in default load context.
WRN: The same assembly was loaded into multiple contexts of an application domain:
WRN: Context: Default | Domain ID: 2 | Assembly Name: wwReflection20, Version=4.61.0.0, Culture=neutral, PublicKeyToken=null
WRN: Context: LoadFrom | Domain ID: 2 | Assembly Name: wwReflection20, Version=4.61.0.0, Culture=neutral, PublicKeyToken=null
WRN: This might lead to runtime failures.
WRN: It is recommended to inspect your application on whether this is intentional or not.
WRN: See whitepaper http://go.microsoft.com/fwlink/?LinkId=109270 for more information and common solutions to this issue.
Notice that the fusion log clearly shows that the .NET loader makes no attempt to even load the assembly from the path I explicitly specified.
Remember your Assembly Locations
As mentioned earlier all failures I've seen like this ultimately resulted from different versions of the same type being available in the two AppDomains. At first sight that seems ridiculous - how could the types be different and why would you have multiple assemblies - but there are actually a number of scenarios where it's quite possible to have multiple copies of the same assembly floating around in multiple places.
If you're hosting different environments (like hosting the Razor Engine, or ASP.NET Runtime for example) it's common to create a private BIN folder and it's important to make sure that there's no overlap of assemblies.
In my case of Html Help Builder the problem started because I'm using COM interop to access the .NET assembly and the above code. COM Interop has very specific requirements on where assemblies can be found and because I was mucking around with the loader code today, I ended up moving assemblies around to a new location for explicit loading. The explicit load works in the main AppDomain, but failed in the remote domain as I showed. The solution here was simple enough: Delete the extraneous assembly which was left around by accident.
Not a common problem, but one that when it bites is pretty nasty to figure out because it seems so unlikely that types wouldn't match. I know I've run into this a few times and writing this down hopefully will make me remember in the future rather than poking around again for an hour trying to debug the issue as I did today. Hopefully it'll save some of you some time as well in the future.
Other Posts you might also like