Comment ajouter un dossier au chemin de recherche d'assembly lors de l'exécution dans .NET?

131

Mes DLL sont chargées par une application tierce, que nous ne pouvons pas personnaliser. Mes assemblys doivent se trouver dans leur propre dossier. Je ne peux pas les mettre dans GAC (mon application doit être déployée avec XCOPY). Lorsque la DLL racine tente de charger une ressource ou un type à partir d'une autre DLL (dans le même dossier), le chargement échoue (FileNotFound). Est-il possible d'ajouter le dossier dans lequel se trouvent mes DLL au chemin de recherche d'assembly par programme (à partir de la DLL racine)? Je ne suis pas autorisé à modifier les fichiers de configuration de l'application.

isobrétatel
la source

Réponses:

155

On dirait que vous pourriez utiliser l'événement AppDomain.AssemblyResolve et charger manuellement les dépendances à partir de votre répertoire DLL.

Modifier (à partir du commentaire):

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
la source
4
Merci Mattias! Cela fonctionne: AppDomain currentDomain = AppDomain.CurrentDomain; currentDomain.AssemblyResolve + = new ResolveEventHandler (LoadFromSameFolderResolveEventHandler); static Assembly LoadFromSameFolderResolveEventHandler (expéditeur de l'objet, ResolveEventArgs args) {string folderPath = Path.GetDirectoryName (Assembly.GetExecutingAssembly (). Location); string assemblyPath = Path.Combine (folderPath, args.Name + ".dll"); Assembly assembly = Assembly.LoadFrom (assemblyPath); assemblage de retour; }
isobretatel
1
Que feriez-vous si vous vouliez "revenir" au résolveur de base. par exempleif (!File.Exists(asmPath)) return searchInGAC(...);
Tomer W
Cela a fonctionné et je n'ai pas pu trouver d'alternatives. Merci
TByte
57

Vous pouvez ajouter un chemin de détection au fichier .config de votre application, mais cela ne fonctionnera que si le chemin de détection est contenu dans le répertoire de base de votre application.

Mark Seemann
la source
3
Merci d'avoir ajouté ceci. J'ai vu la AssemblyResolvesolution tant de fois, c'est bien d'avoir une autre option (et plus simple).
Samuel Neff
1
N'oubliez pas de déplacer le fichier App.config avec votre application si vous copiez votre application ailleurs ..
Maxter
12

Mise à jour pour Framework 4

Étant donné que Framework 4 déclenche également l'événement AssemblyResolve pour les ressources, ce gestionnaire fonctionne mieux. Il est basé sur le concept que les localisations sont dans les sous-répertoires de l'application (un pour la localisation avec le nom de la culture, c'est-à-dire C: \ MyApp \ it pour l'italien) À l'intérieur, il y a un fichier de ressources. Le gestionnaire fonctionne également si la localisation est un pays-région, c'est-à-dire it-IT ou pt-BR. Dans ce cas, le gestionnaire "peut être appelé plusieurs fois: une fois pour chaque culture de la chaîne de secours" [de MSDN]. Cela signifie que si nous retournons null pour le fichier de ressources "it-IT", le framework déclenche l'événement en demandant "it".

Crochet d'événement

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

Gestionnaire d'événements

    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
la source
Vous pouvez utiliser le AssemblyNameconstructeur pour décoder le nom de l'assembly au lieu de vous fier à l'analyse de la chaîne d'assembly.
Sebazzz
10

La meilleure explication de MS lui - même :

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
la source
AssemblyResolveest pour CurrentDomain, non valide pour un autre domaineAppDomain.CreateDomain
Kiquenet
8

Pour les utilisateurs C ++ / CLI, voici la réponse de @Mattias S (qui fonctionne pour moi):

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
la source
6

J'ai utilisé la solution de @Mattias S. Si vous souhaitez réellement résoudre les dépendances à partir du même dossier, vous devez essayer d'utiliser Requesting assembly location, comme indiqué ci-dessous. args.RequestingAssembly doit être vérifié pour la 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;
 };
Aryéh Radlé
la source
4

regardez dans AppDomain.AppendPrivatePath (obsolète) ou AppDomainSetup.PrivateBinPath

Vincent Lidou
la source
11
À partir de MSDN : la modification des propriétés d'une instance AppDomainSetup n'affecte aucun AppDomain existant. Il peut affecter uniquement la création d'un nouvel AppDomain, lorsque la méthode CreateDomain est appelée avec l'instance AppDomainSetup en tant que paramètre.
Nathan
2
AppDomain.AppendPrivatePathLa documentation de semble suggérer qu'elle devrait prendre en charge l'expansion dynamique du AppDomainchemin de recherche de, juste que la fonctionnalité est obsolète. Si cela fonctionne, c'est une solution beaucoup plus propre que la surcharge AssemblyResolve.
binki
Pour référence, il semble AppDomain.AppendPrivatePath ne rien faire dans .NET Core et les mises .PrivateBinPathà jour dans le cadre complet .
Kevinoid le
3

Je suis venu ici d' une autre question (marquée en double) sur l'ajout de la balise de sondage au fichier App.Config.

Je veux ajouter une note à cela - Visual Studio avait déjà généré un fichier App.config, mais l'ajout de la balise de détection à la balise d'exécution prégénérée ne fonctionnait pas! vous avez besoin d'une balise d'exécution séparée avec la balise de détection incluse. En bref, votre App.Config devrait ressembler à ceci:

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

Cela a pris du temps à comprendre, donc je le poste ici. Crédits également au package PrettyBin NuGet . C'est un package qui déplace automatiquement les dll. J'ai aimé une approche plus manuelle donc je ne l'ai pas utilisée.

Aussi - voici un script post build qui copie tous les fichiers .dll / .xml / .pdb dans / Lib. Cela désencombre le dossier / debug (ou / release), ce que je pense que les gens essaient de réaliser.

:: 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
la source