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