O serviço pode ser considerado um processo distribuído no tempo, que possui vários pontos através dos quais é possível influenciar o seu resultado (cancelar no portal, recusar no departamento, enviar informação sobre a alteração do estado do serviço para o portal, e também enviar o resultado da sua prestação). Nesse sentido, cada serviço passa por seu próprio ciclo de vida por meio desse processo, acumulando dados sobre a solicitação do usuário, os erros recebidos, os resultados do serviço, etc. Isso permite a qualquer momento ser capaz de controlar e tomar uma decisão sobre outras ações para processar o serviço.
Falaremos mais sobre como e com que ajuda você pode organizar esse processamento.
Escolhendo um mecanismo de automação de processos de negócios
Para organizar o processamento de dados, existem bibliotecas e sistemas para automatizar processos de negócios, amplamenteno mercado: de soluções embarcadas a sistemas completos que fornecem uma estrutura para controle de processos. Escolhemos o Workflow Core como uma ferramenta para automatizar processos de negócios. Esta escolha foi feita por vários motivos: em primeiro lugar, o motor é escrito em C # para a plataforma .NET Core (esta é a nossa principal plataforma de desenvolvimento), por isso é mais fácil incluí-lo no esboço geral do produto, ao contrário, por exemplo, do Camunda BPM. Além disso, é um mecanismo integrado, que oferece amplas oportunidades para o gerenciamento de instâncias de processos de negócios. Em segundo lugar, entre as muitas opções de armazenamento suportadas, também existe o PostgreSQL usado em nossas soluções. Em terceiro lugar, o mecanismo fornece uma sintaxe simples para descrever um processo na forma de uma API fluente (há também uma variante de descrever um processo em um arquivo JSON, no entanto,parecia menos conveniente de usar devido ao fato de se tornar difícil detectar um erro na descrição do processo antes de sua execução real).
Processos de negócios
Entre as ferramentas geralmente aceitas para descrever processos de negócios, a notação BPMN deve ser observada . Por exemplo, a solução para o problema FizzBuzz na notação BPMN pode ser assim:

O mecanismo Workflow Core contém a maioria dos blocos de construção e instruções apresentadas na notação e, conforme mencionado acima, permite que você use a API fluente ou dados JSON para descrever processos específicos. A implementação deste processo por meio do motor Workflow Core pode assumir a seguinte forma :
// .
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++;
}));
}
}
}
Obviamente, o processo pode ser descrito de forma mais simples adicionando a saída dos valores desejados nas etapas seguintes às verificações de cardinalidade. No entanto, com a implementação atual, você pode ver que cada etapa é capaz de fazer algumas alterações no "cofrinho" geral dos dados do processo, e também pode aproveitar os resultados das etapas anteriores. Neste caso, os dados do processo são armazenados em uma instância
FizzBuzzWfData
, cujo acesso é fornecido a cada etapa no momento de sua execução.
Método
Build
usa um objeto de construtor de processo como um argumento, que serve como um ponto de partida para chamar uma cadeia de métodos de extensão que descreve sequencialmente as etapas de um processo de negócios. Os métodos de extensão, por sua vez, podem conter uma descrição das ações diretamente no código atual na forma de expressões lambda passadas como argumentos, ou podem ser parametrizados. No primeiro caso, que é apresentado na listagem, um algoritmo simples se traduz em um conjunto bastante complexo de instruções. No segundo, a lógica das etapas está oculta em classes separadas que herdam do tipo Step
(ou AsyncStep
para variantes assíncronas), o que permite ajustar processos complexos em uma descrição mais concisa. Na prática, a segunda abordagem parece ser mais adequada, a primeira é suficiente para exemplos simples ou processos de negócios extremamente simples.
A classe de descrição do processo real implementa a interface parametrizada
IWorkflow
e, executando o contrato, contém o identificador do processo e o número da versão. Graças a essas informações, o mecanismo é capaz de gerar instâncias de processo na memória, enchendo-as de dados e fixando seu estado no armazenamento. O suporte ao controle de versão permite criar novas variações de processo sem o risco de afetar as instâncias existentes no repositório. Para criar uma nova versão, basta criar uma cópia da descrição existente, atribuir um próximo número à propriedade Version
e alterar o comportamento deste processo conforme a necessidade (o identificador deve ser deixado inalterado).
Exemplos de processos de negócios no contexto de nossa tarefa são:
- – .
- – , , .
- – .
- – , .
Como você pode ver nos exemplos, todos os processos são condicionalmente subdivididos em "cíclicos", cuja execução envolve repetição periódica, e "lineares", realizados no contexto de declarações específicas e, no entanto, não excluem a presença de algumas estruturas cíclicas dentro delas.
Vamos considerar um exemplo de um dos processos que funcionam em nossa solução para pesquisar a fila de solicitações de entrada:
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)));
}
}
Nas linhas marcadas com *, você pode ver o uso de métodos de extensão parametrizados, que instruem o mecanismo a usar classes de etapas (mais sobre isso posteriormente) correspondentes aos parâmetros de tipo. Com a ajuda de métodos de extensão
Input
e Output
temos a oportunidade de definir os dados iniciais passados para a etapa antes de iniciar a execução e, consequentemente, alterar os dados do processo (e eles são representados por uma instância da classe LoadRequestWfData
) em conexão com as ações realizadas pela etapa. E é assim que o processo se parece em um diagrama BPMN:

Passos
Conforme mencionado acima, é razoável colocar a lógica das etapas em classes separadas. Além de tornar o processo mais conciso, permite criar etapas reutilizáveis para operações comuns.
De acordo com o grau de exclusividade das ações realizadas em nossa solução, as etapas são divididas em duas categorias: gerais e específicas. O primeiro pode ser reutilizado em qualquer módulo para qualquer projeto, portanto, eles são colocados em uma biblioteca de solução compartilhada. Estes últimos são únicos para cada cliente, por isso o seu lugar nos módulos de design correspondentes. Exemplos de etapas comuns incluem:
Enviar solicitações de confirmação para uma resposta.
- Upload de arquivos para armazenamento de arquivos.
- Extraindo dados do pacote SMEV, etc.
Passos específicos:
- Criação de objetos no IAS, possibilitando ao operador a prestação de um serviço.
- .
- ..
Ao descrever as etapas do processo, aderimos ao princípio de responsabilidade limitada para cada etapa. Isso permitiu não ocultar fragmentos da lógica do processo de negócios de alto nível em etapas e expressá-la explicitamente na descrição do processo. Por exemplo, se um erro for encontrado nos dados do aplicativo, é necessário enviar uma mensagem sobre a recusa de processar o aplicativo ao SMEV, então o bloco correspondente da condição estará localizado bem no código do processo de negócios e diferentes classes corresponderão às etapas para determinar o fato do erro e responder a ele.
Deve-se observar que as etapas devem ser registradas no contêiner de dependências, para que o mecanismo seja capaz de utilizar instâncias de etapas conforme cada processo avança em seu ciclo de vida.
Cada etapa é um link de conexão entre o código que contém a descrição de alto nível do processo e o código que resolve os problemas do aplicativo - serviços.
Serviços
Os serviços representam o próximo nível inferior de solução de problemas. Cada etapa no desempenho de sua função depende, via de regra, de um ou mais serviços (NB O conceito de "serviço" neste contexto é mais próximo do conceito análogo de "serviço de nível de aplicativo" do domínio do design específico de domínio (DDD)).
Exemplos de serviços são:
- O serviço de recebimento de uma resposta da fila de resposta SMEV prepara o pacote de dados correspondente em formato SOAP, envia-o ao SMEV e converte a resposta em um formato adequado para processamento posterior.
- Serviço de download de arquivos do repositório SMEV - possibilita a leitura dos arquivos anexados à aplicação desde o portal desde o repositório de arquivos por meio do protocolo FTP.
- O serviço de obtenção do resultado da prestação de um serviço - lê os dados sobre os resultados do serviço do SAI e forma o objeto correspondente, com base no qual outro serviço irá construir um pedido SOAP para envio ao portal.
- Serviço de upload de arquivos relacionados ao resultado do serviço para o armazenamento de arquivos SMEV.
Os serviços da solução são divididos em grupos com base no sistema, interação com a qual fornecem:
- Serviços SMEV.
- Serviços IAS.
Serviços para trabalhar com a infraestrutura interna da solução de integração (registro de informações sobre pacotes de dados, vinculação das entidades da solução de integração com objetos IAS, etc.).
Em termos de arquitetura, os serviços são o nível mais baixo, no entanto, eles também podem contar com classes de utilitários para resolver seus problemas. Assim, por exemplo, na solução existe uma camada de código que resolve os problemas de serialização e desserialização de pacotes de dados SOAP para diferentes versões do protocolo SMEV. Em termos gerais, a descrição acima pode ser resumida em um diagrama de classes:

A interface
IWorkflow
e a classe abstrata estão diretamente relacionadas ao mecanismo StepBodyAsync
(no entanto, você pode usar seu StepBody analógico síncrono). O diagrama abaixo mostra a implementação de "blocos de construção" - classes concretas com descrições de processos de negócios de Workflow e as etapas usadas neles ( Step
). No nível inferior, são apresentados os serviços que, na sua essência, já são específicos para esta implementação particular da solução e, ao contrário dos processos e etapas, são opcionais.
Os serviços, como as etapas, devem ser registrados no contêiner de dependência para que as etapas que usam seus serviços possam obter as instâncias necessárias deles por injeção por meio do construtor.
Incorporando o motor na solução
No momento do início da criação do sistema de integração com o portal, a versão 2.1.2 do motor estava disponível no repositório Nuget. Ele é integrado ao contêiner de dependência da maneira padrão em um método de
ConfigureServices
classe Startup
:
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddWorkflow(opts =>
opts.UsePostgreSQL(connectionString, false, false, schemaName));
// ...
}
O motor pode ser configurado para um dos armazéns de dados suportados (existem outros entre eles : MySQL, MS SQL, SQLite, MongoDB). No caso do PostgreSQL, o mecanismo usa Entity Framework Core na variante Code First para trabalhar com processos. Assim, se houver um banco de dados vazio, é possível fazer a migração e obter a estrutura da tabela desejada. O uso da migração é opcional, pode ser controlado usando os argumentos do método
UsePostgreSQL
: os argumentos do tipo booleano second ( canCreateDB
) e third ( canMigrateDB
) permitem que você diga ao mecanismo se ele pode criar um banco de dados se ele não existir e aplicar migrações.
Uma vez que com a próxima atualização do mecanismo há uma probabilidade diferente de zero de alterar seu modelo de dados e a aplicação correspondente da próxima migração pode danificar os dados já acumulados, decidimos abandonar esta opção e manter a estrutura do banco de dados por conta própria, com base no mecanismo de componentes do banco de dados que é usado em nossos outros projetos.
Portanto, o problema de armazenamento de dados e registro do mecanismo no contêiner de dependência foi resolvido, vamos prosseguir para iniciar o mecanismo. Para esta tarefa, a opção de serviço hospedado surgiu, e aquiveja um exemplo de classe base para criar tal serviço). O código tomado como base foi ligeiramente modificado para manter a modularidade, o que significa dividir a solução de integração (chamada "Onyx") em uma parte comum que fornece a inicialização do motor e execução de alguns procedimentos de serviço, e uma parte específica para cada cliente específico (módulos de integração) ...
Cada módulo contém descrições de processos, infraestrutura para execução de lógica de negócios, bem como algum código unificado para permitir que o sistema de integração desenvolvido reconheça e carregue dinamicamente as descrições de processos em uma instância do mecanismo Workflow Core:

Registro e lançamento de processos de negócios
Agora que temos as descrições prontas dos processos de negócios e do mecanismo conectado à solução, é hora de informar ao mecanismo sobre quais processos ele funcionará.
Isso é feito usando o seguinte código, que pode ser localizado dentro do serviço hospedado anteriormente mencionado (o código que inicia o registro dos processos nos módulos conectados também pode ser colocado aqui):
public async Task RunWorkflowsAsync(IWorkflowHost host,
CancellationToken token)
{
host.RegisterWorkflow<LoadRequestWf, LoadRequestWfData>();
// ...
await host.StartAsync(token);
token.WaitHandle.WaitOne();
host.Stop();
}
Conclusão
Em termos gerais, cobrimos as etapas que você precisa seguir para usar o Workflow Core em uma solução de integração. O mecanismo permite que você descreva os processos de negócios de uma maneira flexível e conveniente. Tendo em conta que se trata da tarefa de integração com o portal "Gosuslug" via SMEV, é de prever que os processos de negócio projectados abranjam um leque de tarefas bastante diversas (polling de fila, upload / download de ficheiros, garantindo o cumprimento do protocolo de troca e garantindo confirmação de recebimento de dados, tratamento de erros em diferentes etapas, etc.). Portanto, será bastante natural esperar a ocorrência de alguns momentos não óbvios de implementação à primeira vista, e é a eles que dedicaremos o próximo artigo final do ciclo.