2010-06-11 68 views
28

UPDATE:我现在有一个解决方案,我对此感到非常高兴,虽然没有解决所有我问过的问题,但确实如此。我已经更新了自己的答案以反映这一点。如何为AppDomain预加载所有已部署的程序集

原始的问题

给定一个应用程序域,也有融合(.NET程序集加载器)会探测一个给定的装配许多不同的位置。显然,我们认为这个功能是理所当然的,因为探测似乎嵌入在.Net运行时(内部方法似乎是反射加载时的入口点 - 并且我认为隐式加载可能覆盖了相同的底层算法),作为开发者,我们似乎无法获得对这些搜索路径的访问。

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

我已经写过的基本加载算法如下。它为任何.dll(.exes被排除在目前)深入扫描一组文件夹,并且使用Assembly.LoadFrom加载dll,如果它的AssemblyName不能在已经加载到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托管的应用程序域是文件系统虚拟化以来的另一个故事) - 但几天前我遇到了一个有趣的问题,不起作用:nUnit测试跑步者。

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

当然,还有一些其他托管方案可能还没有考虑过;但必须是有效的,否则Fusion会在加载程序集时窒息。

我想停止感觉并在黑客入侵时适应这些新场景 - 我想要的是,给定一个AppDomain及其设置信息,能够生成我应该扫描的这个文件夹列表为了拿起所有要加载的DLL;不管AppDomain如何设置。如果Fusion可以看到它们完全相同,那么我的代码也应该如此。

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

任何想法!?

+0

不是一个真正的答案,但对我来说非常有帮助这MSDN-文章:http://msdn.microsoft.com/en-us/library/yx7xezcf。 ASPX)。 而提到的Fuslogvw.exe应该有助于获得“为什么没有找到该DLL”。 – 2010-06-21 06:35:14

+0

ralf.w:我确实想知道是否可以通过为一个不存在的程序集获取一个融合日志并解析它来查看它正在查找的所有地方来作弊。虽然它可能会起作用,但我感觉我不得不在目标盒上打开Fusion Logging(=慢);加上我会被异常编码的事实,这只是错误!感谢您的文章链接 - 也许多一点MSDN挖掘可能会给我答案... – 2010-06-21 07:18:14

+0

我有这个问题,你愿意分享你到达的代码吗?谢谢:) – 2010-10-20 09:46:00

回答

18

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

自从发现的黄金法则是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事件以确保它们在正常探测失败时被加载,因此所有事情都像以前一样工作。

+0

这太棒了。正是我在找的东西。感谢您发布更新的代码! – 2011-04-27 21:36:23

+0

你是否在Assembly.Load检查它是否是.net dll之前检​​查dll文件,或者你是否只是捕获异常并继续前进? – JJS 2016-08-17 14:11:09

+0

@JJS - 是的,所有潜在的突破点都将在try/catch中 - 在我使用它的情况下,我们没有任何非CLR DLL,所以它从来都不是问题,但它应该没事的。 – 2016-09-01 09:00:31

0

你有没有试过看Assembly.GetExecutingAssembly()。位置?这应该为您提供代码运行所在的程序集的路径。在NUnit的情况下,我认为这是组件被阴影复制到的地方。

+0

是的,这是我的第一次尝试这样做,但它是不可靠的一些更强化情况(例如VS测试跑步者)。另外,就Asp.Net而言,例如,程序集加载程序实际上有许多文件夹,它将搜索执行程序集位置之外的文件夹。 – 2010-06-17 07:25:01

+0

我绝对同意,仅仅覆盖所有情况是不够的......我认为它会给你简单的一个数据点。所以如果我正确理解你,你是否在寻找一个你可以调用的API,它会给你所有的Fusions搜索路径? – Andy 2010-06-18 00:11:50

+0

好吧,那肯定是圣杯。我在.Net框架中看不到任何东西,以及非托管API;我并没有被这些东西吓倒(多年来我在C++上削减了我的牙齿),但很难找到任何东西。我曾看过Fusion API,但主要是与GAC工作 – 2010-06-18 08:20:28

4

这是我做的:

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

有趣 - 这看起来像一个很好的解决方案;然而,PrivateBinPath文件夹显然可以是一个由ApplicationBase下分号分隔的文件夹列表,将从其中加载DLL。它们可以是相对的或绝对的(尽管如果不在ApplicationBase下,它们将被忽略)。所以也许需要调整。 另外 - 在Asp.Net应用程序中,使用LoadFrom时首先从Asp.Net临时目录加载dll是很重要的。如果您从bin \加载A.dll,运行时将在以后从temp文件夹再次加载它,并最终得到相同程序集的两个副本:( – 2010-11-02 22:34:22

+0

即时通讯在一个asp.net应用程序中使用它,到目前为止没有任何问题。我需要先从temp加载?grrr这是一个非常糟糕的问题。 – 2010-11-02 22:44:27

+0

是的,你确实是绝对安全的(它似乎取决于运行时将如何定位程序集,如果有机地加载,我只能设法删除duped组件这种方式)这是一个痛苦的屁股调试这个特殊问题! 然而 - +1无论如何:) – 2010-11-25 22:09:49

相关问题