# Урок 23. Создание кастомных команд для серверной части

В уроке продолжим знакомиться с созданием кастомных библиотек для WT-приложений и создадим команду для отправки e-mail на стороне сервера.

{% hint style="success" %}
Если хотите начать практику с этого урока, то вам необходимо развернуть учебный проект по инструкции в статье [Разворачивание проекта](https://wfsys.gitbook.io/workflow-technology/setting-up-dev-environment/manual-deployment-project).

При разворачивании проекта используйте backup базы данных, который можете найти в архиве из раздела [Ответы](https://wfsys.gitbook.io/wt-practice/lesson_custom_command_on_form#answer) прошлого урока. Скопируйте папки *Forms*, *Workflow* и *Patterns* в папку с развернутым проектом, например, в папку *D:\WT\Projects\Template\Projects\1. Template*.

Инструкция по подключению шаблонов находится по [ссылке](https://wfsys.gitbook.io/wt-practice/main/lesson_list_form#podklyuchenie-shablonov-k-proektu).
{% endhint %}

## Подготовка <a href="#preparation" id="preparation"></a>

В этом уроке нам понадобятся бинарники, которые мы скачивали при разворачивании [серверной части](https://wfsys.gitbook.io/workflow-technology/setting-up-dev-environment/manual-deployment-project#server). В уроке так же будем работать с отдельным каталогом для штатных библиотек.

### Создание проекта <a href="#create-new-project" id="create-new-project"></a>

В папке **\Template\Projects\1. Template** создадим папку **Engine**, в которой будут храниться исходники кастомных объектов для серверной части.

Запустите Visual Studio 2022 и создайте новый проект из шаблона **Class library** для .NET или .NET Standart:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FzUetVL9jwHMjVxKi89EY%2Fimage.png?alt=media&#x26;token=cccb1487-7cf8-4beb-9500-9ab6fd4bb44d" alt=""><figcaption></figcaption></figure>

В[ прошлом уроке](https://wfsys.gitbook.io/wt-practice/customization/lesson_21#new_project) мы подробно рассмотрели процесс создания нового приложения из шаблона **Class library** для .NET.

### Сборка проекта <a href="#building-project" id="building-project"></a>

Откроем свойства проекта, вызвав контекстное меню и выбрав пункт **Properties**. Или нажав комбинацию клавиш **Alt+Enter**.

На вкладке **Build->Events** необходимо прописать команду, которая будет выполняться после сборки решения:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FjnYpv8I6ezzVkqM5AwbQ%2Fimage.png?alt=media&#x26;token=e4d26e68-2487-44aa-9a3f-8d88e21b8b8f" alt=""><figcaption></figcaption></figure>

В поле **Post-build event command line** пропишем команду копирования собранного dll-файла из папки проекта в папку, в которую установили серверную часть:

```
copy "$(ProjectDir)$(OutDir)$(TargetName).dll" "D:\WorkflowEngine\Template"
```

По умолчанию Visual Studio собирает проект в папку:

\Template\Projects\1. Template\Engine\TemplateEngine\TemplateEngine\bin\Debug\netcoreapp3.1

Давайте пересоберем проект, для этого в контекстном меню выберем пункт **Build** или **Rebuild**:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2F2OOXXjjcSs63IN64QYmX%2Fimage.png?alt=media&#x26;token=e9a01a62-85b4-4660-8b7a-2a6a15032abc" alt=""><figcaption></figcaption></figure>

Проверьте, что файл TemplateEngine.dll скопировался в папку с установленной серверной частью.

### Отладка проекта <a href="#debug" id="debug"></a>

Чтобы запускать сервер из Visual Studio напрямую, в свойствах проекта на вкладке **Debug** создайте профиль запуска проекта, как мы делали в [прошлом уроке](https://wfsys.gitbook.io/wt-practice/lesson_custom_command_on_form#setting-debug-mode). В профиле укажем следующие настройки:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FPrYdFPZojhBRyeKOnSUG%2Fimage.png?alt=media&#x26;token=c5819455-68dc-475e-af0e-ff35447fb740" alt=""><figcaption></figcaption></figure>

В поле **Executable** прописываем путь до exe-файла самого web-приложения, размещенного в папке развернутого сервера. В поле **Command line argument** прописываем `--console` аналогично аргументу из файла \_start.bat, который используем для запуска серверной части. А в поле **Working directory** укажем путь до папки, в которой развернут сервер учебного проекта.

Теперь мы можем запустить приложение, нажав клавишу **F5** или кнопку на панели инструментов:

![](https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FlILJ7iaYZMlG6j9IakmY%2Fimage.png?alt=media\&token=ab9654da-2c74-4f48-a180-dab7a367d09d)

Перед запуском проекта не забудьте остановить серверную часть приложения, если она у вас запущена.

В проекте появился новый файл **launchSettings.json**, в котором будут храниться настройки профилей запуска приложения:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2F74iIGnhQw70wqTQFjhmt%2Fimage.png?alt=media&#x26;token=0c522b94-74e0-4c87-b557-14b5b8cd9755" alt=""><figcaption></figcaption></figure>

## Кастомная команда <a href="#custom-command" id="custom-command"></a>

В этом уроке создадим кастомную команду для отправки e-mail на стороне сервера.

Первым делом необходимо описать саму команду, чтобы иметь представление, какие элементы нам понадобятся.

Добавим в серверный xml-файл тэг `<Commands>`, который можно разместить перед тэгом `<SqlQueries>`. Опишем в нем команду для отправки клиентам писем с поздравлением с днем рождения:

{% code title="Template.xml" %}

```xml
<Command Name="HappyBirthdayEmailSendCommand" Type="EmailSendCommand" Assembly="TemplateEngine">
  <EmailSettingsSqlQuery>
    <Text>
      SELECT
        smtp_server AS "SmtpServerAddress",
        smtp_port AS "SmtpServerPort",
        ssl AS "SSL",
	email_address AS "AuthorEmailAddress",
	email_password AS "AuthorEmailPassword"
      FROM template.settings
      LIMIT 1;
    </Text>
  </EmailSettingsSqlQuery>
  <MessageMailingSqlQuery>
    <Text>
      SELECT
        'С днём рождения!' AS "Subject",
        'Уважаемый(ая), ' || C.title || '! Поздравляем Вас с днем рождения!' AS "Text",
        C.email AS "Email"
      FROM
        template.client C
      WHERE
        C.email NOTNULL AND
        date_part('month'::text, C.date_of_birth) = date_part('month'::text, CURRENT_DATE) AND
        date_part('day'::text, C.date_of_birth) = date_part('day'::text, CURRENT_DATE);
    </Text>
  </MessageMailingSqlQuery>
</Command>
```

{% endcode %}

Здесь стоит обратить внимание на несколько моментов.

Во-первых, у тэга `<Command>` есть атрибут `Assembly`. В нем указывается имя библиотеки, в которой описан класс команды. Так как это кастомная команда, то в атрибуте указано имя кастомной сборки **TemplateEngine**, которую мы создали ранее.

Во-вторых, в атрибуте `Type` указывается имя класса, который описывает логику выполнения команды.

Для отправки писем нам понадобятся адреса клиентов и настройки почтового агента. Поэтому в команде описаны вложенные тэги `<EmailSettingsSqlQuery>` и `<MessageMailingSqlQuery>`, которые будут содержать sql-запросы на получение необходимых данных. Первый будет предоставлять настройки почтового агента, а второй будет получать из базы адреса клиентов и текст сообщений.

### Подключение библиотеки <a href="#connecting-library" id="connecting-library"></a>

Подключим необходимые библиотеки. Для этого в контекстном меню проекта выберем пункт **Add -> Project Reference...**, в открывшемся окне на вкладке **Browse**, где по кнопке **Browse...** откроем диалоговое окно выбора файла и в папке со штатными бинарниками серверной части найдем файлы WorkflowEngine.dll и Common.dll и добавим их.

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FIfAT5QEN9Yt3yqgxt3vh%2Fimage.png?alt=media&#x26;token=35946613-4894-4b59-abfb-2bf24f1becf0" alt=""><figcaption></figcaption></figure>

### Создание классов <a href="#creating-classes" id="creating-classes"></a>

Удалим файл Class1.cs и создадим папку Commands, в которую добавим новые классы EmailSendCommand и EmailSendCommandExecutor:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FQUVa2hWr8Qp4wzockRNP%2Fimage.png?alt=media&#x26;token=b0b28d17-a80c-4f40-ba32-81320a12d318" alt=""><figcaption></figcaption></figure>

При старте сервера для каждой команды из серверного xml-файла создается один экземпляр класса EmailSendCommand. А каждый раз при обращении к команде для ее выполнения создается экземпляр EmailSendCommandExecutor.

Базовый код кастомной команды EmailSendCommand.cs имеет вид:

{% code title="EmailSendCommand.cs" %}

```csharp
using System.Xml;

namespace WorkflowEngine
{
    public class EmailSendCommand : AbstractCommand
    {
        public override void Init(XmlNode node, IWorkflowType workflowType)
        {
            base.Init(node, workflowType);
        }

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

{% endcode %}

Метод `void Init(XmlNode, IWorkflowType)` будет выполняться в момент старта сервера и парсинга серверного xml файла. В качестве первого параметра XmlNode `node` метод принимает текст описания команды на xml, из которого мы и будем получать необходимые данные. Второй параметр метода IWorkflowType `workflowType` содержит процесс, в рамках которого запущен сервер.

Обратите внимание, что для кастомных команд в качестве `namespace` указывается **WorkflowEngine** - это позволяет обращаться к публичным классам штатных библиотек платформы без использования директивы `using` для импорта типов.

Базовый код класса исполняемой части имеет вид:

{% code title="EmailSendCommandExecutor.cs" %}

```csharp
using System.Threading.Tasks;

namespace WorkflowEngine
{
    public class EmailSendCommandExecutor : AbstractCommandExecutor
    {
        private readonly EmailSendCommand _command;

        public EmailSendCommandExecutor(EmailSendCommand command) : base(command)
        {
            _command = command;
        }

        public override async Task Execute(IParameterized parameters)
        {
            
        }
    }
}
```

{% endcode %}

В конструкторе сохраняем контекст команды. Метод `Task Execute(IParameterized)` вызывается в момент выполнения команды. В этом методе будет описываться вся бизнес-логика.

### Парсинг xml-кода

Перейдем в файл EmailSendCommand.cs и займемся реализацией метода инициализации команды.

Вспомним описание нашей команды из серверного xml-файла:

<details>

<summary>HappyBirthdayEmailSendCommand</summary>

{% code title="Template.xml" %}

```xml
<Command Name="HappyBirthdayEmailSendCommand" Type="EmailSendCommand" Assembly="TemplateEngine">
  <EmailSettingsSqlQuery>
    <Text>
      SELECT
        smtp_server AS "SmtpServerAddress",
        smtp_port AS "SmtpServerPort",
        ssl AS "SSL",
	email_address AS "AuthorEmailAddress",
	email_password AS "AuthorEmailPassword"
      FROM template.settings
      LIMIT 1;
    </Text>
  </EmailSettingsSqlQuery>
  <MessageMailingSqlQuery>
    <Text>
      SELECT
        'С днём рождения!' AS "Subject",
        'Уважаемый(ая), ' || C.title || '! Поздравляем Вас с днем рождения!' AS "Text",
        C.email AS "Email"
      FROM
        template.client C
      WHERE
        C.email NOTNULL AND
        date_part('month'::text, C.date_of_birth) = date_part('month'::text, CURRENT_DATE) AND
        date_part('day'::text, C.date_of_birth) = date_part('day'::text, CURRENT_DATE);
    </Text>
  </MessageMailingSqlQuery>
</Command>
```

{% endcode %}

</details>

Для начала создадим константы с именами вложенных тэгов и текстом сообщения об ошибке:

{% code title="EmailSendCommand.cs" %}

```csharp
private const string TEXT_ELEMENT = "Text";
private const string EMAIL_SETTINGS_SQL_QUERY_ELEMENT = "EmailSettingsSqlQuery";
private const string MESSAGE_MAILING_SQL_QUERY_ELEMENT = "MessageMailingSqlQuery";
```

{% endcode %}

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

{% code title="EmailSendCommand.cs" %}

```csharp
public string EmailSettingsSqlQueryText { get; private set; }
public string MessageMailingSqlQueryText { get; private set; }
```

{% endcode %}

Теперь можем в методе Init() прописать код для получения значений тэгов:

{% code title="EmailSendCommand.cs" %}

```csharp
EmailSettingsSqlQueryText = XmlParser.GetRequiredElementValue<string>(
    node, $"{EMAIL_SETTINGS_SQL_QUERY_ELEMENT}/{TEXT_ELEMENT}", Name, this);                

MessageMailingSqlQueryText = XmlParser.GetRequiredElementValue<string>(
    node, $"{MESSAGE_MAILING_SQL_QUERY_ELEMENT}/{TEXT_ELEMENT}", Name, this);
```

{% endcode %}

Значения тэгов `<EmailSettingsSqlQuery>` и `<MessageMailingSqlQuery>` по сути просто строки, поэтому для получения их значений используем метод [GetRequiredElementValue](https://wfsys.gitbook.io/wt-knowledge-base/customization-server/platform-classes/xmlparser/methods/get_required_element_value#get_required_element_value) статического класса [XmlParser](https://wfsys.gitbook.io/wt-knowledge-base/customization-server/platform-classes/xmlparser). Чтобы иметь возможность работать с методами класса XmlParser, добавьте в код `using Common`.

Метод GetRequiredElementValue принимает параметры:

* Первый параметр XmlNode `node` - это сам тэг `<Command>`;
* Второй параметр string `path` - путь до нужного элемента. Так как нам нужно получить значение тэга `<Text>`, вложенного в тэг `<EmailSettingsSqlQuery>`, то в параметр передаем строку вида "EmailSettingsSqlQuery/Text";
* Третий параметр string `name` - имя объекта, в котором происходит получение значения. Это имя будет указываться в текст сообщения об ошибке;
* Последний параметр object `targetObject` - класс объекта, в котором происходит получение значения.

Давайте поставим точку останова на строке вызова базового метода Init и запустим приложение для отладки:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FUTb1VGQaHFzxSNFPA4dT%2Fimage.png?alt=media&#x26;token=c13ef9a7-c7b8-478c-88d7-4b30264048c9" alt=""><figcaption></figcaption></figure>

Нажимая клавишу **F10**, вы можете пройтись по всем строкам метода и посмотреть, что находится в параметрах метода Init и в наших переменных.&#x20;

Если навести курсор на свойство **EmailSettingsSqlQueryText**, то увидим всплывающую подсказку с содержимым:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FwAjIbOAxEx33mi527BhI%2Fimage.png?alt=media&#x26;token=8581fff8-10c2-46c2-a826-8aa599cc3639" alt=""><figcaption></figcaption></figure>

Нажмите на значок лупы, чтобы посмотреть текст содержимого:

![](https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FxuauYipLenwv35g34PPM%2F01.png?alt=media\&token=917e9fb8-197a-45d6-8e31-28baf3c389a4)

Отлично! На этом работу с данным классом можно считать законченной. В дальнейшем будем возвращаться к нему только тогда, когда будет необходимость добавить новый элемент и передавать в него значение через xml-файл.

<details>

<summary>Полный код файла EmailSendCommand.cs</summary>

{% code title="EmailSendCommand.cs" %}

```csharp
using Common;
using System.Xml;

namespace WorkflowEngine
{
    public class EmailSendCommand : AbstractCommand
    {
        private const string TEXT_ELEMENT = "Text";
        private const string EMAIL_SETTINGS_SQL_QUERY_ELEMENT = "EmailSettingsSqlQuery";
        private const string MESSAGE_MAILING_SQL_QUERY_ELEMENT = "MessageMailingSqlQuery";

        public string EmailSettingsSqlQueryText { get; private set; }

        public string MessageMailingSqlQueryText { get; private set; }

        public override void Init(XmlNode node, IWorkflowType workflowType)
        {
            base.Init(node, workflowType);

            EmailSettingsSqlQueryText = XmlParser.GetRequiredElementValue<string>(
                node, $"{EMAIL_SETTINGS_SQL_QUERY_ELEMENT}/{TEXT_ELEMENT}", Name, this);                

            MessageMailingSqlQueryText = XmlParser.GetRequiredElementValue<string>(
                node, $"{MESSAGE_MAILING_SQL_QUERY_ELEMENT}/{TEXT_ELEMENT}", Name, this);
        }

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

{% endcode %}

</details>

### Исполнитель

Перейдем в файл EmailSendCommandExecutor.cs.&#x20;

Из xml мы получили тексты запросов. Теперь нам понадобятся объекты для подключения к базе данных, чтобы иметь возможность выполнять эти запросы в момент исполнения команды и получать актуальные данные.

Добавим соответствующие глобальные переменные:

{% code title="EmailSendCommandExecutor.cs" %}

```csharp
private readonly IDatabaseConnection _connection;
private readonly ISqlQueryHelper _sqlQueryHelper;
```

{% endcode %}

А в конструкторе `EmailSendCommandExecutor(EmailSendCommand)` пропишем строки:

{% code title="EmailSendCommandExecutor.cs" %}

```csharp
_connection = ServiceProvider.GetRequiredService<IDatabaseConnection>();
_sqlQueryHelper = ServiceProvider.GetRequiredService<ISqlQueryHelper>();
```

{% endcode %}

{% hint style="info" %}
Через ServiceProvider можно получить различные сервисы, например, контекст пользователя, в котором хранится информация о текущем пользователе, инициировавшем выполнение команды:

`ServiceProvider.GetRequiredService(typeof(IUserContext))`
{% endhint %}

Если у вас подсветились ошибки, это значит, что в проекте не подключены дополнительные библиотеки. Чтобы подключить их, необходимо через контекстное меню по пункту **Manage NuGet Packages...** открыть окно управления пакетами:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FIK6B67G2hT8WHWssuWsS%2Fimage.png?alt=media&#x26;token=e8aaf32d-f78a-423e-9d6d-a716d9744352" alt=""><figcaption></figcaption></figure>

На вкладке Installed можно увидеть, что нет ни одного установленного пакета:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FJa3sd0KzBVTyVQvVyoQl%2Fimage.png?alt=media&#x26;token=3be6579e-64fb-46fe-a007-d6943f687f49" alt=""><figcaption></figcaption></figure>

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

![](https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FvbymqIAAYfLog1AW760D%2F02.png?alt=media\&token=9eb4ad8e-28f6-42ed-a890-5f58df9dc715)

В поисковой строке введем имя пакета **Microsoft.Extensions.DependencyInjection** и в списке ниже выберем нужный пакет. В правой части окна нужно выбрать версию пакета. В платформе используется версия 3.1.2 - ее и нужно устанавливать. Нажмите на кнопку Install, чтобы запустить мастер установки пакета.

После успешной установки вернемся в файл EmailSendCommandExecutor.cs. Поставим курсор на место подсвеченной ошибки и нажмем комбинацию клавиш **Alt+Enter**, чтобы вызвать контекстное меню:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FzYb2Cm659CmTtsX2Bq5q%2Fimage.png?alt=media&#x26;token=c9309b56-02ab-48f8-a7b0-ae7c954fefd6" alt=""><figcaption></figcaption></figure>

Нажмем клавишу Enter, чтобы применился выбранный вариант замены, который добавит в файл директиву `using` с загрузкой необходимого пространства имен.

Прежде чем приступить к реализации бизнес-логики команды, необходимо обеспечить вызов команды на сервере, чтобы иметь возможность отладки кода. Для этого воспользуемся [`<Scheduler>`](https://wfsys.gitbook.io/workflow-engine-syntax/workflow_engine/sheduler), который по расписанию будет вызывать нашу команду.

Скопируйте код и добавьте его в серверный xml-файл перед описанием тэга `<Commands>`:

{% code title="Template.xml" %}

```xml
<Scheduler>
  <Task Name="HappyBirthdayEmailSendTask">
    <Condition Type="OnStart" />
    <Commands>
      <Command Name="HappyBirthdayEmailSendCommand" />
    </Commands>
  </Task>
</Scheduler>
```

{% endcode %}

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

Тэг `<Task>` представляет конкретную задачу, которая будет выполнять последовательность команд, описанную в тэге `<Commands>` по расписанию или при старте сервера. В нашем случае задача будет выполняться при старте сервера, о чем говорит значение атрибута `Type` тэга `<Condition>`.

Теперь можем перейти к реализации метода `Task Execute(IParameterized)`.

В качестве входного параметра метод принимает контекст параметров, которые мы можем передавать в команду при ее вызове в xml-файле и использовать в тексте sql-запросов:

```xml
<Command Name="HappyBirthdayEmailSendCommand">
  <Parameter Name=""></Parameter>
</Command>
```

Чтобы иметь возможность работать с такими параметрами, как с привычным словарем, добавим в метод Execute строку:

{% code title="EmailSendCommandExecutor.cs" %}

```csharp
var parameterDictionary = parameters.ToDictionary();
```

{% endcode %}

### Обращение к базе данных

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

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

Подключим к проекту библиотеку **Exceptions.dll**:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FL1gyNfeo28xABQSMP6jg%2Fimage.png?alt=media&#x26;token=ea08b388-4ac4-4da7-ba01-42772e7c4244" alt=""><figcaption></figcaption></figure>

Добавим в метод Execute код на выполнения запроса на получение настроек:

{% code title="EmailSendCommandExecutor.cs" %}

```csharp
var settingsData = await _connection.ExecuteQueryAsync(_sqlQueryHelper.GetQueryText(_command.EmailSettingsSqlQueryText, parameterDictionary));
if (settingsData.Rows.Count == 0)
{
    throw new InvalidXmlException($"Запрос на получение настроек подключения SMTP сервера не вернул ни одной строки в команде \"{_command.Name}\" типа {GetType().Name}.");
}
```

{% endcode %}

В метод GetQueryText передаем текст запроса и словарь параметров. Метод сам произведет подстановку значений вместо переменных.

### Логирование

Ошибку InvalidXmlException, которую мы генерируем, необходимо отлавливать и писать в журнал событий.

Для логирования ошибок необходимо получить соответствующий сервис. Добавим глобальную переменную:

{% code title="EmailSendCommandExecutor.cs" %}

```csharp
private readonly ILogger<EmailSendCommand> _logger;
```

{% endcode %}

Мы видим, что подсвечивается ошибка. Поставим курсор и нажмем комбинацию клавиш **Alt+Enter**. В контекстном меню выберем пункт **Install package** и затем пункт **Install with package manager...**:

![](https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FjFi9DOvneb3Q6mkhf4cI%2Fimage.png?alt=media\&token=6670c88a-84da-4a8d-b50f-d7318d8db9f4)

В открывшейся вкладке выберем нужный пакет и версию пакета 3.1.2:

![](https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FWQSg2xVH7tmgD73cSv6P%2F04.png?alt=media\&token=1c7b0d97-8d93-4563-9b48-03140836f9a7)

Теперь в конструктор класса добавим строку:

{% code title="EmailSendCommandExecutor.cs" %}

```csharp
_logger = ServiceProvider.GetService<ILogger<EmailSendCommand>>();
```

{% endcode %}

И обернем код в конструкцию **try-catch**, чтобы отлавливать ошибки при выполнении запросов и обработки данных:

{% code title="EmailSendCommandExecutor.cs" %}

```csharp
try
{
    var settingsData = await _connection.ExecuteQueryAsync(_sqlQueryHelper.GetQueryText(_command.EmailSettingsSqlQueryText, parameterDictionary));
    if (settingsData.Rows.Count == 0)
    {
        throw new InvalidXmlException($"Запрос на получение настроек подключения SMTP сервера не вернул ни одной строки в команде \"{_command.Name}\" типа {GetType().Name}.");
    }
}
catch (Exception e)
{
    _logger.LogError(message: $"Ошибка выполнения команды отправки сообщений:\r\n{e.ToPrettyString()}");
}
```

{% endcode %}

В блоке **catch** с помощью метода LogError будет писать в журнал событий сообщение об ошибке.&#x20;

{% hint style="info" %}
Есть разные уровни сообщений для логирования в журнале событий:

* LogCritical - форматирует и записывает критическое сообщение журнала;
* LogDebug - форматирует и записывает в журнал сообщение отладки;
* LogError - форматирует и записывает в журнал сообщение об ошибке;
* LogInformation - форматирует и записывает в журнал информационное сообщение;
* LogTrace - форматирует и записывает в журнал сообщение трассировки;
* LogWarning - форматирует и записывает в журнал сообщение с предупреждением.
  {% endhint %}

### Обработка результата sql-запроса

Метод ExecuteQueryAsync возвращает объект типа DataTable, который имеет массив строк. Но так как у нас только одна запись в таблице, то достаточно получать первую строку из этого массива и из значений ее ячеек формировать словарь для хранения настроек.

Создадим метод для разбора строки:

{% code title="EmailSendCommandExecutor.cs" %}

```csharp
private Dictionary<string, object> GetSettings(DataRow row)
{
    return new Dictionary<string, object>()
    {
        ["SmtpServerAddress"] = row["SmtpServerAddress"] is string smtpAddress ? smtpAddress : throw new Exception($"Не задан адрес SMTP в команде \"{_command.Name}\" типа {GetType().Name}."),
        ["SmtpServerPort"] = row["SmtpServerPort"] is int smptPort ? smptPort : throw new Exception($"Не корректно задан порт SMTP в команде \"{_command.Name}\" типа {GetType().Name}."),
        ["AuthorEmailAddress"] = row["AuthorEmailAddress"] is string authorEmail ? authorEmail : throw new Exception($"Не задан почтовый адрес отправителя в команде \"{_command.Name}\" типа {GetType().Name}."),
        ["AuthorEmailPassword"] = row["AuthorEmailPassword"] is string authorPassword ? authorPassword : throw new Exception($"Не задан пароль почтового адреса отправителя в команде \"{_command.Name}\" типа {GetType().Name}."),
        ["AuthorName"] = string.Empty,
        ["SSL"] = row["SSL"] is bool ssl ? ssl : throw new Exception($"Не корректно указан признак шифрования SSL в команде \"{_command.Name}\" типа {GetType().Name}."),
        ["Timeout"] = 1000
    };
}
```

{% endcode %}

Добавим в метод Execute вызов этого метода:

{% code title="EmailSendCommandExecutor.cs" %}

```csharp
var settings = GetSettings(settingsData.Rows[0]);
```

{% endcode %}

Создайте файл EmailClient.cs и скопируйте в него код:

<details>

<summary>EmailClient.cs</summary>

```csharp
using System.Net;
using System.Net.Mail;

namespace WorkflowEngine
{
    public class EmailClient
    {
        public static SmtpClient GetSmtpClient(string smtpServerAddress, int smtpServerPort, string authorEmailAddress, string authorEmailPassword, int timeout, bool enableSsl)
        {
            return new SmtpClient(smtpServerAddress, smtpServerPort)
            {
                EnableSsl = enableSsl,
                DeliveryMethod = SmtpDeliveryMethod.Network,
                UseDefaultCredentials = false,
                Timeout = timeout,
                Credentials = new NetworkCredential(authorEmailAddress, authorEmailPassword)
            };
        }

        public static MailMessage CreateMailMessage(string fromEmailAddress, string fromName, string toEmailAddress, string toSubject, string toText)
        {
            var mailMessage = new MailMessage
            {
                From = new MailAddress(fromEmailAddress, fromName),
                Subject = toSubject,
                Body = toText
            };

            if (!string.IsNullOrEmpty(toEmailAddress))
            {
                mailMessage.To.Add(new MailAddress(toEmailAddress));
                mailMessage.Bcc.Add(new MailAddress(toEmailAddress));
            }

            return mailMessage;
        }
    }
}
```

</details>

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

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

{% code title="EmailSendCommandExecutor.cs" %}

```csharp
var smtpClient = EmailClient.GetSmtpClient((string)settings["SmtpServerAddress"],
                                           (int)settings["SmtpServerPort"],
                                           (string)settings["AuthorEmailAddress"],
                                           (string)settings["AuthorEmailPassword"],
                                           (int)settings["Timeout"],
                                           (bool)settings["SSL"]);
```

{% endcode %}

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

{% code title="EmailSendCommandExecutor.cs" %}

```csharp
var messageMailingData = await _connection.ExecuteQueryAsync(_sqlQueryHelper.GetQueryText(_command.MessageMailingSqlQueryText, parameterDictionary));
```

{% endcode %}

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

{% code title="EmailSendCommandExecutor.cs" %}

```csharp
var emailClient = string.Empty;

foreach (DataRow sendInfoRow in messageMailingData.Rows)
{
    emailClient = sendInfoRow["Email"] != null ? (string)sendInfoRow["Email"] : throw new Exception($"Не задан почтовый адрес адресата в команде \"{_command.MessageMailingSqlQueryText}\" типа {GetType().Name}.");
    var subject = sendInfoRow["Subject"] is string sub ? sub : string.Empty;
    var text = sendInfoRow["Text"] is string txt ? txt : string.Empty;

    var mailMessage = EmailClient.CreateMailMessage((string)settings["AuthorEmailAddress"],
                                                    (string)settings["AuthorName"],
                                                    emailClient,
                                                    subject,
                                                    text);

    await smtpClient.SendMailAsync(mailMessage);
}
```

{% endcode %}

Запустите проект из студии и проверьте, что письма пришли на вашу тестовую почту.

<details>

<summary>Полный код файла EmailSendCommandExecutor.cs</summary>

```csharp
using Common.Extensions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Data;
using System.Threading.Tasks;

namespace WorkflowEngine
{
    public class EmailSendCommandExecutor : AbstractCommandExecutor
    {
        private readonly EmailSendCommand _command;
        private readonly IDatabaseConnection _connection;
        private readonly ISqlQueryHelper _sqlQueryHelper;
        private readonly ILogger<EmailSendCommand> _logger;

        public EmailSendCommandExecutor(EmailSendCommand command) : base(command)
        {
            _connection = ServiceProvider.GetRequiredService<IDatabaseConnection>();
            _sqlQueryHelper = ServiceProvider.GetRequiredService<ISqlQueryHelper>();
            _logger = ServiceProvider.GetService<ILogger<EmailSendCommand>>();

            _command = command;
        }

        public override async Task Execute(IParameterized parameters)
        {
            var parameterDictionary = parameters.ToDictionary();

            try
            {
                var settingsData = await _connection.ExecuteQueryAsync(_sqlQueryHelper.GetQueryText(_command.EmailSettingsSqlQueryText, parameterDictionary));
                if (settingsData.Rows.Count == 0)
                {
                    throw new InvalidXmlException($"Запрос на получение настроек подключения SMTP сервера не вернул ни одной строки в команде \"{_command.Name}\" типа {GetType().Name}.");
                }

                var settings = GetSettings(settingsData.Rows[0]);

                var smtpClient = EmailClient.GetSmtpClient((string)settings["SmtpServerAddress"],
                                           (int)settings["SmtpServerPort"],
                                           (string)settings["AuthorEmailAddress"],
                                           (string)settings["AuthorEmailPassword"],
                                           (int)settings["Timeout"],
                                           (bool)settings["SSL"]);

                var messageMailingData = await _connection.ExecuteQueryAsync(_sqlQueryHelper.GetQueryText(_command.MessageMailingSqlQueryText, parameterDictionary));

                var emailClient = string.Empty;

                foreach (DataRow sendInfoRow in messageMailingData.Rows)
                {
                    emailClient = sendInfoRow["Email"] != null ? (string)sendInfoRow["Email"] : throw new Exception($"Не задан почтовый адрес адресата в команде \"{_command.MessageMailingSqlQueryText}\" типа {GetType().Name}.");
                    var subject = sendInfoRow["Subject"] is string sub ? sub : string.Empty;
                    var text = sendInfoRow["Text"] is string txt ? txt : string.Empty;

                    var mailMessage = EmailClient.CreateMailMessage((string)settings["AuthorEmailAddress"],
                                                                    (string)settings["AuthorName"],
                                                                    emailClient,
                                                                    subject,
                                                                    text);

                    await smtpClient.SendMailAsync(mailMessage);
                }
            }
            catch (Exception e)
            {
                _logger.LogError(message: $"Ошибка выполнения команды отправки сообщений:\r\n{e.ToPrettyString()}");
            }
        }

        private Dictionary<string, object> GetSettings(DataRow row)
        {
            return new Dictionary<string, object>()
            {
                ["SmtpServerAddress"] = row["SmtpServerAddress"] is string smtpAddress ? smtpAddress : throw new Exception($"Не задан адрес SMTP в команде \"{_command.Name}\" типа {GetType().Name}."),
                ["SmtpServerPort"] = row["SmtpServerPort"] is int smptPort ? smptPort : throw new Exception($"Не корректно задан порт SMTP в команде \"{_command.Name}\" типа {GetType().Name}."),
                ["AuthorEmailAddress"] = row["AuthorEmailAddress"] is string authorEmail ? authorEmail : throw new Exception($"Не задан почтовый адрес отправителя в команде \"{_command.Name}\" типа {GetType().Name}."),
                ["AuthorEmailPassword"] = row["AuthorEmailPassword"] is string authorPassword ? authorPassword : throw new Exception($"Не задан пароль почтового адреса отправителя в команде \"{_command.Name}\" типа {GetType().Name}."),
                ["AuthorName"] = string.Empty,
                ["SSL"] = row["SSL"] is bool ssl ? ssl : throw new Exception($"Не корректно указан признак шифрования SSL в команде \"{_command.Name}\" типа {GetType().Name}."),
                ["Timeout"] = 1000
            };
        }
    }
}
```

</details>

## Итоги <a href="#results" id="results"></a>

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

В приложении [Серверная часть](https://wfsys.gitbook.io/wt_knowledge_base/customization/server) собраны статьи о написании кастомных команд и запросов.

## Ответы <a href="#answer" id="answer"></a>

В архиве присутствуют xml-файлы форм и серверный xml-файл, исходный код кастомки TemplateEngine, а также бэкап базы данных - с помощью файлов можете проверить себя.

<table data-card-size="large" data-view="cards"><thead><tr><th></th><th data-hidden data-type="content-ref"></th></tr></thead><tbody><tr><td>lesson23-answer.zip</td><td><a href="https://wfsys.ru/download/wt_practice_desktop_answers/lesson23-answer.zip">https://wfsys.ru/download/wt_practice_desktop_answers/lesson23-answer.zip</a></td></tr></tbody></table>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://wfsys.gitbook.io/wt-practice/customization/lesson_custom_command_on_server.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
