使用AssemblyLoadContext加载插件时遇ReflectionTypeLoadException问题
我用AssemblyLoadContext在运行时加载多个插件程序集,遇到以下问题:加载使用主程序(此处为单元测试)已加载接口的插件时,触发ReflectionTypeLoadException。接口版本完全一致,文件二进制100%匹配,仅加载路径不同。强制让PlugInLoadContext从插件目录重新加载接口能解决问题,但这种方式有其他限制且不规范。
为复现问题,我制作了最小化项目,需创建4个.NET7项目:
环境搭建
- 共享类库SharedLib:定义单个接口
namespace SharedLib; public interface ISharedInterface { }
- 接口类库InterfaceLib:引用SharedLib,定义接口
using SharedLib; namespace InterfaceLib; public interface IInterface { Task Action(ISharedInterface shared); }
- 用户类库ClassLib:引用上述两个项目
using InterfaceLib; using SharedLib; namespace ClassLib; public class Implementation : IInterface { public Task Action(ISharedInterface shared) => throw new NotImplementedException(); } // 移除该类则不会触发异常 public class SharedImplementation : ISharedInterface { }
- 主程序Program:仅引用InterfaceLib,注意必须保留
IInterface引用才能复现问题
// 注释该行则不会触发异常! InterfaceLib.IInterface _; var pluginPath = Path.GetFullPath(AppContext.BaseDirectory + @"..\\..\\..\\..\\ClassLib\\bin\\Debug\\net7.0\\ClassLib.dll"); var context = new PlugInLoadContext(); context.AddPlugIn(pluginPath); // 此处触发异常 var types = context.PlugIns.SelectMany(suba => suba.GetTypes().ToArray()).ToArray();
PlugInLoadContext实现代码
public class PlugInLoadContext : AssemblyLoadContext { private readonly List<AssemblyDependencyResolver> _resolvers = new List<AssemblyDependencyResolver>(); private readonly Dictionary<string, Assembly> _knownPlugIns = new Dictionary<string, Assembly>(); public IEnumerable<Assembly> PlugIns => _knownPlugIns.Select(a => a.Value); public Assembly AddPlugIn(string plugInPath) { if (_knownPlugIns.TryGetValue(plugInPath, out var assembly)) { return assembly; } var result = Load(AssemblyName.GetAssemblyName(plugInPath)); if (result == null) { _resolvers.Add(new AssemblyDependencyResolver(plugInPath)); result = LoadFromAssemblyPath(plugInPath); } _knownPlugIns.Add(plugInPath, result); return result; } protected override Assembly? Load(AssemblyName assemblyName) { var result = LoadInternal(assemblyName); if (result != null) { Console.WriteLine($"{assemblyName.Name} loaded from {result.Location}"); } return result; } protected Assembly? LoadInternal(AssemblyName assemblyName) { var knownAssembly = Default.Assemblies.FirstOrDefault(a => a.FullName == assemblyName.FullName); if (knownAssembly != null) { return knownAssembly; } var assemblyPath = _resolvers.Select(r => r.ResolveAssemblyToPath(assemblyName)).Where(p => p != null).FirstOrDefault(); if (assemblyPath != null) { return LoadFromAssemblyPath(assemblyPath); } return null; } }
运行输出
System.Runtime loaded from C:\Program Files\dotnet\shared\Microsoft.NETCore.App\7.0.11\System.Runtime.dll SharedLib loaded from <userpath>\ReflectionTypeLoadException\ClassLib\bin\Debug\net7.0\SharedLib.dll InterfaceLib loaded from <userpath>\ReflectionTypeLoadException\Program\bin\Debug\net7.0\InterfaceLib.dll Unhandled exception. System.Reflection.ReflectionTypeLoadException: Unable to load one or more of the requested types. Method 'Action' in type 'ClassLib.Implementation' from assembly 'ClassLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' does not have an implementation. at System.Reflection.RuntimeModule.GetTypes(RuntimeModule module) at System.Reflection.Assembly.GetTypes() at Program.<<Main>$>b__0_0(Assembly suba) in <userpath>\ReflectionTypeLoadException\Program\Program.cs:line 17 at System.Linq.Enumerable.SelectManySingleSelectorIterator`2.ToArray() at Program.<Main>$(String[] args) in <userpath>\ReflectionTypeLoadException\Program\Program.cs:line 17 System.TypeLoadException: Method 'Action' in type 'ClassLib.Implementation' from assembly 'ClassLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' does not have an implementation.
问题核心是**.NET中类型的唯一性由「加载上下文+程序集+类型全名」共同决定**——即便程序集二进制完全一致,只要加载上下文不同,对应的类型就会被视为不同类型。
具体到你的场景:
- 主程序(
Default加载上下文)提前加载了InterfaceLib,其依赖的SharedLib也被加载到Default上下文。 - 加载
ClassLib时,由于ClassLib包含实现ISharedInterface的SharedImplementation,PlugInLoadContext会先尝试加载SharedLib——此时Default上下文的SharedLib尚未被加载(主程序未直接引用),因此会从插件目录加载SharedLib到PlugInLoadContext。 - 随后加载
InterfaceLib时,Default上下文已存在该程序集,直接复用Default上下文的InterfaceLib。 - 矛盾由此产生:
InterfaceLib中IInterface.Action的参数是Default上下文的ISharedInterface,但ClassLib.Implementation实现的Action参数是PlugInLoadContext上下文的ISharedInterface——CLR判定两个方法签名不匹配,认为Implementation未实现IInterface的Action方法,抛出异常。
1. 统一共享程序集的加载路径
修改PlugInLoadContext.LoadInternal方法,优先从主程序目录加载共享程序集(如SharedLib、InterfaceLib),确保同一共享程序集仅在一个上下文加载:
protected Assembly? LoadInternal(AssemblyName assemblyName) { // 先检查主程序目录是否存在目标程序集 var appAssemblyPath = Path.Combine(AppContext.BaseDirectory, $"{assemblyName.Name}.dll"); if (File.Exists(appAssemblyPath)) { return LoadFromAssemblyPath(appAssemblyPath); } // 再复用Default上下文已加载的程序集 var knownAssembly = Default.Assemblies.FirstOrDefault(a => a.FullName == assemblyName.FullName); if (knownAssembly != null) { return knownAssembly; } // 最后从插件目录加载依赖 var assemblyPath = _resolvers.Select(r => r.ResolveAssemblyToPath(assemblyName)).Where(p => p != null).FirstOrDefault(); if (assemblyPath != null) { return LoadFromAssemblyPath(assemblyPath); } return null; }
此方式会让SharedLib从主程序目录加载到PlugInLoadContext,与InterfaceLib依赖的SharedLib类型保持一致,避免类型不匹配。
2. 给共享程序集添加强命名
给SharedLib和InterfaceLib签名强命名后,CLR会将相同强命名的程序集视为同一个(只要版本匹配),不受加载路径和上下文影响。步骤:
- 在项目属性中启用「签名」,创建强命名密钥文件。
- 重新编译所有引用这些程序集的项目。
此方式从根源上解决了同一程序集多上下文加载的类型冲突问题。
3. 完全隔离插件上下文(不推荐,除非必要)
让PlugInLoadContext不复用Default上下文的任何程序集,所有依赖都从插件目录加载。修改LoadInternal方法,移除对Default上下文程序集的复用:
protected Assembly? LoadInternal(AssemblyName assemblyName) { // 仅从插件目录加载依赖 var assemblyPath = _resolvers.Select(r => r.ResolveAssemblyToPath(assemblyName)).Where(p => p != null).FirstOrDefault(); if (assemblyPath != null) { return LoadFromAssemblyPath(assemblyPath); } // 系统程序集交给基类处理 return null; }
但此方式会导致同一程序集被多次加载,增加内存开销,不符合共享程序集的设计初衷,仅适用于完全隔离的插件场景。
内容的提问来源于stack exchange,提问作者tris

