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.

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

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.


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

    }

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

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

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

4

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


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%"
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.