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

后端 未结 6 1738
伪装坚强ぢ
伪装坚强ぢ 2020-11-21 07:10

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 c

相关标签:
6条回答
  • 2020-11-21 07:19

    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.

    C# Corner Shell Examples

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

    0 讨论(0)
  • 2020-11-21 07:21

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

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

    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 CSharpCodeProvider which 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.

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

    0 讨论(0)
  • 2020-11-21 07:35

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

    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();
            }
        }
    }
    
    0 讨论(0)
  • 2020-11-21 07:36

    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.Decompiler and Microsoft.CodeAnalysis NuGet packages. Here's the class:

    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 Run methods as below:

    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"); }
    }
    
    0 讨论(0)
  • 2020-11-21 07:37

    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:

            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:

            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.

    0 讨论(0)
  • 2020-11-21 07:43

    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 interface as a base type. If you add a single new method to the interface in the future all existing client-supplied classes that implement the interface now become abstract, meaning you won't be able to compile or instantiate the client-supplied class at runtime.

    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.

    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.

    0 讨论(0)
提交回复
热议问题