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

132

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

isobretatel
источник

Ответы:

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);
msarahan
источник
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
Для справки, похоже, AppDomain.AppendPrivatePath что ничего не делает в .NET Core и обновляется .PrivateBinPathв полной структуре .
Kevinoid
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%"
sommmen
источник