← Accueil

Comment injecter du code C# dans un connecteur Power Automate ?

👨🏻‍💻 Par Julien Leture · Expert technique SharePoint, Power Platform et Microsoft 365 Copilot
🗓️ 17 septembre 2024 · 🕖 15 minutes de lecture · 📖 Article original sur LinkedIn

Si Power Automate est avant tout un outil low-code utilisable par des personnes non techniques, il est possible d'étendre cet outil avec des connecteurs personnalisés et même d'injecter du code C# pour aller plus loin. Cet article, réalisé sur la base d'un REX, présente deux cas d'usage, avec un exemple à reproduire en pas à pas.

Qu'est-ce qu'un connecteur ?

Au sein de la Power Platform, la notion de connecteur est très importante. Cela permet de communiquer entre des services.

Par exemple, pour un créer un flux qui doit envoyer un email ou poster un message dans Teams on utilisera les connecteurs Outlook Office 365 ou Teams, fournis en standard par Microsoft. Pour alimenter une table dans un écran d'une Power Apps, le connecteur SQL Server pourrait être utilisé si la source de données est une base SQL hébergée dans Azure ou sur un serveur local.

Mais un connecteur ne se limite pas aux produits Microsoft, il en existe plus de 1000 (et à peine 250 quand j'ai commencé à travailler dessus en 2018 !). Des éditeurs tiers peuvent également fournir des connecteurs, notamment pour de la signature électronique, de la génération de PDF ou exposer des données métiers.

Certains connecteurs sont accessibles sans surcoût (connecteur standard) et d'autres nécessitent une licence complémentaire (connecteur premium ou connecteur personnalisé) à payer à Microsoft (la licence Power Automate premium est proposée autour de 14 € HT / mois / utilisateur).

Bien sûr, si un connecteur utilise un service tiers payant, le coût de ce service s'ajoute à celui de la licence Power Automate premium.

Développer son propre connecteur

Au-delà des connecteurs mis à̀ disposition en standard, il est possible de développer ses propres connecteurs. C'était l'objet d'un précédent article rédigé pour LinkedIn en 2023 : Comment créer facilement un connecteur Power Platform ?

Aujourd'hui, le but est de voir comment injecter du code C# dans des connecteurs personnalisés afin de transformer la réponse reçue d'une API.

Par exemple, prenons une API de prévisions météorologiques qui ne retournait les températures qu'en degré Fahrenheit (°F). L'injection d'un bout de code C# permettrait de compléter la réponse pour ajouter la même valeur mais en degré Celsius (°C). On pourrait aussi ajouter un indicateur en fonction de la valeur et de la saison (température basse, normale, élevée, canicule, etc.).

L'idée est de pouvoir faire un traitement directement dans le connecteur et non dans le flux Power Automate dans une nouvelle action à positionner après la réponse reçue du connecteur.

Les motivations sont nombreuses :

  • Faciliter le traitement en C# plutôt qu'avec de nombreuses actions dans Power Automate ;
  • Centraliser et mutualiser des traitements dans le connecteur et non dans le flux ;
  • Appeler ce même connecteur depuis Power Apps, en plus de Power Automate ;
  • Améliorer les performances du traitement grâce à du code compilé.

REX sur un cas concret

Dernièrement, j'ai eu un cas concret où l'usage de code C# dans un connecteur avait du sens. Cela concernait une API de manipulation d'utilisateurs. L'objectif était de se connecter à une API tierce (celle d'un ERP métier) pour récupérer des utilisateurs afin de les recréer dans un annuaire Entra ID côté Microsoft 365.

Bien que le besoin soit issu d'un cas concret, l'exemple qui sera décrit dans la suite de l'article est fictif et permet d'illustrer cet article avec du code C#. Par ailleurs, le site JSONPlaceholder sera utilisé afin d'obtenir une API fonctionnelle pour mettre en place ce connecteur personnalisé. Cette API de test qui retourne justement des utilisateurs fictifs.

1️⃣ Premier besoin : traitement des chaînes de caractères

Le premier besoin concernait le formatage des données reçues par l’API :

Si la mise en majuscule est simple côté Power Automate avec l’usage de la fonction toUpper, ne mettre que la première lettre en majuscule puis la suite en minuscule pour le prénom est déjà plus complexe.

Alors qu'avec Power Apps il suffit d'utiliser la méthode Proper, côté Power Automate il n'y a pas d'équivalent. Il faut combiner toUpper sur le premier caractère puis utiliser toLower sur la suite. Il faut aussi répéter le traitement en découpant la chaîne de caractères avec la méthode split à chaque espace, tiret ou apostrophe dans le cas des prénoms composés.

La suppression des accents est également simple, mais peu lisible. S’il s’agit tout bêtement de remplacer é par e avec la méthode replace, il faut penser à tous les cas, à toutes les lettres.

Ici un outil comme ChatGPT est très efficace puisqu’il va permettre de générer la bonne expression Power Automate pour vous. Il va même penser à toutes les lettres, notamment a, i, u, o. Et comme il ne pensera pas immédiatement à remplacer ç par c (heureusement que l’humain est là), il faudra lui indiquer explicitement de prendre en compte en cas.

Julien : Je réalise un flux cloud Power Automate. Je dois remplacer les accents d'une chaîne de caractères (exemple : remplacer éèêë par eeee). Quelle est la bonne syntaxe en PowerFx ?
ChatGPT : Pour remplacer les accents d’une chaîne de caractères dans Power Automate, vous pouvez utiliser l’expression PowerFx replace. Cependant, PowerFx n’a pas une fonction directe pour remplacer plusieurs caractères en une seule opération, donc vous devrez chaîner plusieurs fonctions replace. Voici un exemple de syntaxe pour remplacer les accents é, è, ê, ë par e dans Power Automate :
 replace(
    replace(
        replace(
            replace(
                original_string,
                "é", "e"),
            "è", "e"),
        "ê", "e"),
    "ë", "e")

Bien que possible en low-code côté Power Automate, toutes ces opérations commencent à augmenter le temps de traitement de quelques secondes. Si vous devez traiter une liste, le temps peuvent devenir rapidement conséquent, au-delà de la minute, voire beaucoup plus !

Dans mon REX, l'ensemble des traitements appliqués pour un utilisateur était de 3 secondes. Appelé dans le cadre de l'import d'un fichier CSV, donc avec de nombreuses lignes, le temps de traitement total était potentiellement important. Passer par du code C# peut donc être une solution pour améliorer les performances du traitement.

En C# :

Exemple de code C# :

// suppression des accents
public static string RemoveDiacritics(string text)
{
    if (string.IsNullOrWhiteSpace(text))
        return text;
    return text.Normalize(System.Text.NormalizationForm.FormD);
}
// mise en majuscule de la première lettre d'un mot
public static string CapitalizeEachPart(string name)
{
    if (string.IsNullOrWhiteSpace(name))
        return name;
    return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(name.ToLower());
}

Je ne vais pas rentrer en détail sur la mise en place d’un connecteur personnalisé puisque cela était l’objet de l’article précédent : Comment créer facilement un connecteur Power Platform ?

En résumé, depuis l'interface web de Power Automate ou Power Apps, il faut créer un nouveau connecteur à partir de zéro. Pour cet exemple, j'utilise l'API de test JSONPlaceholder :
Création d'un nouveau connecteur
Création d'un nouveau connecteur
Aucune authentification
Aucune authentification

Créer une nouvelle action :

Cliquer sur « ajouter une réponse » puis « importer à partir de l’exemple » et coller la réponse JSON obtenue depuis la réponse jsonplaceholder.typicode.com/users

Importer la réponse à partir d'un exemple
Importer la réponse à partir d'un exemple

Cliquer sur « Créer un connecteur » puis aller dans l’onglet « 6. Tester » pour créer une connexion et appeler la méthode get-users :

Création d'une connexion
Création d'une connexion
Réponse non modifiée retournée par l'API
Réponse non modifiée retournée par l'API
Le connecteur avec l'API de test est bien fonctionnel ! On va donc pouvoir injecter le code C#, et notamment les méthodes décrites plus haut pour manipuler les chaînes de caractères.

Pour cela, aller dans l’onglet « 5. Code » :

Code C# initial, non actif par défaut
Code C# initial, non actif par défaut

Les conditions sont clairement énoncées dans le bloc de gauche avec un lien avec la documentation officielle de Microsoft :

Le code personnalisé transforme les charges utiles des demandes et des réponses au-delà des modèles de stratégie. Vous pouvez soit coller votre code, soit charger un fichier contenant votre code. Votre code doit être en C#, avoir un temps d'exécution maximum de 5 secondes et ne peut pas dépasser 1 Mo. En savoir plus

Initialement le code fourni est le suivant :

public class Script : ScriptBase
{
    public override async Task ExecuteAsync()
    {
        HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.OK);
        response.Content = CreateJsonContent("{\"message\": \"Hello World\"}");
        return response;
    }
}

Pour appliquer plus finement le traitement à la bonne opération du connecteur (car un connecteur peut avoir de nombreuses opérations) il faut utiliser la propriété this.Context et notamment this.Context.OperationId.

L'interface qui décrit cet objet est la suivante :

public interface IScriptContext
{
    // Correlation Id
    string CorrelationId { get; }
 
    // Connector Operation Id
    string OperationId { get; }

    // Incoming request
    HttpRequestMessage Request { get; } 

    // Logger instance
    ILogger Logger { get; }

    // Used to send an HTTP request
    // Use this method to send requests instead of HttpClient.SendAsync
    Task SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken);
}

La première étape va être de définir la classe qui correspond à l’objet retourné. Ici, même si l’API retourne de nombreuses propriétés associées.à un utilisateur (email, adresse, téléphone), on ne souhaite déclarer que les propriétés que l’on souhaite conserver pour notre cas d'utilisation (le nom et le prénom de l'utilisateur).

Exemple de classe User (⚠️ le nom des propriétés publiques doit correspondre exactement, donc avec la même casse, à celui retourné par l’API afin de simplifier la sérialisation) :

public class User
{
    public string name { get; set; }
    public string username { get; set; }
}

C’est via cette classe que l’on va pouvoir appliquer le traitement sur les chaînes de caractères évoqué pour ce besoin :

public class User
{
    private string _name;
    private string _username;

    public string name
    {
        get { return _name; }
        set { _name = CapitalizeEachPart(RemoveDiacritics(value)); }
    }

    public string username
    {
        get { return _username; }
        set { _username = RemoveDiacritics(value).ToUpper(); }
    }

    public string DisplayName
    {
        get { return string.Concat(username, " ", name); }
    }

    public string Login
    {
        get
        {
            string[] firstNameParts = name.Split(' ');
            string initials = string.Concat(firstNameParts.Select(part => part[0]));
            return string.Concat(initials, ".", username).ToLower();
        }
    }

    public static string RemoveDiacritics(string text)
    {
        if (string.IsNullOrWhiteSpace(text))
            return text;
        return text.Normalize(System.Text.NormalizationForm.FormD);
    }

    public static string CapitalizeEachPart(string name)
    {
        if (string.IsNullOrWhiteSpace(name))
            return name;
        return System.Globalization.CultureInfo.CurrentCulture.TextInfo.ToTitleCase(name.ToLower());
    }
}

Toujours depuis l’onglet « 5. Code », cocher « ✔️ Code activé » et remplacer le code par la classe suivante :

Injection du code C#
Injection du code C#
public class Script : ScriptBase
{
    public override async Task ExecuteAsync()
    {
        HttpResponseMessage response = await this.Context.SendAsync(this.Context.Request, this.CancellationToken).ConfigureAwait(continueOnCapturedContext: false);
        string responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(continueOnCapturedContext: false);
        switch(this.Context.OperationId)
        {
            case "get-users":
                response.Content = ConvertObject(responseString);
                break;
        }

        return response;
    }

    private static HttpContent ConvertObject(string responseString)
    {
        List list = JsonConvert.DeserializeObject>(responseString);
        return CreateJsonContent(new JObject
        {
            ["count"] = list?.Count ?? 0,
            ["values"] = JToken.FromObject(list)
        }.ToString());
    }
}

Cette méthode permet d’intercepter l’opération get-users (c'est-à-dire l'opération dont l'ID est get-users) et d’appeler une méthode qui va se charger de désérialiser la réponse dans l’objet User que l’on vient de définir.

Petit bonus, en plus de la réponse attendue (la liste des utilisateurs), j’ai ajouté une propriété « count » afin d’avoir directement le nombre d’éléments sans devoir demander ultérieurement la taille de la liste.

Ajouter à la suite la classe Script, la classe User, et cliquer sur « Mettre à jour le connecteur » pour regénérer le connecteur avec votre code. S’il y a une erreur de syntaxe ou de compilation, le connecteur ne sera pas mis à jour et un message d'erreur explicite sera affiché.

Deuxième bonus, l'objet this.Context contient une propriété Logger. Il est ainsi possible d'injecter des logs tout au long du traitement. Exemple d'utilisation :
this.Context.Logger.LogInformation("Execution started.");
this.Context.Logger.LogWarning("This is a warning message.");
this.Context.Logger.LogError($"An error occurred: {ex.Message}", ex);

Retour à l’onglet « 6. Test » afin de tester le résultat :

La réponse a été modifiée en C#
La réponse a été modifiée en C#
Accès aux logs personnalisés
Accès aux logs personnalisés
Et voilà ! La liste ne contient plus que les propriétés souhaitées (nom et prénom) avec la transformation sur les majuscules, minuscules et accents, ainsi que les nouvelles propriétés (DisplayName et Login). Cela correspond à ce premier besoin fictif de traitement de la réponse directement dans le connecteur, et non, dans le flux Power Automate.

Dans la mesure où la réponse a été modifiée par le code C#, il est recommandé (mais pas obligatoire) de modifier le schéma de la réponse. Cela permettra aux personnes qui vont utiliser le connecteur (les makers ou citizen developers) d'avoir directement dans le concepteur les nouvelles propriétés ajoutées à la volée.

Il y a deux façons pour le faire :

2️⃣ Deuxième besoin : transformation d'un tableau

L’autre besoin, et c’est celui qui justifie cet article car il est impossible de le contourner avec des opérations côté Power Automate, est de « corriger » la réponse reçue par l’API métier que je devais utiliser quand il s’agissait d’une liste d’éléments.

En effet, quand l’API métier ne retournait qu’un seul objet, par exemple un utilisateur suivant un login demandé, il n’y avait pas de problème, l'API retournait bien l’objet avec le format attendu en JSON.

En revanche, quand l’API métier retournait une liste d’objets, plutôt que de retourner un tableau d’objets avec la notation standard sous forme de crochets [], l’API métier développée en PHP retournait un tableau associatif avec une propriété incrémentale 0, 1, 2, etc.

Réponse reçue par l'API (exemple fictif) :
{
  "0": {
    nom: "Dupont",
    prenom: "Pierre"
  },
  "1": {
    nom: "Dupont",
    prenom: "Martin"
   },
  "2": {
    nom: "Martin",
    prenom: "Albertine"
  }
}
Réponse attendue en suivant la norme JSON :
[{
    nom: "Dupont",
    prenom: "Pierre"
},
{
    nom: "Dupont",
    prenom: "Martin"
},
{
    nom: "Martin",
    prenom: "Albertine"
}]
Et c’est le drame ! Sans tableau (absence de [] dans la réponse reçue), impossible de connaître la longueur du tableau (méthode length), impossible de prendre le premier ou dernier élément (first ou last), impossible de faire une boucle avec Power Automate avec l'action A chaque élément.

Sauf à tester explicitement la non nullité de chaque propriété incrémentale, cette API est inutilisable !

Utilisation possible avec l'API actuelle :

outputs('Utilisateurs')?["0"]?["nom"]
Le problème est que l'on ne connait pas le nombre d'éléments, puisque la réponse n'est pas un tableau. Il faut tester chaque valeur (0, 1, 2, etc.) jusqu'à obtenir une valeur null. Cela peut s'effectuer avec une action Exécuter jusqu'à.

Utilisation avec un tableau :

first(outputs('Utilisateurs')?["nom"])

Le producteur de la donnée (l’éditeur de l’API de l'ERP métier) avait bien conscience de cette subtilité de conception puisque la documentation officielle indiquait justement qu’il fallait transformer la donnée sous forme de tableau avant de l’utiliser. Un bout de code en PHP était même fourni. Mais qui consomme une API en PHP en 2024 ? Et pourquoi ne pas répondre directement avec un tableau ?

Devoir transformer la réponse avant de l’utiliser rend impossible son utilisation en low-code …sauf à injecter du code dans un connecteur Power Automate.

Un tableau associatif avec une clé numérique sérialisée correspond en C# à un dictionnaire avec une clé de type String et une valeur dont le type est une classe qui décrit l’objet manipulé.

En utilisant la méthode DeserializeObject de JsonConvert, il alors assez simple de reconstruire le dictionnaire, puis de ne récupérer que les valeurs sous forme de tableau (puisque la clé numérique n’a pas de sens, c’est l’indice du tableau qui joue ce rôle)

Dictionary dictionary = JsonConvert.DeserializeObject>(responseString);

List list = dictionary?.Values.ToList();

Où :

La variable list contient donc le tableau tant attendu.

Ce traitement peut être injecter dans une méthode ConvertObjectToArray :

private HttpContent ConvertObjectToArray(string responseString)
    {
        Dictionary dictionary = JsonConvert.DeserializeObject>(responseString);
        List list = dictionary?.Values.ToList();
        return CreateJsonContent(new JObject
        {
            ["count"] = list?.Count ?? 0,
            ["values"] = JToken.FromObject(list)
        }.ToString());
    }
Ainsi, en injectant du code C# dans un connecteur personnalisé, on serait en mesure de transformer la réponse reçue afin d'avoir un vrai tableau et non un tableau associatif. Ce tableau est alors utilisable avec les actions classiques de Power Automate (A chaque élément, length, first, last, etc.)

Les limitations imposées par Microsoft

Attention ! Il peut y avoir une incompréhension sur les limitations imposées. Quand Microsoft indique « Votre code doit être en C#, avoir un temps d’exécution maximum de 5 secondes et ne peut pas dépasser 1 Mo » cela ne concerne pas que la partie C# que vous avez développé mais intègre aussi le temps de réponse de l’API tierce.

Le délai de 5 secondes englobe donc :

Le poids de 1 Mo ne correspond pas à celui du fichier C#, mais bien à celui du flux de la réponse. Même si 1 Mo permet d'avoir une réponse en JSON conséquente, pour une API mal optimisée (sans possibilité de filtre) et trop verbeuse (pas de sélection des colonne à retourner) cela peut vite être limitant.

Une autre limitation concerne les bibliothèques que l’on peut utiliser. Il est en effet impossible d’indiquer soi-même les « Using » de notre choix en entête de la classe C#. Heureusement, la manipulation de JSON avec Newtonsoft.Json est autorisée.

Seules ces bibliothèques sont utilisables :

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Security;
using System.Security.Authentication;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Xml;
using System.Xml.Linq;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq; 

Dans le cadre de mon projet, l'API tierce de l'ERP métier qui ne retourne pas un vrai tableau dans les réponses de type liste mais un objet avec un incrément n'avait aucun filtre sur la méthode GET, par exemple, il n'y avait pas de OData avec les propriétés $select et $filter.

La volumétrie étant importante (puisque tous les utilisateurs étaient retournés) le temps d'exécution était de 30 secondes et le flux de plus de 10 Mo 😭 Conclusion : erreur 500 lors de l'exécution du connecteur avec un message de timeout.

Impossible de passer outre cette limitation imposée par Microsoft pour préserver son architecture cloud mutualisée.
Utilisation de l'API métier dans un connecteur personnalité sans code C# : résultat OK même si la réponse est longue
Résultat OK
Résultat OK
Utilisation de l'API métier dans un connecteur personnalisé avec code C# : erreur 500 timeout dès 5 secondes
Erreur 500 à cause de la limite à 5 secondes
Erreur 500 à cause de la limite à 5 secondes

Conclusion

L'injection de code C# dans un connecteur personnalisé Power Automate est finalement assez simple. En quelques lignes de code, notamment en jouant avec la sérialisation dans une classe modèle, on peut appliquer des traitements pour ajouter des propriétés, nettoyer des valeurs ou en masquer.

Malheureusement la limite à 5 secondes et à 10 Mo va souvent être bloquante : on ne contrôle pas l'API, notamment s'il s'agit d'un service tiers, et on s'expose alors à un comportement hasardeux si le temps de réponse est très aléatoire en fonction de la charge du service.

Dans ce cas, il sera plus simple de créer une fonction dans Azure (Azure Function, c'est-à-dire c'est le service serverless d'Azure avec un coût facturé à l'utilisation) afin d'héberger du code C# ou des scripts PowerShell. Le connecteur personnalisé se chargera alors d'appeler la fonction Azure qui, elle, fera l'appel à l'API tierce et appliquera les traitements nécessaires.

Autre solution : utiliser Azure Logic Apps, l'équivalent pro-code de Power Automate.
On aurait presque oublié Azure Logic Apps...
On aurait presque oublié Azure Logic Apps...

Mais l'usage de code C# peut également s'envisager dans un connecteur qui n'est connecté à rien ! En créant un connecteur pointant sur un domaine fictif (par exemple https://localhost ou https://client.local), si tout le traitement est côté C#, il n'y a aura aucun appel HTTP. C'est intéressant si on souhaite manipuler plus facilement des chaînes de caractères, des tableaux ou encore des dates avec du code mieux maîtrisé que les actions Power Automate ou formules PowerFx dans Power Apps.

Pour rappel, la création et l'utilisation d'un connecteur personnalisé nécessite une licence premium (Power Apps Premium ou Power Automate Premium) pour tous les utilisateurs du cas d'utilisation. S'il s'agit d'un traitement planifié dans un flux cloud Power Automate, par exemple quotidien, avec aucun lien avec une interface alors un seul compte (et donc une seule licence) est possible.

#powerautomate #connecteur #apirest