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

发布于 2024-09-05 09:58:05 字数 2199 浏览 5 评论 0原文

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

原始问题

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

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

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

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

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

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

有什么想法吗!?

UPDATE: I now have a solution I'm much happier with that, whilst not solving all the problems I ask about, it does leave the way clear to do so. I've updated my own answer to reflect this.

Original Question

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!?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

捎一片雪花 2024-09-12 09:58:05

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

我后来发现的黄金法则是 始终使用负载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);
    }
  }
}

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

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

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

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

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

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.

烟火散人牵绊 2024-09-12 09:58:05

这就是我所做的:

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

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);
    }
}
末骤雨初歇 2024-09-12 09:58:05

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

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.

菊凝晚露 2024-09-12 09:58:05

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

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

Based on the answer I went and expanded the solution to a reusable utility with programmable functions to change default assembly discovery behavior. Please correct me if subfolders should be included in the discovery.

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;
        }
    }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文