You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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'
 ---&gt; 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.&lt;&gt;c.&lt;Main&gt;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

补充尝试

  1. 使用AssemblyDependencyResolver后可运行,但Runner、Module1、Module2均加载同一版本RestWrapper,未实现各自版本隔离;
  2. 完整代码如下:

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

解决方案

问题根源

  1. Assembly.LoadFrom的陷阱:在Resolving事件中使用Assembly.LoadFrom会将程序集加载到默认加载上下文,而非当前的PluginLoadContext,导致版本冲突(默认上下文已加载了Runner的v3.0.19,无法再加载v3.0.20);
  2. Module2的事件绑定错误:代码中将Module2的Resolving事件绑定到了ctx1而非ctx2,导致Module2的依赖会错误地从Module1目录加载;
  3. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 15:07:01