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).
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 ?
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 :
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.
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.
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 !
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 ?
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
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 :
Pour cela, aller dans l’onglet « 5. Code » :
Les conditions sont clairement énoncées dans le bloc de gauche avec un lien avec la documentation officielle de Microsoft :
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 :
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.
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é.
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 :
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 :
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.
{
"0": {
nom: "Dupont",
prenom: "Pierre"
},
"1": {
nom: "Dupont",
prenom: "Martin"
},
"2": {
nom: "Martin",
prenom: "Albertine"
}
}
[{
nom: "Dupont",
prenom: "Pierre"
},
{
nom: "Dupont",
prenom: "Martin"
},
{
nom: "Martin",
prenom: "Albertine"
}]
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"]
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 ?
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)
Dictionarydictionary = 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()); }
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.
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.
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.
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.
#powerautomate #connecteur #apirest