Come aggiungere una cartella al percorso di ricerca degli assembly in fase di esecuzione in .NET?


130

Le mie DLL sono caricate da un'applicazione di terze parti, che non possiamo personalizzare. I miei assembly devono trovarsi nella loro cartella. Non riesco a inserirli in GAC (la mia applicazione ha un requisito da distribuire tramite XCOPY). Quando la DLL di root tenta di caricare risorse o tipi da un'altra DLL (nella stessa cartella), il caricamento non riesce (FileNotFound). È possibile aggiungere la cartella in cui si trovano le mie DLL al percorso di ricerca degli assembly a livello di codice (dalla DLL principale)? Non sono autorizzato a modificare i file di configurazione dell'applicazione.

Risposte:


155

Sembra che potresti usare l'evento AppDomain.AssemblyResolve e caricare manualmente le dipendenze dalla directory DLL.

Modifica (dal commento):

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
Grazie Mattias! Funziona così: AppDomain currentDomain = AppDomain.CurrentDomain; currentDomain.AssemblyResolve + = new ResolveEventHandler (LoadFromSameFolderResolveEventHandler); Assembly statico LoadFromSameFolderResolveEventHandler (mittente oggetto, ResolveEventArgs args) {string folderPath = Path.GetDirectoryName (Assembly.GetExecutingAssembly (). Location); string assemblyPath = Path.Combine (folderPath, args.Name + ".dll"); Assembly assembly = Assembly.LoadFrom (assemblyPath); gruppo di ritorno; }
isobretatel,

1
Cosa faresti se volessi "fallback" al Resolver di base. es.if (!File.Exists(asmPath)) return searchInGAC(...);
Tomer W,

Questo ha funzionato e non sono stato in grado di trovare alternative. Grazie
TByte

57

È possibile aggiungere un percorso di sondaggio al file .config dell'applicazione, ma funzionerà solo se il percorso di sondaggio è contenuto nella directory di base dell'applicazione.


3
Grazie per averlo aggiunto. Ho visto la AssemblyResolvesoluzione così tante volte, bello avere un'altra (e più semplice) opzione.
Samuel Neff,

1
Non dimenticare di spostare il file App.config con l'app se copi l'app da qualche altra parte ..
Maxter

12

Aggiornamento per Framework 4

Dal momento che Framework 4 genera l'evento AssemblyResolve anche per le risorse, in realtà questo gestore funziona meglio. Si basa sul concetto che le localizzazioni si trovano nelle sottodirectory delle app (una per la localizzazione con il nome della cultura, ad esempio C: \ MyApp \ it per l'italiano) All'interno sono presenti file di risorse. Il gestore funziona anche se la localizzazione è paese-regione, cioè it-IT o pt-BR. In questo caso il gestore "potrebbe essere chiamato più volte: una volta per ogni cultura nella catena di fallback" [da MSDN]. Ciò significa che se restituiamo null per il file di risorse "it-IT", il framework genera l'evento chiedendolo "it".

Hook per eventi

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

Gestore di eventi

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

    }

È possibile utilizzare il AssemblyNamecostruttore per decodificare il nome dell'assembly anziché fare affidamento sull'analisi della stringa dell'assembly.
Sebazzz,

10

La migliore spiegazione della stessa SM :

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è per CurrentDomain, non valido per un altro dominioAppDomain.CreateDomain
Kiquenet il

8

Per gli utenti C ++ / CLI, ecco la risposta di @Mattias S (che funziona per me):

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

Ho usato la soluzione di @Mattias S. Se si desidera effettivamente risolvere le dipendenze dalla stessa cartella, è necessario provare a richiedere la posizione dell'assembly , come mostrato di seguito. args.RequestingAssembly deve essere verificato per nullità.

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

esaminare AppDomain.AppendPrivatePath (obsoleto) o AppDomainSetup.PrivateBinPath


11
Da MSDN : la modifica delle proprietà di un'istanza AppDomainSetup non ha alcun effetto su AppDomain esistente. Può influire solo sulla creazione di un nuovo AppDomain, quando il metodo CreateDomain viene chiamato con l'istanza AppDomainSetup come parametro.
Nathan,

2
AppDomain.AppendPrivatePathLa documentazione sembra suggerire che dovrebbe supportare l'espansione dinamica del AppDomainpercorso di ricerca, solo che la funzione è obsoleta. Se funziona, è una soluzione molto più pulita del sovraccarico AssemblyResolve.
binki,

Per riferimento, sembra che AppDomain.AppendPrivatePath non faccia nulla in .NET Core e si aggiorni .PrivateBinPathin pieno framework .
Kevinoid,

3

Sono venuto qui da un'altra domanda (contrassegnata come duplicata) sull'aggiunta del tag di sondaggio al file App.Config.

Voglio aggiungere un sidenote a questo: Visual Studio aveva già generato un file App.config, tuttavia l'aggiunta del tag di sondaggio al tag di runtime pregenerato non ha funzionato! è necessario un tag di runtime separato con il tag di sondaggio incluso. In breve, App.Config dovrebbe apparire così:

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

Ci è voluto del tempo per capire, quindi sto pubblicando qui. Inoltre crediti per il pacchetto NuGet PrettyBin . È un pacchetto che sposta automaticamente le dll. Mi è piaciuto un approccio più manuale, quindi non l'ho usato.

Inoltre - ecco uno script post build che copia tutto .dll / .xml / .pdb su / Lib. Questo disordina la cartella / debug (o / release), cosa penso che la gente cerchi di ottenere.

:: 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%"
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.