В прошлом, когда мне понадобился движок определяемых пользователем правил в .NET, я исследовал написание пользовательского предметно-ориентированного языка с помощью комплекта реализации языка Irony. Но в основном я использовал SpringFramework.NET, который включает в себя потрясающий движок оценки выражений. Механизм оценки выражений позволял писать правила на основе строк и даже встроенные функции, что позволяло создавать базовый, настраиваемый пользователем механизм правил в .NET без лишней суеты.

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

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

Хотя репозиторий SpringFramework.NET все еще показывает коммиты, библиотека, похоже, выдохлась, и мейнтейнер передал бразды правления сообществу.

Что делать, если в 2022 году с .NET 6 нужен настраиваемый пользователем механизм правил с поддержкой сценариев?

Введите Джинт.

Jint — это интерпретатор Javascript для .NET, который может работать на любой современной платформе .NET, поскольку он поддерживает цели .NET Standard 2.0 и .NET 4.6.1 (и вверх). Поскольку Jint не генерирует байт-код .NET и не использует DLR, он очень быстро выполняет относительно небольшие сценарии.

Проверив список поддерживаемых функций JavaScript, он на самом деле довольно богат, за исключением некоторых более продвинутых функций.

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

Начиная

Если вы хотите пропустить вперед, полный репозиторий находится здесь, на GitHub:



Для начала мы создадим простой интерфейс, который позволит пользователю пройти:

  1. Контекст или набор входных параметров,
  2. Сценарий, который может использовать этот контекст и манипулировать результирующим выходным ответом.

Когда пользователь нажимает EXECUTE, мы запускаем указанный пользователем Script, передавая контекст, а затем отображаем результат в Result.

Для контекста мы хотим передать объект JSON, который представляет наши входные данные для наших правил. Это может представлять некоторое текущее состояние внешнего интерфейса, некоторое JSON-представление сущности или другой контекст данных.

Например:

{ "firstName": "Charles", "lastName": "Chen" }

Мы можем написать простой скрипт для работы с этими данными:

let msg = "Hello, " + ctx.firstName + " " + ctx.lastName + "!";
res.message = msg;

Несколько замечаний:

  1. Мы можем объявлять такие переменные, как msg, и присваивать им значения.
  2. Я ссылаюсь на входной контекст как ctx, как в ctx.firstName и ctx.lastName
  3. Я назначаю сообщение свойству message объекта res

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

Выполнение внутреннего скрипта

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

В строке 11 request.Ctx объединяется со сценарием, чтобы сделать переменную ctx доступной для нашего сценария.

Затем в строке 16 ExpandoObject (кто придумал эти имена?) передается с именем res. Воспользовавшись преимуществом динамического типа .NET, мы можем получить поведение, подобное объекту JavaScript, что позволяет нам прикреплять произвольные свойства к объекту res во время выполнения. Сладкий!

Наконец, в строке 19 мы просто сериализуем ExpandoObject, переданный в движок, и получаем результат JSON.

Этот сверхпростой код теперь позволяет нам выполнять JavaScript на сервере от имени нашего пользователя! Если мы запустим это:

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

Добавление взаимодействия

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

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

Я также утверждаю, что в большинстве случаев для пользовательских скриптов и правил, возможно, в любом случае лучше ограничить возможности среды выполнения. (Мы рассмотрим, как ограничить время выполнения чуть позже).

А пока давайте представим механизм, позволяющий выполнять HTTP-запрос и получать длину содержимого ответа.

Для этого мы можем создать простой класс HttpPlugin на стороне .NET:

Этот класс имеет единственный метод, который просто делает запрос к указанному URL-адресу и возвращает заголовок длины содержимого ответа.

Чтобы сделать это доступным для движка Jint, мы просто регистрируем тип в движке Jint и присваиваем ему псевдоним в строках 17–19:

(Эту регистрацию можно было бы выполнить автоматически с помощью отражения, если бы мы хотели сделать это более динамично для большего количества подключаемых модулей)

Теперь, если мы изменим наш скрипт, чтобы создать экземпляр плагина и сделать вызов, мы сможем получить длину содержимого целевого URL-адреса:

Хороший!

При таком подходе мы можем передать предварительно настроенный HTTP-клиент, который может выполнять ряд действий, таких как настройка аутентификации и авторизации, ограничения на домены и URL-адреса и другие способы управления доступом. Мы можем заранее создать библиотеку действий, к которой автор сценария может подключиться и запустить контролируемым образом на сервере.

Мы также можем включать функции в наш скрипт и вызывать их. Посмотрите на эту функцию sayHi() в разделе Script, которая заменяет конкатенацию строк:

Это означает, что можно даже создать стандартный набор функций JavaScript, которые вы внедряете в скрипт (через конкатенацию) и позволяете авторам ваших скриптов иметь доступ к стандартному набору действий.

Очевидно, что если вы разрешаете пользователям вводить произвольный сценарий, вы захотите иметь возможность контролировать область выполнения с точки зрения ресурсов, таких как глубина выполнения, память и ресурсы ЦП. В документации Jint приведены примеры некоторых ограничений по умолчанию:

Также можно реализовать пользовательские ограничения (например, ограничения ЦП, ограничения по размеру и т. д.).

В то время как вычисление выражений в SpringFramework.NET хорошо помогало мне в прошлом, Jint открывает совершенно новый набор опций для создания механизма выполнения правил JavaScript в .NET.

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

Включить Jint в ваше решение .NET невероятно просто, а библиотека действительно элегантно спроектирована с точки зрения удобства использования. Использование этого подхода позволяет избежать ловушек и сложности выполнения произвольного JavaScript на сервере (например, вставлять его в контейнер Node), обеспечивая при этом гибкую, контролируемую среду выполнения для ваших пользовательских скриптов и предоставляя стандартные действия как через функции JavaScript, так и через взаимодействие с .NET.