Roslyn,如何在运行时在脚本中实例化一个类并调用该类的方法?

时间:2017-11-10 09:00:27

标签: c# roslyn runtime-compilation

我理解如何在C#中使用Roslyn执行整个脚本,但我现在要完成的是在脚本中编译一个类,实例化它,将其解析为接口,然后调用已编译和实例化的类实现的方法。

Roslyn是否公开了这样的功能?你能指点我这样的方法吗?

由于

1 个答案:

答案 0 :(得分:7)

我认为你可以做你想做的事,例如:

namespace ConsoleApp2 {
    class Program {
        static void Main(string[] args) {
            // create class and return its type from script
            // reference current assembly to use interface defined below
            var script = CSharpScript.Create(@"
        public class Test : ConsoleApp2.IRunnable {
            public void Run() {
                System.Console.WriteLine(""test"");
            }
        }
        return typeof(Test);
        ", ScriptOptions.Default.WithReferences(Assembly.GetExecutingAssembly()));
            script.Compile();
            // run and you get Type object for your fresh type
            var testType = (Type) script.RunAsync().Result.ReturnValue;
            // create and cast to interface
            var runnable = (IRunnable)Activator.CreateInstance(testType);
            // use
            runnable.Run();
            Console.ReadKey();
        }
    }

    public interface IRunnable {
        void Run();
    }
}

您可以使用全局变量并以此方式返回,而不是返回您从脚本创建的类型:

namespace ConsoleApp2 {
    class Program {
        static void Main(string[] args) {

            var script = CSharpScript.Create(@"
        public class Test : ConsoleApp2.IRunnable {
            public void Run() {
                System.Console.WriteLine(""test"");
            }
        }
        MyTypes.Add(typeof(Test).Name, typeof(Test));
        ", ScriptOptions.Default.WithReferences(Assembly.GetExecutingAssembly()), globalsType: typeof(ScriptGlobals));
            script.Compile();
            var globals = new ScriptGlobals();
            script.RunAsync(globals).Wait();            
            var runnable = (IRunnable)Activator.CreateInstance(globals.MyTypes["Test"]);
            runnable.Run();
            Console.ReadKey();
        }
    }

    public class ScriptGlobals {
        public Dictionary<string, Type> MyTypes { get; } = new Dictionary<string, Type>();
    }

    public interface IRunnable {
        void Run();
    }
}

编辑以回答您的评论。

  

如果我知道脚本中类的名称和类型怎么办?我的   理解是script.Compile()将已编译的程序集添加到   GAC?我不对吗?如果我然后简单地使用   Activator.CreateInstance(typeofClass)这不会解决我的问题   甚至不必运行脚本

编译程序集未添加到gac中 - 它被编译并存储在内存中,类似于使用Assembly.Load(someByteArray)加载程序集的方式。无论如何,在您致电Compile后,程序集已加载到当前应用程序域中,这样您就可以在不使用RunAsunc()的情况下访问您的类型。问题是这个程序集有一个神秘的名字,例如:ℛ*fde34898-86d2-42e9-a786-e3c1e1befa78#1-0。要找到它,你可以这样做:

script.Compile();
var asmAfterCompile = AppDomain.CurrentDomain.GetAssemblies().Single(c =>
     String.IsNullOrWhiteSpace(c.Location) && c.CodeBase.EndsWith("Microsoft.CodeAnalysis.Scripting.dll"));

但请注意,这不稳定,因为如果您在应用程序域中编译多个脚本(甚至多次编译同一个脚本) - 会生成多个此类程序集,因此很难区分它们。如果这对您来说不是问题 - 您可以使用这种方式(但要确保正确测试所有这些)。

找到生成的装配后 - 问题还没有结束。所有脚本内容都在包装类下编译。我看到它命名为“提交#0”,但我不能保证它总是这样命名。所以假设你的脚本中有Test个类。它将是该包装器的子类,因此实际类型名称将为“提交#0 +测试”。因此,为了从生成的程序集中获取类型,最好这样做:

var testType = asmAfterCompile.GetTypes().Single(c => c.Name == "Test");

我认为这种方法与之前的方法相比有些脆弱,但如果之前的方法不适合你 - 试试这个。

评论中提出了另一种选择:

script.Compile();
var stream = new MemoryStream();
var emitResult = script.GetCompilation().Emit(stream);
if (emitResult.Success) {
    var asm = Assembly.Load(stream.ToArray());
}

这样你就可以自己创建程序集,因此不需要在当前的应用程序域中搜索它。