# Урок 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>
