【译】在C#中获取程序集比你想得要困难
原文链接:传送门。
某一天我正在写一些反射代码,目的是遍历所有的程序集来查找一个特定的接口,然后在Startup中调用其上的一个方法。看起来这个功能似乎很简单,但是在现实中,却没有一个清晰的,简单的,适合各种情形的方式来获取一个程序集。这篇文章获取对某些人来说非常的枯燥,但是如果我能够帮助哪怕一个人来解决此类问题,那么这篇文章也是值得的。
说真的,由于有多种获取程序集的方法,我将不会说”使用这个方法”。很有可能,对于你的特定的工程来说,也许只有一种方式可以工作,所以依赖其他的方式是毫无意义的。让我们简单的对所有的方式做个测试,然后看看哪一个方法是最合理的。
使用AppDomain.GetAssemblies
你可能会遇到的第一个选项是AppDomain.GetAssemblies。它(看似)加载了在AppDomain中的所有程序集,基本上可以说加载了你项目使用到的每一个程序集。但是却存在着大量的警告。在.NET中程序集是延迟加载到AppDomain中的,它不可能一次性加载所有的程序集,而是等你调用了一个程序集中的方法/类的时候,它才会将它加载进来--也就是即时加载。这是合理的,因为如果你从不使用一个程序集的话,是没有理由加载它的。
但问题是在你调用AppDomain.GetAssemblies()的那个时间点上,如果你并没有调用某个特定的程序集的方法,它便不会被加载。现在如果你要为了Startup方法得到所有的程序集,很有可能你还没有调用到那个程序集,这就意味着它还没有加载到AppDomain,所以便获取不到这个接口方法。
用代码来说:
AppDomain.CurrentDomain.GetAssemblies(); // Does not return SomeAssembly as it hasn't been called yet. SomeAssembly.SomeClass.SomeMethod(); AppDomain.CurrentDomain.GetAssemblies(); // Will now return SomeAssembly.
虽然这看起来是一个很有吸引力的选项,但是要知道,对于这个方法来说,时机是一切。
使用AssemblyLoad事件
因为你不能确保当你调用CurrentDomain.GetAssemblies()时所有程序集都被加载了,而实际上当AppDomain加载另一个程序集的时候有一个事件会运行。基本上说,当一个程序集被延迟加载的时候,你可以被通知到。它看起来像是这样:
AppDomain.CurrentDomain.AssemblyLoad += (sender, args) => { var assembly = args.LoadedAssembly; };
如果你只是想当程序集加载的时候检查下一些东西,那这或许是一个不错的解决方案,但是这个过程在某个特定的点并不是必然会发生(在你的.NET Core app的Startup.cs类中并不会发生)。
这个方法的另一个问题是到你添加你的事件处理器的那个时刻,并不能保证程序集还没有被加载(事实上它们很可能已经加载过了)。所以呢?你需要付出双份的努力,首先添加你的事件处理器,之后迅速的检查AppDomain.CurrentDomain.GetAssemblies,找到已经被加载了的东西。
这是一个完美的解决方案,但是如果你习惯于使用延迟加载的程序集来做事的话,这就不能正常工作了。
使用 GetReferencedAssemblies()
排名中的下一个是GetReferencedAssemblies()。本质上你可以通过一个程序集,比如你的入口点程序集,一般来说便是你的web项目,来得到所有引用的程序集。
其代码本身看起来像是这样:
Assembly.GetEntryAssembly().GetReferencedAssemblies();
再一次,看起来像是在玩把戏,但是这个方法有另一个很大的问题。在许多项目中会有一个“模式分离”的概念,比如 Web Project>>Service Project>>Data Project。Web Project本身并不直接引用Data Project。而当你调用“GetReferencedAssemblies”时其意味着直接引用。因此如果你期望在程序集列表中得到Data Project,你将会失望不已。
所以,再一次的,在一些情况下可以正常工作,但并不是一个普遍的解决方案。
循环GetReferencedAssemblies()
使用GetReferencedAssemblies()的另一个选择是创建一个方法来遍历所有的程序集。类似于这样:
public static List GetAssemblies() { var returnAssemblies = new List(); var loadedAssemblies = new HashSet(); var assembliesToCheck = new Queue(); assembliesToCheck.Enqueue(Assembly.GetEntryAssembly()); while(assembliesToCheck.Any()) { var assemblyToCheck = assembliesToCheck.Dequeue(); foreach(var reference in assemblyToCheck.GetReferencedAssemblies()) { if(!loadedAssemblies.Contains(reference.FullName)) { var assembly = Assembly.Load(reference); assembliesToCheck.Enqueue(assembly); loadedAssemblies.Add(reference.FullName); returnAssemblies.Add(assembly); } } } return returnAssemblies; }
这个方法的边界处理得有点粗糙,但是它的确可以工作并且意味着在Startup方法中,你可以立即看到所有的程序集。
关于这个方法你可能会被卡住的一次是如果你正在动态加载程序集,并且它们实际上并不会被任何项目所引用。对于这种情况,你需要下一个方法。
目录DLL加载
一个很粗糙的获取所有解决方案dll的方式是将它们加载出你的bin文件夹。看起来像是这样:
public static Assembly[] GetSolutionAssemblies() { var assemblies = Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "*.dll") .Select(x => Assembly.Load(AssemblyName.GetAssemblyName(x))); return assemblies.ToArray(); }
它可以正常工作但的确是一个粗糙的解决方案。但是使用这个方式的一个最大的好处是一个dll只需要简单的放置在需要加载的目录中就可以。因此如果你出于任何原因动态的加载DLLs,对于你来说,这很可能是唯一的方法(除过在AppDomain中监听AssemblyLoad)。
这是做这件事情的看起来像恶作剧的方式之一。但是很可能你已经被阻挡到角落之中而这正是解决问题的唯一方式。
仅仅得到“我的”程序集
使用这些方法中的任何一种,您会很快发现你正在将Nuget下的每个程序集加载到你的项目中,包括Nuget包、.NET核心库甚至运行时特定的dll。在.NET世界中,程序集就是程序集。没有“是的,但这是我的程序集”并且它们很特别的概念。
过滤的唯一方法就是检查名字。您可以将其作为白名单来执行,因此如果解决方案中的所有项目都以“MySolution”开头。因而你可以像这样来进行过滤:
Assembly.GetEntryAssembly().GetReferencedAssemblies().Where(x => x.Name.StartsWith("MySolution."))
或者你可以选择一个黑名单选项,这个选项并不真正限制你的程序集,但至少可以减少你正在加载/检查/处理的程序集的数量。就像这样:
Assembly.GetEntryAssembly().GetReferencedAssemblies() .Where(x => !x.Name.StartsWith("Microsoft.") && !x.Name.StartsWith("System."))
黑名单可能看起来很蠢,但在某些情况下,如果您正在构建一个实际上不知道最终解决方案名称的库,那么这是减少您试图加载的内容的唯一方法。