How to add folder to assembly search path at runtime in .NET?

.NetSearchAssembliesPath

.Net Problem Overview


My DLLs are loaded by a third-party application, which we can not customize. My assemblies have to be located in their own folder. I can not put them into GAC (my application has a requirement to be deployed using XCOPY). When the root DLL tries to load resource or type from another DLL (in the same folder), the loading fails (FileNotFound). Is it possible to add the folder where my DLLs are located to the assembly search path programmatically (from the root DLL)? I am not allowed to change the configuration files of the application.

.Net Solutions


Solution 1 - .Net

Sounds like you could use the AppDomain.AssemblyResolve event and manually load the dependencies from your DLL directory.

Edit (from the comment):

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

Solution 2 - .Net

You can add a probing path to your application's .config file, but it will only work if the probing path is a contained within your application's base directory.

Solution 3 - .Net

Update for Framework 4

Since Framework 4 raise the AssemblyResolve event also for resources actually this handler works better. It's based on the concept that localizations are in app subdirectories (one for localization with the name of the culture i.e. C:\MyApp\it for Italian) Inside there are resources file. The handler works also if the localization is country-region i.e. it-IT or pt-BR. In this case the handler "might be called multiple times: once for each culture in the fallback chain" [from MSDN]. This means that if we return null for "it-IT" resource file the framework raises the event asking for "it".

Event hook

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

Event handler

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

    }

Solution 4 - .Net

The best explanation from MS itself:

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

Solution 5 - .Net

For C++/CLI users, here is @Mattias S' answer (which works for 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);

Solution 6 - .Net

I've used @Mattias S' solution. If you actually want to resolve dependencies from the same folder - you should try using Requesting assembly location, as shown below. args.RequestingAssembly should be checked for nullity.

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

Solution 7 - .Net

I came here from another (marked duplicate) question about adding the probing tag to the App.Config file.

I want to add a sidenote to this - Visual studio had already generated an App.config file, however adding the probing tag to the pregenerated runtime tag did not work! you need a seperate runtime tag with the probing tag included. In short, your App.Config should look like this:

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

This took some time to figure out so I am posting it here. Also credits to The PrettyBin NuGet Package. It is a package that moves the dlls automatically. I liked a more manual approach so I did not use it.

Also - here is a post build script that copies all .dll/.xml/.pdb to /Lib. This unclutters the /debug (or /release) folder, what I think people try to achieve.

:: 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%"

Solution 8 - .Net

look into AppDomain.AppendPrivatePath (deprecated) or AppDomainSetup.PrivateBinPath

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionisobretatelView Question on Stackoverflow
Solution 1 - .NetMattias SView Answer on Stackoverflow
Solution 2 - .NetMark SeemannView Answer on Stackoverflow
Solution 3 - .NetbubiView Answer on Stackoverflow
Solution 4 - .NetnawfalView Answer on Stackoverflow
Solution 5 - .NetmsarahanView Answer on Stackoverflow
Solution 6 - .NetAryéh RadléView Answer on Stackoverflow
Solution 7 - .NetsommmenView Answer on Stackoverflow
Solution 8 - .NetVincent LidouView Answer on Stackoverflow