Thursday, January 27, 2011

Using Xslt in MSBuild

Recently, I had the opportunity to integrate some of my Xslt skills into our MSBuild script.  As MSBuild is a relatively new technology for me (compared to NAnt) as I ventured into this task I was reminded that there’s always more than one way to get the job done.  Prior to .NET 4.0, MSBuild didn’t natively support Xslt capabilities so the developer community independently produced their own solutions.  So far, I’ve identified the following three implementations:

I thought it would be fun to contrast the different implementations.

MSBuild Community Tasks: Xslt

Hosted on tigris.org, the msbuildtasks implementation was the task I ultimately ended up using. Of the three listed, this one seems to be the most feature complete – but working with it is extremely frustrating because there is no online help.  Fortunately, there is a help file that ships with the installer that provides decent coverage for the Task (albeit a bit buried).

The interesting feature that sets this Task apart is that it aggregates your xml files into a single in-memory document and performs the transformation once.  Keep in mind that this has an impact on how you structure your XPath queries.  For example, an expression like count(//element-name) will count all instances in the aggregated contents.

Because it is an aggregator, your xml documents will be loaded as children elements of a root element.  As such, you must supply the name for the Root element. If you are only transforming a single file, the root element can be left blank (“”).

My only complaint about this implementation is that it only supports writing the transformation to a file. For my purposes, I would have really liked the ability to pipe the output into a Property or ItemGroup. To achieve this, you must read the contents of the file into your script. Not a big deal, just an extra step.
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
   <Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets" />

   <Target Name="Example">

      <ItemGroup>
        <XmlFiles Include="*.xml" />
      </ItemGroup>

      <PropertyGroup>
        <XslFile>example.xslt</XslFile>
      </PropertyGroup>

      <!-- perform our xslt transformation for all xml files -->
      <Xslt
         Inputs="@(XmlFiles)"
         Xsl="$(XslFile)"
         RootTag="Root"
         Output="xslt-result.html"/>

      <!-- read the file into a property -->
      <ReadLinesFromFile File="xslt-result.html">
         <Output TaskParameter="Lines" ItemName="MyOuput"/>
      </ReadLinesFromFile>  

      <!-- display the output (with no delimiter) -->
      <Message Text="@(MyOutput,'')" />

    </Target>

</Project>

In reading the help documentation, I noticed that this task will pass the extended meta-data of the Inputs into the document as parameters to the Xslt.  This looks interesting and I may want to revisit this in a future post.

MSBuild Extension Pack: XmlTask

I really liked this implementation but unfortunately our build server wasn’t using this set of extensions, so I had to retrofit to use the community tasks version.

This task boasts a few interesting features:

  1. The output of the transformation can write to a file or a property/item.
  2. Ability to validate the Xml.
  3. Ability to suppress the Xml declaration (though you can do this in your xslt, see below)

This example is comparable to above but uses the Output element of the Task to route the transformation into an ItemGroup (MyOutput).

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
   <Import Project="$(MSBuildExtensions)\ExtensionPack\4.0\MSBuild.ExtensionPack.tasks" />
            
   <Target Name="Example">

      <ItemGroup>
        <XmlFiles Include="*.xml" />
      </ItemGroup>

      <PropertyGroup>
        <XslFile>example.xslt</XslFile>
      </PropertyGroup>

      <MSBuild.ExtensionPack.Xml.XmlTask 
          TaskAction="Transform" 
          XmlFile="%(XmlFiles.Identity)" 
          XslTransformFile="$(XslFile)"
          >
          <Output 
             ItemName="MyOutput" 
             TaskParameter="Output"
             />
      </MSBuild.ExtensionPack.Xml.XmlTask>        
        
      <Message Text="@(MyOutput, '')" />
        
   </Target>

</Project>

Note that I can switch between using multiple and a single input files simply by changing the XmlFile attribute from an ItemGroup to a Property.  Unlike the Community Tasks implementation, my Xslt stylesheet doesn’t need special consideration for an artificial root element which makes the stylesheet shorter to develop and easier to maintain.

MSBuild 4.0: XsltTransformation

Last but not least is the newcomer on the block, MSBuild 4.0’s XsltTransformation Task.

This task, to my knowledge, can only write the transformation to a file.  If you’re only transforming a single file this won’t be a problem for you, but if you have multiple files that you want to concatenate into a single output, it’ll take some finesse.

I solved this problem by creating a unique output file for each transformation.  Aside from some extra files to clean-up, the task worked great.

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
            
   <Target Name="Example">

      <ItemGroup>
        <XmlFiles Include="*.xml" />
      </ItemGroup>

      <PropertyGroup>
        <XslFile>example.xslt</XslFile>
      </PropertyGroup>

      <!-- Perform the transform for each file, but
              write the output of each transformation to its
              own file. -->
      <XslTransformation
         OutputPaths="output.%(XmlFiles.FileName).html"
         XmlInputPaths="%(XmlFiles.Identity)"
         XslInputPath="$(XslFile)"
         />
 
      <!-- Read all our transformation outputs back into an ItemGroup -->
      <ReadLinesFromFile File="output.%(XmlFiles.FileName).html">
         <Output TaskParameter="Lines" ItemName="MyOutput" />
      </ReadLinesFromFile>
        
      <Message Text="@(MyOutput, '')" />
        
   </Target>

</Project>

This implementation also supports passing parameters, which may come in handy.

Some Xslt Love

And for completeness sake, here’s a skeleton Xslt file that supresses the xml-declaration.

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
    
    <xsl:output method="html" omit-xml-declaration="yes" />
    
    <xsl:template match="/">
        <p>your output here</p>
    </xsl:template>

</xsl:stylesheet>

So go out there and unleash some Xslt-Fu.

submit to reddit

3 comments:

Jim said...

Referenced you on StackOverflow:
http://stackoverflow.com/q/1688778/36737

Thanks for the nice post!

CW said...

.NET 4.0's XslTransformation task accepts a parameter called 'Parameters', which supplies XSLT parameters to the transform being invoked. This is not well documented; a poster to a MSDN community forum used Reflector to puzzle it out.
It accepts an XML fragment that consists of one or more <Parameter Name="..." Value="..." /> elements. These can be passed as an escaped string, but they can also be specified as child elements of a Property element in a project file:

<PropertyGroup Label="Transform_Parameters">
<ParamName>ExportPath</ParamName>
<ParamValue>..\$(InputDir)\ExportData.xml</ParamValue>
<ParamObjXML>
<Parameter Name='$(ParamName)' Value='$(ParamValue)' />
</ParamObjXML>
</PropertyGroup>

<XslTransformation
Parameters="$(ParamObjXML)" ... />

CW said...

PS: Somehow I missed the link in your post to the MSDN forum post on passing parameters. That is the very same post I came across. I guessed that specifying the parameters as child elements of a property and using the property value might work, and lo and behold, it did!