是否可以动态编译和执行 C# 代码片段?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/826398/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-05 03:12:02  来源:igfitidea点击:

Is it possible to dynamically compile and execute C# code fragments?

c#

提问by esac

I was wondering if it is possible to save C# code fragments to a text file (or any input stream), and then execute those dynamically? Assuming what is provided to me would compile fine within any Main() block, is it possible to compile and/or execute this code? I would prefer to compile it for performance reasons.

我想知道是否可以将 C# 代码片段保存到文本文件(或任何输入流),然后动态执行它们?假设提供给我的内容可以在任何 Main() 块中编译良好,是否可以编译和/或执行此代码?出于性能原因,我更愿意编译它。

At the very least, I could define an interface that they would be required to implement, then they would provide a code 'section' that implemented that interface.

至少,我可以定义一个他们需要实现的接口,然后他们将提供一个实现该接口的代码“部分”。

采纳答案by Noldorin

The best solution in C#/all static .NET languages is to use the CodeDOMfor such things. (As a note, its other main purpose is for dynamically constructing bits of code, or even whole classes.)

C#/所有静态 .NET 语言的最佳解决方案是将CodeDOM用于此类事情。(注意,它的另一个主要目的是动态构建代码位,甚至整个类。)

Here's a nice short example take from LukeH's blog, which uses some LINQ too just for fun.

这是来自LukeH 的博客的一个很好的简短示例,它也使用了一些 LINQ 只是为了好玩。

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CSharp;
using System.CodeDom.Compiler;

class Program
{
    static void Main(string[] args)
    {
        var csc = new CSharpCodeProvider(new Dictionary<string, string>() { { "CompilerVersion", "v3.5" } });
        var parameters = new CompilerParameters(new[] { "mscorlib.dll", "System.Core.dll" }, "foo.exe", true);
        parameters.GenerateExecutable = true;
        CompilerResults results = csc.CompileAssemblyFromSource(parameters,
        @"using System.Linq;
            class Program {
              public static void Main(string[] args) {
                var q = from i in Enumerable.Range(1,100)
                          where i % 2 == 0
                          select i;
              }
            }");
        results.Errors.Cast<CompilerError>().ToList().ForEach(error => Console.WriteLine(error.ErrorText));
    }
}

The class of primary importance here is the CSharpCodeProviderwhich utilises the compiler to compile code on the fly. If you want to then run the code, you just need to use a bit of reflection to dynamically load the assembly and execute it.

这里最重要的类是CSharpCodeProvider利用编译器动态编译代码的类。如果你想然后运行代码,你只需要使用一点反射来动态加载程序集并执行它。

Hereis another example in C# that (although slightly less concise) additionally shows you precisely how to run the runtime-compiled code using the System.Reflectionnamespace.

是 C# 中的另一个示例(虽然稍微不那么简洁)另外向您展示了如何使用System.Reflection命名空间运行运行时编译的代码。

回答by Gary.Ray

To compile you could just initiate a shell call to the csc compiler. You may have a headache trying to keep your paths and switches straight but it certainly can be done.

要编译,您只需启动对 csc 编译器的 shell 调用。试图保持路径和开关笔直可能会让您头疼,但这当然可以做到。

C# Corner Shell Examples

C# 角壳示例

EDIT: Or better yet, use the CodeDOM as Noldorin suggested...

编辑:或者更好的是,按照诺多林的建议使用 CodeDOM...

回答by Brian Ensink

Others have already given good answers on how to generate code at runtime so I thought I would address your second paragraph. I have some experience with this and just want to share a lesson I learned from that experience.

其他人已经就如何在运行时生成代码给出了很好的答案,所以我想我会解决你的第二段。我在这方面有一些经验,只是想分享我从那次经历中学到的教训。

At the very least, I could define an interface that they would be required to implement, then they would provide a code 'section' that implemented that interface.

至少,我可以定义一个他们需要实现的接口,然后他们将提供一个实现该接口的代码“部分”。

You may have a problem if you use an interfaceas a base type. If you add a single new method to the interfacein the future all existing client-supplied classes that implement the interfacenow become abstract, meaning you won't be able to compile or instantiate the client-supplied class at runtime.

如果您使用 aninterface作为基本类型,您可能会遇到问题。如果interface将来向所有现有的客户端提供的实现interfacenow 的类添加一个新方法,则将变为抽象,这意味着您将无法在运行时编译或实例化客户端提供的类。

I had this issue when it came time to add a new method after about 1 year of shipping the old interface and after distributing a large amount of "legacy" data that needed to be supported. I ended up making a new interface that inherited from the old one but this approach made it harder to load and instantiate the client-supplied classes because I had to check which interface was available.

在发布旧接口大约 1 年后,在分发了大量需要支持的“遗留”数据之后,我在添加新方法时遇到了这个问题。我最终创建了一个继承自旧接口的新接口,但这种方法使得加载和实例化客户端提供的类变得更加困难,因为我必须检查哪个接口可用。

One solution I thought of at the time was to instead use an actual class as a base type such as the one below. The class itself can be marked abstract but all methods should be empty virtual methods (not abstract methods). Clients can then override the methods they want and I can add new methods to the base class without invalidating existing client-supplied code.

我当时想到的一种解决方案是使用实际的类作为基类型,如下所示。类本身可以标记为抽象,但所有方法都应该是空的虚拟方法(不是抽象方法)。然后客户端可以覆盖他们想要的方法,我可以向基类添加新方法而不会使现有的客户端提供的代码无效。

public abstract class BaseClass
{
    public virtual void Foo1() { }
    public virtual bool Foo2() { return false; }
    ...
}

Regardless of whether this problem applies you should consider how to version the interface between your code base and the client-supplied code.

无论此问题是否适用,您都应该考虑如何对代码库和客户端提供的代码之间的接口进行版本控制。

回答by tugberk

You can compile a piece C# of code into memory and generate assembly byteswith Roslyn. It's already mentioned but would be worth adding some Roslyn example for this here. The following is the complete example:

您可以将一段 C# 代码编译到内存中并使用 Roslyn生成程序集字节。已经提到过,但值得在这里添加一些 Roslyn 示例。以下是完整示例:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;

namespace RoslynCompileSample
{
    class Program
    {
        static void Main(string[] args)
        {
            // define source code, then parse it (to the type used for compilation)
            SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(@"
                using System;

                namespace RoslynCompileSample
                {
                    public class Writer
                    {
                        public void Write(string message)
                        {
                            Console.WriteLine(message);
                        }
                    }
                }");

            // define other necessary objects for compilation
            string assemblyName = Path.GetRandomFileName();
            MetadataReference[] references = new MetadataReference[]
            {
                MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
                MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location)
            };

            // analyse and generate IL code from syntax tree
            CSharpCompilation compilation = CSharpCompilation.Create(
                assemblyName,
                syntaxTrees: new[] { syntaxTree },
                references: references,
                options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

            using (var ms = new MemoryStream())
            {
                // write IL code into memory
                EmitResult result = compilation.Emit(ms);

                if (!result.Success)
                {
                    // handle exceptions
                    IEnumerable<Diagnostic> failures = result.Diagnostics.Where(diagnostic => 
                        diagnostic.IsWarningAsError || 
                        diagnostic.Severity == DiagnosticSeverity.Error);

                    foreach (Diagnostic diagnostic in failures)
                    {
                        Console.Error.WriteLine("{0}: {1}", diagnostic.Id, diagnostic.GetMessage());
                    }
                }
                else
                {
                    // load this 'virtual' DLL so that we can use
                    ms.Seek(0, SeekOrigin.Begin);
                    Assembly assembly = Assembly.Load(ms.ToArray());

                    // create instance of the desired class and call the desired function
                    Type type = assembly.GetType("RoslynCompileSample.Writer");
                    object obj = Activator.CreateInstance(type);
                    type.InvokeMember("Write",
                        BindingFlags.Default | BindingFlags.InvokeMethod,
                        null,
                        obj,
                        new object[] { "Hello World" });
                }
            }

            Console.ReadLine();
        }
    }
}

回答by Chris Moschini

Found this useful - ensures the compiled Assembly references everything you currently have referenced, since there's a good chance you wanted the C# you're compiling to use some classes etc in the code that's emitting this:

发现这很有用 - 确保编译后的程序集引用您当前引用的所有内容,因为您很有可能希望正在编译的 C# 在发出此内容的代码中使用某些类等:

        var refs = AppDomain.CurrentDomain.GetAssemblies();
        var refFiles = refs.Where(a => !a.IsDynamic).Select(a => a.Location).ToArray();
        var cSharp = (new Microsoft.CSharp.CSharpCodeProvider()).CreateCompiler();
        var compileParams = new System.CodeDom.Compiler.CompilerParameters(refFiles);
        compileParams.GenerateInMemory = true;
        compileParams.GenerateExecutable = false;

        var compilerResult = cSharp.CompileAssemblyFromSource(compileParams, code);
        var asm = compilerResult.CompiledAssembly;

In my case I was emitting a class, whose name was stored in a string, className, which had a single public static method named Get(), that returned with type StoryDataIds. Here's what calling that method looks like:

在我的例子中,我发出了一个类,它的名字存储在一个字符串中className,它有一个名为 的公共静态方法Get(),返回类型为StoryDataIds。这是调用该方法的样子:

        var tempType = asm.GetType(className);
        var ids = (StoryDataIds)tempType.GetMethod("Get").Invoke(null, null);

Warning: Compilation can be surprisingly, extremely slow. A small, relatively simple 10-line chunk of code compiles at normal priority in 2-10 seconds on our relatively fast server. You should never tie calls to CompileAssemblyFromSource()to anything with normal performance expectations, like a web request. Instead, proactively compile code you need on a low-priority thread and have a way of dealing with code that requires that code to be ready, until it's had a chance to finish compiling. For example you could use it in a batch job process.

警告:编译可能会令人惊讶,极其缓慢。一小段相对简单的 10 行代码块在我们相对较快的服务器上以正常优先级在 2-10 秒内编译。您永远不应该将调用CompileAssemblyFromSource()与具有正常性能预期的任何内容联系起来,例如 Web 请求。相反,在低优先级线程上主动编译您需要的代码,并有一种方法来处理需要准备好该代码的代码,直到它有机会完成编译。例如,您可以在批处理作业过程中使用它。

回答by dahall

I recently needed to spawn processes for unit testing. This post was useful as I created a simple class to do that with either code as a string or code from my project. To build this class, you'll need the ICSharpCode.Decompilerand Microsoft.CodeAnalysisNuGet packages. Here's the class:

我最近需要为单元测试生成进程。这篇文章很有用,因为我创建了一个简单的类来使用我的项目中的代码作为字符串或代码来执行此操作。要构建此类,您将需要ICSharpCode.DecompilerMicrosoft.CodeAnalysisNuGet 包。这是课程:

using ICSharpCode.Decompiler;
using ICSharpCode.Decompiler.CSharp;
using ICSharpCode.Decompiler.TypeSystem;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;

public static class CSharpRunner
{
   public static object Run(string snippet, IEnumerable<Assembly> references, string typeName, string methodName, params object[] args) =>
      Invoke(Compile(Parse(snippet), references), typeName, methodName, args);

   public static object Run(MethodInfo methodInfo, params object[] args)
   {
      var refs = methodInfo.DeclaringType.Assembly.GetReferencedAssemblies().Select(n => Assembly.Load(n));
      return Invoke(Compile(Decompile(methodInfo), refs), methodInfo.DeclaringType.FullName, methodInfo.Name, args);
   }

   private static Assembly Compile(SyntaxTree syntaxTree, IEnumerable<Assembly> references = null)
   {
      if (references is null) references = new[] { typeof(object).Assembly, typeof(Enumerable).Assembly };
      var mrefs = references.Select(a => MetadataReference.CreateFromFile(a.Location));
      var compilation = CSharpCompilation.Create(Path.GetRandomFileName(), new[] { syntaxTree }, mrefs, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

      using (var ms = new MemoryStream())
      {
         var result = compilation.Emit(ms);
         if (result.Success)
         {
            ms.Seek(0, SeekOrigin.Begin);
            return Assembly.Load(ms.ToArray());
         }
         else
         {
            throw new InvalidOperationException(string.Join("\n", result.Diagnostics.Where(diagnostic => diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error).Select(d => $"{d.Id}: {d.GetMessage()}")));
         }
      }
   }

   private static SyntaxTree Decompile(MethodInfo methodInfo)
   {
      var decompiler = new CSharpDecompiler(methodInfo.DeclaringType.Assembly.Location, new DecompilerSettings());
      var typeInfo = decompiler.TypeSystem.MainModule.Compilation.FindType(methodInfo.DeclaringType).GetDefinition();
      return Parse(decompiler.DecompileTypeAsString(typeInfo.FullTypeName));
   }

   private static object Invoke(Assembly assembly, string typeName, string methodName, object[] args)
   {
      var type = assembly.GetType(typeName);
      var obj = Activator.CreateInstance(type);
      return type.InvokeMember(methodName, BindingFlags.Default | BindingFlags.InvokeMethod, null, obj, args);
   }

   private static SyntaxTree Parse(string snippet) => CSharpSyntaxTree.ParseText(snippet);
}

To use it, call the Runmethods as below:

要使用它,请调用如下Run方法:

void Demo1()
{
   const string code = @"
   public class Runner
   {
      public void Run() { System.IO.File.AppendAllText(@""C:\Temp\NUnitTest.txt"", System.DateTime.Now.ToString(""o"") + ""\n""); }
   }";

   CSharpRunner.Run(code, null, "Runner", "Run");
}

void Demo2()
{
   CSharpRunner.Run(typeof(Runner).GetMethod("Run"));
}

public class Runner
{
   public void Run() { System.IO.File.AppendAllText(@"C:\Temp\NUnitTest.txt", System.DateTime.Now.ToString("o") + "\n"); }
}