如何在.NET 7中用AssemblyLoadContext加载程序集?代码迁移求助
将基于AppDomain的模块目录代码迁移到.NET 7(适配AssemblyLoadContext)
需要将这段基于AppDomain的模块目录加载代码转换为.NET 7兼容版本,核心是实现LoadModuleCatalog方法。由于.NET 7已废弃AppDomain,官方推荐使用AssemblyLoadContext来隔离加载程序集,但相关适配资料较少,难以完成代码迁移。
原代码如下:
public class DynamicDirectoryModuleCatalog : ModuleCatalog { void LoadModuleCatalog(string path, bool isFile = false) { AppDomain? parentDomain = AppDomain.CurrentDomain; Evidence evidence = new Evidence(parentDomain.Evidence); AppDomainSetup setup = parentDomain.SetupInformation; AppDomain childDomain = AppDomain.CreateDomain("DiscoveryRegion", evidence, setup); try { List<string> loadedAssemblies = new List<string>(); var assemblies = ( from Assembly assembly in AppDomain.CurrentDomain.GetAssemblies() where !(assembly is System.Reflection.Emit.AssemblyBuilder) && assembly.GetType().FullName != "System.Reflection.Emit.InternalAssemblyBuilder" && !String.IsNullOrEmpty(assembly.Location) select assembly.Location ); loadedAssemblies.AddRange(assemblies); Type loaderType = typeof(InnerModuleInfoLoader); if (loaderType.Assembly != null) { var loader = (InnerModuleInfoLoader)childDomain.CreateInstanceFrom(loaderType.Assembly.Location, loaderType.FullName).Unwrap(); loader.LoadAssemblies(loadedAssemblies); ModuleInfo[] modules = loader.GetModuleInfos(path, isFile); this.Items.AddRange(modules); if (isFile) LoadModules(modules); } } finally { AppDomain.Unload(childDomain); } } private class InnerModuleInfoLoader : MarshalByRefObject { [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")] internal ModuleInfo[] GetModuleInfos(string path, bool isFile = false) { Assembly moduleReflectionOnlyAssembly = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies().First( asm => asm.FullName == typeof(IModule).Assembly.FullName); Type IModuleType = moduleReflectionOnlyAssembly.GetType(typeof(IModule).FullName); FileSystemInfo info = null; if (isFile) info = new FileInfo(path); else info = new DirectoryInfo(path); ResolveEventHandler resolveEventHandler = delegate(object sender, ResolveEventArgs args) { return OnReflectionOnlyResolve(args, info); }; AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += resolveEventHandler; IEnumerable<ModuleInfo> modules = GetNotAllreadyLoadedModuleInfos(info, IModuleType); AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve -= resolveEventHandler; return modules.ToArray(); } private static IEnumerable<ModuleInfo> GetNotAllreadyLoadedModuleInfos(FileSystemInfo info, Type IModuleType) { List<FileInfo> validAssemblies = new List<FileInfo>(); Assembly[] alreadyLoadedAssemblies = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies(); FileInfo fileInfo = info as FileInfo; if (fileInfo != null) { if (alreadyLoadedAssemblies.FirstOrDefault(assembly => String.Compare(Path.GetFileName(assembly.Location), fileInfo.Name, StringComparison.OrdinalIgnoreCase) == 0) == null) { var moduleInfos = Assembly.ReflectionOnlyLoadFrom(fileInfo.FullName).GetExportedTypes() .Where(IModuleType.IsAssignableFrom) .Where(t => t != IModuleType) .Where(t => !t.IsAbstract).Select(t => CreateModuleInfo(t)); return moduleInfos; } } DirectoryInfo directory = info as DirectoryInfo; var files = directory.GetFiles("*.dll").Where(file => alreadyLoadedAssemblies. FirstOrDefault(assembly => String.Compare(Path.GetFileName(assembly.Location), file.Name, StringComparison.OrdinalIgnoreCase) == 0) == null); foreach (FileInfo file in files) { try { Assembly.ReflectionOnlyLoadFrom(file.FullName); validAssemblies.Add(file); } catch (BadImageFormatException) { // skip non-.NET Dlls } } return validAssemblies.SelectMany(file => Assembly.ReflectionOnlyLoadFrom(file.FullName) .GetExportedTypes() .Where(IModuleType.IsAssignableFrom) .Where(t => t != IModuleType) .Where(t => !t.IsAbstract) .Select(type => CreateModuleInfo(type))); } private static Assembly OnReflectionOnlyResolve(ResolveEventArgs args, FileSystemInfo info) { Assembly loadedAssembly = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies().FirstOrDefault( asm => string.Equals(asm.FullName, args.Name, StringComparison.OrdinalIgnoreCase)); if (loadedAssembly != null) { return loadedAssembly; } DirectoryInfo directory = info as DirectoryInfo; if (directory != null) { AssemblyName assemblyName = new AssemblyName(args.Name); string dependentAssemblyFilename = Path.Combine(directory.FullName, assemblyName.Name + ".dll"); if (File.Exists(dependentAssemblyFilename)) { return Assembly.ReflectionOnlyLoadFrom(dependentAssemblyFilename); } } return Assembly.ReflectionOnlyLoad(args.Name); } [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")] internal void LoadAssemblies(IEnumerable<string> assemblies) { foreach (string assemblyPath in assemblies) { try { Assembly.ReflectionOnlyLoadFrom(assemblyPath); } catch (FileNotFoundException) { // Continue loading assemblies even if an assembly can not be loaded in the new AppDomain } } } private static ModuleInfo CreateModuleInfo(Type type) { // create ModuleInfo, implementation is not relevant } } }
迁移后的.NET 7实现方案
核心改动是用自定义AssemblyLoadContext替代AppDomain实现程序集隔离加载,同时移除.NET Core+不再支持的ReflectionOnlyLoad相关API,改用普通加载但隔离在独立ALC中,避免污染主应用程序集上下文。
关键改动点
- 移除所有AppDomain相关代码,自定义
ModuleDiscoveryLoadContext隔离加载程序集 - 替换
ReflectionOnlyLoad:.NET Core+已移除反射仅加载模式,改用独立ALC加载程序集来实现隔离 - 移除
MarshalByRefObject:无需跨AppDomain通信,直接在当前上下文调用加载逻辑 - 在自定义ALC中实现程序集解析逻辑,替代原
ReflectionOnlyAssemblyResolve事件
完整迁移代码
using System.Reflection; using System.Runtime.Loader; using System; using System.Collections.Generic; using System.IO; using System.Linq; public class DynamicDirectoryModuleCatalog : ModuleCatalog { public void LoadModuleCatalog(string path, bool isFile = false) { // 创建自定义AssemblyLoadContext用于隔离模块发现的程序集 using var discoveryContext = new ModuleDiscoveryLoadContext(); try { // 加载主上下文已有的程序集到隔离上下文(保持类型兼容性) var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies() .Where(asm => !(asm is System.Reflection.Emit.AssemblyBuilder) && asm.GetType().FullName != "System.Reflection.Emit.InternalAssemblyBuilder" && !string.IsNullOrEmpty(asm.Location)) .Select(asm => asm.Location); foreach (var assemblyPath in loadedAssemblies) { try { discoveryContext.LoadFromAssemblyPath(assemblyPath); } catch (FileNotFoundException) { // 忽略加载失败的程序集,继续执行 } } // 获取IModule类型(从隔离上下文加载) var moduleAssembly = discoveryContext.LoadFromAssemblyPath(typeof(IModule).Assembly.Location); Type IModuleType = moduleAssembly.GetType(typeof(IModule).FullName)!; FileSystemInfo info = isFile ? new FileInfo(path) : new DirectoryInfo(path); var modules = GetNotAlreadyLoadedModuleInfos(info, IModuleType, discoveryContext); Items.AddRange(modules.ToArray()); if (isFile) LoadModules(modules.ToArray()); } finally { // 释放ALC,自动卸载加载的程序集 discoveryContext.Unload(); } } private static IEnumerable<ModuleInfo> GetNotAlreadyLoadedModuleInfos(FileSystemInfo info, Type IModuleType, ModuleDiscoveryLoadContext loadContext) { var loadedAssemblyNames = loadContext.Assemblies.Select(a => a.GetName()).ToList(); if (info is FileInfo fileInfo) { // 检查该程序集是否已在隔离上下文中加载 var assemblyName = AssemblyName.GetAssemblyName(fileInfo.FullName); if (!loadedAssemblyNames.Any(n => AssemblyName.ReferenceMatchesDefinition(n, assemblyName))) { return LoadModuleInfosFromFile(fileInfo, IModuleType, loadContext); } return Enumerable.Empty<ModuleInfo>(); } if (info is DirectoryInfo directoryInfo) { var validAssemblies = new List<FileInfo>(); foreach (var file in directoryInfo.GetFiles("*.dll")) { try { var asmName = AssemblyName.GetAssemblyName(file.FullName); if (!loadedAssemblyNames.Any(n => AssemblyName.ReferenceMatchesDefinition(n, asmName))) { loadContext.LoadFromAssemblyPath(file.FullName); validAssemblies.Add(file); } } catch (BadImageFormatException) { // 跳过非.NET程序集 } } return validAssemblies.SelectMany(file => LoadModuleInfosFromFile(file, IModuleType, loadContext)); } return Enumerable.Empty<ModuleInfo>(); } private static IEnumerable<ModuleInfo> LoadModuleInfosFromFile(FileInfo file, Type IModuleType, ModuleDiscoveryLoadContext loadContext) { var assembly = loadContext.LoadFromAssemblyPath(file.FullName); return assembly.GetExportedTypes() .Where(t => IModuleType.IsAssignableFrom(t) && t != IModuleType && !t.IsAbstract) .Select(t => CreateModuleInfo(t)); } private static ModuleInfo CreateModuleInfo(Type type) { // 保留原ModuleInfo创建逻辑 } // 自定义AssemblyLoadContext,用于隔离模块发现的程序集 private class ModuleDiscoveryLoadContext : AssemblyLoadContext { public ModuleDiscoveryLoadContext() : base(isCollectible: true) { } protected override Assembly? Load(AssemblyName assemblyName) { // 优先尝试从应用程序基目录加载,找不到则返回null让系统处理 try { return LoadFromAssemblyName(assemblyName); } catch { return null; } } // 重写ResolveUnmanagedDll,处理非托管依赖(如果需要) protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) { // 根据需求实现非托管库加载逻辑,这里默认返回0表示不处理 return IntPtr.Zero; } } }
说明
ModuleDiscoveryLoadContext标记为isCollectible: true,确保在调用Unload()时可以回收加载的程序集,模拟原AppDomain卸载的效果- 程序集解析逻辑在自定义ALC的
Load方法中实现,替代原反射仅加载的解析事件 - 移除了跨AppDomain的对象传递逻辑,直接在当前上下文调用加载方法,简化代码结构
内容的提问来源于stack exchange,提问作者Furqan Safdar
相关产品推荐
相关产品推荐

