Command

Шаблон кастомной команды

Шаблон описания кастомной команды в серверном xml-файле:

<Command Name="MyCustomCommand" Type="CustomCommand" Assembly="TemplateEngine">
  <!-- Элементы команды -->
</Command>

В качестве значения атрибута Assembly указывается имя кастомной сборки, в которой реализована команда. А в атрибуте Type указывается имя класса, описывающего команду.

На сервере команды разделены на две части: инициируемая часть и исполняемая часть. Инициируемая часть представляется классом AbstractCommand, а исполняемая часть - AbstractCommandExecutor. Инициируемая часть используется для парсинга xml-кода, а исполняемая часть - для выполнения бизнес-логики.

Инициируемая часть

Шаблон кода инициируемой части:

CustomCommand.cs
using Common;
using System.Xml;

namespace WorkflowEngine
{
    public class CustomCommand : AbstractCommand
    {
        /*
         * Набор свойств для хранения данных
         * public string PropertyName { get; private set; }
         */
         
        public override void Init(XmlNode node, IWorkflowType workflowType)
        {
            base.Init(node, workflowType);
            
            /*
             * Здесь должен быть парсинг xml-кода,
             * чтобы получить значения тэгов и их атрибутов.
             */
        }

        public override ICommandExecutor CreateExecutor()
        {
            return new CustomCommandExecutor(this);
        }
    }
}

Метод Init() принимает параметр XmlNode node, который соответствует тэгу <Command>, описанному в xml-файле формы. Из объекта node можем получить информацию обо всех вложенных тэгах и их атрибутах. Значения, полученные из xml, сохраняются в публичные свойства, доступ к которым будет у исполняемой части через прямую ссылку на экземпляр класса CustomCommand, переданную в конструктор класса CustomCommandExecutor.

Исполняемая часть

Пример кода исполняемой части:

CustomCommandExecutor.cs
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Data;
using System.Threading.Tasks;

namespace WorkflowEngine
{
    public class CustomCommandExecutor : AbstractCommandExecutor
    {
        private readonly CustomCommand _command;

        public CustomCommandExecutor(CustomCommand command) : base(command)
        {
            _command = command;

            /*
             * Через класс ServiceProvider можно получить дополнительные сервисы.
             * Например, объект типа ILogger для добавления записей в журнал событий
             * или объект типа IDatabaseConnection для выполнения SQL-запросов.
             */
        }

        public override async Task Execute(IParameterized parameters)
        {
            /*
             * Получение словаря из контекста параметров.
             * Этот словарь можно использовать для подстановки значений в SQL-запросы.
             */
            var parameterDictionary = parameters.ToDictionary();
                
            /*
             * Здесь должен быть код бизнес-логики: выполнение SQL-запросов и команд,
             * а так же обработка их результатов
             */
        }
    }
}

Метод Execute принимает параметр parameters типа IParameterized, хранящий контекст параметров, общий для всех команд. Из контекста можно получить словарь параметров, который можно использовать для подстановки значений в SQL-запросы вместо переменных.

Как это работает

При старте сервер разбирает серверный xml-файл и создает экземпляр класса CustomCommand для каждого тэга <Command> типа CustomCommand. Когда происходит вызов команды, первым делом выполняется метод CreateExecutor из класса CustomCommand, который создает экземпляр класса CustomCommandExecutor.

Элементы команды

SQL-запросы

Пример команды с указанием текста SQL-запроса:

<Command Name="MyCustomCommand" Type="CustomCommand" Assembly="TemplateEngine">
  <SqlQuery>
    <Text>
      SELECT
        'qwerty' AS "Field0",
        123 AS "Field1",
        false AS "Field2";
    </Text>
  </SqlQuery>
</Command>

Инициируемая часть

Для получения значения обязательного тэга в методе Init необходимо использовать статический метод GetRequiredElementValue класса XmlParser:

// public string SqlQueryText { get; private set; }

SqlQueryText = XmlParser.GetRequiredElementValue<string>(
    node, "SqlQuery/Text", Name, this);

Вторым параметром (string path) указывается полный путь до тэга, значение которого нужно получить. В третьем параметре (string name) указывается имя объекта, в котором происходит получение значения, в четвертом параметре (object targetObject) - класс объекта, в котором происходит получение значения. Имя объекта и его класс используются в формировании текста сообщения об ошибке. В качестве результата метод возвращает строку. Если элемент отсутствует, будет возвращено исключение типа InvalidXmlException.

Если необходимо получить значение необязательного элемента, то следует использовать метод GetElementValue. Этот метод в случае отсутствия элемента будет возвращать значение по умолчанию, переданное в параметрах метода.

Исполняемая часть

Если команда должна работать с SQL-запросами, то в конструкторе CustomCommandExecutor, используя статический метод GetRequiredService класса ServiceProvider, необходимо получить ссылки на сервисы обеспечивающие работу с базой данных.

Первый сервис - объект типа IDatabaseConnection, который устанавливает соединение с базой данных и выполняет SQL-запрос:

// private readonly IDatabaseConnection _connection;

_connection = ServiceProvider.GetRequiredService<IDatabaseConnection>();

Второй сервис - объект типа ISqlQueryHelper, который помогает построить текст запроса, заменив в нем переменные в круглых скобках {} на нужные значения из словаря параметров:

// private readonly ISqlQueryHelper _sqlQueryHelper;

_sqlQueryHelper = ServiceProvider.GetRequiredService<ISqlQueryHelper>();

Для выполнения SQL-запросов в методе Execute используем метод ExecuteQueryAsync у объекта типа IDatabaseConnection, результатом которого будет объект типа DataTable:

var data = await _connection.ExecuteQueryAsync(
    _sqlQueryHelper.GetQueryText(_command.SqlQueryText, parameterDictionary)
);

В метод передается текст SQL-запроса, в котором при необходимости заменяются переменные на значения параметров из словаря, для этого используется сервис ISqlQueryHelper.

Команды

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

<Command Name="MyCustomCommand" Type="CustomCommand" Assembly="TemplateEngine">
  <AnyCommand Name="AnySqlQueryCommand" />
</Command>

В примере рассматривается команда AnySqlQueryCommand типа SqlQueryCommand, но это может быть команда любого типа, в том числе и кастомного. Вызов команды AnySqlQueryCommand можно реализовать в xml-файле перед вызовом команды MyCustomCommand, либо перенести в исходный код кастомки.

Инициируемая часть

Для получения значения обязательного атрибута тэга в методе Init необходимо использовать статический метод GetRequiredAttributeValue класса XmlParser:

// public string AnyCommandName { get; private set; }

AnyCommandName = XmlParser.GetRequiredAttributeValue<string>(
    node, "AnyCommand", NAME_ATTRIBUTE);

Вторым параметром (string path) указывается полный путь до тэга, значение атрибута которого нужно получить. Имя атрибута указывается в третьем параметре (string attribute). В данном случае используется константа NAME_ATTRIBUTE, описанная в базовом классе. Если элемент или его атрибут отсутствует, будет возвращено исключение типа InvalidXmlException.

Если необходимо получить значение необязательного атрибута, то следует использовать метод GetAttributeValue. Этот метод в случае отсутствия элемента или его атрибута будет возвращать значение по умолчанию, переданное в параметрах метода.

Исполняемая часть

При работе с командой первым делом нужно получить ее Executor:

var executor = _command.WorkflowType.GetCommandExecutor(_command.AnyCommandName);

Через свойство WorkflowType нашей команды получаем текущий процесс, у которого с помощью метода GetCommandExecutor получаем Executor нужной команды по ее имени.

Результат выполнения команды храниться в свойстве Value:

var result = (DataTable)executor.Value;

Так как в примере команда AnyCommandName имеет тип SqlQueryCommand, то ее результат можно привести к типу DataTable, что облегчит его обработку.

Но, прежде чем использовать результат выполнения команды, проверяем значение свойства Value. И если там пусто, то выполняем команду, вызвав метод Execute и передав в него параметры:

if (executor.Value == null)
{
    await executor.Execute(parameters).ConfigureAwait(false);
}

Логирование

Для возможности добавления записей в журнал событий Windows необходимо получить сервис ILogger, для этого в конструкторе CustomCommandExecutor используем статический метод GetService класса ServiceProvider.

// private readonly ILogger<CustomCommand> _logger;

_logger = ServiceProvider.GetService<ILogger<CustomCommand>>();

Чтобы записать в журнал событий сообщение об ошибке, необходимо вызвать метод LogError:

_logger.LogError("Текст сообщения об ошибки.");

Есть разные уровни сообщений для записи в журнал событий:

  • LogCritical() - форматирует и записывает критическое сообщение журнала;

  • LogDebug() - форматирует и записывает в журнал сообщение отладки;

  • LogError() - форматирует и записывает в журнал сообщение об ошибке;

  • LogInformation() - форматирует и записывает в журнал информационное сообщение;

  • LogTrace() - форматирует и записывает в журнал сообщение трассировки;

  • LogWarning() - форматирует и записывает в журнал сообщение с предупреждением.

Last updated