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.
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:
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 } }
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.
As soon as the function is deployed we can use it with Postman for instance:
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: