¿Cómo agregar una carpeta a la ruta de búsqueda de ensamblado en tiempo de ejecución en .NET?

130

Mis archivos DLL son cargados por una aplicación de terceros, que no podemos personalizar. Mis ensamblajes deben ubicarse en su propia carpeta. No puedo ponerlos en GAC (mi aplicación tiene un requisito para implementarse usando XCOPY). Cuando la DLL raíz intenta cargar recursos o tipos desde otra DLL (en la misma carpeta), la carga falla (FileNotFound). ¿Es posible agregar la carpeta donde se encuentran mis archivos DLL a la ruta de búsqueda de ensamblado mediante programación (desde el archivo DLL raíz)? No tengo permiso para cambiar los archivos de configuración de la aplicación.

isobretatel
fuente

Respuestas:

154

Parece que podría usar el evento AppDomain.AssemblyResolve y cargar manualmente las dependencias desde su directorio DLL.

Editar (del comentario):

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
fuente
44
¡Gracias Mattias! Esto funciona: AppDomain currentDomain = AppDomain.CurrentDomain; currentDomain.AssemblyResolve + = new ResolveEventHandler (LoadFromSameFolderResolveEventHandler); Ensamblado estático LoadFromSameFolderResolveEventHandler (remitente de objeto, arcos ResolveEventArgs) {string folderPath = Path.GetDirectoryName (Assembly.GetExecutingAssembly (). Ubicación); string assemblyPath = Path.Combine (folderPath, args.Name + ".dll"); Assembly assembly = Assembly.LoadFrom (assemblyPath); ensamblaje de retorno; }
isobretatel
1
¿Qué harías si quisieras "recurrir" al Resolver básico? Por ejemploif (!File.Exists(asmPath)) return searchInGAC(...);
Tomer W
57

Puede agregar una ruta de prueba al archivo .config de su aplicación, pero solo funcionará si la ruta de prueba está contenida dentro del directorio base de su aplicación.

Mark Seemann
fuente
3
Gracias por agregar esto. He visto la AssemblyResolvesolución muchas veces, es bueno tener otra opción (y más fácil).
Samuel Neff
1
No olvide mover el archivo App.config con su aplicación si copia su aplicación en otro lugar ..
Maxter
12

Actualización para Framework 4

Dado que Framework 4 plantea el evento AssemblyResolve también para recursos, este manejador funciona mejor. Se basa en el concepto de que las localizaciones están en subdirectorios de la aplicación (uno para la localización con el nombre de la cultura, es decir, C: \ MyApp \ it para el italiano) En el interior hay un archivo de recursos. El controlador también funciona si la localización es país-región, es decir, it-IT o pt-BR. En este caso, el controlador "podría llamarse varias veces: una vez para cada cultura en la cadena de respaldo" [de MSDN]. Esto significa que si devolvemos un valor nulo para el archivo de recursos "it-IT", el marco genera el evento pidiendo "it".

Gancho de evento

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

Controlador 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
fuente
Puede usar el AssemblyNameconstructor para decodificar el nombre del ensamblado en lugar de confiar en analizar la cadena del ensamblado.
Sebazzz
10

La mejor explicación de la propia 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
fuente
AssemblyResolvees para CurrentDomain, no válido para otro dominioAppDomain.CreateDomain
Kiquenet
8

Para usuarios de C ++ / CLI, aquí está la respuesta de @Mattias S (que funciona para mí):

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

He usado la solución @Mattias S '. Si realmente desea resolver dependencias de la misma carpeta, debe intentar usar la ubicación del ensamblaje Solicitante , como se muestra a continuación. args.RequestingAssembly debe verificarse por nulidad.

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é
fuente
4

busque en AppDomain.AppendPrivatePath (en desuso) o AppDomainSetup.PrivateBinPath

Vincent Lidou
fuente
11
Desde MSDN : Cambiar las propiedades de una instancia de AppDomainSetup no afecta a ningún AppDomain existente. Solo puede afectar la creación de un nuevo AppDomain, cuando se llama al método CreateDomain con la instancia AppDomainSetup como parámetro.
Nathan
2
AppDomain.AppendPrivatePathLa documentación parece sugerir que debería soportar expandir dinámicamente la AppDomainruta de búsqueda de la página, solo que la característica está en desuso. Si funciona, es una solución mucho más limpia que la sobrecarga AssemblyResolve.
binki
Como referencia, parece AppDomain.AppendPrivatePath que no hace nada en .NET Core y se actualiza .PrivateBinPathen el marco completo .
Kevinoid
3

Vine aquí desde otra pregunta (marcada como duplicado) acerca de agregar la etiqueta de prueba al archivo App.Config.

Quiero agregar una nota al margen de esto: Visual Studio ya había generado un archivo App.config, sin embargo, ¡agregar la etiqueta de prueba a la etiqueta de tiempo de ejecución pregenerada no funcionó! necesita una etiqueta de tiempo de ejecución separada con la etiqueta de prueba incluida. En resumen, su App.Config debería verse así:

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

Esto tomó un tiempo para darse cuenta, así que lo estoy publicando aquí. También se acredita a The PrettyBin NuGet Package . Es un paquete que mueve los dlls automáticamente. Me gustó un enfoque más manual, así que no lo usé.

Además, aquí hay un script de compilación posterior que copia todo .dll / .xml / .pdb a / Lib. Esto despeja la carpeta / debug (o / release), lo que creo que la gente intenta lograr.

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