Exécution d’une map XSLT dans une application de fonction sur Azure

Kevin Bacas
Publié par Kevin
Catégorie : API Management / Azure
22/05/2019

L’Integration Account est un service Azure qui permet de gérer des composants d’intégration de type « schéma » ou « map ». Ces artefacts peuvent être ensuite utilisés dans des Logic Apps pour valider la forme d’un message ou encore réaliser des transformations (mapping). L’Integration Account est un service qui peut représenter un coût conséquent pour le run d’un projet. C’est pourquoi ici je vais vous faire part d’une alternative peu coûteuse à l’Integration Account. Nous allons utiliser des fonctions Azure pour exécuter nos transformations XSLT.

 

Pourquoi une fonction Azure ?

Il y a plusieurs raisons à cela :

  • L’Application de fonction propose un système de fichier intégré pour stocker les XSLT;
  • Les fonctions Azure sont facturées au temps d’exécution et non au nombre de maps;
  • Une fonction Azure peut être utilisée dans d’autres contextes qu’une Logic App car celle-ci peut être par exemple appelée par http.

 

Exécution d’une feuille de transformation XSLT en C#

Pour exécuter les feuilles XSLT que nous allons mettre à disposition, nous allons utiliser la classe XslCompiledTransform qui permet de charger une feuille XSLT, la compiler, configurer son exécution et récupérer le résultat.

var xsl = new XslCompiledTransform();
var mapsFolder = Path.GetFullPath(Path.Combine(GetScriptPath(), "maps"));
var xsltFullPath = Path.GetFullPath(Path.Combine(mapsFolder, $"{name}.xslt"));

// Ajout des configurations d'execution du XML
XmlReaderSettings settings = new XmlReaderSettings();
settings.IgnoreWhitespace = true;
settings.IgnoreComments = true;

// Creation du reader XML
XmlReader reader = XmlReader.Create(xsltFullPath, settings);

// Configuration d'execution de la XSLT
XsltSettings sets = new XsltSettings(true, false);
var resolver = new XmlUrlResolver();

// Chargement de la map
xsl.Load(reader, sets, resolver);
string result = null;

if(!String.IsNullOrWhiteSpace(xml)) {
    using (StringReader sri = new StringReader(xml)) 
    {
        using (XmlReader xri = XmlReader.Create(sri))
        {
            // Utilisation des paramètres de sortie XSLT afin d'avoir une sortie en HTML
            using (StringWriter sw = new StringWriter())
            using (XmlWriter xwo = XmlWriter.Create(sw, xsl.OutputSettings))
            {
                xsl.Transform(xri, xwo);
                result = sw.ToString();
            }
        }
    }
}

L’idée en utilisant ce code est de rendre le nom de la feuille XSLT à exécuter dynamique. Il suffira d’utiliser la même fonction Azure pour appeler toutes nos feuilles de transformations. Seul le paramètre « name » devra changer.

En somme, cette fonction Azure aura besoin de 2 paramètres :

  • name : Nom de la XSLT à exécuter
  • xml : Contenu XML sous forme de chaîne de caractère à faire passer en entrée de la feuille XSLT.

 

Le code complet de la fonction est le suivant :

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.Xml.Xsl;
using System.Xml;

namespace IntegrationAsAFunction.Functions
{
    public static class ExecuteXSLT
    {
        [FunctionName("ExecuteXSLT")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");

            string name = req.Query["name"];
            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
            dynamic data = JsonConvert.DeserializeObject(requestBody);
            name = name ?? data?.name;

            string xml = req.Query["xml"];
            xml = xml ?? data?.xml;

            var xsl = new XslCompiledTransform();
            var mapsFolder = Path.GetFullPath(Path.Combine(GetScriptPath(), "maps"));
            var xsltFullPath = Path.GetFullPath(Path.Combine(mapsFolder, $"{name}.xslt"));

            log.LogInformation("Creating settings...");
            XmlReaderSettings settings = new XmlReaderSettings();
            settings.IgnoreWhitespace = true;
            settings.IgnoreComments = true;
            log.LogInformation("Creating reader...");
            XmlReader reader = XmlReader.Create(xsltFullPath, settings);
            log.LogInformation("Creating xsl settings");
            XsltSettings sets = new XsltSettings(true, false);
            log.LogInformation("Creating xsl url resolver");
            var resolver = new XmlUrlResolver();
            log.LogInformation("Loading the XSL");
            xsl.Load(reader, sets, resolver);
            string result = null;

            if(!String.IsNullOrWhiteSpace(xml)) {
                log.LogInformation("Found XML");
                using (StringReader sri = new StringReader(xml)) // xmlInput is a string that contains xml
                {
                    using (XmlReader xri = XmlReader.Create(sri))
                    {
                        using (StringWriter sw = new StringWriter())
                        using (XmlWriter xwo = XmlWriter.Create(sw, xsl.OutputSettings)) // use OutputSettings of xsl, so it can be output as HTML
                        {
                            log.LogInformation("Transforming: {xml}", xml);
                            xsl.Transform(xri, xwo);
                            result = sw.ToString();
                        }
                        log.LogInformation("Result: {result}", result);
                    }
                }
            }

            return name != null
                ? (ActionResult)new OkObjectResult($"{result}")
                : new BadRequestObjectResult("Please pass a name on the query string or in the request body");
        }

        #region GETS
        private static string GetScriptPath()
        => Path.Combine(GetEnvironmentVariable("HOME"), @"site\wwwroot");

        private static string GetEnvironmentVariable(string name)
        => System.Environment.GetEnvironmentVariable(name, EnvironmentVariableTarget.Process);
        #endregion
    }
}

 

Configuration de la fonction

Cette fonction s’attend à avoir un dossier « maps » qui contiendra toutes les fichiers XSLT disponibles à l’exécution.

La structure de mon projet est la suivante :

Il faut ensuite bien ajouter la référence vers chaque XSLT dans le fichier .csproj :

C’est ce qui va permettre lors du déploiement, le stockage des XSLT dans le dossier « maps/ » du système de fichier de l’application de fonction.

 

Appel de la fonction en http

Une fois le déploiement réalisé nous pouvons maintenant tester cette fonction avec Postman par exemple. Celle-ci va nous retourner directement le résultat de la transformation sous forme de document XML :

 

Conclusion

L’exécution d’une map est rapide et peu coûteuse à mettre en place dans une Application de Fonction. De plus, cette approche permet un déploiement automatisé très simple contrairement à l’Integration Account. Comme tout autres fonctions Azure, celle-ci profite de toutes les fonctionnalités de scalabilité horizontale et de haute disponibilité.

 

Limitations connues :

  • Impossible d’exécuter un script inline C# dans une map;
  • Intégration légèrement moins native qu’une map dans un Integration Account;
  • Si le nom d’une map change, il faut mettre à jour toutes les références dans toutes les applications qui utilise cette map.