Integración con "Gosuslugi". Aplicación del núcleo del flujo de trabajo (Parte II)

La última vez miramos el lugar SMEV en el problema de la integración con el portal "servicios públicos". Al proporcionar un protocolo de comunicación unificado entre los participantes, SMEV facilita enormemente la interacción entre muchos departamentos y organizaciones diferentes que deseen proporcionar sus servicios utilizando el portal.



El servicio puede ser considerado como un proceso distribuido en el tiempo, el cual tiene varios puntos a través de los cuales se puede influir en su resultado (cancelar a través del portal, rechazar en el departamento, enviar información sobre el cambio en el estado del servicio al portal, y también enviar el resultado de su prestación). En este sentido, cada servicio pasa por su propio ciclo de vida a través de este proceso, acumulando datos sobre la solicitud del usuario, los errores recibidos, los resultados del servicio, etc. Esto permite en cualquier momento poder controlar y tomar una decisión sobre acciones posteriores para procesar el servicio.



Hablaremos más sobre cómo y con qué ayuda puede organizar dicho procesamiento.



Elegir un motor de automatización de procesos empresariales



Para organizar el procesamiento de datos, existen bibliotecas y sistemas para automatizar los procesos comerciales, ampliamenteen el mercado: desde soluciones integradas hasta sistemas con todas las funciones que proporcionan un marco para el control de procesos. Elegimos Workflow Core como herramienta para automatizar los procesos comerciales. Esta elección se hizo por varias razones: en primer lugar, el motor está escrito en C # para la plataforma .NET Core (esta es nuestra principal plataforma de desarrollo), por lo que es más fácil incluirlo en el esquema general del producto, a diferencia de, por ejemplo, Camunda BPM. Además, es un motor integrado, que brinda amplias oportunidades para administrar instancias de procesos comerciales. En segundo lugar, entre las muchas opciones de almacenamiento compatibles, también se utiliza PostgreSQL en nuestras soluciones. En tercer lugar, el motor proporciona una sintaxis simple para describir un proceso en forma de una API fluida (también hay una variante de describir un proceso en un archivo JSON, sin embargo,parecía menos conveniente de usar debido a que se vuelve difícil detectar un error en la descripción del proceso antes de su ejecución real).



Procesos de negocios



Entre las herramientas generalmente aceptadas para describir los procesos comerciales, cabe destacar la notación BPMN . Por ejemplo, la solución al problema de FizzBuzz en notación BPMN podría verse así:





El motor Workflow Core contiene la mayoría de los bloques de construcción y las declaraciones que se presentan en la notación y, como se mencionó anteriormente, le permite usar la API fluida o los datos JSON para describir procesos específicos. La implementación de este proceso mediante el motor Workflow Core puede tomar la siguiente 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++;
              }));
    }
  }
}


Por supuesto, el proceso se puede describir más simplemente agregando la salida de los valores deseados en los pasos que siguen a las verificaciones de cardinalidad. Sin embargo, con la implementación actual, puede ver que cada paso puede realizar algunos cambios en la "alcancía" general de los datos del proceso, y también puede aprovechar los resultados de los pasos anteriores. En este caso, los datos del proceso se almacenan en una instancia FizzBuzzWfData, cuyo acceso se proporciona a cada paso en el momento de su ejecución.



MétodoBuildtoma un objeto generador de procesos como argumento, que sirve como punto de partida para invocar una cadena de métodos de extensión que describen secuencialmente los pasos de un proceso empresarial. Los métodos de extensión, a su vez, pueden contener una descripción de acciones directamente en el código actual en forma de expresiones lambda pasadas como argumentos, o se pueden parametrizar. En el primer caso, que se presenta en la lista, un algoritmo simple se traduce en un conjunto de instrucciones bastante complejo. En el segundo, la lógica de los pasos se oculta en clases separadas que heredan del tipo Step(o AsyncSteppara variantes asincrónicas), lo que le permite ajustar procesos complejos en una descripción más concisa. En la práctica, el segundo enfoque parece ser más adecuado, mientras que el primero es suficiente para ejemplos simples o procesos comerciales extremadamente simples.



La clase de descripción del proceso real implementa la interfaz parametrizada IWorkflowy, al ejecutar el contrato, contiene el identificador del proceso y el número de versión. Gracias a esta información, el motor puede generar instancias de proceso en la memoria, llenarlas con datos y fijar su estado en el almacenamiento. El soporte de versiones le permite crear nuevas variaciones de proceso sin el riesgo de afectar las instancias existentes en el repositorio. Para crear una nueva versión, basta con crear una copia de la descripción existente, asignar un siguiente número a la propiedad Versiony cambiar el comportamiento de este proceso según sea necesario (el identificador debe dejarse sin cambios).



Ejemplos de procesos comerciales en el contexto de nuestra tarea son:



  • – .
  • – , , .
  • – .
  • – , .


Como puede ver en los ejemplos, todos los procesos se subdividen condicionalmente en "cíclicos", cuya ejecución implica repetición periódica, y "lineales", realizados en el contexto de declaraciones específicas y, sin embargo, no excluyen la presencia de algunas estructuras cíclicas dentro de sí mismos.



Consideremos un ejemplo de uno de los procesos que funcionan en nuestra solución para sondear la cola de solicitudes entrantes:



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)));
  }
}


En las líneas marcadas con *, puede observar el uso de métodos de extensión parametrizados, que instruyen al motor a usar las clases de pasos (más sobre esto más adelante) correspondientes a los parámetros de tipo. Con la ayuda de métodos de extensión Inputy Outputque tenemos la capacidad de establecer los datos iniciales se pasan a la etapa antes de comenzar la ejecución, y, en consecuencia, cambiar los datos de proceso (y están representados por una instancia de la clase LoadRequestWfData) en relación con las acciones realizadas por el paso. Y así es como se ve el proceso en un diagrama BPMN:





Pasos



Como se mencionó anteriormente, es razonable colocar la lógica de los pasos en clases separadas. Además de hacer el proceso más conciso, le permite crear pasos reutilizables para operaciones comunes.



Según el grado de singularidad de las acciones realizadas en nuestra solución, los pasos se dividen en dos categorías: generales y específicas. El primero se puede reutilizar en cualquier módulo para cualquier proyecto, por lo que se colocan en una biblioteca de solución compartida. Estos últimos son únicos para cada cliente, por lo que su lugar en los módulos de diseño correspondientes. Algunos ejemplos de pasos comunes incluyen:



Enviar solicitudes de Ack para una respuesta.



  • Subir archivos al almacenamiento de archivos.
  • Extraer datos del paquete SMEV, etc.


Pasos específicos:



  • Creación de objetos en el IAS, que permitan al operador prestar un servicio.
  • .
  • ..


Al describir los pasos del proceso, nos adherimos al principio de responsabilidad limitada para cada paso. Esto permitió no ocultar fragmentos de la lógica del proceso de negocios de alto nivel en pasos y expresarlo explícitamente en la descripción del proceso. Por ejemplo, si se encuentra un error en los datos de la solicitud, es necesario enviar un mensaje sobre la negativa a procesar la solicitud a SMEV, luego el bloque correspondiente de la condición se ubicará justo en el código del proceso comercial, y diferentes clases corresponderán a los pasos para determinar el hecho del error y responder a él.



Cabe señalar que los pasos deben registrarse en el contenedor de dependencias, de modo que el motor pueda utilizar instancias de pasos a medida que cada proceso avanza en su ciclo de vida.



Cada paso es un vínculo de conexión entre el código que contiene la descripción de alto nivel del proceso y el código que resuelve los problemas de la aplicación: los servicios.



Servicios



Los servicios representan el siguiente nivel inferior de resolución de problemas. Cada paso en el cumplimiento de su deber se basa, por regla general, en uno o más servicios (NB El concepto de "servicio" en este contexto está más cerca del concepto análogo de "servicio a nivel de aplicación" del dominio del diseño específico de dominio (DDD)).



Ejemplos de servicios son:



  • El servicio para recibir una respuesta de la cola de respuestas SMEV prepara el paquete de datos correspondiente en formato SOAP, lo envía al SMEV y convierte la respuesta en un formato adecuado para su posterior procesamiento.
  • Servicio para descargar archivos del repositorio SMEV: proporciona la lectura de archivos adjuntos a la aplicación desde el portal desde el repositorio de archivos mediante el protocolo FTP.
  • El servicio para obtener el resultado de la prestación de un servicio: lee datos sobre los resultados del servicio del IAS y forma el objeto correspondiente, sobre la base del cual otro servicio construirá una solicitud SOAP para enviar al portal.
  • Servicio de carga de archivos relacionados con el resultado del servicio al almacenamiento de archivos SMEV.


Los servicios de la solución se dividen en grupos según el sistema, interacción con la que brindan:



  • Servicios SMEV.
  • Servicios IAS.


Servicios para trabajar con la infraestructura interna de la solución de integración (registro de información sobre paquetes de datos, vinculación de las entidades de la solución de integración con objetos IAS, etc.).



En términos de arquitectura, los servicios son el nivel más bajo, sin embargo, también pueden confiar en clases de servicios públicos para resolver sus problemas. Entonces, por ejemplo, en la solución hay una capa de código que resuelve los problemas de serialización y deserialización de paquetes de datos SOAP para diferentes versiones del protocolo SMEV. En términos generales, la descripción anterior se puede resumir en un diagrama de clases:





La interfaz IWorkflowy la clase abstracta están directamente relacionadas con el motor StepBodyAsync(sin embargo, puede usar su StepBody analógico síncrono). El siguiente diagrama muestra la implementación de "bloques de construcción": clases concretas con descripciones de los procesos de negocio de flujo de trabajo y los pasos utilizados en ellos ( Step). En el nivel inferior, se presentan los servicios que, en esencia, ya son específicos de esta implementación particular de la solución y, a diferencia de los procesos y pasos, no son obligatorios.



Los servicios, como los pasos, deben registrarse en el contenedor de dependencias para que los pasos que usan sus servicios puedan obtener las instancias necesarias de ellos mediante la inyección a través del constructor.



Integrando el motor en la solución



En el momento del inicio de la creación del sistema de integración con el portal, la versión 2.1.2 del motor estaba disponible en el repositorio de Nuget. Está integrado en el contenedor de dependencias de la forma estándar en un método de ConfigureServicesclase Startup:



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


El motor se puede configurar para uno de los almacenes de datos compatibles (entre los que hay otros : MySQL, MS SQL, SQLite, MongoDB). En el caso de PostgreSQL, el motor usa Entity Framework Core en la variante Code First para trabajar con procesos. En consecuencia, si hay una base de datos vacía, es posible aplicar la migración y obtener la estructura de tabla deseada. El uso de la migración es opcional, se puede controlar mediante los argumentos del método UsePostgreSQL: los argumentos de tipo booleano segundo ( canCreateDB) y tercero ( canMigrateDB) le permiten decirle al motor si puede crear una base de datos si no existe y aplicar migraciones.



Dado que con la próxima actualización del motor existe una probabilidad distinta de cero de cambiar su modelo de datos, y la correspondiente aplicación de la próxima migración puede dañar los datos ya acumulados, decidimos abandonar esta opción y mantener la estructura de la base de datos por nuestra cuenta, basándonos en el mecanismo de componentes de la base de datos que se utiliza en nuestros otros proyectos.



Entonces, el problema de almacenar datos y registrar el motor en el contenedor de dependencias ha sido resuelto, pasemos al arranque del motor. Para esta tarea, surgió la opción de servicio alojado, y aquívea un ejemplo de una clase base para crear dicho servicio). El código tomado como base se modificó ligeramente para mantener la modularidad, lo que significa dividir una solución de integración (llamada "Onyx") en una parte común que proporciona la inicialización del motor y la ejecución de algunos procedimientos de servicio, y una parte específica para cada cliente específico (módulos de integración) ...



Cada módulo contiene descripciones de procesos, infraestructura para ejecutar la lógica empresarial, así como un código unificado para permitir que el sistema de integración desarrollado reconozca y cargue dinámicamente descripciones de procesos en una instancia del motor Workflow Core:







Registro y lanzamiento de procesos comerciales



Ahora que tenemos descripciones listas para usar de los procesos comerciales y el motor conectado a la solución, es hora de informarle al motor con qué procesos funcionará.



Esto se realiza mediante el siguiente código, que se puede ubicar dentro del servicio mencionado anteriormente (el código que inicia el registro de procesos en los módulos conectados también se puede colocar aquí):



public async Task RunWorkflowsAsync(IWorkflowHost host,
  CancellationToken token)
{
  host.RegisterWorkflow<LoadRequestWf, LoadRequestWfData>();
  //   ...

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


Conclusión



En términos generales, cubrimos los pasos que debe seguir para usar Workflow Core en una solución de integración. El motor le permite describir los procesos comerciales de una manera flexible y conveniente. Teniendo en cuenta que nos enfrentamos a la tarea de integración con el portal "Gosuslug" a través de SMEV, es de esperar que los procesos de negocio proyectados cubran un abanico de tareas bastante diversas (sondear la cola, cargar / descargar archivos, asegurar el cumplimiento del protocolo de intercambio y asegurar confirmación de recepción de datos, manejo de errores en diferentes etapas, etc.). Por lo tanto, será bastante natural esperar la ocurrencia de algunos momentos de implementación no obvios a primera vista, y es a ellos a los que dedicaremos el próximo artículo final del ciclo.



Enlaces de estudio






All Articles