Intégration avec "Gosuslugi". Application de Workflow Core (partie II)

La dernière fois que nous nous sommes penchés sur la place de SMEV dans le problème de l'intégration avec le portail «services publics». En fournissant un protocole de communication unifié entre les participants, SMEV facilite grandement l'interaction entre de nombreux départements et organisations souhaitant fournir leurs services à l'aide du portail.



Le service peut être considéré comme un processus distribué dans le temps, qui comporte plusieurs points par lesquels vous pouvez influencer son résultat (annuler via le portail, refuser l'agence, envoyer des informations sur le changement de statut du service au portail, et également envoyer le résultat de sa fourniture). À cet égard, chaque service passe par son propre cycle de vie à travers ce processus, accumulant des données sur la demande de l'utilisateur, les erreurs reçues, les résultats du service, etc. Cela permet à tout moment de pouvoir contrôler et prendre une décision sur les actions ultérieures pour traiter le service.



Nous parlerons plus en détail de la manière et avec quelle aide vous pouvez organiser un tel traitement.



Choisir un moteur d'automatisation des processus métier



Pour organiser le traitement des données, il existe des bibliothèques et des systèmes d'automatisation des processus métier, largementsur le marché: des solutions embarquées aux systèmes complets fournissant un cadre pour le contrôle des processus. Nous avons choisi Workflow Core comme outil d'automatisation des processus métier. Ce choix a été fait pour plusieurs raisons: premièrement, le moteur est écrit en C # pour la plateforme .NET Core (c'est notre principale plateforme de développement), il est donc plus facile de l'inclure dans la présentation générale du produit, contrairement par exemple à Camunda BPM. En outre, il s'agit d'un moteur intégré, qui offre de nombreuses possibilités de gestion des instances de processus métier. Deuxièmement, parmi les nombreuses options de stockage prises en charge, PostgreSQL est également utilisé dans nos solutions. Troisièmement, le moteur fournit une syntaxe simple pour décrire le processus sous la forme d'une API fluide (il existe également une variante de la description du processus dans un fichier JSON, cependant,il semble moins pratique à utiliser car il devient difficile de détecter une erreur dans la description du processus jusqu'au moment de son exécution effective).



Processus d'affaires



Parmi les outils généralement acceptés pour décrire les processus métier, il convient de noter la notation BPMN . Par exemple, la solution au problème FizzBuzz en notation BPMN pourrait ressembler à ceci:





Le moteur Workflow Core contient la plupart des blocs de construction et des instructions présentés dans la notation et, comme mentionné ci-dessus, vous permet d'utiliser l'API fluide ou les données JSON pour décrire des processus spécifiques. La mise en œuvre de ce processus au moyen du moteur Workflow Core peut prendre la forme suivante :



  //    .
  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++;
              }));
    }
  }
}


Bien entendu, le processus peut être décrit plus simplement en ajoutant la sortie des valeurs souhaitées directement dans les étapes suivant les contrôles de cardinalité. Cependant, avec l'implémentation actuelle, vous pouvez voir que chaque étape peut apporter des modifications à la «tirelire» générale des données de processus, et peut également tirer parti des résultats des étapes précédentes. Dans ce cas, les données de processus sont stockées dans une instance FizzBuzzWfDatadont l'accès est fourni à chaque étape au moment de son exécution.



MéthodeBuildprend un objet de création de processus comme argument, qui sert de point de départ pour appeler une chaîne de méthodes d'extension qui décrivent séquentiellement les étapes d'un processus métier. Les méthodes d'extension, à leur tour, peuvent contenir une description des actions directement dans le code actuel sous la forme d'expressions lambda passées en arguments, ou elles peuvent être paramétrées. Dans le premier cas, qui est présenté dans la liste, un algorithme simple se traduit par un ensemble d'instructions assez complexe. Dans le second, la logique des étapes est cachée dans des classes distinctes qui héritent du type Step(ou AsyncSteppour les variantes asynchrones), ce qui vous permet d'intégrer des processus complexes dans une description plus concise. En pratique, la seconde approche semble plus adaptée, tandis que la première suffit pour des exemples simples ou des processus métier extrêmement simples.



La classe de description de processus réelle implémente l'interface paramétrée IWorkflowet, lors de l'exécution du contrat, contient l'identificateur de processus et le numéro de version. Grâce à ces informations, le moteur est capable de générer des instances de processus en mémoire, de les remplir de données et de corriger leur état dans le stockage. La prise en charge de la gestion des versions vous permet de créer de nouvelles variantes de processus sans risquer d'affecter les instances existantes dans le référentiel. Pour créer une nouvelle version, il suffit de créer une copie de la description existante, d'attribuer un numéro suivant à la propriété Versionet de modifier le comportement de ce processus si nécessaire (l'identifiant doit rester inchangé).



Des exemples de processus commerciaux dans le cadre de notre tâche sont:



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


Comme vous pouvez le voir dans les exemples, tous les processus sont conditionnellement subdivisés en «cycliques», dont l'exécution implique une répétition périodique, et «linéaires», exécutés dans le contexte d'énoncés spécifiques et, cependant, n'excluent pas la présence de certaines structures cycliques en eux-mêmes.



Prenons un exemple de l'un des processus fonctionnant dans notre solution pour interroger la file d'attente des demandes 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)));
  }
}


Dans les lignes marquées d'un *, vous pouvez voir l'utilisation de méthodes d'extension paramétrées, qui indiquent au moteur d'utiliser des classes d'étape (plus à ce sujet plus tard) correspondant aux paramètres de type. Avec l'aide de méthodes d'extension Inputet Outputnous avons la possibilité de définir les données initiales passées à l'étape avant de commencer l'exécution, et, en conséquence, de modifier les données de processus (et elles sont représentées par une instance de la classe LoadRequestWfData) en relation avec les actions effectuées par l'étape. Et voici à quoi ressemble le processus sur un diagramme BPMN:





Pas



Comme mentionné ci-dessus, il est raisonnable de placer la logique des étapes dans des classes distinctes. En plus de rendre le processus plus concis, il vous permet de créer des étapes réutilisables pour les opérations courantes.



Selon le degré d'unicité des actions effectuées dans notre solution, les étapes sont divisées en deux catégories: générale et spécifique. Les premiers peuvent être réutilisés dans tous les modules de tous les projets, ils sont donc placés dans une bibliothèque de solutions partagée. Ces derniers sont uniques pour chaque client, à travers cela leur place dans les modules de conception correspondants. Voici des exemples d'étapes courantes:



Envoi de demandes d'acquittement pour une réponse.



  • Téléchargement de fichiers vers le stockage de fichiers.
  • Extraction des données du package SMEV, etc.


Etapes spécifiques:



  • Création d'objets dans l'IAS, permettant à l'opérateur de fournir un service.
  • .
  • ..


En décrivant les étapes du processus, nous avons adhéré au principe de responsabilité limitée pour chaque étape. Cela permettait de ne pas cacher des fragments de logique de processus métier de haut niveau dans les étapes et de l'exprimer explicitement dans la description du processus. Par exemple, si une erreur est trouvée dans les données de l'application, il est nécessaire d'envoyer un message sur le refus de traiter la demande au SMEV, alors le bloc correspondant de la condition sera situé directement dans le code du processus métier, et différentes classes correspondront aux étapes pour déterminer le fait d'une erreur et y répondre.



Il convient de noter que les étapes doivent être enregistrées dans le conteneur de dépendances, afin que le moteur puisse utiliser des instances d'étapes à mesure que chaque processus se déplace dans son cycle de vie.



Chaque étape est un lien de connexion entre le code contenant la description de haut niveau du processus et le code qui résout les problèmes d'application - les services.



Prestations de service



Les services représentent le niveau suivant, inférieur de résolution de problèmes. Chaque étape de l'accomplissement de sa mission repose, en règle générale, sur un ou plusieurs services (NB Le concept de «service» dans ce contexte est plus proche du concept analogue de «service au niveau de l'application» du domaine de la conception spécifique au domaine (DDD)).



Des exemples de services sont:



  • Le service de réception d'une réponse de la file d'attente de réponse SMEV prépare le paquet de données correspondant au format SOAP, l'envoie au SMEV et convertit la réponse en une forme appropriée pour un traitement ultérieur.
  • Service de téléchargement de fichiers depuis le référentiel SMEV - fournit la lecture des fichiers attachés à l'application depuis le portail depuis le référentiel de fichiers en utilisant le protocole FTP.
  • Le service pour obtenir le résultat de la fourniture d'un service - lit les données sur les résultats du service à partir de l'IAS et forme l'objet correspondant, sur la base duquel un autre service construira une demande SOAP à envoyer au portail.
  • Service de téléchargement de fichiers liés au résultat du service vers le stockage de fichiers SMEV.


Les services de la solution sont divisés en groupes en fonction du système, de l'interaction avec lequel ils fournissent:



  • Services SMEV.
  • Services IAS.


Services pour travailler avec l'infrastructure interne de la solution d'intégration (journalisation des informations sur les paquets de données, liaison des entités de la solution d'intégration avec des objets IAS, etc.).



En termes d'architecture, les services sont le niveau le plus bas, mais ils peuvent également s'appuyer sur des classes d'utilité pour résoudre leurs problèmes. Ainsi, par exemple, dans la solution, il y a une couche de code qui résout les problèmes de sérialisation et de désérialisation des paquets de données SOAP pour différentes versions du protocole SMEV. En termes généraux, la description ci-dessus peut être résumée dans un diagramme de classes:





L'interface IWorkflowet la classe abstraite sont directement liées au moteur StepBodyAsync(cependant, vous pouvez utiliser son StepBody analogique synchrone). Le diagramme ci-dessous montre l'implémentation de «blocs de construction» - des classes concrètes avec des descriptions des processus métier de Workflow et les étapes qu'ils utilisent ( Step). Au niveau inférieur, des services sont présentés, qui, par essence, sont déjà spécifiques à cette implémentation particulière de la solution et, contrairement aux processus et aux étapes, sont facultatifs.



Les services, comme les étapes, doivent être enregistrés dans le conteneur de dépendances afin que les étapes qui utilisent leurs services puissent en obtenir les instances nécessaires par injection via le constructeur.



Intégrer le moteur dans la solution



Au moment du début de la création du système d'intégration avec le portail, la version 2.1.2 du moteur était disponible dans le référentiel Nuget. Il est intégré au conteneur de dépendances de la manière standard dans une méthode de ConfigureServicesclasse Startup:



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


Le moteur peut être configuré pour l'un des entrepôts de données pris en charge (parmi lesquels il y en a d' autres : MySQL, MS SQL, SQLite, MongoDB). Dans le cas de PostgreSQL, le moteur utilise Entity Framework Core dans la variante Code First pour travailler avec les processus. En conséquence, s'il existe une base de données vide, il est possible d'appliquer la migration et d'obtenir la structure de table souhaitée. L'utilisation de la migration est facultative, elle peut être contrôlée à l'aide des arguments de méthode UsePostgreSQL: les arguments de type booléen deuxième ( canCreateDB) et troisième ( canMigrateDB) permettent d'indiquer au moteur s'il peut créer une base de données si elle n'existe pas et d'appliquer des migrations.



Étant donné qu'avec la prochaine mise à jour du moteur, il y a une probabilité non nulle de changer son modèle de données et que l'utilisation correspondante de la prochaine migration peut endommager les données déjà accumulées, nous avons décidé d'abandonner cette option et de maintenir la structure de la base de données par nous-mêmes, sur la base du mécanisme des composants de la base de données utilisé dans nos autres projets.



Ainsi, le problème du stockage des données et de l'enregistrement du moteur dans le conteneur de dépendances a été résolu, passons au démarrage du moteur. Pour cette tâche, l'option de service hébergé est apparue, et icivoir un exemple de classe de base pour créer un tel service). Le code pris comme base a été légèrement modifié pour conserver la modularité, ce qui signifie diviser une solution d'intégration (appelée «Onyx») en une partie commune qui assure l'initialisation du moteur et l'exécution de certaines procédures de service, et une partie spécifique à chaque client spécifique (modules d'intégration) ...



Chaque module contient des descriptions de processus, une infrastructure pour exécuter la logique métier, ainsi qu'un code unifié permettant au système d'intégration développé de reconnaître et de charger dynamiquement les descriptions de processus dans une instance du moteur Workflow Core:







Enregistrement et lancement des processus métiers



Maintenant que nous avons des descriptions toutes faites des processus métier et du moteur connecté à la solution, il est temps d'indiquer au moteur les processus avec lesquels il fonctionnera.



Cela se fait à l'aide du code suivant, qui peut être situé dans le service hébergé précédemment mentionné (le code qui lance l'enregistrement des processus dans les modules connectés peut également être placé ici):



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

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


Conclusion



En termes généraux, nous avons couvert les étapes à suivre pour utiliser Workflow Core dans une solution d'intégration. Le moteur vous permet de décrire les processus métier de manière flexible et pratique. Compte tenu du fait que nous nous occupons de la tâche d'intégration avec le portail «Gosuslug» via le SMEV, il faut s'attendre à ce que les processus commerciaux projetés couvrent une gamme de tâches assez diverses (interrogation de la file d'attente, chargement / téléchargement de fichiers, garantie du respect du protocole d'échange et confirmation de réception des données, traitement des erreurs à différentes étapes, etc.). Par conséquent, il sera tout à fait naturel de s'attendre à l'apparition de moments de mise en œuvre à première vue non évidents, et c'est à eux que nous consacrerons le prochain article final du cycle.



Liens d'étude






All Articles