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

如何在.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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 06:57:05