【问题标题】:How to pre-load all deployed assemblies for an AppDomain如何为 AppDomain 预加载所有已部署的程序集
【发布时间】:2011-03-02 14:03:54
【问题描述】:

更新:我现在有了一个让我更满意的解决方案,虽然没有解决我提出的所有问题,但它确实为这样做留下了明确的道路。我已经更新了自己的答案以反映这一点。

原始问题

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

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

我已经写过的基本加载算法如下。它深度扫描一组文件夹中的任何 .dll(.exe 正在被排除此刻),如果在程序集中找不到它的 AssemblyName,则使用 Assembly.LoadFrom 加载 dll已经加载到 AppDomain 中(这实现效率低,但可以稍后优化):

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 放置动态生成的页面类和 Asp 页面代码引用的任何程序集) ),然后是站点的 Bin 文件夹 - 可以从 AppDomain.SetupInformation.PrivateBinPath 属性中找到。

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

这使用了影子复制(因此我的算法需要从影子复制放置文件夹中发现并加载它们,而不是从 bin 文件夹中),并将 PrivateBinPath 设置为相对于基本目录。

当然还有很多我可能没有考虑过的其他托管场景;但它必须是有效的,否则 Fusion 会在加载程序集时阻塞。

我想停止四处摸索,并在 hack 上引入 hack 以适应这些新场景的出现 - 我想要的是,在给定 AppDomain 及其设置信息的情况下,能够生成我应该扫描的文件夹列表为了获取所有将要加载的 DLL;不管 AppDomain 是如何设置的。如果 Fusion 可以将它们视为完全相同,那么我的代码也应该如此。

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

有什么想法吗!?

【问题讨论】:

  • 不是一个真正的答案,但对我很有帮助的是这篇 MSDN 文章:msdn.microsoft.com/en-us/library/yx7xezcf.aspx)。并且提到的 Fuslogvw.exe 应该有助于获得“为什么找不到那个 dll”。
  • ralf.w:我确实想知道我是否可以通过获取一个不存在的程序集的融合日志并对其进行解析以查看它正在寻找的所有位置来作弊。虽然它可能会起作用,但我觉得我必须在目标框上打开 Fusion Logging(= 慢);加上我会按异常编码的事实,这完全是错误的!感谢文章的链接——也许更多的 MSDN 挖掘可能会给我答案......
  • 我正好有这个问题,你愿意分享你得到的代码吗?谢谢:)
  • @Andrew Bullock:愿意;但我还没有完成编写和测试它呢!一旦我有了,我肯定会在这里发布代码。确实,如果您事先得到一个好的解决方案,请随时在此处添加答案,我会将您标记为答案:)

标签: c# .net reflection assemblies


【解决方案1】:

您是否尝试过查看 Assembly.GetExecutingAssembly().Location?这应该为您提供运行代码的程序集的路径。在 NUnit 的情况下,我希望这是程序集被影子复制到的位置。

【讨论】:

  • 是的,这是我第一次尝试这样做,但在某些更强大的情况下(例如 VS 测试运行程序)它是不可靠的。此外,例如,在 Asp.Net 的情况下,程序集加载器实际上有许多将在执行程序集位置之外搜索的文件夹。
  • 我绝对同意仅靠它本身不足以涵盖所有情况...我认为它只会为您提供更多数据点。因此,如果我的理解正确,您是否正在寻找一种可以调用的 API 来为您提供所有 Fusions 搜索路径?
  • 那肯定是圣杯;我在 .Net 框架中看不到任何可以做到这一点的东西以及非托管 API;我并不被那些东西吓到(多年来我一直在 C++ 上咬牙切齿),但是很难找到任何东西。我看过 Fusion API,但它主要用于与 GAC 合作
  • 我不同意 Assembly.GetExecutingAssembly() 在 VS 测试运行器的情况下不可靠(假设您的意思是 Visual Studio 单元测试)。单元测试从临时目录运行,例如调试期间的 Environment.CurrentDirectory,不等同于最终输出路径。也许您需要的只是更多的调试。我编写的程序要求我将我的第 3 方 DLL 作为我的 [ClassInitialize()] 例程的一部分手动复制到所述临时目录,以便在单元测试期间正确解析程序集。
  • @P.Brian.Mackey:感谢您的评论。在 VS Test Runner 中,该系统使用 AppDomain.BaseDirectory 运行得非常愉快,它通常解析为当前测试结果的 Out/ 目录。但是,nUnit 测试运行程序(由同事使用)使用卷影复制和 PrivateBinPath 的附加相对路径,因此它不起作用。同样,在 Asp.Net 中 GetExecutingAssembly 只能解决部分问题,因为那将是动态 dll为当前页面创建;它提供动态目录,但不提供 bin\ 文件夹,因此我也必须加倍使用 BaseDirectory。
【解决方案2】:

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

我发现的黄金法则是always use the load context,而不是 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);
    }
  }
}

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

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

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

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

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

【讨论】:

  • 这太棒了。正是我想要的。感谢您发布更新的代码!!!!
  • 您是在 Assembly.Load 之前对 dll 文件进行任何检查以检查它是否甚至是 .net dll,还是只是捕获异常并继续前进?
  • @JJS - 是的,所有潜在的断点都在try/catch - 在我使用它的情况下,我们没有任何非 CLR DLL,所以它从来都不是问题,但应该没问题。
【解决方案3】:

这就是我的工作:

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);
    }
}

【讨论】:

  • 很有趣——这看起来是个不错的解决方案;但是,PrivateBinPath 文件夹显然可以是 ApplicationBase 下由分号分隔的文件夹列表,将从中加载 DLL。它们可以是相对的或绝对的(尽管如果不在 ApplicationBase 下,这些将被忽略)。所以也许需要在那里进行调整。此外 - 在 Asp.Net 应用程序中,使用 LoadFrom 时首先从 Asp.Net 临时目录加载 dll 很重要。如果您从 bin\ 加载 A.dll,则运行时稍后将再次从 temp 文件夹加载它,您最终会得到同一个程序集的两个副本:(
  • 我在 asp.net 应用程序中使用它,到目前为止没有任何问题。我需要先从温度加载? grrr 这是一个非常糟糕的问题。
  • 是的,你这样做是绝对安全的(如果有机加载,它似乎取决于运行时将程序集定位在哪里,我只设法以这种方式删除了被欺骗的程序集)。调试那个特定问题也很痛苦!但是-无论如何+1 :)
  • 我现在发布了我最新的改进解决方案。它与我原来的和你的有细微的不同——我可以保证它工作得很好(除非有一个 PrivateBinPath 就地,我认为这将是一个简单的修复)。它适用于 Web 应用程序、普通 Windows 应用程序和 MS Test 托管的测试。
【解决方案4】:

根据答案,我将解决方案扩展为具有可编程功能的可重用实用程序,以更改默认程序集发现行为。如果子文件夹应该包含在发现中,请纠正我。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;

namespace TheOperator.Foundation.Features
{
    public static class AssemblyLoading
    {
        public static bool IsAssemblyFile(
            string path)
        {
            return File.Exists(path) && Path.GetExtension(path).Equals(".dll", StringComparison.OrdinalIgnoreCase);
        }

        public static Assembly[] TryLoadFromDirectory(
            string path)
            => TryLoadFromDirectoryCore(path).ToArray();

        private static IEnumerable<Assembly> TryLoadFromDirectoryCore(
            string path)
        {
            if (Directory.Exists(path))
            {
                foreach (var assemblyFile in DiscoverFromDirectory(path))
                {
                    if (!IsLoaded(assemblyFile))
                    {
                        yield return Assembly.LoadFrom(assemblyFile);
                    }
                }
            }
        }

        public static Func<IEnumerable<Assembly>> GetLoaded
            = () => AppDomain.CurrentDomain.GetAssemblies();

        public static Func<string, IEnumerable<string>> DiscoverFromDirectory
            = x => Directory.GetFiles(x).Where(xx => IsAssemblyFile(xx));

        public static Action<List<string>> CollectDiscoveryDirectories
            = x =>
            {
                x.Add(AppDomain.CurrentDomain.BaseDirectory);
                x.Add(AppDomain.CurrentDomain.SetupInformation.PrivateBinPath);
            };

        public static void TryLoadFromDiscoveryDirectories()
        {
            var locations = new List<string>();
            CollectDiscoveryDirectories(locations);
            foreach (var location in locations)
            {
                TryLoadFromDirectory(location);
            }
        }

        public static bool TryLoadFromFile(
            string path)
        {
            if (IsAssemblyFile(path))
            {
                if (!IsLoaded(path))
                {
                    Assembly.Load(path);
                    return true;
                }
                else
                {
                    return false;
                }
            }
            else
            {
                return false;
            }
        }

        public static bool IsLoaded(
            string assembly)
        {
            foreach (var loadedAssembly in GetLoaded())
            {
                if (!loadedAssembly.IsDynamic && loadedAssembly.Location.Equals(assembly, StringComparison.OrdinalIgnoreCase))
                {
                    return true;
                }
            }
            return false;
        }
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-07-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多