Jak dodać folder do ścieżki wyszukiwania zestawu w czasie wykonywania w .NET?

132

Moje biblioteki DLL są ładowane przez aplikację innej firmy, której nie możemy dostosować. Moje zestawy muszą znajdować się we własnym folderze. Nie mogę umieścić ich w GAC (moja aplikacja wymaga wdrożenia przy użyciu XCOPY). Gdy główna biblioteka DLL próbuje załadować zasób lub typ z innej biblioteki DLL (w tym samym folderze), ładowanie kończy się niepowodzeniem (FileNotFound). Czy możliwe jest programowe dodanie folderu, w którym znajdują się moje biblioteki DLL, do ścieżki wyszukiwania zestawu (z głównej biblioteki DLL)? Nie wolno mi zmieniać plików konfiguracyjnych aplikacji.

isobretatel
źródło

Odpowiedzi:

159

Wygląda na to, że można użyć zdarzenia AppDomain.AssemblyResolve i ręcznie załadować zależności z katalogu DLL.

Edytuj (z komentarza):

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
źródło
4
Dziękuję Mattias! To działa: AppDomain currentDomain = AppDomain.CurrentDomain; currentDomain.AssemblyResolve + = new ResolveEventHandler (LoadFromSameFolderResolveEventHandler); static Assembly LoadFromSameFolderResolveEventHandler (nadawca obiektu, ResolveEventArgs args) {string folderPath = Path.GetDirectoryName (Assembly.GetExecutingAssembly (). Location); string assemblyPath = Path.Combine (folderPath, args.Name + ".dll"); Assembly Assembly = Assembly.LoadFrom (assemblyPath); montaż powrotny; }
isobretatel
1
Co byś zrobił, gdybyś chciał „cofnąć się” do podstawowego programu Resolver. np.if (!File.Exists(asmPath)) return searchInGAC(...);
Tomer W
To zadziałało i nie mogłem znaleźć żadnej alternatywy. Dzięki
TByte
58

Możesz dodać ścieżkę sondowania do pliku .config aplikacji, ale będzie ona działać tylko wtedy, gdy ścieżka sondowania znajduje się w katalogu podstawowym aplikacji.

Mark Seemann
źródło
3
Dzięki za dodanie tego. AssemblyResolveWiele razy widziałem to rozwiązanie, dobrze jest mieć inną (i łatwiejszą) opcję.
Samuel Neff
1
Nie zapomnij przenieść pliku App.config z aplikacją, jeśli skopiujesz ją gdzie indziej ..
Maxter
12

Aktualizacja dla Framework 4

Ponieważ Framework 4 podnosi zdarzenie AssemblyResolve również dla zasobów, w rzeczywistości ta procedura obsługi działa lepiej. Opiera się na założeniu, że lokalizacje znajdują się w podkatalogach aplikacji (jeden do lokalizacji z nazwą kultury, np. C: \ MyApp \ it dla włoskiego). Wewnątrz znajduje się plik zasobów. Program obsługi działa również, jeśli lokalizacja to kraj-region, tj. It-IT lub pt-BR. W tym przypadku procedura obsługi „może zostać wywołana wiele razy: raz dla każdej kultury w łańcuchu awaryjnym” [z MSDN]. Oznacza to, że jeśli zwrócimy wartość null dla pliku zasobów „it-IT”, framework zgłasza zdarzenie z prośbą o „it”.

Hak zdarzenia

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

Procedura obsługi zdarzeń

    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
źródło
Możesz użyć AssemblyNamekonstruktora, aby zdekodować nazwę zestawu zamiast polegać na analizowaniu ciągu zestawu.
Sebazzz
10

Najlepsze wyjaśnienie od samego 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
źródło
AssemblyResolvedotyczy domeny CurrentDomain, nie dotyczy innej domenyAppDomain.CreateDomain
Kiquenet
8

Dla użytkowników C ++ / CLI, oto odpowiedź @Mattias S (która działa dla mnie):

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
źródło
To jedyna odpowiedź, która działa dla mnie w C ++ / CLI. Kiedy jest to C #, jest to takie cholernie proste, po prostu załaduj z dowolnego miejsca, ale kiedy stało się c ++ / CLI, wypróbowałem co najmniej 5 fragmentów kodu różnicowego i przeczytałem rozdział książki o konwencji ładowania CLI. Dziękuję bardzo.
Alen Wesker
6

Użyłem rozwiązania @Mattias S. Jeśli faktycznie chcesz rozwiązać zależności z tego samego folderu - powinieneś spróbować użyć Żądania lokalizacji zestawu , jak pokazano poniżej. args.RequestingAssembly należy sprawdzić pod kątem nieważności.

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é
źródło
4

zajrzyj do AppDomain.AppendPrivatePath (przestarzałe) lub AppDomainSetup.PrivateBinPath

Vincent Lidou
źródło
11
Z MSDN : zmiana właściwości wystąpienia AppDomainSetup nie wpływa na żadne istniejące AppDomain. Może wpływać tylko na tworzenie nowej AppDomain, gdy metoda CreateDomain jest wywoływana z wystąpieniem AppDomainSetup jako parametrem.
Nathan,
2
AppDomain.AppendPrivatePathDokumentacja wydaje się sugerować, że powinna obsługiwać dynamiczne rozszerzanie AppDomainścieżki przeszukiwania, tylko że ta funkcja jest przestarzała. Jeśli to zadziała, jest to znacznie czystsze rozwiązanie niż przeciążenie AssemblyResolve.
binki
Dla odniesienia wygląda na to, że AppDomain.AppendPrivatePath nic nie robi w .NET Core i aktualizuje .PrivateBinPathw pełnej strukturze .
Kevinoid
4

Przyszedłem tutaj z innego (oznaczonego jako duplikat) pytania o dodanie tagu sondującego do pliku App.Config.

Chcę dodać do tego notatkę boczną - program Visual studio wygenerował już plik App.config, jednak dodanie tagu sondującego do wstępnie wygenerowanego tagu runtime nie zadziałało! potrzebujesz osobnego tagu runtime z dołączonym tagiem sondującym. W skrócie, Twój App.Config powinien wyglądać tak:

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

Zrozumienie tego zajęło trochę czasu, więc zamieszczam to tutaj. Również kredyty dla pakietu NuGet PrettyBin . Jest to pakiet, który automatycznie przenosi biblioteki DLL. Podobało mi się bardziej ręczne podejście, więc nie korzystałem z niego.

Ponadto - tutaj jest skrypt po kompilacji, który kopiuje wszystkie pliki .dll / .xml / .pdb do / Lib. To porządkuje folder / debug (lub / release), co moim zdaniem ludzie próbują osiągnąć.

:: 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
źródło