Assembly Private Bin Path Pitfall

May 6, 2010

one comment

When you configure a .NET AppDomain, one of the things you can set is the private probing path (relative to that AppDomain’s base directory) that will be used by the CLR loader when trying to load an assembly in that AppDomain. With that in mind, the following code is allegedly supposed to work, provided that the SecondAssembly assembly is indeed located in the PrivatePath subdirectory:

 class InOtherAppDomain : MarshalByRefObject 
 {
   public void SomeMethod()
   {
     AppDomain thisAD = AppDomain.CurrentDomain;
 
     Console.WriteLine(thisAD.BaseDirectory);
     Console.WriteLine(thisAD.RelativeSearchPath);
 
     string privateBinDirectory =
       thisAD.BaseDirectory + @"\\" + thisAD.RelativeSearchPath;
     Console.WriteLine(privateBinDirectory);
 
     Console.WriteLine(File.Exists(
       Path.Combine(privateBinDirectory, "SecondAssembly.dll" )));
 
     try 
     {
       Assembly.Load("SecondAssembly" );
       Console.WriteLine("Assembly loaded successfully" );
     }
     catch (Exception ex)
     {
       Console.WriteLine("Error loading assembly: " + ex.Message);
     }
   }
 }
 
 class Program 
 {
   static void Main(string[] args)
   {
     AppDomainSetup setup = new AppDomainSetup();
     setup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;
     setup.PrivateBinPath = @"\\PrivatePath";
 
     AppDomain secondAD = AppDomain.CreateDomain(
       "SecondAD", AppDomain.CurrentDomain.Evidence, setup);
 
     InOtherAppDomain obj = (InOtherAppDomain)
       secondAD.CreateInstanceAndUnwrap(
         typeof(InOtherAppDomain).Assembly.FullName,
         typeof(InOtherAppDomain).FullName);
 
     obj.SomeMethod();
   }
 }
 

Well, this program’s output is in fact the following:

D:\Development\Scratch\AssemblyPrivateBinPath\AssemblyPrivateBinPath\bin\Debug\

\PrivatePath

D:\Development\Scratch\AssemblyPrivateBinPath\AssemblyPrivateBinPath\bin\Debug\\\PrivatePath

True

Error loading assembly: Could not load file or assembly ‘SecondAssembly’ or one of its dependencies. The system cannot find the file specified.

Hmm. The secondary AppDomain’s base directory looks all right, as does the private probing path. The combined directory looks weird (the three slashes) but File.Exists says the assembly is there, so what’s with the exception?

If you examine the FusionLog property of the FileNotFoundException, you’ll find the following text (edited for brevity):

=== Pre-bind state information ===

LOG: User = Sasha-PC\Sasha

LOG: DisplayName = SecondAssembly

(Partial)

. . .

LOG: Appbase = file:///D:/Dev/AssemblyPrivateBinPath/bin/Debug/

LOG: Initial PrivatePath = \PrivatePath

Calling assembly : AssemblyPrivateBinPath, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.

===

LOG: This bind starts in default load context.

LOG: No application configuration file found.

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:///D:/Dev/AssemblyPrivateBinPath/bin/Debug/SecondAssembly.DLL.

LOG: Attempting download of new URL file:///D:/Dev/AssemblyPrivateBinPath/bin/Debug/SecondAssembly/SecondAssembly.DLL.

LOG: Attempting download of new URL file:///D:/Dev/AssemblyPrivateBinPath/bin/Debug/SecondAssembly.EXE.

LOG: Attempting download of new URL file:///D:/Dev/AssemblyPrivateBinPath/bin/Debug/SecondAssembly/SecondAssembly.EXE.

Hmm. It’s not even looking in the PrivatePath subdirectory—it’s doing the default bind and nothing else. Well, there might be some additional information and the only way to get it is to enable the Fusion log.

There are several ways to enable the Fusion log, the easiest one is probably using the Assembly Binding Log Viewer (run fuslogvw.exe from an elevated Visual Studio command prompt). After enabling it and running the application again, I got the following bind information in the log output for SecondAssembly (edited for brevity):

*** Assembly Binder Log Entry  (5/6/2010 @ 3:32:20 PM) ***

The operation failed.

Bind result: hr = 0×80070002. The system cannot find the file specified.

. . . (same as earlier)

. . .

WRN: Not probing location file:///D:/PrivatePath/SecondAssembly.DLL, because the location falls outside of the appbase.

WRN: Not probing location file:///D:/PrivatePath/SecondAssembly/SecondAssembly.DLL, because the location falls outside of the appbase.

WRN: Not probing location file:///D:/PrivatePath/SecondAssembly.EXE, because the location falls outside of the appbase.

WRN: Not probing location file:///D:/PrivatePath/SecondAssembly/SecondAssembly.EXE, because the location falls outside of the appbase.

LOG: Attempting download of new URL file:///D:/Dev/AssemblyPrivateBinPath/bin/Debug/SecondAssembly.DLL.

LOG: Attempting download of new URL file:///D:/Dev/AssemblyPrivateBinPath/bin/Debug/SecondAssembly/SecondAssembly.DLL.

LOG: Attempting download of new URL file:///D:/Dev/AssemblyPrivateBinPath/bin/Debug/SecondAssembly.EXE.

LOG: Attempting download of new URL file:///D:/Dev/AssemblyPrivateBinPath/bin/Debug/SecondAssembly/SecondAssembly.EXE.

LOG: All probing URLs attempted and failed.

Oh boy! It’s looking at D:\PrivatePath, which is outside the AppDomain’s base directory, and so it can’t be probed… But why would it go there if the directory is relative to the AppDomain’s base anyway??

Well, actually, I cheated here a little bit. When initializing privateBinDirectory in the above program, I didn’t use Path.Combine, but used direct string concatenation. And the reason is that if I used Path.Combine, the resulting combined directory would be . . . (dramatic suspense)

. . . (still dramatic suspense)

. . . (there could be more dramatic suspense here, but modern displays are really big and it’s really easy to scroll nowadays. Oh well, let’s give it away . . .)

\PrivatePath

Yes, that’s right, \PrivatePath. And thus the exception—if there’s a leading backslash in the beginning of the private probing path, the combined path is treated as an absolute path on the current volume, which is highly likely to fall outside the AppDomain’s base and cause this exception.

Add comment
facebook linkedin twitter email

Leave a Reply

Your email address will not be published.

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

one comment

  1. MattMay 22, 2013 ב 3:40 PM

    I came across this thread when attempting to dynamically load a dll file from a directory outside of the bin directory. Long story short, I was able to accomplish this by using the AppDomain.CurrentDomain.AssemblyResolve event. Here is the code:

    //–begin example:

    public MyClass(){
    AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
    }

    private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
    {
    foreach (var moduleDir in _moduleDirectories)
    {
    var di = new DirectoryInfo(moduleDir);
    var module = di.GetFiles().FirstOrDefault(i => i.Name == args.Name+”.dll”);
    if (module != null)
    {
    return Assembly.LoadFrom(module.FullName);
    }
    }
    return null;
    }

    //—end example

    The method CurrentDomain_AssemblyResolve is called each time the AppDomain.CurrentDomain.Load(“…”) method is called. This custom event handler does the job of locating the assembly using your own custom logic (which means you can tell it to look anywhere, even outside of the bin path, etc). I hope this saves someone else a few hours…

    Reply