.NET Framework WPF应用迁移至.NET6时,替代反射仅加载程序集的实现困境与解决方案咨询
我来帮你梳理这个问题的核心原因,并给出适配.NET 6的解决方案。
先澄清你的核心疑问:AssemblyLoadContext vs AppDomain
在.NET Core/.NET 5+体系里,AppDomain已经基本退出历史舞台——它现在仅作为兼容旧代码的遗留机制存在,真正的程序集隔离逻辑都由AssemblyLoadContext(ALC)实现。每个ALC是一个独立的程序集加载容器:
- 主应用的程序集默认加载在
AssemblyLoadContext.Default这个全局ALC中 - 你创建的
TempAssemblyLoadContext是一个可收集的自定义ALC,它和默认ALC完全隔离,加载的程序集实例不会进入默认ALC(除非你主动在默认ALC中加载) - 你看到"临时ALC加载的程序集出现在AppDomain中"是因为AppDomain在.NET 6中只是一个壳子,它会枚举所有ALC中的程序集,但这不代表程序集被加载到了主应用的执行上下文里
你遇到的资源找不到异常的根本原因
问题出在:即使你卸载了临时ALC,WPF的资源解析系统已经被临时ALC中加载的程序集实例干扰了。
WPF的pack URI解析依赖程序集的标识,但不同ALC中的同一程序集会被视为完全独立的实例。当你在临时ALC中加载了主ALC已存在的程序集后,WPF的资源缓存可能会错误地指向临时ALC中的程序集实例——当临时ALC被卸载后,这个实例失效,后续查找资源时就会抛出找不到的异常,哪怕主ALC里的程序集明明还在。
适配.NET 6的隔离扫描方案:用MetadataLoadContext替代反射仅加载
要实现类似.NET Framework中ReflectionOnlyLoad的隔离扫描,最佳方式是只读取程序集的元数据,完全不加载程序集到任何执行上下文。.NET 6提供了MetadataLoadContext来做这件事,它不会加载程序集的IL代码,也不会干扰主应用的程序集或WPF资源系统。
具体实现步骤
先安装两个NuGet包:
Install-Package System.Reflection.Metadata Install-Package System.Reflection.MetadataLoadContext修改你的扫描逻辑,用
MetadataLoadContext读取元数据:private void LoadAssemblies(string folder) { // 配置元数据加载上下文的解析器,需要包含必要的系统程序集 var systemAssemblyPaths = new[] { Path.Combine(RuntimeEnvironment.GetRuntimeDirectory(), "System.Runtime.dll"), Path.Combine(RuntimeEnvironment.GetRuntimeDirectory(), "System.Reflection.dll"), Path.Combine(RuntimeEnvironment.GetRuntimeDirectory(), "PresentationCore.dll"), Path.Combine(RuntimeEnvironment.GetRuntimeDirectory(), "PresentationFramework.dll"), Path.Combine(RuntimeEnvironment.GetRuntimeDirectory(), "WindowsBase.dll") }; var resolver = new PathAssemblyResolver(systemAssemblyPaths); var assemblyFiles = Directory.GetFiles(folder, "*.Client.dll"); foreach (var assemblyFile in assemblyFiles) { using (var mlc = new MetadataLoadContext(resolver)) { // 仅读取元数据,不加载程序集到执行上下文 var metadataAssembly = mlc.LoadFromAssemblyPath(assemblyFile); if (ContainsCustomAttr(metadataAssembly)) { // 检查程序集是否已在主ALC中加载 var targetAssemblyName = metadataAssembly.GetName().FullName; var existingAssembly = AssemblyLoadContext.Default.Assemblies .FirstOrDefault(a => a.GetName().FullName == targetAssemblyName); if (existingAssembly != null) { // 直接使用已加载的程序集实例 ProcessAssembly(existingAssembly); } else { // 加载到主ALC并处理 var loadedAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyFile); ProcessAssembly(loadedAssembly); } } } } } // 适配MetadataLoadContext的特性检查逻辑 private bool ContainsCustomAttr(Assembly assembly) { // 替换成你的自定义特性的完整类型名 var customAttrType = assembly.GetType("YourNamespace.YourCustomAssemblyAttribute"); if (customAttrType == null) return false; var attributes = assembly.GetCustomAttributes(customAttrType, inherit: false); return attributes.Any(); }
这个方案的优势
- 完全隔离:仅读取程序集元数据,不会加载任何执行代码,彻底避免干扰主应用的程序集和WPF资源系统
- 性能更优:比加载程序集到临时ALC再卸载的开销小得多
- 行为和.NET Framework的
ReflectionOnlyLoad一致,符合你的原始需求
对之前代码问题的补充说明
你之前用临时ALC的方案,哪怕卸载了ALC,WPF的资源系统还是会残留对临时ALC中程序集的引用——这是WPF资源解析的缓存机制导致的,很难通过常规手段清除。而用MetadataLoadContext则从根源上避免了这个问题,因为它根本不会加载程序集到执行上下文里。
内容的提问来源于stack exchange,提问作者Andrew Stephens

