Como adicionar pasta ao caminho de pesquisa de montagem em tempo de execução no .NET?

130

Minhas DLLs são carregadas por um aplicativo de terceiros, que não podemos personalizar. Minhas montagens devem estar localizadas em sua própria pasta. Não posso colocá-los no GAC (meu aplicativo precisa ser implantado usando o XCOPY). Quando a DLL raiz tenta carregar o recurso ou o tipo de outra DLL (na mesma pasta), o carregamento falha (FileNotFound). É possível adicionar a pasta onde minhas DLLs estão localizadas ao caminho de pesquisa do assembly programaticamente (a partir da DLL raiz)? Não tenho permissão para alterar os arquivos de configuração do aplicativo.

isobretatel
fonte

Respostas:

154

Parece que você pode usar o evento AppDomain.AssemblyResolve e carregar manualmente as dependências do diretório DLL.

Editar (a partir do comentário):

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;
}
Mattias S
fonte
4
Obrigado Mattias! Isso funciona: AppDomain currentDomain = AppDomain.CurrentDomain; currentDomain.AssemblyResolve + = new ResolveEventHandler (LoadFromSameFolderResolveEventHandler); Assembly estático LoadFromSameFolderResolveEventHandler (remetente do objeto, args ResolveEventArgs) {string folderPath = Path.GetDirectoryName (Assembly.GetExecutingAssembly (). Location); string assemblyPath = Path.Combine (folderPath, args.Name + ".dll"); Assembly assembly = Assembly.LoadFrom (assemblyPath); montagem de retorno; }
isobretatel
1
O que você faria se quisesse "recuar" para o Resolver básico. por exemploif (!File.Exists(asmPath)) return searchInGAC(...);
Tomer W 29/01
57

Você pode adicionar um caminho de investigação ao arquivo .config do aplicativo, mas ele só funcionará se o caminho de investigação estiver contido no diretório base do aplicativo.

Mark Seemann
fonte
3
Obrigado por adicionar isso. Eu já vi a AssemblyResolvesolução tantas vezes, é bom ter outra opção (e mais fácil).
Samuel Neff
1
Não se esqueça de mover o arquivo App.config com seu aplicativo se você copiar seu aplicativo em outro lugar ..
Maxter
12

Atualização para o Framework 4

Como o Framework 4 gera o evento AssemblyResolve também para recursos, na verdade, esse manipulador funciona melhor. É baseado no conceito de que as localizações estão nos subdiretórios de aplicativos (um para localização com o nome da cultura, por exemplo, C: \ MyApp \ it for Italian). Dentro, existem arquivos de recursos. O manipulador também funciona se a localização for região-país, ou seja, it-IT ou pt-BR. Nesse caso, o manipulador "pode ​​ser chamado várias vezes: uma vez para cada cultura na cadeia de fallback" [do MSDN]. Isso significa que, se retornarmos nulos para o arquivo de recursos "it-IT", a estrutura acionará o evento solicitando "it".

Gancho de evento

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

Manipulador de eventos

    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;
        }

    }
bubi
fonte
Você pode usar o AssemblyNameconstrutor para decodificar o nome da montagem em vez de confiar na análise da cadeia de montagem.
Sebazzz 17/07/19
10

A melhor explicação do próprio 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;          
}
nawfal
fonte
AssemblyResolveé para CurrentDomain, não é válido para outro domínioAppDomain.CreateDomain
Kiquenet
8

Para usuários de C ++ / CLI, aqui está a resposta do @Mattias S (que funciona para mim):

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
fonte
6

Eu usei a solução @Mattias S. Se você realmente deseja resolver dependências da mesma pasta - tente usar Solicitando local da montagem , como mostrado abaixo. args.RequestingAssembly deve ser verificado quanto à nulidade.

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;
 };
Aryéh Radlé
fonte
4

procure em AppDomain.AppendPrivatePath (descontinuado) ou AppDomainSetup.PrivateBinPath

Vincent Lidou
fonte
11
No MSDN : a alteração das propriedades de uma instância do AppDomainSetup não afeta nenhum AppDomain existente. Isso pode afetar apenas a criação de um novo AppDomain, quando o método CreateDomain é chamado com a instância AppDomainSetup como parâmetro.
21411 Nathan
2
AppDomain.AppendPrivatePathA documentação de parece sugerir que ele deve suportar a expansão dinâmica do AppDomaincaminho de pesquisa, apenas que o recurso foi descontinuado. Se funcionar, é uma solução muito mais limpa do que a sobrecarga AssemblyResolve.
binki
Para referência, parece que AppDomain.AppendPrivatePath não faz nada no .NET Core e atualizações .PrivateBinPathna estrutura completa .
Kevinoid 26/06
3

Eu vim aqui de outra pergunta (marcada como duplicada) sobre como adicionar a marca de análise ao arquivo App.Config.

Quero adicionar uma nota lateral a isso - o Visual Studio já gerou um arquivo App.config, no entanto, a adição da marca de análise à marca de tempo de execução pré-gerado não funcionou! você precisa de uma tag de tempo de execução separada com a tag de análise incluída. Em resumo, seu App.Config deve ficar assim:

<?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>

Demorou algum tempo para descobrir, então estou postando aqui. Também créditos para o pacote PrettyBin NuGet . É um pacote que move as DLLs automaticamente. Eu gostei de uma abordagem mais manual, por isso não a usei.

Além disso - aqui está um script de pós-compilação que copia todos os arquivos .dll / .xml / .pdb para / Lib. Isso organiza a pasta / debug (ou / release), o que eu acho que as pessoas tentam alcançar.

:: 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
fonte