使用AssemblyLoadContext处理程序集与调用应用的同依赖多版本冲突问题
多版本依赖加载问题排查与解决方案
问题场景
有一个包含三个项目的解决方案:
- 控制台应用
Runner,依赖RestWrapper v3.0.19; - 类库
Module1,依赖RestWrapper v3.0.20; - 类库
Module2,依赖RestWrapper v3.0.18。
三者均需对Runner获取的用户指定URL执行HTTP GET请求。Runner尝试通过AssemblyLoadContext、LoadFromAssemblyPath、Resolving事件及Activator.CreateInstance创建并调用Module1和Module2中的Process方法,但运行Module1时触发System.IO.FileLoadException,提示无法加载RestWrapper v3.0.20。
已做配置与确认
- 已确认
Module1和Module2的输出文件(含依赖)已复制到Runner对应目录的module1/和module2/子目录; Module1和Module2的.csproj中已配置:<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <PublishSingleFile>true</PublishSingleFile> <SelfContained>true</SelfContained>
核心代码片段
模块实例化代码
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);
REST请求代码
using (RestRequest req = new RestRequest(url)) { using (RestResponse resp = req.Send()) { Console.WriteLine("| Status (runner) : " + resp.StatusCode + " " + resp.ContentLength + " bytes"); } }
错误输出
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
补充尝试
- 使用
AssemblyDependencyResolver后可运行,但Runner、Module1、Module2均加载同一版本RestWrapper,未实现各自版本隔离; - 完整代码如下:
Runner的Program.cs
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; } } } }
Module1的Processor.cs
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"); } } } } }
Module2的Processor.cs
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"); } } } } }
解决方案
问题根源
Assembly.LoadFrom的陷阱:在Resolving事件中使用Assembly.LoadFrom会将程序集加载到默认加载上下文,而非当前的PluginLoadContext,导致版本冲突(默认上下文已加载了Runner的v3.0.19,无法再加载v3.0.20);- Module2的事件绑定错误:代码中将Module2的
Resolving事件绑定到了ctx1而非ctx2,导致Module2的依赖会错误地从Module1目录加载; PluginLoadContext的Load方法优先级:自定义Load方法会先于Resolving事件触发,而当前实现中AssemblyDependencyResolver可能会错误地从全局路径加载依赖,而非模块目录。
修复步骤
1. 替换Assembly.LoadFrom为当前上下文的LoadFromAssemblyPath
在Resolving事件中,必须使用当前AssemblyLoadContext的LoadFromAssemblyPath方法,确保程序集加载到对应上下文:
ctx1.Resolving += (ctx, assemblyName) => { Console.WriteLine("| Module 1 loading assembly " + assemblyName.FullName); var parts = assemblyName.FullName.Split(','); string name = parts[0]; string filename = Path.GetFullPath($"module1/{name}.dll"); Console.WriteLine("| Module 1 loading from file " + filename); // 关键:使用当前上下文的LoadFromAssemblyPath,而非Assembly.LoadFrom Assembly asm = ctx.LoadFromAssemblyPath(filename); Console.WriteLine("| Module 1 loaded assembly " + filename); return asm; };
2. 修正Module2的事件绑定
将Module2的Resolving事件绑定到ctx2,而非ctx1:
// 原错误代码:ctx1.Resolving += ... ctx2.Resolving += (ctx, assemblyName) => { Console.WriteLine("| Module 2 loading assembly " + assemblyName.FullName); var parts = assemblyName.FullName.Split(','); string name = parts[0]; string filename = Path.GetFullPath($"module2/{name}.dll"); Console.WriteLine("| Module 2 loading from file " + filename); Assembly asm = ctx.LoadFromAssemblyPath(filename); Console.WriteLine("| Module 2 loaded assembly " + filename); return asm; };
3. 调整PluginLoadContext的Load方法,确保模块优先加载自身目录依赖
修改Load方法,避免AssemblyDependencyResolver错误加载全局版本:
protected override Assembly? Load(AssemblyName name) { Console.WriteLine(" | Loading assembly: " + name); // 优先从模块目录加载,而非依赖解析器的全局查找 string modulePath = Path.Combine(_Resolver.BaseDirectory, $"{name.Name}.dll"); if (File.Exists(modulePath)) { return LoadFromAssemblyPath(modulePath); } // 再尝试依赖解析器 string path = _Resolver.ResolveAssemblyToPath(name); if (path != null) { return LoadFromAssemblyPath(path); } return null; }
4. 移除PublishSingleFile配置(调试阶段)
PublishSingleFile会将所有依赖打包到单个文件,导致模块的依赖无法独立存在于子目录,调试阶段建议禁用:
<!-- 移除或注释该行 --> <!-- <PublishSingleFile>true</PublishSingleFile> -->
验证效果
修复后,每个模块的RestWrapper会加载对应目录的版本,控制台输出应显示:
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 .../module1/RestWrapper.dll | Module 1 loaded assembly .../module1/RestWrapper.dll | Status (module1): 200 57841 bytes | Module 2 loading assembly RestWrapper, Version=3.0.18.0, Culture=neutral, PublicKeyToken=null | Module 2 loading from file .../module2/RestWrapper.dll | Module 2 loaded assembly .../module2/RestWrapper.dll | Status (module2): 200 57841 bytes Finished URL https://www.google.com
内容的提问来源于stack exchange,提问作者joelc
相关产品推荐
相关产品推荐

