I believe I've exhausted the answers and comments on SO related to this topic. Full source code for the project I used to demonstrate the problem can be found here: https://github.com/jchristn/DependencyExample
Assume there is a solution with three projects. The first is a console app (Runner
) with dependency on RestWrapper v3.0.19. The second is a class library (Module1
) with dependency on RestWrapper v3.0.20. The third is a class library (Module2
) with dependency on RestWrapper v3.0.18.
All three of these attempt to perform an HTTP GET to a user-specified URL that is retrieved in Runner
. Runner
then uses AssemblyLoadContext
, LoadFromAssemblyPath
, Resolving
, and Activator.CreateInstance
to create and invoke the Process
method within Runner1
and then Runner2
, which perform the same HTTP GET, e.g.
AssemblyLoadContext ctx1 = new AssemblyLoadContext("module1", false);
Assembly asm1 = ctx1.LoadFromAssemblyPath(Path.GetFullPath("module1/Module1.dll"));
ctx1.Resolving += (ctx, assemblyName) =>
{
Console.WriteLine("| Module 1 loading assembly " + assemblyName.FullName);
var parts = assemblyName.FullName.Split(',');
string name = parts[0];
var version = Version.Parse(parts[1].Split('=')[1]);
string filename = new FileInfo(Path.GetFullPath("module1/" + name + ".dll")).FullName;
Console.WriteLine("| Module 1 loading from file " + filename);
Assembly asm = Assembly.LoadFrom(filename);
Console.WriteLine("| Module 1 loaded assembly " + filename);
return asm;
};
Type type1 = asm1.GetType("Module1.Processor", true);
dynamic instance1 = Activator.CreateInstance(type1);
instance1.Process(url);
using (RestRequest req = new RestRequest(url))
{
using (RestResponse resp = req.Send())
{
Console.WriteLine("| Status (runner) : " + resp.StatusCode + " " + resp.ContentLength + " bytes");
}
}
I can confirm that the files actually exist:
C:\Code\Misc\DependencyExample\Runner\bin\Debug\net8.0>dir module1
Volume in drive C is OS
Volume Serial Number is 541C-D54E
Directory of C:\Code\Misc\DependencyExample\Runner\bin\Debug\net8.0\module1
05/24/2024 08:22 AM <DIR> .
05/24/2024 08:22 AM <DIR> ..
05/24/2024 08:17 AM 37,648 Module1.deps.json
05/24/2024 08:17 AM 4,608 Module1.dll
05/24/2024 08:17 AM 10,548 Module1.pdb
05/20/2024 10:26 PM 29,184 RestWrapper.dll
05/20/2024 10:12 PM 11,776 Timestamps.dll
5 File(s) 93,764 bytes
2 Dir(s) 51,069,906,944 bytes free
C:\Code\Misc\DependencyExample\Runner\bin\Debug\net8.0>dir module2
Volume in drive C is OS
Volume Serial Number is 541C-D54E
Directory of C:\Code\Misc\DependencyExample\Runner\bin\Debug\net8.0\module2
05/24/2024 08:22 AM <DIR> .
05/24/2024 08:22 AM <DIR> ..
05/24/2024 08:17 AM 37,648 Module2.deps.json
05/24/2024 08:17 AM 4,608 Module2.dll
05/24/2024 08:17 AM 10,548 Module2.pdb
05/20/2024 10:26 PM 29,184 RestWrapper.dll
05/20/2024 10:12 PM 11,776 Timestamps.dll
5 File(s) 93,764 bytes
2 Dir(s) 51,069,906,944 bytes free
Both the Module1
and Module2
have entries in their .csproj
to copy dependency files into the output directory:
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
I've operated under the assumption that by having these loaded in separate AssemblyLoadContext
s with their own Resolving
function that they would be able to load these differing versions, but it fails with an System.IO.FileLoadException: Could not load file or assembly 'RestWrapper, Version=3.0.20.0, Culture=neutral, PublicKeyToken=null'. Could not find or load a specific file. (0x80131621)
exception when attempting to run Runner1
.
C:\Code\Misc\DependencyExample\Runner\bin\Debug\net8.0>runner
URL: https://www.google.com
| Status (runner) : 200 57841 bytes
| Module 1 loading assembly RestWrapper, Version=3.0.20.0, Culture=neutral, PublicKeyToken=null
| Module 1 loading from file C:\Code\Misc\DependencyExample\Runner\bin\Debug\net8.0\module1\RestWrapper.dll
System.IO.FileLoadException: Could not load file or assembly 'RestWrapper, Version=3.0.20.0, Culture=neutral, PublicKeyToken=null'. Could not find or load a specific file. (0x80131621)
File name: 'RestWrapper, Version=3.0.20.0, Culture=neutral, PublicKeyToken=null'
---> System.IO.FileLoadException: Could not load file or assembly 'RestWrapper, Version=3.0.20.0, Culture=neutral, PublicKeyToken=null'.
at System.Runtime.Loader.AssemblyLoadContext.LoadFromAssemblyPath(String assemblyPath)
at System.Reflection.Assembly.LoadFrom(String assemblyFile)
at DependencyExample.Program.<>c.<Main>b__0_0(AssemblyLoadContext ctx, AssemblyName assemblyName) in C:\Code\Misc\DependencyExample\Runner\Program.cs:line 49
at System.Runtime.Loader.AssemblyLoadContext.GetFirstResolvedAssemblyFromResolvingEvent(AssemblyName assemblyName)
at System.Runtime.Loader.AssemblyLoadContext.ResolveUsingEvent(AssemblyName assemblyName)
at System.Runtime.Loader.AssemblyLoadContext.ResolveUsingResolvingEvent(IntPtr gchManagedAssemblyLoadContext, AssemblyName assemblyName)
at Module1.Processor.Process(String url)
at System.Dynamic.UpdateDelegates.UpdateAndExecuteVoid2[T0,T1](CallSite site, T0 arg0, T1 arg1)
at DependencyExample.Program.Main(String[] args) in C:\Code\Misc\DependencyExample\Runner\Program.cs:line 56
I've uploaded the full source (intentionally leaving out a .gitignore
) at the link above. The bin/Debug/net8.0
output of Module1
and Module2
are copied into subdirectory module1/
and module2/
in the Runner
's bin/Debug/net8.0
directory.
Also, another wrinkle to the problem, is that I need to run this on both Ubuntu and Windows.
Any help would be really appreciated!
I've tried also using AssemblyDependencyResolver
(examples from https://tsuyoshiushio.medium.com/understand-advanced-assemblyloadcontext-with-c-16a9d0cfeae3 and https://gist.github.com/TsuyoshiUshio/551cb0f71c704aca75209552af50fc7a#file-pluginloadcontext-cs), which works, however all three of Runner
, Module1
, and Module2
somehow load the same version of the RestWrapper
dependency, and not the version packaged in each's output directory.
It was requested that the code be pasted here.
namespace DependencyExample
{
using RestWrapper;
using System;
using System.IO;
using System.Reflection;
using System.Runtime.Loader;
internal class Program
{
static void Main(string[] args)
{
while (true)
{
Console.Write(Environment.NewLine + "URL: ");
string url = Console.ReadLine();
if (String.IsNullOrEmpty(url)) continue;
try
{
#region Local
using (RestRequest req = new RestRequest(url))
{
using (RestResponse resp = req.Send())
{
Console.WriteLine("| Status (runner) : " + resp.StatusCode + " " + resp.ContentLength + " bytes");
}
}
#endregion
#region Module1
PluginLoadContext ctx1 = new PluginLoadContext("module1/");
Assembly asm1 = ctx1.LoadFromAssemblyPath(Path.GetFullPath("module1/Module1.dll"));
ctx1.Resolving += (ctx, assemblyName) =>
{
Console.WriteLine("| Module 1 loading assembly " + assemblyName.FullName);
var parts = assemblyName.FullName.Split(',');
string name = parts[0];
var version = Version.Parse(parts[1].Split('=')[1]);
string filename = new FileInfo(Path.GetFullPath("module1/" + name + ".dll")).FullName;
Console.WriteLine("| Module 1 loading from file " + filename);
Assembly asm = Assembly.LoadFrom(filename);
Console.WriteLine("| Module 1 loaded assembly " + filename);
return asm;
};
Type type1 = asm1.GetType("Module1.Processor", true);
dynamic instance1 = Activator.CreateInstance(type1);
instance1.Process(url);
#endregion
#region Module2
PluginLoadContext ctx2 = new PluginLoadContext("module2/");
Assembly asm2 = ctx2.LoadFromAssemblyPath(Path.GetFullPath("module2/Module2.dll"));
ctx1.Resolving += (ctx, assemblyName) =>
{
Console.WriteLine("| Module 2 loading assembly " + assemblyName.FullName);
var parts = assemblyName.FullName.Split(',');
string name = parts[0];
var version = Version.Parse(parts[1].Split('=')[1]);
string filename = new FileInfo(Path.GetFullPath("module2/" + name + ".dll")).FullName;
Console.WriteLine("| Module 2 loading from file " + filename);
Assembly asm = Assembly.LoadFrom(filename);
Console.WriteLine("| Module 2 loaded assembly " + filename);
return asm;
};
Type type2 = asm2.GetType("Module2.Processor", true);
dynamic instance2 = Activator.CreateInstance(type2);
instance2.Process(url);
#endregion
Console.WriteLine("Finished URL " + url);
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
}
}
private class PluginLoadContext : AssemblyLoadContext
{
private AssemblyDependencyResolver _Resolver;
public PluginLoadContext(string baseDirectory)
{
_Resolver = new AssemblyDependencyResolver(Path.GetFullPath(baseDirectory));
}
protected override Assembly? Load(AssemblyName name)
{
Console.WriteLine(" | Loading assembly: " + name);
string path = _Resolver.ResolveAssemblyToPath(name);
if (path != null)
{
return LoadFromAssemblyPath(path);
}
return null;
}
protected override IntPtr LoadUnmanagedDll(string name)
{
Console.WriteLine(" | Loading unmanaged assembly: " + name);
string path = _Resolver.ResolveUnmanagedDllToPath(name);
if (path != null)
{
return LoadUnmanagedDllFromPath(path);
}
return IntPtr.Zero;
}
}
}
}
namespace Module1
{
using RestWrapper;
using System;
public class Processor
{
public Processor()
{
}
public void Process(string url)
{
using (RestRequest req = new RestRequest(url))
{
using (RestResponse resp = req.Send())
{
Console.WriteLine("| Status (module1): " + resp.StatusCode + " " + resp.ContentLength + " bytes");
}
}
}
}
}
namespace Module2
{
using RestWrapper;
using System;
public class Processor
{
public Processor()
{
}
public void Process(string url)
{
using (RestRequest req = new RestRequest(url))
{
using (RestResponse resp = req.Send())
{
Console.WriteLine("| Status (module2): " + resp.StatusCode + " " + resp.ContentLength + " bytes");
}
}
}
}
}
I think that the problem is that you are loading your dependencies (inside AssemblyLoadContext.Resolving
event handler) using Assembly.LoadFrom
. It fails due to its behavior.
I think that the right way to do this is to invoke the AssemblyLoadContext.LoadFromAssemblyPath method.
ctx1.Resolving += (ctx, assemblyName) =>
{
Console.WriteLine("| Module 1 loading assembly " + assemblyName.FullName);
var parts = assemblyName.FullName.Split(',');
string name = parts[0];
var version = Version.Parse(parts[1].Split('=')[1]);
string filename = new FileInfo(Path.GetFullPath("module1/" + name + ".dll")).FullName;
Console.WriteLine("| Module 1 loading from file " + filename);
//Assembly asm = Assembly.LoadFrom(filename); This line is replaced with the one below
Assembly asm = ctx.LoadFromAssemblyPath(filename);
Console.WriteLine("| Module 1 loaded assembly " + filename);
return asm;
};
Same apply for ctx2
. Please note that when loading Module2 you are still using ctx1
instead of ctx2
so this also needs to be fixed.
AssemblyLoadContext ctx2 = new AssemblyLoadContext("module2", false);
Assembly asm2 = ctx2.LoadFromAssemblyPath(Path.GetFullPath("module2/Module2.dll")); // Replaced ctx1 with ctx2
ctx2.Resolving += (ctx, assemblyName) => // Replaced ctx1 with ctx2
{
Console.WriteLine("| Module 2 loading assembly " + assemblyName.FullName);
var parts = assemblyName.FullName.Split(',');
string name = parts[0];
var version = Version.Parse(parts[1].Split('=')[1]);
string filename = new FileInfo(Path.GetFullPath("module2/" + name + ".dll")).FullName;
Console.WriteLine("| Module 2 loading from file " + filename);
//Assembly asm = Assembly.LoadFrom(filename); This line is replaced with the one below
Assembly asm = ctx.LoadFromAssemblyPath(filename);
Console.WriteLine("| Module 2 loaded assembly " + filename);
return asm;
};