views:

428

answers:

3

Given an App Domain, there are many different locations that Fusion (the .Net assembly loader) will probe for a given assembly. Obviously, we take this functionality for granted and, since the probing appears to be embedded within the .Net runtime (Assembly._nLoad internal method seems to be the entry-point when Reflect-Loading - and I assume that implicit loading is probably covered by the same underlying algorithm), as developers we don't seem to be able to gain access to those search paths.

My problem is that I have a component that does a lot of dynamic type resolution, and which needs to be able to ensure that all user-deployed assemblies for a given AppDomain are pre-loaded before it starts its work. Yes, it slows down startup - but the benefits we get from this component totally outweight this.

The basic loading algorithm I've already written is as follows. It deep-scans a set of folders for any .dll (.exes are being excluded at the moment), and uses Assembly.LoadFrom to load the dll if it's AssemblyName cannot be found in the set of assemblies already loaded into the AppDomain (this is implemented inefficiently, but it can be optimized later):

void PreLoad(IEnumerable<string> paths)
{
  foreach(path p in paths)
  {
    PreLoad(p);
  }
}

void PreLoad(string p)
{
  //all try/catch blocks are elided for brevity
  string[] files = null;

  files = Directory.GetFiles(p, "*.dll", SearchOption.AllDirectories);

  AssemblyName a = null;
  foreach (var s in files)
  {
    a = AssemblyName.GetAssemblyName(s);
    if (!AppDomain.CurrentDomain.GetAssemblies().Any(
        assembly => AssemblyName.ReferenceMatchesDefinition(
        assembly.GetName(), a)))
      Assembly.LoadFrom(s);
  }    
}

LoadFrom is used because I've found that using Load() can lead to duplicate assemblies being loaded by Fusion if, when it probes for it, it doesn't find one loaded from where it expects to find it.

So, with this in place, all I now have to do is to get a list in precedence order (highest to lowest) of the search paths that Fusion is going to be using when it searches for an assembly. Then I can simply iterate through them.

The GAC is irrelevant for this, and I'm not interested in any environment-driven fixed paths that Fusion might use - only those paths that can be gleaned from the AppDomain which contain assemblies expressly deployed for the app.

My first iteration of this simply used AppDomain.BaseDirectory. This works for services, form apps and console apps.

It doesn't work for an Asp.Net website, however, since there are at least two main locations - the AppDomain.DynamicDirectory (where Asp.Net places it's dynamically generated page classes and any assemblies that the Aspx page code references), and then the site's Bin folder - which can be discovered from the AppDomain.SetupInformation.PrivateBinPath property.

So I now have working code for the most basic types of apps now (Sql Server-hosted AppDomains are another story since the filesystem is virtualised) - but I came across an interesting issue a couple of days ago where this code simply doesn't work: the nUnit test runner.

This uses both Shadow Copying (so my algorithm would need to be discovering and loading them from the shadow-copy drop folder, not from the bin folder) and it sets up the PrivateBinPath as being relative to the base directory.

And of course there are loads of other hosting scenarios that I probably haven't considered; but which must be valid because otherwise Fusion would choke on loading the assemblies.

I want to stop feeling around and introducing hack upon hack to accommodate these new scenarios as they crop up - what I want is, given an AppDomain and its setup information, the ability to produce this list of Folders that I should scan in order to pick up all the DLLs that are going to be loaded; regardless of how the AppDomain is setup. If Fusion can see them as all the same, then so should my code.

Of course, I might have to alter the algorithm if .Net changes its internals - that's just a cross I'll have to bear. Equally, I'm happy to consider SQL Server and any other similar environments as edge-cases that remain unsupported for now.

Any ideas!?

A: 

Have you tried looking at Assembly.GetExecutingAssembly().Location? That should give you the path to the assembly where your code is running from. In the NUnit case, I would expect that to be where the assemblies were shadow copied to.

Andy
Yeah, that was one of my first forays into doing this, but it's unreliable in some of the more enhanced situations (the VS test runner, for example). Also, in the case of Asp.Net, for example, the Assembly Loader actually has numerous folders that it will search that are outside the executing assembly's location.
Andras Zoltan
I definitely agree that it is not sufficient by itself to cover all cases...I was thinking it would give you simply one more data point. So if I understand you correctly, are you looking for one API you can call that will give you all of Fusions search paths?
Andy
@Andy - well that would be the holy grail for sure; I can't see anything in the .Net framework that does it and as for unmanaged APIs; I'm not frightened by that stuff (cut my teeth on C++ for many years), but it's difficult to find anything. I've had a look a the Fusion API, but it's mainly for working with the GAC
Andras Zoltan
I disagree that the Assembly.GetExecutingAssembly() is unreliable in the case of the VS test runner (assuming you mean visual studio unit testing). Unit tests are run from temp directories which, like Environment.CurrentDirectory during debugging, do not equate to the final output path. Perhaps all you need is some more debugging. I have written programs that require me to copy my 3rd party DLL's manually as part of my [ClassInitialize()] routine to said temp directories in order for the assemblies to resolve properly during unit tests.
P.Brian.Mackey
@P.Brian.Mackey: Thanks for your comment.In the VS Test Runner this system runs quite happily using the AppDomain.BaseDirectory, which typically resolves to the Out/ directory of the current test result. However, the nUnit test runner (used by a colleague) uses shadow copying and additional relative paths for PrivateBinPath so it doesn't work.Equally, in Asp.Net GetExecutingAssembly only solves part of the problem, because that'll be the dynamic dll created for the current page; that provides the dynamic directory, but not the bin\ folder, hence I have to double up with BaseDirectory as well.
Andras Zoltan
A: 

I have been unable to find any public API for this; and the closest I've got is to simply read the documentation on how assemblies are located by the runtime.

Either way, it looks as if I'm going to have to hand-crank what will initially be a black-hole method that follows these heuristics as close as possible, and then find as many different ways to do some integration testing!

Andras Zoltan