钱行慕

导航

【译】在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."))

黑名单可能看起来很蠢,但在某些情况下,如果您正在构建一个实际上不知道最终解决方案名称的库,那么这是减少您试图加载的内容的唯一方法。

posted on 2020-07-22 21:16  钱行慕  阅读(2917)  评论(1编辑  收藏  举报