如何为AppDomain预加载所有已部署的程序集

And*_*tan 29 .net c# reflection assemblies

更新:我现在有了一个解决方案,我对此更加满意,虽然没有解决我提出的所有问题,但它确实让我们明白了.我已经更新了自己的答案以反映这一点.

原始问题

给定App域,Fusion(.Net程序集加载器)将针对给定程序集进行探测,有许多不同的位置.显然,我们认为这个功能是理所当然的,因为探测似乎嵌入在.Net运行时Assembly._nLoad内部(内部方法似乎是反射加载时的入口点 - 我假设隐式加载可能由同一个底层覆盖算法),作为开发人员,我们似乎无法访问这些搜索路径.

我的问题是我有一个组件可以执行大量动态类型解析,并且需要能够确保在开始工作之前预先加载给定AppDomain的所有用户部署的程序集.是的,它减慢了启动速度 - 但我们从这个组件中获得的好处完全超出了这一点.

我已经编写的基本加载算法如下.它深扫描任何.dll文件(.EXE文件被排除一组文件夹的那一刻),并使用Assembly.LoadFrom如果它的AssemblyName不能在集已经加载到AppDomain中组件可以找到加载DLL(这实现效率低下,但可以在以后进行优化):

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);
  }    
}
Run Code Online (Sandbox Code Playgroud)

使用LoadFrom是因为我发现使用Load()会导致Fusion加载重复的程序集,如果它在探测它时,它找不到从它希望找到它的地方加载的程序集.

因此,有了这个,我现在要做的就是获得Fusion在搜索程序集时将要使用的搜索路径的优先顺序(从高到低)的列表.然后我可以简单地遍历它们.

GAC与此无关,我对Fusion可能使用的任何环境驱动的固定路径不感兴趣 - 只有那些可以从AppDomain收集的路径,其中包含为应用程序明确部署的程序集.

我的第一次迭代只使用了AppDomain.BaseDirectory.这适用于服务,表单应用程序和控制台应用程序.

但是,它不适用于Asp.Net网站,因为至少有两个主要位置 - AppDomain.DynamicDirectory(其中Asp.Net放置动态生成的页面类和Aspx页面代码引用的任何程序集),以及然后是站点的Bin文件夹 - 可以从AppDomain.SetupInformation.PrivateBinPath属性中找到它.

所以我现在有最基本类型的应用程序的工作代码(自文件系统虚拟化以来,Sql Server托管的AppDomains是另一个故事) - 但是我几天前遇到了一个有趣的问题,这个代码根本不起作用:nUnit测试运行器.

这使用了Shadow Copying(因此我的算法需要从shadow-copy drop文件夹中发现并加载它们,而不是从bin文件夹中加载)并且它将PrivateBinPath设置为相对于基本目录.

当然还有很多其他托管方案,我可能没有考虑过; 但必须有效,否则Fusion会加载装配.

我想停止感觉并引入黑客入侵以适应这些新场景,因为它们突然出现 - 我想要的是,给定AppDomain及其设置信息,能够生成我应扫描的文件夹列表以便选择所有要加载的DLL; 无论AppDomain如何设置.如果Fusion可以看到它们都一样,那么我的代码也应如此.

当然,如果.Net更改其内部结构,我可能不得不改变算法 - 这只是我必须承受的交叉.同样,我很高兴将SQL Server和任何其他类似的环境视为现在仍然不受支持的边缘情况.

有任何想法吗!?

And*_*tan 19

我现在能够得到更接近最终解决方案的东西,除了它仍然没有正确处理私有bin路径.我用这个替换了我之前的实时代码,并且还解决了我讨价还价的一些讨厌的运行时错误(C#代码的动态编译引用了太多的dll).

我发现的黄金法则总是使用加载上下文,而不是LoadFrom上下文,因为Load上下文总是第一个.Net在执行自然绑定时看起来.因此,如果您使用LoadFrom上下文,只有在实际从自然绑定它的地方加载它时才会受到影响 - 这并不总是很容易.

此解决方案适用于Web应用程序,考虑到bin文件夹与"标准"应用程序的区别.PrivateBinPath一旦我能够准确地掌握它的读取方式(!),它就可以轻松扩展以适应问题

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);
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

首先,我们有用于检索我们选择的"app文件夹"的方法.这些是已部署用户部署的程序集的位置.这是一个IEnumerable因为的PrivateBinPath边缘的情况下(这可以是一系列的位置),但实际上它是目前唯一曾经在一个文件夹:

下一个方法是PreLoadDeployedAssemblies(),在做任何事情之前被调用(这里它被列为private static- 在我的代码中,这是从一个更大的静态类中获取的,该静态类具有公共端点,它总是会在第一次执行任何操作之前触发此代码.

最后是肉和骨头.这里最重要的是获取一个汇编文件并获取它的汇编名称,然后传递给它Assembly.Load(AssemblyName)- 而不是使用它LoadFrom.

我以前认为这LoadFrom更可靠,你必须手动去网页应用程序中找到临时的Asp.Net文件夹.你没有.您所需要的只是知道应该确定加载的程序集的名称 - 并将其传递给Assembly.Load.毕竟,这几乎就是.Net的参考加载例程:)

同样,这种方法也适用于通过挂起AppDomain.AssemblyResolve事件实现的自定义程序集探测:将应用程序的bin文件夹扩展到您可能拥有的任何插件容器文件夹,以便对它们进行扫描.你可能已经处理过这个AssemblyResolve事件,以确保在正常探测失败时它们被加载,所以一切都像以前一样工作.