Executing XSLT in a Function app on Azure Cloud

Kevin BACAS
Published by Kevin BACAS
Category : Azure / Azure Functions
22/05/2019

For some customer, Integration Account pricing can be a deal. Especially if they only need a few mapping artifacts. Costs will be the same no matter if you use artifacts frequently or not. This is why we will look to an alternative to store and use these artifacts. As a reminder, mapping files published inside an Integration Account Service can be XSLT or liquid files. We will focus on XSLT files in this post.

 

Why using a Function App?

  • Every Function App has dedicated storage that you can use to store your XSLT files,
  • Function app is billed on a consumption plan. Considering the price of an Integration account lately, there is a high chance that using a Function App will be way cheaper,
  • Function App can be used in many other cases than just in Logic App. So, the Function App is by definition more agnostic.

 

Executing XSLT in C#

In order to execute an XSLT file in C#, we are going to use the built-in class XslCompiledTransform. This class is able to load and XSLT, configure its runtime, run it and give the result.

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

// Confuguring the XSLT parser
XmlReaderSettings settings = new XmlReaderSettings();
settings.IgnoreWhitespace = true;
settings.IgnoreComments = true;

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

// Configuring XSLT runtime
XsltSettings sets = new XsltSettings(true, false);
var resolver = new XmlUrlResolver();

// Loading the XSLT
xsl.Load(reader, sets, resolver);
string result = null;

if(!String.IsNullOrWhiteSpace(xml)) {
    using (StringReader sri = new StringReader(xml)) 
    {
        using (XmlReader xri = XmlReader.Create(sri))
        {
            // Using HTML renderer
            using (StringWriter sw = new StringWriter())
            using (XmlWriter xwo = XmlWriter.Create(sw, xsl.OutputSettings))
            {
                xsl.Transform(xri, xwo);
                result = sw.ToString();
            }
        }
    }
}

 

In this approach, the idea is to have a single piece of code that requires the name of the XSLT file to execute so we are using the same function for all our XSLTs.

 

At the end of the day, this function has two parameters:

  • name: The name of the file containing the XSLT
  • xml: The XML we want to apply the transformation to.

 

Here is the full code we will be using:

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

 

Configuring the function

This function comes with a folder named “maps” which will contain all of our XSLTs.

So the project structure must match the following:

For every new XSLT that we want to use inside the Function app, we need to map it to the remote folder in the .csproj file:

With those lines in the .csproj, the deployment process will know where to store the XSLTs so we can use it later inside the Function App.

 

Calling the function in http

As soon as the function is deployed we can use it with Postman for instance:

 

Conclusion

Storing and running an XLST on a Function App is fast and requires just a tiny bit of development.

This approach can be managed with Azure DevOps Pipelines in order to update our Function app when we need to add another XSLT. The Integration Account has no built-in solution for that, so maintaining a Function app is easier in the long-term.

Like any other Function App, this one will come with scalability and high availability features.

 

Known limitations:

  • Integration is less native than an Integration Account can provide,
  • If you change the name of one XSLT, all of its references must be changed manually,
  • There is no way to execute a custom C# script inside the XSLT.