Возможно ли динамически компилировать и выполнять фрагменты кода С#?

Мне было интересно, можно ли сохранить фрагменты кода С# в текстовый файл (или любой входной поток), а затем выполнить их динамически? Предполагая, что мне предоставлено, будет компилироваться в любом блоке Main(), можно ли скомпилировать и/или выполнить этот код? Я бы предпочел скомпилировать его по соображениям производительности.

По крайней мере, я мог бы определить интерфейс, который им потребуется реализовать, тогда они предоставят раздел кода, который реализовал этот интерфейс.

Ответ 1

Лучшим решением на С#/всех статических языках .NET является использование CodeDOM для таких вещей. (В качестве примечания, его другой основной целью является динамическое построение битов кода или даже целых классов.)

Вот хороший пример из LukeH blog, который использует некоторые 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));
    }
}

Класс первостепенной важности здесь - это CSharpCodeProvider, который использует компилятор для компиляции кода "на лету". Если вы хотите затем запустить код, вам просто нужно использовать немного отражения для динамической загрузки сборки и ее выполнения.

Вот еще один пример в С#, который (хотя и немного менее краткий) дополнительно показывает вам, как выполнять компилируемый во время выполнения код с помощью System.Reflection namespace.

Ответ 2

Вы можете скомпилировать фрагмент кода С# в память и сгенерировать байты сборки с 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)
        {
            SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(@"
                using System;

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

            string assemblyName = Path.GetRandomFileName();
            MetadataReference[] references = new MetadataReference[]
            {
                MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
                MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location)
            };

            CSharpCompilation compilation = CSharpCompilation.Create(
                assemblyName,
                syntaxTrees: new[] { syntaxTree },
                references: references,
                options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

            using (var ms = new MemoryStream())
            {
                EmitResult result = compilation.Emit(ms);

                if (!result.Success)
                {
                    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
                {
                    ms.Seek(0, SeekOrigin.Begin);
                    Assembly assembly = Assembly.Load(ms.ToArray());

                    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();
        }
    }
}

Ответ 3

Другие уже дали хорошие ответы о том, как сгенерировать код во время выполнения, поэтому я подумал, что рассмотрю ваш второй абзац. У меня есть некоторый опыт в этом и просто хочу поделиться уроком, который я узнал из этого опыта.

По крайней мере, я мог бы определить интерфейс, который им потребуется для реализации, тогда они раздел "код", который интерфейс.

У вас может возникнуть проблема, если вы используете interface как базовый тип. Если вы добавите новый метод к interface, в будущем все существующие классы, поставляемые клиентом, которые реализуют interface, теперь станут абстрактными, что означает, что вы не сможете компилировать или создавать экземпляр класса, поставляемого клиентом во время выполнения.

У меня была эта проблема, когда пришло время добавить новый метод примерно через год после отправки старого интерфейса и после распределения большого количества "старых" данных, которые необходимо было поддерживать. Я закончил создание нового интерфейса, унаследованного от старого, но этот подход усложнил загрузку и создание экземпляров классов, предоставленных клиентом, потому что мне нужно было проверить, какой интерфейс доступен.

Одно из решений, о котором я думал в то время, заключалось в том, чтобы вместо этого использовать фактический класс в качестве базового типа, например, ниже. Сам класс может быть помечен как абстрактный, но все методы должны быть пустыми виртуальными методами (а не абстрактными методами). Клиенты могут затем переопределить методы, которые они хотят, и я могу добавить новые методы в базовый класс без аннулирования существующего кода, предоставленного клиентом.

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

Независимо от того, применяется ли эта проблема, вы должны подумать о том, как изменить интерфейс между базой кода и кодом, поставляемым клиентом.

Ответ 4

Для компиляции вы можете просто инициировать вызов оболочки для компилятора csc. У вас может быть головная боль, пытающаяся сохранить ваши пути и переключатели прямо, но это, безусловно, можно сделать.

Примеры корневых оболочек С#

РЕДАКТИРОВАТЬ: Или еще лучше, используйте CodeDOM, как предложено Noldorin...

Ответ 5

Необходимо использовать пространство имен System.CodeDom.Compiler. См. в этой статье

Ответ 6

Обнаружено, что это полезно - гарантирует, что скомпилированная сборка ссылается на все, на что вы ссылаетесь в данный момент, так как есть хороший шанс, что вы хотите, чтобы С#, который вы компилируете, использовал некоторые классы и т.д. в коде, который испускает это:

        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;

В моем случае я испускал класс, чье имя хранилось в строке, className, у которого был один открытый статический метод с именем Get(), который возвращался с типом StoryDataIds. Вот как выглядит этот метод:

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

Ответ 7

Недавно мне нужно было порождать процессы для модульного тестирования. Этот пост был полезен, так как я создал простой класс, чтобы сделать это либо с кодом в виде строки, либо с кодом из моего проекта. Для создания этого класса вам понадобятся ICSharpCode.Decompiler и Microsoft.CodeAnalysis NuGet. Здесь класс:

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);
}

Чтобы использовать его, вызовите методы 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"); }
}