Custom web.config transforms and merges
AppHarbor supports applying Web.config transformations when applications are deployed on the platform. We also built a popular tool that you can use to test your config transformations live.
In addition to applying any transforms you push with your code, we use the Web.config transformation API to perform various other types of configuration transformations that have to happen for the platform to work. For example, for add-ons that require setup more sophisticated setup than simply injecting settings into the appSettings
element.
The built-in vocabulary of transforms is somewhat limited, especially if you have to write transforms that must work correctly with all the varied Web.config files encountered by AppHarbor and not just with your own well-known config file. Fortunately, the Web.config transform engine can be extended to support additional transformations.
The API that external transforms have to implement is found in the Microsoft.Web.Publishing.Tasks
namespace. It's unpleasant and not documented by Microsoft but information can be found in this blog post and in this Stack Overflow answer.
A Transform that we needed was one that would insert an element if it's missing but leave the element alone if it's already present. Imagine, for example, that AppHarbor needs to be able to add connection strings to the connectionStrings
element. If we encounter a Web.config file without a connectionStrings
element, we need to first insert a new element and then add the connection string in question to that. If the connectionStrings
element is already present, we want to leave any connection strings it contains alone and just add the new one.
To accomplish this, we extended the transform engine with a Merge
transform that looks like this:
using System.Linq;
using System.Xml;
using Microsoft.Web.Publishing.Tasks;
namespace AppHarbor.TransformTester.Transforms
{
public class Merge : Transform
{
public Merge() : base(TransformFlags.UseParentAsTargetNode)
{}
protected override void Apply()
{
Apply((XmlElement)TargetNode, (XmlElement)TransformNode);
}
public void Apply(XmlElement targetElement, XmlElement transformElement)
{
var targetChildElement = targetElement.ChildNodes.OfType<XmlElement>()
.FirstOrDefault(x => x.LocalName == transformElement.LocalName);
if (targetChildElement == null)
{
targetElement.AppendChild(transformElement);
return;
}
foreach (var transformChildElement in transformElement.ChildNodes
.OfType<XmlElement>())
{
Apply(targetChildElement, transformChildElement);
}
}
}
}
To use a custom transform, you have to import the relevant namespace in your transformation XML:
<xdt:Import assembly="AppHarbor.TransformTester"
namespace="AppHarbor.TransformTester.Transforms"/>
You can then use it to add the connectionStrings
element, if it's missing, like this:
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
<connectionStrings xdt:Transform="Merge" />
<connectionStrings>
<add name="bar" connectionString="value" xdt:Transform="Insert"/>
</connectionStrings>
</configuration>
We have added our custom transforms to the Web.config transformation tester which is open source (and we accept contributions). Check out the Merge and MergeBefore (which takes an XPath expression like InsertBefore
) transforms and the unit tests to get an idea of how custom transforms work. And give them a try live on the transformation tester running on AppHarbor.