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

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

【问题讨论】:

    标签: .net search assemblies path


    【解决方案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 Coreupdates .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%"
      

      【讨论】:

        猜你喜欢
        • 2015-12-19
        • 2019-12-22
        • 2010-11-17
        • 2012-09-16
        • 1970-01-01
        • 1970-01-01
        • 2016-07-11
        相关资源
        最近更新 更多