«Мечта лентяя» или скриптовый движок на самом себе +11
Эту давнюю мечту можно было бы назвать «мечтой лентяя», если бы имеющиеся общедоступные встраиваемые скриптовые средства были бы просты. Готовые средства существовали давно, например на платформе Windows, ещё в прошлом веке можно было использовать интерфейсы VBScript и Jscript через COM-интерфейс IActiveScriptSite. В настоящее время существует большое количество и других решений, например на базе Lua, но все они имеют одну неприятную особенность, сильно ограничивающую желание их применять.
Скрипты прекрасно работают и сами по себе, на них можно выполнять и логику, и арифметику, но пользы от них ровным счётом никакой, если сложно или нет возможности:
• добавлять функции и объекты для доступа к объектам разрабатываемой системы,
• проводить синтаксический контроль исходного скрипта и генерировать сообщения о синтаксических ошибках,
• выполнять скрипт в пошаговом режиме, подобно отладчику, с нотификацией точки исполнения и статусом.
И ещё, хотелось бы, чтобы делалось всё это просто и интуитивно понятно и не приходилось бы проводить бессонные ночи за чтением многочисленной документации по новому API. Увы, это удаётся далеко не всегда и весьма нечасто.
Прикладное ПО сейчас очень часто пишется на C#, и хотелось бы иметь что-то знакомое, но гибкое, и позволяющее писать скрипты. Такое решение есть, и оно заслуживает пристального внимания. Это пространство имён System.CodeDom.Compiler с его классом CSharpCodeProvider. Всё это появилось ещё в .NET 4.0, но по какой-то причине в большинстве публикаций по C# не затрагивался вопрос написания скриптов на C#, используя сам же язык C# в качестве базового. А это очень и очень удобно для написания и дальнейшего сопровождения продукта.
В этом случае самый главный и интересный метод — CompileAssemblyFromSource(), который выполняет компиляцию, генерирует сообщения об ошибках, и мы уже запросто можем написать «Hello world!»
using System;
using System.IO;
using Microsoft.CSharp;
using System.CodeDom.Compiler;
using System.Reflection;
using System.Text;
namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            // готовим текст скрипта
            StringBuilder sb = new StringBuilder();
            sb.AppendLine("using System;");
            sb.AppendLine("namespace ConsoleApplication1");
            sb.AppendLine("{");
            sb.AppendLine("   public class MyScripter");
            sb.AppendLine("   {");
            sb.AppendLine("       public void Hello()");
            sb.AppendLine("       {");
            sb.AppendLine("             Console.WriteLine(\"Hello world!\");");
            sb.AppendLine("       }");
            sb.AppendLine("   }");
            sb.AppendLine("}");
            // компилируем
            CSharpCodeProvider codeProvider = new CSharpCodeProvider();
            CompilerResults compileResults = codeProvider.CompileAssemblyFromSource(
                       new CompilerParameters(), new string[] { sb.ToString() });
            // выводим ошибки, если они есть
            foreach (CompilerError err in compileResults.Errors)
                Console.WriteLine("Error({0:1}): {2} {3}", err.Line, err.Column, 
                                  err.ErrorNumber, err.ErrorText);
            if (compileResults.Errors.HasErrors) return;
            // загружаем получившуюся dll в память
            byte[] dllBytes = File.ReadAllBytes(compileResults.PathToAssembly);
            Assembly asmDll = Assembly.Load(dllBytes, null);
            Type objType = asmDll.GetType("ConsoleApplication1.MyScripter");
            // создаём объект класса из скрипта
            object oClassInst = Activator.CreateInstance(objType);
            // получаем точка входа и выполняем её
            MethodInfo entry = objType.GetMethod("Hello", new Type[] {});
            entry.Invoke(oClassInst, null);
        }
    }
}
Запускаем на исполнение:

Итак, в простейшем виде скрипт на C# успешно работает. Простым изменением текста скрипта мы можем влиять на его работу. Собственно, осталось лишь передать в скрипт в качестве примера какой-либо объект из основной программы.
В качестве такого объекта вполне подойдёт объект типа string:
using System;
using System.IO;
using Microsoft.CSharp;
using System.CodeDom.Compiler;
using System.Reflection;
using System.Text;
namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            string sMyStr = "Before Script.";
            // готовим текст скрипта
            StringBuilder sb = new StringBuilder();
            sb.AppendLine("using System;");
            sb.AppendLine("namespace ConsoleApplication1");
            sb.AppendLine("{");
            sb.AppendLine("   public class MyScripter");
            sb.AppendLine("   {");
            sb.AppendLine("       public void Hello(ref string s)");
            sb.AppendLine("       {");
            sb.AppendLine("             Console.WriteLine(\"Hello world!\");");
            sb.AppendLine("             s=\"After Script.\";");
            sb.AppendLine("       }");
            sb.AppendLine("   }");
            sb.AppendLine("}");
            // компилируем
            CSharpCodeProvider codeProvider = new CSharpCodeProvider();
            CompilerResults compileResults = codeProvider.CompileAssemblyFromSource(
                     new CompilerParameters(), new string[] { sb.ToString() });
            // выводим ошибки, если они есть
            foreach (CompilerError err in compileResults.Errors)
                Console.WriteLine("Error({0:1}): {2} {3}", err.Line, err.Column, 
                                   err.ErrorNumber, err.ErrorText);
            if (compileResults.Errors.HasErrors) return;
            // загружаем получившуюся dll в память
            byte[] dllBytes = File.ReadAllBytes(compileResults.PathToAssembly);
            Assembly asmDll = Assembly.Load(dllBytes, null);
            Type objType = asmDll.GetType("ConsoleApplication1.MyScripter");
            // создаём объект класса из скрипта
            object oClassInst = Activator.CreateInstance(objType);
            // получаем точка входа и готовим параметры
            MethodInfo entry = objType.GetMethod("Hello", 
                          new Type[] { typeof(string).MakeByRefType() });
            Object[] param = new Object[] { sMyStr };
            Console.WriteLine(param[0]);        // до выполнения скрипта
            
            entry.Invoke(oClassInst, param);    // вызов метода
            Console.WriteLine(param[0]);        // после выполнения скрипта
        }
    }
}
Запускаем на исполнение:

Видно, что теперь мы можем передавать и возвращать значения из кода скрипта. Если в качестве параметра передать не ссылку на строку, а какой-то внутренний объект информационной системы, то мы вполне можем воздействовать на систему из скрипта.
У данного механизма есть режим исполнения в режиме отладчика, для этого нужно подключать .pdb файл, есть и много других интересных возможностей.
Недостатком подхода можно считать только то, что при компиляции создаётcя dll во временном каталоге ОС.
Путь разрешения этого недостатка ведёт нас в сторону использования пространства System.Reflection.Emit, но это достаточно объёмный материал, подходящий для отдельной статьи. Это сложно, т. к. в данном случае компилятор и генерацию придется писать самостоятельно. Но зато какие возможности по придумыванию своего собственного синтаксиса! Да и назвать новый язык программирования можно в честь себя или любимой кошки.
Удачи!
Аркадий Пчелинцев, архитектор проектов
Комментарии (9)
 - molnij24.04.2017 10:44+8- Ох сколько вам открытий чудных… 
 Во-первых, если вы будете исполнять скрипты чуть больше чем hello world, вдруг выяснится отличие интерпретируемых от компилируемых языков — фаза компиляции ни разу не бесплатная. И если у интерпретируемых она естественным образом встроена в воркфлоу, то у шарпа вполне можно огрести заметные тормоза на ней.
 Потом обнаружится, что «гадить» в общий домен временными сборками скрипта — идея так себе, особенно если вспомнить, что удалить однажды загруженную сборку из домена невозможно (это слегка коррелирует с наличием временных файлов .dll, но проблема совсем в другом). А если делать по-науке и плодить домены, то придется изрядно позаниматься любовью с передачей нетривиальных параметров между доменами.
 А статей на эту тему достаточно. Даже на хабре поиск по CSharpCodeProvider выдает вполне адекватную подборку.- Sinatr25.04.2017 10:46- Хочется добавить, что решение проблемы (необходимость скриптовать некие сценарии) с помощью CodeDOM не совсем верно в общем случае (а случай в статье как раз общий, ни слова о конкретном сценарии). 
 
 Полезность примера выводящего «Hello world» в данном случае тоже близка с 0.
 
 Единственно что обьяснили — это как скормить исходник и получить список ошибок, а это, извините, на статью не тянет, тема не раскрыта вообще от слова «полностью».
 
 Я в свое время (эмм… десять лет назад) использовал CodeDOM для пре-компиляции формул (вызывался метод, тело которого пользователь писал на C#, методу передавался список входных/выходных параметров, которые можно было использовать в вычислениях и логике и возвращать результат), с появление Roslyn это конечно смешно (хотя уверен новичкам сгодилось), и тут эта статья…
 
 - kuda7824.04.2017 11:20+1- добавлять функции и объекты для доступа к объектам разрабатываемой системы 
 Необходимо реализовать у объекта интерфейс IDispatch. В C# с использованием рефлексии реализация доступа к методам и свойствам будет элементарен.
 
 - проводить синтаксический контроль исходного скрипта и генерировать сообщения о синтаксических ошибках 
 А чем этот момент не устроил в ActiveScriptSite? В ошибке будет указан и номер строки и что за ошибка.
 
 - выполнять скрипт в пошаговом режиме, подобно отладчику, с нотификацией точки исполнения и статусом. 
 С этим вопросом тоже все очень просто.
 https://docs.microsoft.com/en-us/scripting/winscript/active-script-debugging-overview
 - mxms24.04.2017 12:45- В этом отношении идеальный вариант это web-сервер на Forth, конфигурируемый Forth и со скриптингом на Forth. 
 
           
 

ColdPhoenix
простите, но вы, как минимум, на год опоздали.
Сейчас гораздо лучше брать Roslyn и интегрировать его.
EasyLy
Roslyn – хорошая штука, но требует .Net 4.5.2. Ниже он не соберётся. Да и великоват он
для простых задач.