Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
West Wind WebSurge - Rest Client and Http Load Testing for Windows

Assembly loading for AppDomain's in COM Interop - solved


:P
On this page:

I’ve been trying to change some functionality in my Assembly importer to work inside of a separate AppDomain. The issue at hand is that when Help Builder does assembly imports, it opens the Assembly in question retrieves the type or types and parses them. In order to do this meta data parsing via Reflection the type actually gets loaded into the current AppDomain. Once an assembly has been loaded into an AppDomain it cannot be unloaded from the AppDomain. The only option is to unload the entire AppDomain.

 

TypeParser tp = new TypeParser();

tp.cFilename = @"D:\projects\wwBusiness\bin\debug\wwBusiness.dll";

tp.cXmlFilename = @"D:\projects\wwBusiness\bin\debug\wwBusiness.xml";

tp.cSyntax = "C#";

tp.RetrieveDeclaredMembersOnly = false;

int count = tp.GetAllObjects(false);

 

At the and of this call tp contains a number of hierarchical collections that contain all the type information. My idea here was to simply create a loader factory that instead of creating the type parser directly instantiates it in a new AppDomain:

 

 

[ClassInterface(ClassInterfaceType.AutoDual)]

[ProgId("wwReflection.TypeParserFactory")]

public class TypeParserFactory

{

      TypeParser Parser = null;

      AppDomain LocalAppDomain = null;

 

 

      public TypeParser CreateTypeParser()

      {

            if (!this.CreateAppDomain(null))

                  return null;

 

            object  T =  this.LocalAppDomain.CreateInstance( "wwReflection",

                                        "Westwind.wwReflection.TypeParser" ).Unwrap();

 

            TypeParser Parser = T as Westwind.wwReflection.TypeParser;

                                         

            if (Parser == null)

                  return new TypeParser();

 

            return Parser;

      }

 

      private bool CreateAppDomain(string lcAppDomain)

      {

            if (lcAppDomain == null)

                  lcAppDomain = "wwReflection" + ;

                         Guid.NewGuid().ToString().GetHashCode().ToString("x");

 

            AppDomainSetup loSetup = new AppDomainSetup();

           

            // *** Point at current directory

            loSetup.ApplicationBase = Directory.GetCurrentDirectory();

 

            this.LocalAppDomain = AppDomain.CreateDomain(lcAppDomain,null,loSetup);

            return true;

      }

 

      public void UnloadTypeParser()

      {

            if (this.LocalAppDomain != null)

                  AppDomain.Unload( this.LocalAppDomain );

      }

}

 

 

[ClassInterface(ClassInterfaceType.AutoDual)]

[ProgId("wwReflection.TypeParser")]

public class TypeParser : MarshalByRefObject

{

}

 

This works beautifully in .NET with:

 

TypeParserFactory tf = new TypeParserFactory();

 

TypeParser tp = tf.CreateTypeParser();

tp.cFilename = @"D:\projects\wwBusiness\bin\debug\wwBusiness.dll";

tp.cXmlFilename = @"D:\projects\wwBusiness\bin\debug\wwBusiness.xml";

tp.cSyntax = "C#";

tp.RetrieveDeclaredMembersOnly = false;

int count = tp.GetAllObjects(false);

 

int x = tp.aObjects.Length;

 

tf.UnloadTypeParser();

 

 

But in FoxPro this same code FAILED – there are no errors but the code fails to do the type conversion into the TypeParser class (bold above). Rather than returning a type parser the CreateInstance in the remote AppDomain returns a MarshalByRef object that can’t be cast.

 

I tried all sorts of different things and today finally I gave up and called MSDN support. After much back and forth the Tech and I tried to figure out what’s going on and as it turns out the problem is not a remoting or even a COM problem but an Assembly loading problem – The code loading Creating the instance is not finding the the DLL housing the assembly even though it is currently running in the active AppDomain.

 

To figure this out I added the following to my TypeParser class:

public string GetVersionInfo()

{

      return Environment.Version.ToString() + "\r\n" + Assembly.GetExecutingAssembly().CodeBase + "\r\n" +

            "ApplicationBase: " + AppDomain.CurrentDomain.SetupInformation.ApplicationBase + "\r\n" +

            "PrivateBinPath: " + AppDomain.CurrentDomain.SetupInformation.PrivateBinPath + "\r\n" +

            "PrivateBinProbe: " + AppDomain.CurrentDomain.SetupInformation.PrivateBinPathProbe  + "\r\n" +

            Directory.GetCurrentDirectory() + "\r\n" + AppDomain.CurrentDomain.FriendlyName + "\r\n";

}

 

A straight call to the class verified a major discrepancy: The ApplicationBase while running in Visual FoxPro doesn’t point at the current directory, but at the Visual FoxPro install directory. Apparently what happens is that COM Interop always loads the Assembly and it’s ApplicationBase path with the same base path as the calling application – in this case d:\programs\vfp9\VFP9.EXE. Because the AppDomain autoloads there’s really no easy way to overload this behavior and you can’t (at least from what I tried) override the PrivateBinPathProbe directories after the AppDomain has been loaded.

 

Soooooooo… the good news is that I figure this out (thanks to the help of the Microsoft tech who spent 2 hours with me on this). The bad news is that this is kinda messy when you’re working with a tool generic like Visual FoxPro. In order to get my development environment to work correctly I needed to compile my assembly into the d:\programs\vfp9 directory. This is the only way it will work! The DLL must live there…

 

For most application this likely won’t be a problem as they can safely store DLLs in the EXE’s startup path or BIN path. But for a tool like VFP (or if you’re plugging DLLs into Word for example) this can be a major headache.

 

The worst part about this issue is the fact that there were no errors in any of this. Even the Fusion Viewer was not returning errors to me. There was no indication whatsoever that it's a missing assembly.

 

Note that the problem is very specific:


The failure occurs only when trying to load an assembly into a new AppDomain and then only if the assembly does not exist in the parent application's AppBase or other Bin/Probing paths. Although the AppDomain is configured to have it’s ApplicationBase in a different directory than the EXE’s base, the remote AppDOmain CreateInstance call doesn’t apparently doesn’t use this path, instead using the current domains ApplicationBase. I’m not sure why this is – this sounds really odd and strange to me.

 

Ultimately the solution was to compile into programs\VFP9 AND set the AppDomain’s ApplicationBase directory the same as hosting process’s (COM Interop) ApplicationBase:

 

private bool CreateAppDomain(string lcAppDomain)

{

      if (lcAppDomain == null)

            lcAppDomain = "wwReflection" + Guid.NewGuid().ToString().GetHashCode().ToString("x");

 

      AppDomainSetup loSetup = new AppDomainSetup();

     

      // *** Point at current directory

      loSetup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;

 

      this.LocalAppDomain = AppDomain.CreateDomain(lcAppDomain,null,loSetup);

     

      return true;

}

 

Note that this is different than the first block of code I showed where I set the ApplicationBase to the current directory!

 

I think you can also override this behavior with a .CONFIG file for the calling application, which determines how COM instantiates the default AppDomain. I couldn’t figure out to do this in a hurry though and it seemed even less flexible than copying the DLL into the VFP directory.

 

Ugly, ugly, ugly, but at least it works…. and oh, joy I finally have my unloadable assembly parser.



West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024