新应用程序域加载动态代码生成的程序集失败 提示找不到指定文件
我有一个asp.net应用,可从用户脚本动态生成程序集,原有功能运行正常。现在我想要在低权限的SandBox应用程序域中运行生成的程序集,但在新应用程序域加载程序集时抛出以下异常:
Could not load file or assembly 'genb3336c1d82074079911fb70bd8bd7e65.dll, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified.
为了排查问题,我把生成的程序集保存到磁盘,编写了简单测试程序,代码如下:
string path = @"test.dll"; byte[] buffer = File.ReadAllBytes(path); var assembly = AppDomain.CurrentDomain.Load(buffer); //此行执行正常 var appDomain = AppDomain.CreateDomain("Test"); assembly = appDomain.Load(buffer); //此行抛出异常
测试结果还是在新域加载程序集时报相同错误,在当前域加载则完全正常。
随后我用生成该dll的源码手动创建了一个类库项目,源码非常精简,方法内仅2行代码,编译得到的dll在新应用程序域中可正常加载。
我用ILSpy打开两个dll对比,结构完全一致:
我现在完全找不到生成的dll的问题所在,我使用如下代码生成程序集:
public class ScriptCompiler { public static (byte[], byte[], Assembly) Compile((string Source, string Path)[] sources, string[] references, params Type[] types) { var assemblyName = "gen" + Guid.NewGuid().ToString().Replace("-", "") + ".dll"; byte[] rawAssembly; byte[] rawPdb; #if DEBUG var encoding = Encoding.UTF8; var symbolsName = Path.ChangeExtension(assemblyName, "pdb"); var sourceTexts = sources .Select(s => new { Buffer = encoding.GetBytes(s.Source), s.Source, s.Path }) .Select(b => new { SourceText = SourceText.From(b.Buffer, b.Buffer.Length, encoding, canBeEmbedded: true), b.Path }); var syntaxTrees = sourceTexts .Select(s => new { Tree = CSharpSyntaxTree.ParseText(s.SourceText, new CSharpParseOptions(), s.Path), s.Path }) .Select(t => new { Node = t.Tree.GetRoot() as CSharpSyntaxNode, t.Path }) .Select(n => CSharpSyntaxTree.Create(n.Node, null, n.Path, encoding)); var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary).WithOptimizationLevel(OptimizationLevel.Debug); #else var syntaxTrees = from source in sources.Select(s => s.Source) select CSharpSyntaxTree.ParseText(source); var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary).WithOptimizationLevel(OptimizationLevel.Release); #endif var typeRefs = new List<MetadataReference>(); foreach (var type in types) { typeRefs.Add(MetadataReference.CreateFromFile(type.Assembly.Location)); } var compilation = CSharpCompilation.Create(assemblyName, options: options, syntaxTrees: syntaxTrees, references: typeRefs.Union(references.Select(r => MetadataReference.CreateFromFile(r)))); EmitResult emitResult; using (var peStream = new MemoryStream()) using (var pdbStream = new MemoryStream()) { #if DEBUG emitResult = compilation.Emit( peStream: peStream, pdbStream: pdbStream, embeddedTexts: sourceTexts.Select(s => EmbeddedText.FromSource(s.Path, s.SourceText)), options: new EmitOptions(debugInformationFormat: DebugInformationFormat.PortablePdb, pdbFilePath: symbolsName) ); #else emitResult = compilation.Emit(peStream: peStream); #endif if (emitResult.Success) { rawAssembly = peStream.GetBuffer(); rawPdb = pdbStream.GetBuffer(); var assembly = Assembly.Load(rawAssembly, rawPdb); return (rawAssembly, rawPdb, assembly); } } var message = string.Join("\r\n", emitResult.Diagnostics); throw new ApplicationException(message); } }
请问是什么原因导致的吗?
补充:
以下是fusionLog日志,我不清楚该怎么解读它:
=== Pre-bind state information === LOG: DisplayName = genb3336c1d82074079911fb70bd8bd7e65.dll, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null (Fully-specified) LOG: Appbase = file:///C:/Users/Admin/source/repos/LoadAssembly/LoadAssembly/bin/Debug/ LOG: Initial PrivatePath = NULL Calling assembly : (Unknown). === LOG: This bind starts in default load context. LOG: Using application configuration file: C:\Users\Admin\source\repos\LoadAssembly\LoadAssembly\bin\Debug\LoadAssembly.exe.Config LOG: Using host configuration file: LOG: Using machine configuration file from C:\Windows\Microsoft.NET\Framework\v4.0.30319\config\machine.config. LOG: Policy not being applied to reference at this time (private, custom, partial, or location-based assembly bind). LOG: Attempting download of new URL file:///C:/Users/Admin/source/repos/LoadAssembly/LoadAssembly/bin/Debug/genb3336c1d82074079911fb70bd8bd7e65.dll.DLL. LOG: Attempting download of new URL file:///C:/Users/Admin/source/repos/LoadAssembly/LoadAssembly/bin/Debug/genb3336c1d82074079911fb70bd8bd7e65.dll/genb3336c1d82074079911fb70bd8bd7e65.dll.DLL. LOG: Attempting download of new URL file:///C:/Users/Admin/source/repos/LoadAssembly/LoadAssembly/bin/Debug/genb3336c1d82074079911fb70bd8bd7e65.dll.EXE. LOG: Attempting download of new URL file:///C:/Users/Admin/source/repos/LoadAssembly/LoadAssembly/bin/Debug/genb3336c1d82074079911fb70bd8bd7e65.dll/genb3336c1d82074079911fb70bd8bd7e65.dll.EXE.
问题原因
从fusion日志可以直接定位核心问题:你调用CSharpCompilation.Create时,把带.dll后缀的文件名直接当成了程序集名称传入,导致程序集的内置名称自带.dll后缀。新AppDomain加载程序集时会把这个后缀当成名称的一部分,去磁盘探测xxx.dll.dll文件,自然找不到对应资源。
而AppDomain.CurrentDomain.Load(byte[])走的是直接加载原始字节流的逻辑,不会触发磁盘文件探测,所以当前域加载完全正常。你手动编译的类库项目默认程序集名称不带后缀,所以可以正常加载。
修复方案
修改编译代码中程序集名称的生成逻辑,去掉.dll后缀即可:
// 原错误代码 var assemblyName = "gen" + Guid.NewGuid().ToString().Replace("-", "") + ".dll"; // 修正为 var assemblyName = "gen" + Guid.NewGuid().ToString().Replace("-", "");
如果需要指定输出dll的文件名,在编译完成后保存到磁盘时再加后缀即可,不要把后缀写到程序集的内置名称里。
优化建议
如果是沙盒AppDomain场景,更推荐用代理类的方式加载内存程序集,避免跨域传递Assembly对象带来的权限泄漏风险:
- 先定义一个继承
MarshalByRefObject的跨域代理类 - 在新AppDomain中实例化代理类
- 在代理类内部执行
Assembly.Load(byte[])加载程序集、执行业务逻辑
这种方式完全基于内存加载,不需要依赖磁盘文件,更符合沙盒场景的安全需求。
内容的提问来源于stack exchange,提问作者alfoks

