Как добавить папку в путь поиска сборки во время выполнения в .NET?


132

Мои библиотеки DLL загружаются сторонним приложением, которое мы не можем настроить. Мои сборки должны находиться в отдельной папке. Я не могу поместить их в GAC (мое приложение требует развертывания с использованием XCOPY). Когда корневая DLL пытается загрузить ресурс или тип из другой DLL (в той же папке), загрузка не выполняется (FileNotFound). Можно ли программно (из корневой DLL) добавить папку, в которой расположены мои библиотеки DLL, в путь поиска сборки? Мне не разрешено изменять файлы конфигурации приложения.

Ответы:


156

Похоже, вы могли бы использовать событие 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;
}

4
Спасибо, Маттиас! Это работает: AppDomain currentDomain = AppDomain.CurrentDomain; currentDomain.AssemblyResolve + = новый ResolveEventHandler (LoadFromSameFolderResolveEventHandler); статическая сборка LoadFromSameFolderResolveEventHandler (отправитель объекта, аргументы ResolveEventArgs) {string folderPath = Path.GetDirectoryName (Assembly.GetExecutingAssembly (). Location); строка assemblyPath = Path.Combine (folderPath, args.Name + «.dll»); Сборка assembly = Assembly.LoadFrom (assemblyPath); возвратная сборка; }
isobretatel 03

1
Что бы вы сделали, если бы захотели «откатиться» к базовому резолверу. напримерif (!File.Exists(asmPath)) return searchInGAC(...);
Tomer W

Это сработало, и я не смог найти никаких альтернатив. Спасибо
TByte

57

Вы можете добавить пробный путь к файлу .config вашего приложения, но он будет работать только в том случае, если пробный путь содержится в базовом каталоге вашего приложения.


3
Спасибо, что добавили это. Я видел AssemblyResolveрешение так много раз, хорошо, что есть другой (и более простой) вариант.
Samuel Neff

1
Не забудьте переместить файл App.config с приложением , если вы копируете приложение где - нибудь еще ..
Макстер

12

Обновление для Framework 4

Поскольку Framework 4 вызывает событие AssemblyResolve также для ресурсов, на самом деле этот обработчик работает лучше. Он основан на концепции, что локализации находятся в подкаталогах приложения (один для локализации с именем культуры, например, C: \ MyApp \ it для итальянского). Внутри находится файл ресурсов. Обработчик также работает, если локализация - страна-регион, т.е. it-IT или pt-BR. В этом случае обработчик «может вызываться несколько раз: один раз для каждого языка и региональных параметров в резервной цепочке» [из MSDN]. Это означает, что если мы возвращаем null для файла ресурсов «it-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конструктор для декодирования имени сборки вместо того, чтобы полагаться на синтаксический анализ строки сборки.
Sebazzz

10

Лучшее объяснение от самой MS :

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
Kiquenet

8

Для пользователей 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);

6

Я использовал решение @Mattias S. Если вы действительно хотите разрешить зависимости из одной и той же папки - попробуйте использовать запрос местоположения сборки , как показано ниже. 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;
 };

4

посмотрите в AppDomain.AppendPrivatePath (устарело) или AppDomainSetup.PrivateBinPath


11
Из MSDN : изменение свойств экземпляра AppDomainSetup не влияет на существующие AppDomain. Это может повлиять только на создание нового AppDomain, когда метод CreateDomain вызывается с экземпляром AppDomainSetup в качестве параметра.
Натан

2
AppDomain.AppendPrivatePathдокументация, кажется, предполагает, что он должен поддерживать динамическое расширение AppDomainпути поиска, просто эта функция устарела. Если это сработает, это гораздо более чистое решение, чем перегрузка AssemblyResolve.
binki


3

Я пришел сюда из-за другого (отмеченного как повторяющегося) вопроса о добавлении проверочного тега в файл 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>

Это заняло некоторое время, поэтому я публикую это здесь. Также кредиты пакету PrettyBin NuGet . Это пакет, который автоматически перемещает библиотеки 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%"
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.