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

动态编译DLL无法卸载求助:AppDomain.Unload提示文件占用

解决脚本编译程序集无法卸载的问题

这个问题我之前也碰到过——.NET里单个程序集是没法在当前AppDomain里卸载的,你现在的代码直接在主域加载了编译后的程序集,还持有scriptAssembly的引用,同时创建了脚本实例,这些都会把文件死死锁住,导致AppDomain.Unload(其实你可能是想卸载程序集,但这个方法只能卸载整个域)报错。

问题根源

当你调用result.CompiledAssembly时,.NET会把编译好的程序集加载到当前应用程序域里,只要这个域里还有任何对该程序集的引用(比如Assembly对象、你创建的IScript实例),程序集文件就会被锁定,无法删除或覆盖,而且单个程序集根本没法在同一个域内卸载——这是.NET的设计限制,只能通过卸载整个AppDomain来释放程序集。

解决方案:用独立的AppDomain加载和运行脚本

核心思路是把脚本的加载、执行都放到一个单独的AppDomain里,执行完后直接卸载这个域,就能彻底释放程序集文件的锁。具体步骤如下:

1. 创建跨域执行的代理类

因为跨AppDomain访问对象需要遵循.NET的远程处理规则,所以我们需要一个继承自MarshalByRefObject的类来作为脚本执行的代理:

public class ScriptExecutor : MarshalByRefObject
{
    public void RunScripts(string assemblyPath, Type interfaceType)
    {
        // 在新AppDomain里加载程序集
        var scriptAssembly = Assembly.LoadFrom(assemblyPath);
        var scriptTypes = Helper.GetTypesImplementingInterface(scriptAssembly, interfaceType);
        
        // 注意:如果IScriptableComponent需要跨域访问,要么标记为[Serializable],要么也继承MarshalByRefObject
        IScriptableComponent component = new DummyComponent();
        
        foreach (var scriptType in scriptTypes)
        {
            var script = (IScript)Activator.CreateInstance(scriptType);
            script.Run(component);
        }
    }
}

2. 修改编译逻辑,避免当前域加载程序集

原来的CompileAssembly方法返回了CompiledAssembly,这会导致当前域加载程序集并锁定文件,我们改成只负责编译,不返回Assembly对象:

public static void CompileAssembly(string[] sourceFiles, string outputAssemblyPath)
{
    var codeProvider = new CSharpCodeProvider();
    var compilerParameters = new CompilerParameters
    {
        GenerateExecutable = false,
        GenerateInMemory = false,
        IncludeDebugInformation = false,
        OutputAssembly = outputAssemblyPath
    };
    compilerParameters.ReferencedAssemblies.Add(Assembly.GetExecutingAssembly().Location);
    
    var result = codeProvider.CompileAssemblyFromFile(compilerParameters, sourceFiles);
    if (result.Errors.HasErrors)
    {
        var errorDetails = string.Join(Environment.NewLine, 
            result.Errors.Cast<CompilerError>().Select(err => $"Line {err.Line}: {err.ErrorText}"));
        throw new Exception($"脚本编译失败:{Environment.NewLine}{errorDetails}");
    }
    // 不要返回result.CompiledAssembly,避免当前域加载程序集
}

3. 修改按钮点击事件,使用独立AppDomain执行脚本

现在我们在主域里创建新的AppDomain,通过代理类执行脚本,执行完成后立即卸载域:

private void StartButton_Click(object sender, EventArgs e)
{
    var compiledAssemblyPath = Path.Combine(Environment.CurrentDirectory, ScriptsDirectory, CompiledScriptsAssemblyName);
    var scriptFiles = Directory.EnumerateFiles(ScriptsDirectory, "*.cs", SearchOption.AllDirectories).ToArray();
    
    // 先完成编译,不加载到当前域
    Helper.CompileAssembly(scriptFiles, compiledAssemblyPath);
    
    // 配置新AppDomain的环境,确保能找到依赖
    var domainSetup = new AppDomainSetup
    {
        ApplicationBase = Environment.CurrentDirectory,
        PrivateBinPath = ScriptsDirectory // 添加脚本目录作为私有路径,方便加载依赖
    };
    
    // 创建独立的AppDomain
    var scriptDomain = AppDomain.CreateDomain("ScriptExecutionDomain", null, domainSetup);
    
    try
    {
        // 在新域中创建代理实例并执行脚本
        var executor = (ScriptExecutor)scriptDomain.CreateInstanceAndUnwrap(
            Assembly.GetExecutingAssembly().FullName,
            typeof(ScriptExecutor).FullName);
        
        executor.RunScripts(compiledAssemblyPath, typeof(IScript));
    }
    finally
    {
        // 必须卸载AppDomain,释放程序集文件锁
        AppDomain.Unload(scriptDomain);
    }
}

注意事项

  • 跨域对象的要求:如果你的IScript、IScriptableComponent或者DummyComponent需要在主域和脚本域之间传递,要么标记为[Serializable](会被序列化复制),要么继承MarshalByRefObject(会创建远程代理,实际对象留在原域)。如果不满足这个条件,跨域调用会报错。
  • 临时文件处理:如果不需要保留编译后的程序集文件,可以把GenerateInMemory设为true,这样编译出来的程序集会直接加载到内存,不会生成物理文件,但同样需要用独立AppDomain来卸载。
  • 异常处理:建议在脚本执行的逻辑里添加异常捕获,避免脚本的错误导致主域崩溃。

内容的提问来源于stack exchange,提问作者user8429238

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 04:06:16