Vilhelm H.
Vilhelm H.

Reputation: 1335

Path of AppDomain when assembly is loaded by COM interop

A .NET assembly that is registered for COM Interop combined with some reflection code that shall load types in assemblies results in some strange behavior. I have analyzed what is going on in the debugger and I have searched the net to find a solution. I have found many articles with help, but nothing that have made me solve the problem completely.

Overview of the problem

A have an exe-file that is not .NET (in my case a VB6 application). It resides in folder A.
I have some .NET dlls in folder B. One of them is a COM dll.
The exe-files instantiates a COM instance of a .NET object of the COM .NET assembly. Then the main path of the AppDomain is folder A, but I would like it to be folder B. Since it is folder A, some reflection type-loading inside my .NET code fails.

Here are the details:

I have a VB6 application. The exe file resides in folder A. Inside it I have a VB6 statement

Set DotNetE2 = CreateObject("MyDotNet.E2")

This creates an instance of my .NET class that is registred for COM interop. The header of the .NET class looks like this:

namespace MyDotNet.E2.COM
{
   [ComVisible(true)]
   [Guid("776FF4EA-2F40-4E61-8EF3-08250CB3712B")]
   [ProgId("MyDotNet.E2")]
   [ClassInterface(ClassInterfaceType.AutoDual)]
   public class E2
   {

My .NET assembly 'MyDotNet.E2.COM.dll" resides in folder B. This assembly has references to two other .NET assemblies called E3 and E4 that resides in the same folder. E3 does not have a reference to E4. I am able to execute code in these assemblies as expected, so the references are OK. So far so good. Now, I have some code in E3 that tries to do some reflection on the types in E4. This fails.

Here is the code:

string dotnetPath = Path.GetDirectoryName(
                 Assembly.GetExecutingAssembly().Location);
string mainDir = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;
string otherDirs = AppDomain.CurrentDomain.SetupInformation.PrivateBinPath;

Assembly assembly = Assembly.LoadFrom(Path.Combine(dotnetPath, "E4.dll"));
Type mytype = assembly.GetType("MyDotnet.E4.MyForm");

Observations

dotnetPath is different from mainDir.
mytype is null. Expected result is a type-instance.
If I move the exe file to folder B, along with the .NET assemblies, it works. Then dotnetPath and mainDir is the same.
If I execute the reflection-code in E2 instead of E4, it works, even though dotnetPath != mainDir.
But in exactly the scenario I have outlines it does not work.

I have found some tips on adding other folders to the AppDomain in the PrivateBinPath by specifying those in the config file. But I have not succeeded on this. I have tried to add a config file to my COM .NET file and my VB6 exe-file but nothing happened to my PrivateBinPath property. Here is the config file I tried to add:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
     <probing privatePath="..\..\FolderB"/>
    </assemblyBinding>
  </runtime>
</configuration>

Don't ask me to restructure my assemblies and types. The project is rather complicated and this is the best architecture.

Upvotes: 1

Views: 999

Answers (1)

Vilhelm H.
Vilhelm H.

Reputation: 1335

I managed to solve this myself. The key was the AssemblyResolve-event that is fired if the system fails to resolve an assembly. It is explained here: http://msdn.microsoft.com/library/system.appdomain.assemblyresolve

It seems like a bug in the .NET framework that I have to use this event, but this workaround turned out to be fairly nice.

string dotnetPath = Path.GetDirectoryName(
                                   Assembly.GetExecutingAssembly().Location);
string mainDir = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;

if (!mainDir.Equals(dotnetPath, StringComparison.CurrentCultureIgnoreCase))
{
   // This will happen if .NET process is fired 
   // from a COM call from another folder.
   // Solution: an event is fired if assembly-resolving fails.
   AppDomain.CurrentDomain.AssemblyResolve += 
                         new ResolveEventHandler(CurrentDomain_AssemblyResolve);
}

The event handler is fairly simple:

Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
   foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
   {
      if (assembly.FullName == args.Name) return assembly;
   }
   return null;
}

Upvotes: 1

Related Questions