有没有一种方法可以强制加载所有引用的程序集到应用程序域中?

95

我的项目设置如下:

  • “定义”项目
  • “实现”项目
  • “消费者”项目

“消费者”项目引用了“定义”和“实现”项目,但未在代码中静态地引用“实现”项目中的任何类型。

当应用程序启动时,“消费者”项目调用“定义”项目中的一个静态方法,该方法需要找到“实现”项目中的类型。

是否有一种办法可以强制将所有被引用的程序集加载到应用程序域中,而不必知道路径或名称,并且最好不需要使用全功能IOC框架?


1
这会引起什么问题?为什么需要强制加载? - Mike Two
它根本没有被加载,可能是因为没有静态依赖。 - Daniel Schaffer
你是如何在实现中尝试“查找类型”的?你是在寻找实现特定接口的内容吗? - Mike Two
2
@Mike:是的。我正在使用AppDomain.CurrentDomain.GetAssemblies,并使用linq查询对它们中的每一个递归调用GetTypes()。 - Daniel Schaffer
10个回答

102

这似乎解决了问题:

var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies().ToList();
var loadedPaths = loadedAssemblies.Select(a => a.Location).ToArray();
            
var referencedPaths = Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "*.dll");
var toLoad = referencedPaths.Where(r => !loadedPaths.Contains(r, StringComparer.InvariantCultureIgnoreCase)).ToList();

toLoad.ForEach(path => loadedAssemblies.Add(AppDomain.CurrentDomain.Load(AssemblyName.GetAssemblyName(path))));

正如Jon所指出的,理想的解决方案需要递归地加载每个程序集的依赖项,但在我的特定情况下,我不必担心这个问题。


更新: .NET 4中包含的托管可扩展框架(System.ComponentModel)具有更好的功能来完成此类操作。


6
这对我没用,我的引用程序集没有被加载,因此它不会出现在AppDomain.CurrentDomain.GetAssemblies()中。嗯... - Ted
11
什么设施?我在搜索过程中没有发现任何东西。 - marknuzz
11
利用MEF,以上代码可以简化为:new DirectoryCatalog(".")(需要引用System.ComponentModel.Composition)。 - Allon Guralnek
2
这个答案解决了我的问题并且对我有用。我有一个MS单元测试项目引用了我的另一个程序集,但是当运行测试时,AppDomain.CurrentDomain.GetAssemblies()没有返回那个程序集。我怀疑即使我的单元测试使用了那个库的代码,由于vs.net加载MS单元测试项目(类库)的方式与运行常规.exe应用程序不同,该程序集可能不会出现在“GetAssemblies”中。如果您的代码使用反射并且未通过单元测试,请记住这一点。 - Dean Lunz
8
想要补充一点,要注意动态加载的程序集。在动态程序集中不支持调用成员。可以通过过滤掉IsDynamic = false的程序集或者如果能够容错加载,可以在对CurrentDomain.Load进行try/catch操作。还有一个需要检查的是assembly.Location,不过这个方法对于IsDynamic程序集是不起作用的。 - Eli Gassert
它非常丑陋,我尝试了各种方法来避免使用它。然而,它确实有效,经过数小时的试错,我终于找到了原因。本可以节省这些时间... - Kobor42

68
你可以使用 Assembly.GetReferencedAssemblies 方法获取一个 AssemblyName[] 数组,然后对每个数组中的元素调用 Assembly.Load(AssemblyName) 方法进行加载。当然,你需要递归调用该方法,并且最好能够跟踪已经加载过的程序集 :)

4
如果您需要加载参考程序集,上述内容将解决您的问题。 如果有所帮助,您还可以要求针对特定类型typeof(T).Assembly。 我有一种感觉,您需要动态加载包含实现(未被引用)的程序集。 如果是这种情况,您将不得不手动保留名称的静态列表并加载它们,或者遍历整个目录,加载然后找到具有正确接口的类型。 - Fadrian Sudaman
1
@vanhelgen:根据我的经验,这通常不是你需要明确处理的事情。通常CLR的“按需加载”可以很好地工作。 - Jon Skeet
3
在正常情况下,这可能是正确的,但是当使用DI容器(通过System.Reflection)发现可用服务时,它自然无法找到尚未加载的程序集中包含的服务。我一直采用的默认方法是在我的应用程序的CompositionRoot中从每个引用程序集的任意类型创建一个虚拟子类,以确保所有依赖项都已就位。我希望我可以通过预先加载所有内容来跳过这些无聊的步骤,即使以进一步增加启动时间为代价。@JonSkeet还有其他方法吗?谢谢 - mfeineis
@vanhelgen:如果依赖注入容器需要查找类型,它应该查看引用的程序集或特定目录中的程序集。但除此之外,我回答中的技术应该是可以的。 - Jon Skeet
16
这种方法的缺陷在于GetReferencedAssemblies函数似乎返回一个"优化"过的列表 - 如果你没有明确调用引用程序集中的代码,它就不会被包含。(参考这个讨论) - FTWinston
显示剩余5条评论

24

我想分享一个递归的例子。我在启动程序中这样调用LoadReferencedAssembly方法:

foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
{
    this.LoadReferencedAssembly(assembly);
}

这是递归方法:

private void LoadReferencedAssembly(Assembly assembly)
{
    foreach (AssemblyName name in assembly.GetReferencedAssemblies())
    {
        if (!AppDomain.CurrentDomain.GetAssemblies().Any(a => a.FullName == name.FullName))
        {
            this.LoadReferencedAssembly(Assembly.Load(name));
        }
    }
}

5
我想知道循环引用是否会导致堆栈溢出异常。 - Ronnie Overby
1
罗尼,我相信不是这样的。代码只在name不在AppDomain.CurrentDomain.GetAssemblies()中时才运行递归,这意味着仅当foreach挑选的AssemblyName尚未加载时才会发生递归。 - Felype
1
我对这个算法的O(n^2)运行时间(foreach中的GetAssemblies().Any(...)))感到不满意。我会使用HashSet将其降至O(n)级别。 - Dai

16
如果您使用Fody.Costura或任何其他程序集合并解决方案,则这个被接受的答案将不起作用。
以下内容加载当前加载的任何程序集的引用程序集。递归留给您自己去实现。

如果您使用 Fody.Costura 或其他程序集合并解决方案,则无法使用已接受的答案。

以下代码会加载任何当前已加载的程序集的引用程序集,但递归处理需要您自己实现。

var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies().ToList();

loadedAssemblies
    .SelectMany(x => x.GetReferencedAssemblies())
    .Distinct()
    .Where(y => loadedAssemblies.Any((a) => a.FullName == y.FullName) == false)
    .ToList()
    .ForEach(x => loadedAssemblies.Add(AppDomain.CurrentDomain.Load(x)));

请问这段代码应该放在哪里? - Telemat
1
我想是在你的引导程序/启动中。 - Meirion Hughes
1
我可能错了,但我认为你可以在你的 .Where 中检查 !y.IsDynamic - Felype
1
这段代码片段不起作用。在我的测试中,初始的 var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies().ToList(); 没有返回未使用的程序集,因此根本不会传播它们。 - Ted
是的,那就是我所说的递归注释——需要加载依赖项的依赖项等等。已经过去很多年了,也许现在有更好的方法。 - Meirion Hughes

2

今天我需要从特定路径加载一个程序集和它的依赖项,因此我编写了这个类来实现。

public static class AssemblyLoader
{
    private static readonly ConcurrentDictionary<string, bool> AssemblyDirectories = new ConcurrentDictionary<string, bool>();

    static AssemblyLoader()
    {
        AssemblyDirectories[GetExecutingAssemblyDirectory()] = true;
        AppDomain.CurrentDomain.AssemblyResolve += ResolveAssembly;

    }

    public static Assembly LoadWithDependencies(string assemblyPath)
    {
        AssemblyDirectories[Path.GetDirectoryName(assemblyPath)] = true;
        return Assembly.LoadFile(assemblyPath);
    }

    private static Assembly ResolveAssembly(object sender, ResolveEventArgs args)
    {
        string dependentAssemblyName = args.Name.Split(',')[0] + ".dll";
        List<string> directoriesToScan = AssemblyDirectories.Keys.ToList();

        foreach (string directoryToScan in directoriesToScan)
        {
            string dependentAssemblyPath = Path.Combine(directoryToScan, dependentAssemblyName);
            if (File.Exists(dependentAssemblyPath))
                return LoadWithDependencies(dependentAssemblyPath);
        }
        return null;
    }

    private static string GetExecutingAssemblyDirectory()
    {
        string codeBase = Assembly.GetExecutingAssembly().CodeBase;
        var uri = new UriBuilder(codeBase);
        string path = Uri.UnescapeDataString(uri.Path);
        return Path.GetDirectoryName(path);
    }
}

1
好的代码,除非没有必要使用字典,在这种情况下,一个简单的列表就足够了。我假设你原来的代码需要知道哪些程序集已加载,哪些没有加载,这就是为什么你有字典的原因。 - Reinis

0

如果您的程序集在编译时没有引用任何代码,那么即使您已将该项目或NuGet包添加为引用,这些程序集也不会作为其他程序集的引用被包含进来。这与DebugRelease构建设置、代码优化等无关。在这种情况下,您必须显式调用Assembly.LoadFrom(dllFileName)来加载程序集。


0

另一种版本(基于Daniel Schaffer的答案)是当您可能不需要加载所有程序集,而只需要预定义数量的情况:

var assembliesToLoad = { "MY_SLN.PROJECT_1", "MY_SLN.PROJECT_2" };

// First trying to get all in above list, however this might not 
// load all of them, because CLR will exclude the ones 
// which are not used in the code
List<Assembly> dataAssembliesNames =
   AppDomain.CurrentDomain.GetAssemblies()
            .Where(assembly => AssembliesToLoad.Any(a => assembly.GetName().Name == a))
            .ToList();

var loadedPaths = dataAssembliesNames.Select(a => a.Location).ToArray();

var compareConfig = StringComparison.InvariantCultureIgnoreCase;
var referencedPaths = Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "*.dll")
    .Where(f =>
    {
       // filtering the ones which are in above list
       var lastIndexOf = f.LastIndexOf("\\", compareConfig);
       var dllIndex = f.LastIndexOf(".dll", compareConfig);

       if (-1 == lastIndexOf || -1 == dllIndex)
       {
          return false;
       }

       return AssembliesToLoad.Any(aName => aName == 
          f.Substring(lastIndexOf + 1, dllIndex - lastIndexOf - 1));
     });

var toLoad = referencedPaths.Where(r => !loadedPaths.Contains(r, StringComparer.InvariantCultureIgnoreCase)).ToList();

toLoad.ForEach(path => dataAssembliesNames.Add(AppDomain.CurrentDomain.Load(AssemblyName.GetAssemblyName(path))));

if (dataAssembliesNames.Count() != AssembliesToLoad.Length)
{
   throw new Exception("Not all assemblies were loaded into the  project!");
}

0
在我的WinForms应用程序中,我为JavaScript(在WebView2控件中)提供了调用各种.NET事物的可能性,例如Microsoft.VisualBasic.dll程序集中的Microsoft.VisualBasic.Interaction方法(例如InputBox()等)。
但是,我的应用程序本身不使用该程序集,因此该程序集从未加载。
因此,为了强制加载程序集,我最终在Form1_Load中添加了以下内容:
if (DateTime.Now < new DateTime(1000, 1, 1, 0, 0, 0)) { // never happens
  Microsoft.VisualBasic.Interaction.Beep();
  // you can add more things here
}

编译器认为可能需要汇编,但实际上这当然永远不会发生。

这不是一个非常复杂的解决方案,但快速而有效。


0

要按名称获取引用的程序集,您可以使用以下方法:

public static Assembly GetAssemblyByName(string name)
{
    var asm = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.FullName == name);
    if (asm == null)
        asm = AppDomain.CurrentDomain.Load(name);
    return asm;
}

-1
我根据@Jon Skeet的答案创建了自己的答案,并添加了名称前缀过滤器,以避免加载不必要的程序集。
public static IEnumerable<Assembly> GetProjectAssemblies(string prefixName)
{
    var assemblies = new HashSet<Assembly>
    {
        Assembly.GetEntryAssembly()
    };

    for (int i = 0; i < assemblies.Count; i++)
    {
        var assembly = assemblies.ElementAt(i);

        var referencedProjectAssemblies = assembly.GetReferencedAssemblies()
            .Where(assemblyName => assemblyName.FullName.StartsWith(prefixName))
            .Select(assemblyName => Assembly.Load(assemblyName));

        assemblies.UnionWith(referencedProjectAssemblies);
    }

    return assemblies;
}

在这种情况下,可能并不重要,但我想指出,在集合上调用 ElementAt(i) 会迭代它,直到达到第 i 个元素,因此导致上面的代码总体上是 _O(N^2)_。我建议使用 foreach(var assembly in assemblies) - vyrp
@vyrp nop: System.InvalidOperationException:'集合已修改;无法执行枚举操作。' - Francisco
啊,我没有意识到“assemblies”正在被修改。那么,按照我的建议会抛出异常。但是,这段代码不是依赖于未记录的行为吗?HashSet的元素不能保证以特定顺序排列。使用UnionWith添加新元素后,元素是否会移动并且ElementAt(i)不会返回您期望的内容?您几乎像使用一个无重复项的Queue一样使用HashSet - vyrp
我总是使用哈希集合,从来没有失败过,哈哈。你有什么建议吗? - Francisco
如果引用链中的第一个引用未被使用,则此方法将无法正常工作。因此: MainApplications 引用: ReferenceA --> ReferenceB --> ReferenceC如果在 MainApplication 中未使用 ReferenceA 中的类,则调用 GetReferencedAssemblies() 将不会返回 ReferenceA,也不会返回 ReferenceB 或 ReferenceC。 - Ted

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接