Wie füge ich zur Laufzeit in .NET einen Ordner zum Assembly-Suchpfad hinzu?

130

Meine DLLs werden von einer Drittanbieteranwendung geladen, die wir nicht anpassen können. Meine Baugruppen müssen sich in einem eigenen Ordner befinden. Ich kann sie nicht in GAC einfügen (meine Anwendung muss mit XCOPY bereitgestellt werden). Wenn die Root-DLL versucht, eine Ressource oder einen Typ aus einer anderen DLL (im selben Ordner) zu laden, schlägt das Laden fehl (FileNotFound). Ist es möglich, den Ordner, in dem sich meine DLLs befinden, programmgesteuert (von der Stamm-DLL) zum Assembly-Suchpfad hinzuzufügen? Ich darf die Konfigurationsdateien der Anwendung nicht ändern.

Isobretatel
quelle

Antworten:

154

Klingt so, als könnten Sie das Ereignis AppDomain.AssemblyResolve verwenden und die Abhängigkeiten manuell aus Ihrem DLL-Verzeichnis laden.

Bearbeiten (aus dem Kommentar):

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.
quelle
4
Danke, Mattias! Dies funktioniert: AppDomain currentDomain = AppDomain.CurrentDomain; currentDomain.AssemblyResolve + = neuer ResolveEventHandler (LoadFromSameFolderResolveEventHandler); statische Assembly LoadFromSameFolderResolveEventHandler (Objektabsender, ResolveEventArgs-Argumente) {string folderPath = Path.GetDirectoryName (Assembly.GetExecutingAssembly (). Location); Zeichenfolge AssemblyPath = Path.Combine (folderPath, args.Name + ".dll"); Assembly Assembly = Assembly.LoadFrom (AssemblyPath); Baugruppe zurückgeben; }
Isobretatel
1
Was würden Sie tun, wenn Sie auf den Basis-Resolver "zurückgreifen" möchten? zBif (!File.Exists(asmPath)) return searchInGAC(...);
Tomer W
57

Sie können einen hinzufügen Sondieren Pfad zu Ihrer Anwendung .config - Datei, aber es funktioniert nur, wenn die Sondierung Pfad ein , die innerhalb Ihrer Anwendung Basisverzeichnisses ist.

Mark Seemann
quelle
3
Vielen Dank für das Hinzufügen. Ich habe die AssemblyResolveLösung so oft gesehen, gut, eine andere (und einfachere) Option zu haben.
Samuel Neff
1
Vergessen Sie nicht, die App.config-Datei mit Ihrer App zu verschieben, wenn Sie Ihre App an einen anderen Ort kopieren.
Maxter
12

Update für Framework 4

Da Framework 4 das AssemblyResolve-Ereignis auch für Ressourcen auslöst, funktioniert dieser Handler tatsächlich besser. Es basiert auf dem Konzept, dass sich Lokalisierungen in App-Unterverzeichnissen befinden (eines für die Lokalisierung mit dem Namen der Kultur, dh C: \ MyApp \ it für Italienisch). Im Inneren befinden sich Ressourcendateien. Der Handler funktioniert auch, wenn die Lokalisierung eine Länderregion ist, dh it-IT oder pt-BR. In diesem Fall kann der Handler "mehrfach aufgerufen werden: einmal für jede Kultur in der Fallback-Kette" [von MSDN]. Dies bedeutet, dass das Framework das Ereignis auslöst, das nach "it" fragt, wenn wir für die Ressourcendatei "it-IT" null zurückgeben.

Ereignishaken

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

Ereignishandler

    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
quelle
Sie können den AssemblyNameKonstruktor verwenden, um den Assemblynamen zu dekodieren, anstatt sich auf das Parsen der Assemblyzeichenfolge zu verlassen.
Sebazzz
10

Die beste Erklärung von MS selbst :

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
quelle
AssemblyResolveist für CurrentDomain, nicht gültig für eine andere DomainAppDomain.CreateDomain
Kiquenet
8

Für C ++ / CLI-Benutzer ist hier die Antwort von @Mattias S (die für mich funktioniert):

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

Ich habe die Lösung von @Mattias S verwendet. Wenn Sie tatsächlich Abhängigkeiten aus demselben Ordner auflösen möchten, sollten Sie versuchen, den Assembly- Speicherort anfordern zu verwenden , wie unten gezeigt. args.RequestingAssembly sollte auf Nichtigkeit überprüft werden.

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

Schauen Sie in AppDomain.AppendPrivatePath (veraltet) oder AppDomainSetup.PrivateBinPath

Vincent Lidou
quelle
11
Von MSDN : Das Ändern der Eigenschaften einer AppDomainSetup-Instanz wirkt sich nicht auf eine vorhandene AppDomain aus. Dies kann sich nur auf die Erstellung einer neuen AppDomain auswirken, wenn die CreateDomain-Methode mit der AppDomainSetup-Instanz als Parameter aufgerufen wird.
Nathan
2
AppDomain.AppendPrivatePathDie Dokumentation scheint darauf hinzudeuten, dass die dynamische Erweiterung des AppDomainSuchpfads unterstützt werden sollte , nur dass die Funktion veraltet ist. Wenn es funktioniert, ist es eine viel sauberere Lösung als Überladung AssemblyResolve.
Binki
Als Referenz sieht es so aus, als AppDomain.AppendPrivatePath würde in .NET Core nichts getan und im vollständigen Framework aktualisiert.PrivateBinPath .
Kevinoid
3

Ich bin von einer anderen (als doppelt markierten) Frage zum Hinzufügen des Sondierungs-Tags zur App.Config-Datei hierher gekommen.

Ich möchte dem eine Randnotiz hinzufügen - Visual Studio hat bereits eine App.config-Datei generiert, das Hinzufügen des Prüf-Tags zum vorgenerierten Laufzeit-Tag hat jedoch nicht funktioniert! Sie benötigen ein separates Laufzeit-Tag mit dem enthaltenen Test-Tag. Kurz gesagt, Ihre App.Config sollte folgendermaßen aussehen:

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

Es hat einige Zeit gedauert, um das herauszufinden, also poste ich es hier. Guthaben auch für das PrettyBin NuGet-Paket . Es ist ein Paket, das die DLLs automatisch verschiebt. Ich mochte einen manuelleren Ansatz, deshalb habe ich ihn nicht verwendet.

Außerdem - hier ist ein Post-Build-Skript, das alle .dll / .xml / .pdb nach / Lib kopiert. Dadurch wird der Ordner / debug (oder / release) aufgeräumt, was meiner Meinung nach versucht wird.

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