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

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

Если хотите начать практику с этого урока, то вам необходимо развернуть учебный проект по инструкции в статье Разворачивание проекта.

При разворачивании проекта используйте backup базы данных, который можете найти в архиве из раздела Ответы прошлого урока. Скопируйте папки Forms, Workflow и Patterns в папку с развернутым проектом, например, в папку D:\WT\Projects\Template\Projects\1. Template.

Инструкция по подключению шаблонов находится по ссылке.

Подготовка

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

Создание проекта

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

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

В прошлом уроке мы подробно рассмотрели процесс создания нового приложения из шаблона Class library для .NET.

Сборка проекта

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

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

В поле 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:

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

Отладка проекта

Чтобы запускать сервер из Visual Studio напрямую, в свойствах проекта на вкладке Debug создайте профиль запуска проекта, как мы делали в прошлом уроке. В профиле укажем следующие настройки:

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

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

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

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

Кастомная команда

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

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

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

Template.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>

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

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

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

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

Подключение библиотеки

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

Создание классов

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

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

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

EmailSendCommand.cs
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);
        }
    }
}

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

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

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

EmailSendCommandExecutor.cs
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)
        {
            
        }
    }
}

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

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

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

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

HappyBirthdayEmailSendCommand
Template.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>

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

EmailSendCommand.cs
private const string TEXT_ELEMENT = "Text";
private const string EMAIL_SETTINGS_SQL_QUERY_ELEMENT = "EmailSettingsSqlQuery";
private const string MESSAGE_MAILING_SQL_QUERY_ELEMENT = "MessageMailingSqlQuery";

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

EmailSendCommand.cs
public string EmailSettingsSqlQueryText { get; private set; }
public string MessageMailingSqlQueryText { get; private set; }

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

EmailSendCommand.cs
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);

Значения тэгов <EmailSettingsSqlQuery> и <MessageMailingSqlQuery> по сути просто строки, поэтому для получения их значений используем метод GetRequiredElementValue статического класса XmlParser. Чтобы иметь возможность работать с методами класса XmlParser, добавьте в код using Common.

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

  • Первый параметр XmlNode node - это сам тэг <Command>;

  • Второй параметр string path - путь до нужного элемента. Так как нам нужно получить значение тэга <Text>, вложенного в тэг <EmailSettingsSqlQuery>, то в параметр передаем строку вида "EmailSettingsSqlQuery/Text";

  • Третий параметр string name - имя объекта, в котором происходит получение значения. Это имя будет указываться в текст сообщения об ошибке;

  • Последний параметр object targetObject - класс объекта, в котором происходит получение значения.

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

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

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

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

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

Полный код файла EmailSendCommand.cs
EmailSendCommand.cs
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);
        }
    }
}

Исполнитель

Перейдем в файл EmailSendCommandExecutor.cs.

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

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

EmailSendCommandExecutor.cs
private readonly IDatabaseConnection _connection;
private readonly ISqlQueryHelper _sqlQueryHelper;

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

EmailSendCommandExecutor.cs
_connection = ServiceProvider.GetRequiredService<IDatabaseConnection>();
_sqlQueryHelper = ServiceProvider.GetRequiredService<ISqlQueryHelper>();

Через ServiceProvider можно получить различные сервисы, например, контекст пользователя, в котором хранится информация о текущем пользователе, инициировавшем выполнение команды:

ServiceProvider.GetRequiredService(typeof(IUserContext))

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

EmailSendCommandExecutor.cs
var parameterDictionary = parameters.ToDictionary();

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

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

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

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

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

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

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

Логирование

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

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

EmailSendCommandExecutor.cs
private readonly ILogger<EmailSendCommand> _logger;

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

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

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

EmailSendCommandExecutor.cs
_logger = ServiceProvider.GetService<ILogger<EmailSendCommand>>();

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

EmailSendCommandExecutor.cs
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()}");
}

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

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

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

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

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

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

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

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

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

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

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

EmailSendCommandExecutor.cs
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
    };
}

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

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

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

EmailClient.cs
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;
        }
    }
}

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

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

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

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

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

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

EmailSendCommandExecutor.cs
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);
}

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

Полный код файла EmailSendCommandExecutor.cs
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
            };
        }
    }
}

Итоги

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

В приложении Серверная часть собраны статьи о написании кастомных команд и запросов.

Ответы

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

Last updated