如何在.NET 运行时将文件夹添加到程序集搜索路径?

Posted

技术标签:

【中文标题】如何在.NET 运行时将文件夹添加到程序集搜索路径?【英文标题】:How to add folder to assembly search path at runtime in .NET? 【发布时间】:2010-11-25 06:40:18 【问题描述】:

我的 DLL 是由我们无法自定义的第三方应用程序加载的。我的程序集必须位于它们自己的文件夹中。我不能将它们放入 GAC(我的应用程序需要使用 XCOPY 进行部署)。 当根 DLL 尝试从另一个 DLL(在同一文件夹中)加载资源或类型时,加载失败 (FileNotFound)。 是否可以以编程方式(从根 DLL)将我的 DLL 所在的文件夹添加到程序集搜索路径中?我不允许更改应用程序的配置文件。

【问题讨论】:

【参考方案1】:

您可以将probing path 添加到应用程序的 .config 文件中,但它仅在探测路径包含在应用程序的基目录中时才有效。

【讨论】:

感谢您添加此内容。我已经多次看到AssemblyResolve 解决方案,很高兴有另一个(更简单的)选项。 如果您将应用复制到其他地方,请不要忘记将 App.config 文件与您的应用一起移动。【参考方案2】:

听起来您可以使用 AppDomain.AssemblyResolve 事件并从您的 DLL 目录手动加载依赖项。

编辑(来自评论):

AppDomain currentDomain = AppDomain.CurrentDomain;
currentDomain.AssemblyResolve += new ResolveEventHandler(LoadFromSameFolder);

static Assembly LoadFromSameFolder(object sender, ResolveEventArgs args)

    string folderPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
    string assemblyPath = Path.Combine(folderPath, new AssemblyName(args.Name).Name + ".dll");
    if (!File.Exists(assemblyPath)) return null;
    Assembly assembly = Assembly.LoadFrom(assemblyPath);
    return assembly;

【讨论】:

谢谢你,马蒂亚斯!这有效:AppDomain currentDomain = AppDomain.CurrentDomain; currentDomain.AssemblyResolve += new ResolveEventHandler(LoadFromSameFolderResolveEventHandler);静态程序集 LoadFromSameFolderResolveEventHandler(object sender, ResolveEventArgs args) string folderPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); string assemblyPath = Path.Combine(folderPath, args.Name + ".dll");程序集程序集 = Assembly.LoadFrom(assemblyPath);返回组装; 如果你想“回退”到基本的解析器,你会怎么做。例如if (!File.Exists(asmPath)) return searchInGAC(...); 这行得通,但我找不到任何替代方案。谢谢 仅供参考:即使在 .Net 5 中也可以使用 :) 非常感谢!【参考方案3】:

查看 AppDomain.AppendPrivatePath(已弃用)或 AppDomainSetup.PrivateBinPath

【讨论】:

来自MSDN:更改 AppDomainSetup 实例的属性不会影响任何现有的 AppDomain。当使用 AppDomainSetup 实例作为参数调用 CreateDomain 方法时,它只会影响新 AppDomain 的创建。 AppDomain.AppendPrivatePath 的文档似乎建议它应该支持动态扩展 AppDomain 的搜索路径,只是该功能已被弃用。如果可行,这是一个比重载AssemblyResolve 更清洁的解决方案。 供参考,它看起来像AppDomain.AppendPrivatePath does nothing in .NET Core 和updates .PrivateBinPath in full framework。 是的,所以它在 .Net 4.7.2 中仍然可以正常工作,但在 .Net 核心中将停止工作。如果您还记得 AppDomains 的概念已不再是一个东西,那么这是有道理的。【参考方案4】:

最好的解释from MS itself:

AppDomain currentDomain = AppDomain.CurrentDomain;
currentDomain.AssemblyResolve += new ResolveEventHandler(MyResolveEventHandler);

private Assembly MyResolveEventHandler(object sender, ResolveEventArgs args)

    //This handler is called only when the common language runtime tries to bind to the assembly and fails.

    //Retrieve the list of referenced assemblies in an array of AssemblyName.
    Assembly MyAssembly, objExecutingAssembly;
    string strTempAssmbPath = "";

    objExecutingAssembly = Assembly.GetExecutingAssembly();
    AssemblyName[] arrReferencedAssmbNames = objExecutingAssembly.GetReferencedAssemblies();

    //Loop through the array of referenced assembly names.
    foreach(AssemblyName strAssmbName in arrReferencedAssmbNames)
    
        //Check for the assembly names that have raised the "AssemblyResolve" event.
        if(strAssmbName.FullName.Substring(0, strAssmbName.FullName.IndexOf(",")) == args.Name.Substring(0, args.Name.IndexOf(",")))
        
            //Build the path of the assembly from where it has to be loaded.                
            strTempAssmbPath = "C:\\Myassemblies\\" + args.Name.Substring(0,args.Name.IndexOf(","))+".dll";
            break;
        

    

    //Load the assembly from the specified path.                    
    MyAssembly = Assembly.LoadFrom(strTempAssmbPath);                   

    //Return the loaded assembly.
    return MyAssembly;          

【讨论】:

AssemblyResolve 适用于 CurrentDomain,不适用于其他域 AppDomain.CreateDomain 我看不懂这段代码。 args.Name.Substring(0, args.Name.IndexOf(",")) 无处不在,所以写起来会更简单: strTempAssmbPath = "C:\\Myassemblies\\" + args.Name.Substring(0 ,args.Name.IndexOf(","))+".dll";没有foreach循环?或者,当程序集不在引用程序集之间时,您是否打算在不太可能的情况下引发 FileNotFoundException?即使在这种情况下, arrReferencedAssmbNames.FirstOrDeault 也是更简单的解决方案。【参考方案5】:

框架 4 更新

由于 Framework 4 也为资源引发了 AssemblyResolve 事件,实际上这个处理程序工作得更好。它基于本地化位于应用程序子目录中的概念(一个用于本地化的文化名称,即 C:\MyApp\it 表示意大利语) 里面有资源文件。 如果本地化是国家/地区,即 it-IT 或 pt-BR,则处理程序也可以工作。在这种情况下,处理程序“可能会被多次调用:后备链中的每种文化一次”[来自 MSDN]。这意味着如果我们为“it-IT”资源文件返回 null,则框架会引发请求“it”的事件。

事件挂钩

        AppDomain currentDomain = AppDomain.CurrentDomain;
        currentDomain.AssemblyResolve += new ResolveEventHandler(currentDomain_AssemblyResolve);

事件处理程序

    Assembly currentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
    
        //This handler is called only when the common language runtime tries to bind to the assembly and fails.

        Assembly executingAssembly = Assembly.GetExecutingAssembly();

        string applicationDirectory = Path.GetDirectoryName(executingAssembly.Location);

        string[] fields = args.Name.Split(',');
        string assemblyName = fields[0];
        string assemblyCulture;
        if (fields.Length < 2)
            assemblyCulture = null;
        else
            assemblyCulture = fields[2].Substring(fields[2].IndexOf('=') + 1);


        string assemblyFileName = assemblyName + ".dll";
        string assemblyPath;

        if (assemblyName.EndsWith(".resources"))
        
            // Specific resources are located in app subdirectories
            string resourceDirectory = Path.Combine(applicationDirectory, assemblyCulture);

            assemblyPath = Path.Combine(resourceDirectory, assemblyFileName);
        
        else
        
            assemblyPath = Path.Combine(applicationDirectory, assemblyFileName);
        



        if (File.Exists(assemblyPath))
        
            //Load the assembly from the specified path.                    
            Assembly loadingAssembly = Assembly.LoadFrom(assemblyPath);

            //Return the loaded assembly.
            return loadingAssembly;
        
        else
        
            return null;
        

    

【讨论】:

您可以使用AssemblyName 构造函数来解码程序集名称,而不是依赖于解析程序集字符串。【参考方案6】:

对于 C++/CLI 用户,这是@Mattias S 的答案(对我有用):

using namespace System;
using namespace System::IO;
using namespace System::Reflection;

static Assembly ^LoadFromSameFolder(Object ^sender, ResolveEventArgs ^args)

    String ^folderPath = Path::GetDirectoryName(Assembly::GetExecutingAssembly()->Location);
    String ^assemblyPath = Path::Combine(folderPath, (gcnew AssemblyName(args->Name))->Name + ".dll");
    if (File::Exists(assemblyPath) == false) return nullptr;
    Assembly ^assembly = Assembly::LoadFrom(assemblyPath);
    return assembly;


// put this somewhere you know it will run (early, when the DLL gets loaded)
System::AppDomain ^currentDomain = AppDomain::CurrentDomain;
currentDomain->AssemblyResolve += gcnew ResolveEventHandler(LoadFromSameFolder);

【讨论】:

这是在 C++/CLI 中对我有用的唯一答案。当它是 C# 时,它非常简单,只需从任何你想要的地方加载,但是一旦它变成了 c++/CLI,我已经尝试了至少 5 个不同的代码片段,并阅读了一本关于 cli 加载约定的书的章节。非常感谢。【参考方案7】:

我使用了@Mattias S 的解决方案。如果您确实想从同一个文件夹中解析依赖项 - 您应该尝试使用 Requesting assembly 位置,如下所示。 args.RequestingAssembly 应检查是否为空。

System.AppDomain.CurrentDomain.AssemblyResolve += (s, args) =>

    var loadedAssembly = System.AppDomain.CurrentDomain.GetAssemblies().Where(a => a.FullName == args.Name).FirstOrDefault();
    if(loadedAssembly != null)
    
        return loadedAssembly;
    

    if (args.RequestingAssembly == null) return null;

    string folderPath = Path.GetDirectoryName(args.RequestingAssembly.Location);
    string rawAssemblyPath = Path.Combine(folderPath, new System.Reflection.AssemblyName(args.Name).Name);

    string assemblyPath = rawAssemblyPath + ".dll";

    if (!File.Exists(assemblyPath))
    
        assemblyPath = rawAssemblyPath + ".exe";
        if (!File.Exists(assemblyPath)) return null;
     

    var assembly = System.Reflection.Assembly.LoadFrom(assemblyPath);
    return assembly;
 ;

【讨论】:

【参考方案8】:

我从another (marked duplicate) question 来到这里,是关于将探测标签添加到 App.Config 文件中。

我想为此添加一个旁注 - Visual Studio 已经生成了一个 App.config 文件,但是将探测标记添加到预生成的运行时标记不起作用!您需要一个单独的运行时标签,其中包含探测标签。简而言之,您的 App.Config 应该如下所示:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
    </startup>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="System.Text.Encoding.CodePages" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-4.1.1.0" newVersion="4.1.1.0" />
      </dependentAssembly>
    </assemblyBinding>
  </runtime>

  <!-- Discover assemblies in /lib -->
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <probing privatePath="lib" />
    </assemblyBinding>
  </runtime>
</configuration>

这需要一些时间才能弄清楚,所以我将其发布在这里。也归功于The PrettyBin NuGet Package。它是一个自动移动 dll 的包。我喜欢更手动的方法,所以我没有使用它。

另外 - 这是一个将所有 .dll/.xml/.pdb 复制到 /Lib 的构建后脚本。这会整理 /debug(或 /release)文件夹,我认为这是人们试图实现的目标。

:: Moves files to a subdirectory, to unclutter the application folder
:: Note that the new subdirectory should be probed so the dlls can be found.
SET path=$(TargetDir)\lib
if not exist "%path%" mkdir "%path%"
del /S /Q "%path%"
move /Y $(TargetDir)*.dll "%path%"
move /Y $(TargetDir)*.xml "%path%"
move /Y $(TargetDir)*.pdb "%path%"

【讨论】:

以上是关于如何在.NET 运行时将文件夹添加到程序集搜索路径?的主要内容,如果未能解决你的问题,请参考以下文章

添加当前 AppDomain 的路径

在运行时将jar添加到类路径

在运行时将 ADO.Net DataSet 指向不同的数据库?

未能加载文件或程序集怎么解决

C#中导入外部dll文件,如果不放在当前目录下,通过设置工程属性-引用路径选项卡添加文件路径,为何报错啊

如何将我的程序添加到右键单击上下文菜单并传递它的路径