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

标签 c# .net reflection assemblies

更新:我现在有了一个解决方案,我对此感到高兴得多,虽然没有解决我所问的所有问题,但它确实为这样做留下了清晰的道路。我已经更新了我自己的答案以反射(reflect)这一点。

原始问题

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

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

我已经写好的基本加载算法如下。它会深入扫描一组文件夹中的任何 .dll(目前正在排除 .exes),如果在已加载到 AppDomain 的程序集中找不到它的 AssemblyName,则使用 Assembly.LoadFrom 加载 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);
  }    
}

使用 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 会在加载程序集时窒息。

我不想再四处乱窜,并在这些新场景出现时引入 hack 到 hack 来适应这些新场景——我想要的是,给定一个 AppDomain 及其设置信息,能够生成我应该扫描的文件夹列表以便选择加载所有将要加载的 DLL;无论 AppDomain 是如何设置的。如果 Fusion 可以将它们视为完全相同,那么我的代码也应该如此。

当然,如果 .Net 改变其内部结构,我可能不得不改变算法——这只是我不得不忍受的一个交叉。同样,我很高兴将 SQL Server 和任何其他类似环境视为目前仍不受支持的边缘情况。

有任何想法吗!?

最佳答案

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

我发现的黄金法则是 always use the load context ,而不是 LoadFrom 上下文,因为 Load 上下文将始终是 .Net 在执行自然绑定(bind)时首先查看的位置。因此,如果您使用 LoadFrom 上下文,则只有当您实际从它自然绑定(bind)它的同一位置加载它时,您才会得到命中 - 这并不总是那么容易。

此解决方案适用于 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);
    }
  }
}

首先,我们有用于检索我们选择的“应用程序文件夹”的方法。这些是用户部署的程序集将被部署的地方。由于 PrivateBinPath,它是一个 IEnumerable边缘情况(它可以是一系列位置),但实际上目前它只有一个文件夹:

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

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

我之前以为LoadFrom更可靠,并且您必须手动去查找 Web 应用程序中的临时 Asp.Net 文件夹。你没有。您所要做的就是知道您知道肯定应该加载的程序集的名称 - 并将其传递给 Assembly.Load .毕竟,这实际上就是 .Net 的引用加载例程所做的:)

同样,这种方法与通过挂断 AppDomain.AssemblyResolve 实现的自定义程序集探测配合得很好。事件也是如此:将应用程序的 bin 文件夹扩展到您可能拥有的任何插件容器文件夹,以便它们被扫描。您可能已经处理过 AssemblyResolve无论如何,以确保它们在正常探测失败时被加载,所以一切都像以前一样。

关于c# - 如何为 AppDomain 预加载所有已部署的程序集,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/3021613/

相关文章:

.net - 跨多个线程管理状态

java - 如何在不使用instanceOf或getClass()的情况下知道类类型?

java - 如何通过反射调用方法,并提供方法作为参数

c# - .Net Core API Controller 接受表单或正文数据

c# - .NET SMTP 邮件 - 附件文件名被重命名

c# - 在 C# 中构造树算法的最佳方法

c# - DateTime 在当前上下文中不存在

c# - 使用 InvokeMember 通过 C# 在 Excel 中的文化特定属性

c# - 调用 net tcp 端点时,具有 net tcp 和 http 绑定(bind)的 WCF 服务不会激活

java - 检查并打印嵌套的 Javabean 属性