如何将Roslyn脚本提交用作其他Roslyn编译中的程序集

dav*_*ick 6 .net c# .net-assembly roslyn

我想在另一个非脚本编写的Roslyn编译中重用一个脚本作为动态程序集,但我不能为我的生活弄清楚如何使其工作.

例如,假设我以正常方式创建脚本,然后使用以下内容将脚本作为程序集发送到字节流:

var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary);
var compilation = script.GetCompilation().WithOptions(compilationOptions);
using (var ms = new MemoryStream())
{
    EmitResult result = compilation.Emit(ms);
    ms.Seek(0, SeekOrigin.Begin);
    assembly = Assembly.Load(ms.ToArray());
}
Run Code Online (Sandbox Code Playgroud)

现在,假设我想将该程序集作为参考提供给另一个非脚本编译.我不能只使用,assembly因为没有一种MetadataReference.CreateFrom...()方法支持传递实际的Assembly实例.作为动态组件,它没有位置,所以我无法使用MetadataReference.CreateFromFile().

在过去,我已经MetadataReference.CreateFromStream()成功地使用了这种类型的东西,但是当程序集代表脚本提交时,这似乎不起作用(我不知道为什么).编译继续进行,但只要您尝试使用提交中的类型,就会出现以下错误:

System.InvalidCastException: [A]Foo cannot be cast to [B]Foo. Type A originates from 'R*19cecf20-a48e-4a31-9b65-4c0163eba857#1-0, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' in the context 'LoadNeither' in a byte array. Type B originates from 'R*19cecf20-a48e-4a31-9b65-4c0163eba857#1-0, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' in the context 'LoadNeither' in a byte array.
Run Code Online (Sandbox Code Playgroud)

我猜测它与提交程序集处于不同的上下文时有关,当评估与作为字节数组加载时.我非常喜欢在以后的非脚本编译中使用脚本提交中定义的对象和方法的最佳方法的任何见解或指导.

更新7/29

我能够得到一个最小的repro来证明这个问题.它可以在https://github.com/daveaglick/ScriptingAssemblyReuse找到.

在生成repro时,很明显这个问题的一个重要组成部分是脚本将Type其中一个类传递给调用代码,然后调用代码使用它Type来实例化对象的实例,然后传递实例进入引用脚本程序集的编译.从主机应用程序创建的类型实例转换为引用编译中的类型时,会发生不匹配.当我重新阅读它听起来很混乱时,所以希望下面的代码能让它更清晰.

以下是触发此问题的所有代码:

namespace ScriptingAssemblyReuse
{
    public class Globals
    {
        public IFactory Factory { get; set; }    
    }

    public interface IFactory
    {
        object Get();
    }

    public class Factory<T> : IFactory where T : new()
    {
        public object Get() => new T();
    }

    public class Program
    {
        public static void Main(string[] args)
        {
            new Program().Run();
        }

        private Assembly _scriptAssembly;

        public void Run()
        {
            AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve;

            // Create the script
            Script<object> script = CSharpScript.Create(@"
                public class Foo { }
                Factory = new ScriptingAssemblyReuse.Factory<Foo>();
                ", ScriptOptions.Default.WithReferences(MetadataReference.CreateFromFile(typeof(IFactory).Assembly.Location)), typeof(Globals));

            // Create a compilation and get the dynamic assembly
            CSharpCompilationOptions scriptCompilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary);
            Compilation scriptCompilation = script.GetCompilation().WithOptions(scriptCompilationOptions);
            byte[] scriptAssemblyBytes;
            using (MemoryStream ms = new MemoryStream())
            {
                EmitResult result = scriptCompilation.Emit(ms);
                ms.Seek(0, SeekOrigin.Begin);
                scriptAssemblyBytes = ms.ToArray();
            }
            _scriptAssembly = Assembly.Load(scriptAssemblyBytes);

            // Evaluate the script
            Globals globals = new Globals();
            script.RunAsync(globals).Wait();

            // Create the consuming compilation
            string assemblyName = Path.GetRandomFileName();
            CSharpParseOptions parseOptions = new CSharpParseOptions();
            SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(@"
                public class Bar
                {
                    public void Baz(object obj)
                    {
                        Script.Foo foo = (Script.Foo)obj;  // This is the line that triggers the exception 
                    }
                }", parseOptions, assemblyName);
            CSharpCompilationOptions compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary);
            string assemblyPath = Path.GetDirectoryName(typeof(object).Assembly.Location);
            CSharpCompilation compilation = CSharpCompilation.Create(assemblyName, new[] {syntaxTree},
                new[]
                {
                    MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "mscorlib.dll")),
                    MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.dll")),
                    MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Core.dll")),
                    MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Runtime.dll"))
                }, compilationOptions);
            using (MemoryStream ms = new MemoryStream(scriptAssemblyBytes))
            {
                compilation = compilation.AddReferences(MetadataReference.CreateFromStream(ms));
            }

            // Get the consuming assembly
            Assembly assembly;
            using (MemoryStream ms = new MemoryStream())
            {
                EmitResult result = compilation.Emit(ms);
                ms.Seek(0, SeekOrigin.Begin);
                byte[] assemblyBytes = ms.ToArray();
                assembly = Assembly.Load(assemblyBytes);
            }

            // Call the consuming assembly
            Type barType = assembly.GetExportedTypes().First(t => t.Name.StartsWith("Bar", StringComparison.Ordinal));
            MethodInfo bazMethod = barType.GetMethod("Baz");
            object bar = Activator.CreateInstance(barType);
            object obj = globals.Factory.Get();
            bazMethod.Invoke(bar, new []{ obj });  // The exception bubbles up and gets thrown here
        }

        private Assembly OnAssemblyResolve(object sender, ResolveEventArgs args)
        {
            if (_scriptAssembly != null && args.Name == _scriptAssembly.FullName)
            {
                // Return the dynamically compiled script assembly if given it's name
                return _scriptAssembly;
            }
            return null;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Eli*_*bel 3

我想我已经解开了这个谜。发生的情况如下:

  • 创建一个脚本。
  • 获取脚本的编译,但覆盖编译选项。具体来说,它使用默认值ScriptClassName,而Script不是由脚本 API 生成的值(例如Submission#0) -这是问题的关键
  • 使用覆盖的选项发出程序集并将其从流加载到内存中。
  • 运行脚本。此时,有两个不同且不兼容的同名程序集已加载到AppDomainusing 字节数组中。
  • 将流作为元数据引用添加到新编译并在代码中使用它。它编译得很好,因为它使用的是从覆盖选项生成的程序集。您将无法使用脚本创建的实际程序集来编译它,因为类似的名称Submission#0在 C# 中是非法的。如果不是,那么您可以将实际的脚本Assembly实例放入全局变量中并在OnAssemblyResolve.
  • Baz使用类型参数调用方法Submission#0+Foo并尝试将其强制转换为Script+Foo.

总而言之 - 我不认为使用当前的 Roslyn 脚本 API 可以实现这一点。然而,这些 API 并不是编译脚本的唯一方法。您可以自己创建一个编译并将其设置SourceCodeKindScript. 您必须自己做很多事情,例如运行主脚本方法、处理全局变量等。我在 RoslynPad 中做了类似的事情,因为我希望脚本程序集加载 PDB(因此异常会有行信息)。