How to pre-load all deployed assemblies for an AppDomain

╄→尐↘猪︶ㄣ 提交于 2019-11-28 03:16:14

I have now been able to get something much closer to a final solution, except it's still not processing the private bin path correctly. I have replaced my previously live code with this and have also solved a few nasty runtime bugs I've had into the bargain (dynamic compilation of C# code referencing far too many dlls).

The golden rule I've since discovered is always use the load context, not the LoadFrom context, since the Load context will always be the first place .Net looks when performing a natural bind. Therefore, if you use the LoadFrom context, you will only get a hit if you actually load it from the same place that it would naturally bind it from - which isn't always easy.

This solution works both for web applications, taking into account the bin folder difference versus 'standard' apps. It can easily be extended to accommodate the PrivateBinPath problem, once I can get a reliable handle on exactly how it is read(!)

private static IEnumerable<string> GetBinFolders()
{
  //TODO: The AppDomain.CurrentDomain.BaseDirectory usage is not correct in 
  //some cases. Need to consider PrivateBinPath too
  List<string> toReturn = new List<string>();
  //slightly dirty - needs reference to System.Web.  Could always do it really
  //nasty instead and bind the property by reflection!
  if (HttpContext.Current != null)
  {
    toReturn.Add(HttpRuntime.BinDirectory);
  }
  else
  {
    //TODO: as before, this is where the PBP would be handled.
    toReturn.Add(AppDomain.CurrentDomain.BaseDirectory);
  }

  return toReturn;
}

private static void PreLoadDeployedAssemblies()
{
  foreach(var path in GetBinFolders())
  {
    PreLoadAssembliesFromPath(path);
  }
}

private static void PreLoadAssembliesFromPath(string p)
{
  //S.O. NOTE: ELIDED - ALL EXCEPTION HANDLING FOR BREVITY

  //get all .dll files from the specified path and load the lot
  FileInfo[] files = null;
  //you might not want recursion - handy for localised assemblies 
  //though especially.
  files = new DirectoryInfo(p).GetFiles("*.dll", 
      SearchOption.AllDirectories);

  AssemblyName a = null;
  string s = null;
  foreach (var fi in files)
  {
    s = fi.FullName;
    //now get the name of the assembly you've found, without loading it
    //though (assuming .Net 2+ of course).
    a = AssemblyName.GetAssemblyName(s);
    //sanity check - make sure we don't already have an assembly loaded
    //that, if this assembly name was passed to the loaded, would actually
    //be resolved as that assembly.  Might be unnecessary - but makes me
    //happy :)
    if (!AppDomain.CurrentDomain.GetAssemblies().Any(assembly => 
      AssemblyName.ReferenceMatchesDefinition(a, assembly.GetName())))
    {
      //crucial - USE THE ASSEMBLY NAME.
      //in a web app, this assembly will automatically be bound from the 
      //Asp.Net Temporary folder from where the site actually runs.
      Assembly.Load(a);
    }
  }
}

First we have the method used to retrieve our chosen 'app folders'. These are the places where the user-deployed assemblies will have been deployed. It's an IEnumerable because of the PrivateBinPath edge case (it can be a series of locations), but in practise it's only ever one folder at the moment:

The next method is PreLoadDeployedAssemblies(), which gets called before doing anything (here it's listed as private static - in my code this is taken from a much larger static class that has public endpoints that will always trigger this code to be run before doing anything for the first time.

Finally there's the meat and bones. The most important thing here is to take an assembly file and get it's assembly name, which you then pass to Assembly.Load(AssemblyName) - and not to use LoadFrom.

I previously thought that LoadFrom was more reliable, and that you had to manually go and find the temporary Asp.Net folder in web apps. You don't. All you have to is know the name of an assembly that you know should definitely be loaded - and pass it to Assembly.Load. After all, that's practically what .Net's reference loading routines do :)

Equally, this approach works nicely with custom assembly probing implemented by hanging off the AppDomain.AssemblyResolve event as well: Extend the app's bin folders to any plugin container folders you may have so that they get scanned. Chances are you've already handled the AssemblyResolve event anyway to ensure they get loaded when the normal probing fails, so everything works as before.

This is what I do:

public void PreLoad()
{
    this.AssembliesFromApplicationBaseDirectory();
}

void AssembliesFromApplicationBaseDirectory()
{
    string baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
    this.AssembliesFromPath(baseDirectory);

    string privateBinPath = AppDomain.CurrentDomain.SetupInformation.PrivateBinPath;
    if (Directory.Exists(privateBinPath))
        this.AssembliesFromPath(privateBinPath);
}

void AssembliesFromPath(string path)
{
    var assemblyFiles = Directory.GetFiles(path)
        .Where(file => Path.GetExtension(file).Equals(".dll", StringComparison.OrdinalIgnoreCase));

    foreach (var assemblyFile in assemblyFiles)
    {
        // TODO: check it isnt already loaded in the app domain
        Assembly.LoadFrom(assemblyFile);
    }
}

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.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!