Pull to refresh

Интеграция с «Госуслугами». Применение Workflow Core (часть II)

Reading time 11 min
Views 4.5K
В прошлый раз мы рассмотрели место СМЭВ в задаче интеграции с порталом «Госуслуг». Предоставляя унифицированный протокол общения между участниками, СМЭВ существенно облегчает взаимодействие между множеством различных ведомств и организаций, желающих предоставлять свои услуги с помощью портала.

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

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

Выбор движка автоматизации бизнес-процессов


Для организации процессной обработки данных существуют библиотеки и системы автоматизации бизнес-процессов, широко представленные на рынке: от встраиваемых решений до полнофункциональных систем, предоставляющих каркас для управления процессами. В качестве средства автоматизации бизнес-процессов мы выбрали Workflow Core. Такой выбор сделан по нескольким причинам: во-первых, движок написан на C# для платформы .NET Core (это наша основная платформа для разработки), поэтому включить его в общую канву продукта проще, в отличие от, например, Camunda BPM. Кроме того, это встраиваемый (embedded) движок, что даёт широкие возможности по управлению экземплярами бизнес-процессов. Во-вторых, среди множества поддерживаемых вариантов хранения данных есть и используемый в наших решениях PostgreSQL. В-третьих, движок предоставляет простой синтаксис для описания процесса в виде fluent API (также есть вариант описания процесса в JSON-файле, однако, он показался менее удобным для использования в силу того, что становится сложно обнаружить ошибку в описании процесса до момента его фактического выполнения).

Бизнес-процессы


Среди общепринятых инструментов описания бизнес-процессов следует отметить нотацию BPMN. Например, решение задачи FizzBuzz в нотации BPMN может выглядеть так:


Движок Workflow Core содержит большинство стандартных блоков и операторов, представленных в нотации, и, как уже говорилось выше, позволяет пользоваться fluent API или данными в формате JSON для описания конкретных процессов. Реализация этого процесса средствами движка Workflow Core может принять такой вид:

  // Класс с данными процесса.
  public class FizzBuzzWfData
  {
    public int Counter { get; set; } = 1;
    public StringBuilder Output { get; set; } = new StringBuilder();
  }

  // Описание процесса.
  public class FizzBuzzWorkflow : IWorkflow<FizzBuzzWfData>
  {
    public string Id => "FizzBuzz";
    public int Version => 1;

    public void Build(IWorkflowBuilder<FizzBuzzWfData> builder)
    {
      builder
        .StartWith(context => ExecutionResult.Next())
        .While(data => data.Counter <= 100)
          .Do(a => a
            .StartWith(context => ExecutionResult.Next())
              .Output((step, data) => data.Output.Append(data.Counter))
            .If(data => data.Counter % 3 == 0 || data.Counter % 5 == 0)
              .Do(b => b
                .StartWith(context => ExecutionResult.Next())
                  .Output((step, data) => data.Output.Clear())
                .If(data => data.Counter % 3 == 0)
                  .Do(c => c
                    .StartWith(context => ExecutionResult.Next())
                      .Output((step, data) =>
                        data.Output.Append("Fizz")))
                .If(data => data.Counter % 5 == 0)
                  .Do(c => c
                    .StartWith(context => ExecutionResult.Next())
                      .Output((step, data) =>
                        data.Output.Append("Buzz"))))
            .Then(context => ExecutionResult.Next())
              .Output((step, data) =>
              {
                Console.WriteLine(data.Output.ToString());
                data.Output.Clear();
                data.Counter++;
              }));
    }
  }
}

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

Метод Build получает в качестве аргумента объект построителя процесса, который служит отправной точкой для вызова цепочки методов расширения, последовательно описывающих шаги бизнес-процесса. Методы расширения, в свою очередь, могут содержать описание действий непосредственно в текущем коде в виде лямбда-выражений, переданных в качестве аргументов, а могут быть параметризованными. В первом случае, который представлен в листинге, простой алгоритм выливается в достаточно непростой набор инструкций. Во втором – логика шагов прячется в отдельных классах-наследниках от типа Step (или AsyncStep для асинхронных вариантов), что позволяет вместить сложные процессы в более лаконичное описание. На практике более пригодным представляется второй подход, первый же достаточен для простых примеров или предельно простых бизнес-процессов.

Собственно класс-описание процесса реализует параметризованный интерфейс IWorkflow, и, выполняя контракт, содержит идентификатор и номер версии процесса. Благодаря этим сведениям движок способен порождать экземпляры процессов в памяти, наполняя их данными и фиксируя их состояние в хранилище. Поддержка версионности позволяет создавать новые вариации процессов без риска затронуть существующие в хранилище экземпляры. Для создания новой версии достаточно создать копию имеющегося описания, присвоить очередной номер свойству Version и изменить нужным образом поведение этого процесса (идентификатор при этом следует оставить неизменным).

Примерами бизнес-процессов в контексте нашей задачи служат:

  • Опрос очереди сообщений СМЭВ с новыми заявлениями на получение услуг – обеспечивает периодический мониторинг очереди сообщений СМЭВ на предмет наличия новых заявлений от пользователей портала.
  • Обработка заявления – содержит шаги по анализу содержимого заявления, отражению его в ИАС, отправке подготовленных результатов на портал.
  • Обработка запроса на отмену заявления – позволяет отразить факт отмены поданного с портала заявления на услугу в ИАС.
  • Опрос очереди сообщений СМЭВ с ответами на запросы о смене статуса заявления – делает возможным изменять статус заявления на портале через ИАС, периодически контролируя очередь ответов портала на поданные запросы о смене статуса.

Как видно из примеров, все процессы условно подразделяются на “циклические”, выполнение которых предполагает периодическое повторение, и “линейные”, выполняемые в контексте конкретных заявлений и, впрочем, не исключающие наличия неких циклических конструкций внутри себя.

Рассмотрим пример одного из работающих в нашем решении процессов по опросу входящей очереди запросов:

public class LoadRequestWf : IWorkflow<LoadRequestWfData>
{
  public const string DefinitionId = "LoadRequest";

  public string Id => DefinitionId;
  public int Version => 1;

  public void Build(IWorkflowBuilder<LoadRequestWfData> builder)
  {
    builder
      .StartWith(then => ExecutionResult.Next())
        .While(d => !d.Quit)
          .Do(x => x
            .StartWith<LoadRequestStep>() // *
              .Output(d => d.LoadRequest_Output, s => s.Output)
            .If(d => d.LoadRequest_Output.Exception != null)
              .Do(then => then
                .StartWith(ctx => ExecutionResult.Next()) // *
                  .Output((s, d) => d.Quit = true))
            .If(d => d.LoadRequest_Output.Exception == null
                && d.LoadRequest_Output.Result.SmevReqType
                  == ReqType.Unknown)
              .Do(then => then
                .StartWith<LogInfoAboutFaultResponseStep>() // *
                  .Input((s, d) =>
                    { s.Input = d.LoadRequest_Output?.Result?.Fault; })
                  .Output((s, d) => d.Quit = false))
            .If(d => d.LoadRequest_Output.Exception == null
               && d.LoadRequest_Output.Result.SmevReqType
                 == ReqType.DataRequest)
              .Do(then => then
                .StartWith<StartWorkflowStep>() // *
                  .Input(s => s.Input, d => BuildEpguNewApplicationWfData(d))
                  .Output((s, d) => d.Quit = false))
            .If(d => d.LoadRequest_Output.Exception == null
          	    && d.LoadRequest_Output.Result.SmevReqType == ReqType.Empty)
              .Do(then => then
                .StartWith(ctx => ExecutionResult.Next()) // *
                  .Output((s, d) => d.Quit = true))
          .If(d => d.LoadRequest_Output.Exception == null
             && d.LoadRequest_Output.Result.SmevReqType
               == ReqType.CancellationRequest)
            .Do(then => then
              .StartWith<StartWorkflowStep>() // *
                .Input(s => s.Input, d => BuildCancelRequestWfData(d))
                .Output((s, d) => d.Quit = false)));
  }
}

В строках, отмеченных *, можно наблюдать использование параметризованных методов расширения, которые инструктируют движок о необходимости использовать классы шагов (об этом далее), соответствующие параметрам-типам. С помощью методов расширения Input и Output мы имеем возможность задавать исходные данные, передаваемые шагу перед началом выполнения, и, соответственно, изменить данные процесса (а они представлены экземпляром класса LoadRequestWfData) в связи с произведёнными шагом действиями. А так процесс выглядит на BPMN-диаграмме:


Шаги


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

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

Отправка подтверждающих (Ack) запросов о получении ответа.

  • Выгрузка файлов в файловое хранилище.
  • Извлечение данных из пакета СМЭВ и т.п.

Специфические шаги:

  • Создание объектов в ИАС, обеспечивающих возможность оператору предоставить услугу.
  • Генерирование документов заданной структуры с данными из заявления и размещение их в ИАС.
  • Отправка результата оказания услуги на портал и т.п.

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

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

Каждый шаг представляет собой связующее звено между кодом, содержащим высокоуровневое описание процесса, и кодом, решающим прикладные задачи – сервисами.

Сервисы


Сервисы представляют собой следующий, более низкий уровень решения задач. Каждый шаг при выполнении своей обязанности опирается, как правило, на один или более сервисов (N.B. Понятие “сервис” в данном контексте более близко́ к аналогичному понятию “сервис уровня приложения” из области предметно-ориентированного проектирования (DDD)).

Примерами сервисов служат:

  • Сервис получения ответа из очереди ответов СМЭВ – готовит соответствующий пакет данных в формате SOAP, отправляет его в СМЭВ и преобразует ответ в вид, пригодный для дальнейшей обработки.
  • Сервис загрузки файлов из хранилища СМЭВ – обеспечивает считывание файлов, приложенных к заявлению с портала, из файлового хранилища по протоколу FTP.
  • Сервис получения результата оказания услуги – считывает из ИАС данные о результатах услуги и формирует соответствующий объект, на основе которого другой сервис построит SOAP-запрос для отправки на портал.
  • Сервис выгрузки файлов, связанных с результатом оказания услуги, в файловое хранилище СМЭВ.

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

  • Сервисы СМЭВ.
  • Сервисы ИАС.

Сервисы для работы со внутренней инфраструктурой интеграционного решения (журналирование информации о пакетах данных, связывание сущностей интеграционного решения с объектами ИАС и пр.).

В архитектурном плане сервисы являются наиболее низким уровнем, однако, они при решении своих задач также могут опираться на утилитарные классы. Так, например, в решении существует пласт кода, решающий задачи сериализации и десериализации SOAP-пакетов данных для разных версий протокола СМЭВ. В общем виде приведённое выше описание можно резюмировать в диаграмме классов:


Непосредственно к движку относятся интерфейс IWorkflow и абстрактный класс StepBodyAsync (впрочем, можно использовать и его синхронный аналог StepBody). Ниже на схеме представлены реализации “строительных блоков” – конкретные классы с описаниями бизнес-процессов Workflow и используемые в них шаги (Step). На нижнем уровне представлены сервисы, которые, по существу, являются уже спецификой именно данной реализации решения и, в отличие от процессов и шагов не являются обязательными.

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

Встраивание движка в решение


На момент начала создания системы интеграции с порталом в репозитории Nuget была доступна версия движка 2.1.2. Он встраивается в контейнер зависимостей стандартным образом в методе ConfigureServices класса Startup:

public void ConfigureServices(IServiceCollection services)
{
  // ...
  services.AddWorkflow(opts =>
    opts.UsePostgreSQL(connectionString, false, false, schemaName));
  // ...
}

Движок можно настроить на одно из поддерживаемых хранилищ данных (среди таковых есть и другие: MySQL, MS SQL, SQLite, MongoDB). В случае PostgreSQL для работы с процессами движок использует Entity Framework Core в варианте Code First. Соответственно, при наличии пустой базы данных есть возможность применить миграцию и получить нужную структуру таблиц. Применение миграции является опциональным, этим можно управлять с помощью аргументов метода UsePostgreSQL: второй (canCreateDB) и третий (canMigrateDB) аргументы логического типа позволяют сообщить движку, может ли он создать БД при её отсутствии и применять миграции.

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

Итак, вопрос с хранением данных и регистрацией движка в контейнере зависимостей решён, перейдём к запуску движка. Для этой задачи подошёл вариант размещённой службы (hosted service, а здесь можно посмотреть пример базового класса для создания такой службы). Код, взятый за основу, был незначительно доработан для поддержания модульности, под которой понимается разделение интеграционного решения (получившего название “Оникс”) на общую часть, обеспечивающую инициализацию движка и выполнение некоторых служебных процедур, и часть специфическую для каждого конкретного заказчика (модули интеграции).

Каждый модуль содержит описания процессов, инфраструктуру для выполнения бизнес-логики, а также некоторый унифицированный код для предоставления возможности разработанной системе интеграции распознать и динамически загрузить описания процессов в экземпляр движка Workflow Core:



Регистрация и запуск бизнес-процессов


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

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

public async Task RunWorkflowsAsync(IWorkflowHost host,
  CancellationToken token)
{
  host.RegisterWorkflow<LoadRequestWf, LoadRequestWfData>();
  // Регистрируем другие процессы...

  await host.StartAsync(token);
  token.WaitHandle.WaitOne();
  host.Stop();
}

Заключение


В общих чертах мы рассмотрели действия, которые необходимо предпринять для использования Workflow Core в интеграционном решении. Движок позволяет описывать бизнес-процессы в достаточно гибкой и удобной манере. Держа в уме тот факт, что мы имеем дело с задачей интеграции с порталом “Госуслуг” посредством СМЭВ, следует ожидать, что проектируемые бизнес-процессы будут охватывать спектр довольно разнообразных задач (опрос очереди, загрузка/выгрузка файлов, гарантирование соблюдения протокола обмена и обеспечение подтверждения получения данных, обработка ошибок на разных этапах и т.п.). Отсюда вполне естественным будет ожидать возникновения некоторых на первый взгляд неочевидных моментов реализации, и именно им мы посвятим следующую, заключительную статью цикла.

Ссылки для изучения


Tags:
Hubs:
+4
Comments 10
Comments Comments 10

Articles