Skip to content

Instantly share code, notes, and snippets.

@michaellwest
Last active March 3, 2026 00:45
Show Gist options
  • Select an option

  • Save michaellwest/ad10b91ff6650f32171cc4db0262b5ff to your computer and use it in GitHub Desktop.

Select an option

Save michaellwest/ad10b91ff6650f32171cc4db0262b5ff to your computer and use it in GitHub Desktop.
Pure PowerShell script to apply an XDT transform with a typicaly web.config found in ASP.Net.
function Get-XdtNodeXPath {
param([System.Xml.XmlNode]$Node)
$parts = @($Node.LocalName)
$current = $Node
while ($current.ParentNode -and $current.ParentNode.NodeType -ne [System.Xml.XmlNodeType]::Document) {
$current = $current.ParentNode
$parts = @($current.LocalName) + $parts
}
return "/" + ($parts -join "/")
}
function Merge-XdtTransform {
<#
.SYNOPSIS
A function to apply Xml Configuration Transforms without the need for Microsoft.Web.XmlTransform.dll.
.NOTES
Michael West
.EXAMPLE
Merge-XdtTransform -Base $xmlBase -Transform $xmlTransform
#>
param(
[Parameter(Mandatory)]
[ValidateNotNull()]
[xml]$Base,
[Parameter(Mandatory)]
[ValidateNotNull()]
[xml]$Transform
)
$document = $Base.Clone()
try {
Write-Host "Processing Xdt Transforms"
#https://learn.microsoft.com/en-us/previous-versions/aspnet/dd465326(v=vs.110)
$namespaceManager = New-Object System.Xml.XmlNamespaceManager($transform.NameTable)
$namespaceManager.AddNamespace("xdt", "http://schemas.microsoft.com/XML-Document-Transform")
foreach ($transformNode in $transform.SelectNodes("//*[@xdt:Transform]", $namespaceManager)) {
$transformType = $transformNode.Attributes["xdt:Transform"]
$nodesToModify = & {
$xpath = Get-XdtNodeXPath $transformNode
$locator = $transformNode.Attributes["xdt:Locator"]
if($locator -and $locator.Value -like "Match*") {
#Locator="Match(comma-delimited list of one or more attribute names)"
#xdt:Locator="Match(key)"
$matchAttributeNames = $locator.Value.Replace("Match(","").Replace(")","").Split(",")
foreach($matchAttributeName in $matchAttributeNames) {
$matchAttributeValue = $transformNode.Attributes[$matchAttributeName].Value
$xpath += "[@$($matchAttributeName)='$($matchAttributeValue)']"
}
$document.SelectNodes($xpath)
} elseif($locator -and $locator.Value -like "XPath*") {
#Locator="XPath(XPath expression)"
#xdt:Locator="XPath(configuration/connectionStrings/add[@name='PaymentsContext'])"
$xpath = $locator.Value.Replace("XPath(","").Replace(")","")
$document.SelectNodes($xpath)
} elseif($locator -and $locator.Value -like "Condition*") {
#Locator="Condition(XPath expression)"
#xdt:Locator="Condition(@key='IsStaging')"
if ($locator.Value -match "Condition\((.+)\)") {
$condition = $matches[1]
$xpath += "[$($condition)]"
$document.SelectNodes($xpath)
}
} elseif (!$locator) {
$document.SelectSingleNode($xpath)
}
}
$keepProcessing = $true
$nodeQueue = New-Object System.Collections.Queue
@($nodesToModify) | ForEach-Object { $nodeQueue.Enqueue($_) > $null }
while($keepProcessing) {
$nodeToModify = if($nodeQueue.Count -gt 0) { $nodeQueue.Dequeue() } else { $null }
if($nodeQueue.Count -eq 0) {
$keepProcessing = $false
}
switch -Wildcard ($transformType.Value) {
"CommentOut" {
if(!$nodeToModify) {
Write-Host "- [CommentOut] $($transformNode.ParentNode.LocalName)/$($transformNode.LocalName) skipped" -ForegroundColor Yellow
continue
}
$parentNode = $nodeToModify.ParentNode
$comment = [string]::Concat(" ", $nodeToModify.OuterXml, " ")
$newNode = $document.CreateComment($comment)
Write-Host "- [CommentOut] $($transformNode.ParentNode.LocalName)/$($transformNode.LocalName)" -ForegroundColor Blue
$parentNode.InsertAfter($newNode, $nodeToModify) > $null
$parentNode.RemoveChild($nodeToModify) > $null
}
"SetAttributes*" {
if(!$nodeToModify) {
Write-Host "- [SetAttributes] $($transformNode.ParentNode.LocalName)/$($transformNode.LocalName) skipped" -ForegroundColor Yellow
continue
}
Write-Host "- [SetAttributes] $($transformNode.ParentNode.LocalName)/$($transformNode.LocalName)" -ForegroundColor Blue
$attributesToSet = $transformType.Value.Replace("SetAttributes","").Replace("(","").Replace(")","").Split(",", [System.StringSplitOptions]::RemoveEmptyEntries)
$attributeLookup = New-Object System.Collections.Generic.HashSet[string](@(,$attributesToSet))
$transformNode.Attributes | ForEach-Object {
if ($_.Name -notlike "xdt:*" -and ($attributeLookup.Count -eq 0 -or $attributeLookup.Contains($_.Name))) {
$nodeToModify.SetAttribute($_.Name, $_.Value) > $null
}
}
}
"Replace" {
if(!$nodeToModify) {
Write-Host "- [Replace] $($transformNode.ParentNode.LocalName)/$($transformNode.LocalName) skipped" -ForegroundColor Yellow
continue
}
$parentNode = $nodeToModify.ParentNode
$newNode = $document.ImportNode($transformNode, $true)
$transformNode.Attributes | ForEach-Object {
if ($_.Name -like "xdt:*") {
$newNode.RemoveAttribute($_.Name) > $null
}
}
Write-Host "- [Replace] $($transformNode.ParentNode.LocalName)/$($transformNode.LocalName)" -ForegroundColor Blue
$parentNode.ReplaceChild($newNode, $nodeToModify) > $null
}
"Insert" {
$parentNode = $nodeToModify.ParentNode
if(!$parentNode) {
#Node is missing so let's get the parent.
$parentNode = $document.SelectSingleNode((Get-XdtNodeXPath $transformNode.ParentNode))
}
if(!$parentNode) {
Write-Host "- [Insert] $($transformNode.ParentNode.LocalName)/$($transformNode.LocalName) skipped" -ForegroundColor Yellow
continue
}
$newNode = $document.ImportNode($transformNode, $true)
$transformNode.Attributes | ForEach-Object {
if ($_.Name -like "xdt:*") {
$newNode.RemoveAttribute($_.Name) > $null
}
}
Write-Host "- [Insert] $($transformNode.ParentNode.LocalName)/$($transformNode.LocalName)" -ForegroundColor Blue
$parentNode.AppendChild($newNode) > $null
}
"InsertIfMissing" {
if($nodeToModify) {
Write-Host "- [InsertIfMissing] $($transformNode.ParentNode.LocalName)/$($transformNode.LocalName) skipped" -ForegroundColor Yellow
continue
}
$parentNode = $document.SelectSingleNode((Get-XdtNodeXPath $transformNode.ParentNode))
$newNode = $document.ImportNode($transformNode, $true)
$transformNode.Attributes | ForEach-Object {
if ($_.Name -like "xdt:*") {
$newNode.RemoveAttribute($_.Name) > $null
}
}
Write-Host "- [InsertIfMissing] $($transformNode.ParentNode.LocalName)/$($transformNode.LocalName)" -ForegroundColor Blue
$parentNode.AppendChild($newNode) > $null
}
"InsertBefore*" {
#Example: InsertBefore(/configuration/system.web/membership)
$path = if ($transformType.Value -match '^InsertBefore\((.+)\)$') { $matches[1] } else { $null }
$beforeNode = if ($path) { $document.SelectSingleNode($path) } else { $null }
$newNode = $document.ImportNode($transformNode, $true)
$transformNode.Attributes | ForEach-Object {
if ($_.Name -like "xdt:*") {
$newNode.RemoveAttribute($_.Name) > $null
}
}
if ($beforeNode) {
$parentNode = $beforeNode.ParentNode
Write-Host "- [InsertBefore] $($transformNode.ParentNode.LocalName)/$($transformNode.LocalName) before $($beforeNode.LocalName)" -ForegroundColor Blue
$parentNode.InsertBefore($newNode, $beforeNode) > $null
} else {
# Note: This behavior deviates from the original Xml Transform library.
# If the element does not exist, the library just errors out.
# Here we go ahead and append it because that's what the user likely wanted anyways.
$parentNode = $document.SelectSingleNode((Get-XdtNodeXPath $transformNode.ParentNode))
if (!$parentNode) {
Write-Host "- [InsertBefore] $($transformNode.ParentNode.LocalName)/$($transformNode.LocalName) skipped" -ForegroundColor Yellow
continue
}
Write-Host "- [InsertBefore] $($transformNode.ParentNode.LocalName)/$($transformNode.LocalName)" -ForegroundColor Blue
$parentNode.AppendChild($newNode) > $null
}
}
"InsertAfter*" {
#Example: InsertAfter(/configuration/system.webServer)
$path = if ($transformType.Value -match '^InsertAfter\((.+)\)$') { $matches[1] } else { $null }
$afterNode = if ($path) { $document.SelectSingleNode($path) } else { $null }
$newNode = $document.ImportNode($transformNode, $true)
$transformNode.Attributes | ForEach-Object {
if ($_.Name -like "xdt:*") {
$newNode.RemoveAttribute($_.Name) > $null
}
}
if ($afterNode) {
$parentNode = $afterNode.ParentNode
Write-Host "- [InsertAfter] $($transformNode.ParentNode.LocalName)/$($transformNode.LocalName) after $($afterNode.LocalName)" -ForegroundColor Blue
$parentNode.InsertAfter($newNode, $afterNode) > $null
} else {
$parentNode = $document.SelectSingleNode((Get-XdtNodeXPath $transformNode.ParentNode))
if (!$parentNode) {
Write-Host "- [InsertAfter] $($transformNode.ParentNode.LocalName)/$($transformNode.LocalName) skipped" -ForegroundColor Yellow
continue
}
Write-Host "- [InsertAfter] $($transformNode.ParentNode.LocalName)/$($transformNode.LocalName)" -ForegroundColor Blue
$parentNode.AppendChild($newNode) > $null
}
}
"Remove" {
if(!$nodeToModify) {
Write-Host "- [Remove] $($transformNode.ParentNode.LocalName)/$($transformNode.LocalName) skipped" -ForegroundColor Yellow
continue
}
$parentNode = $nodeToModify.ParentNode
Write-Host "- [Remove] $($transformNode.ParentNode.LocalName)/$($transformNode.LocalName)" -ForegroundColor Blue
$parentNode.RemoveChild($nodeToModify) > $null
}
"RemoveAll" {
# TODO: Should RemoveAll consider the element type?
$parentNode = $nodeToModify.ParentNode
if(!$parentNode) {
Write-Host "- [RemoveAll] $($transformNode.ParentNode.LocalName)/$($transformNode.LocalName) skipped" -ForegroundColor Yellow
continue
}
Write-Host "- [RemoveAll] $($transformNode.ParentNode.LocalName)/$($transformNode.LocalName)" -ForegroundColor Blue
if($transformNode.Attributes["xdt:Locator"]) {
$parentNode.RemoveChild($nodeToModify) > $null
} else {
$matchingNodes = $parentNode.SelectNodes("$($transformNode.LocalName)")
$matchingNodes | ForEach-Object { $parentNode.RemoveChild($_) > $null }
#$parentNode.RemoveAll() > $null
}
}
"RemoveAttributes*" {
if(!$nodeToModify) {
Write-Host "- [RemoveAttributes] $($transformNode.ParentNode.LocalName)/$($transformNode.LocalName) skipped" -ForegroundColor Yellow
continue
}
Write-Host "- [RemoveAttributes] $($transformNode.ParentNode.LocalName)/$($transformNode.LocalName)" -ForegroundColor Blue
$rawList = $transformType.Value.Replace("RemoveAttributes","").Replace("(","").Replace(")","").Trim()
if ([string]::IsNullOrEmpty($rawList)) {
$nodeToModify.Attributes.RemoveAll() > $null
} else {
$rawList.Split(",") | ForEach-Object {
$nodeToModify.Attributes.Remove($nodeToModify.Attributes[$_]) > $null
}
}
}
}
}
}
} catch {
Write-Error "Error applying XDT transform: $_"
return $Base
}
return $document
}
. .\Merge-XdtTransform.ps1
$XdtDllPath = ".\Microsoft.Web.XmlTransform.dll"
Add-Type -Path $XdtDllPath
function Invoke-XdtTransform {
param(
[string]$Base,
[string]$Transform,
[ref]$Output
)
$stringReader = New-Object System.IO.StringReader($Base)
$xmlReader = [System.Xml.XmlReader]::Create($stringReader)
$target = New-Object Microsoft.Web.XmlTransform.XmlTransformableDocument
$target.PreserveWhitespace = $true
$target.Load($xmlReader)
$transformation = New-Object Microsoft.Web.XmlTransform.XmlTransformation($Transform, $false, $null);
if (!$transformation.Apply($target))
{
return $Base
}
$stringWriter = New-Object System.IO.StringWriter
$xmlWriter = New-Object System.XMl.XmlTextWriter $stringWriter
$target.WriteContentTo($xmlWriter)
$xmlWriter.Flush()
$stringWriter.Flush()
return [xml]($stringWriter.ToString())
}
[xml]$xmlBase = @"
<?xml version="1.0" encoding="utf-8"?>
<!--
For more information on how to configure your ASP.NET application, please visit
https://go.microsoft.com/fwlink/?LinkId=169433
-->
<configuration>
<configSections>
<section name="sitecore" type="Sitecore.Configuration.RuleBasedConfigReader, Sitecore.Kernel" />
</configSections>
<connectionStrings>
<add name="WebContext" connectionString="" providerName="System.Data.SqlClient" />
<add name="PaymentsContext" connectionString="" providerName="System.Data.SqlClient" />
</connectionStrings>
<appSettings file="Base.config">
<add key="Environment" value="" />
<add key="IsTestMode" value="true" />
<add key="IsStaging" value="true" />
<add key="IsDisabled" value="true" />
</appSettings>
<rewrite>
<rules>
<rule />
<rule />
</rules>
</rewrite>
<system.web>
<compilation defaultLanguage="c#" debug="false" targetFramework="4.8" />
<membership defaultProvider="sitecore" hashAlgorithmType="SHA1">
</membership>
<compilation>
<assemblies>
</assemblies>
</compilation>
<application coldstart="true" user="root" />
</system.web>
<system.webServer>
<urlCompression doDynamicCompression="true" />
<httpCookies httpOnlyCookies="true" requireSSL="false" />
</system.webServer>
</configuration>
"@
[xml]$xmlTransform = @"
<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
<configSections>
<section xdt:Transform="Replace" xdt:Locator="Match(name)" name="sitecore" type="Scms.Foundation.Configuration.EnvironmentVariablesConfigReader, Scms.Foundation"/>
</configSections>
<connectionStrings>
<add xdt:Transform="SetAttributes" xdt:Locator="Match(name)" name="WebContext" connectionString="data source=CHS-CADEVDB01;initial catalog=Web;integrated security=True;MultipleActiveResultSets=True;App=EntityFramework" />
<add xdt:Transform="Replace" xdt:Locator="XPath(configuration/connectionStrings/add[@name='PaymentsContext'])" name="PaymentsContext" connectionString="data source=CHS-CADEVDB01;initial catalog=Payments;integrated security=True;MultipleActiveResultSets=True;App=EntityFramework" />
</connectionStrings>
<appSettings>
<add xdt:Transform="SetAttributes" xdt:Locator="Match(key)" key="Environment" value="Development" />
<add xdt:Transform="CommentOut" xdt:Locator="Match(key)" key="IsTestMode" />
<add xdt:Locator="Condition(@key='IsStaging' or @key='IsDisabled')" xdt:Transform="Remove"/>
</appSettings >
<rewrite>
<rules>
<rule xdt:Transform="RemoveAll" />
</rules>
</rewrite>
<system.web>
<machineKey xdt:Transform="Remove" />
<machineKey xdt:Transform="InsertBefore(/configuration/system.web/membership)" validationKey="42F21E06B91BC207DAFFF53FDD44375515938CCEF1BF7F2CA70052B3B024C009B385A6E86D816ED1B0144D7BDEAEEECD9C14B20BBD02A29B1C0B1683493497D6" decryptionKey="DF97132E23EE6B974E7EC9FA3701B71BC4EBA440F1A56C744179529786C6EE58" validation="SHA1" decryption="AES" />
<compilation>
<assemblies xdt:Locator="Condition(count(*) = 0)" xdt:Transform="Remove"/>
</compilation>
<application coldstart="true" user="root" xdt:Transform="Remove" />
<compilation xdt:Locator="Condition(count(*) = 0)" xdt:Transform="Remove" />
<application xdt:Transform="Replace" user="guest" type="web" />
</system.web>
<location xdt:Transform="InsertAfter(/configuration/system.webServer)" path="sitecore">
<system.web>
<authorization>
<deny users="*"/>
</authorization>
</system.web>
</location>
<system.webServer>
<urlCompression xdt:Transform="Insert" doDynamicCompression="true" />
<httpCookies xdt:Transform="InsertIfMissing" httpOnlyCookies="true" requireSSL="true" />
</system.webServer>
<compilation xdt:Transform="RemoveAttributes(debug)" />
</configuration>
"@
function Format-Xml ([xml]$xml, $indent=2) {
$StringWriter = New-Object System.IO.StringWriter
$XmlWriter = New-Object System.XMl.XmlTextWriter $StringWriter
$xmlWriter.Formatting = "indented"
$xmlWriter.Indentation = $Indent
$xml.WriteContentTo($XmlWriter)
$XmlWriter.Flush()
$StringWriter.Flush()
Write-Output $StringWriter.ToString()
}
#if(Merge-XdtTransform -Base $xmlBase -Transform $xmlTransform) {
# Format-Xml -Xml $xmlBase
#}
function Assert-AreEqual {
param(
[string]$TestName,
[object]$Expected,
[object]$Actual
)
$sb = New-Object System.Text.StringBuilder
if ($Expected -ne $Actual) {
$sb.AppendLine("[$($TestName)] Failed") > $null
$sb.AppendLine("Expected") > $null
$sb.AppendLine((Format-Xml -Xml $Expected)) > $null
$sb.AppendLine("Actual") > $null
$sb.AppendLine((Format-Xml -Xml $Actual)) > $null
Write-Host $sb.ToString() -ForegroundColor Red -BackgroundColor White
return $false
} else {
$sb.AppendLine("[$($TestName)] Succeeded") > $null
Write-Host $sb.ToString() -ForegroundColor Green
}
return $true
}
[xml]$replaceTestBaseXml = @"
<?xml version="1.0"?>
<configuration>
<system.web>
<compilation debug="true" strict="true" batch="true" />
<application coldstart="true" user="root">
<subtag>old content</subtag>
<subtag2>old content</subtag2>
</application>
</system.web>
</configuration>
"@
[xml]$replaceTestTransformXml = @"
<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
<system.web>
<!-- Replaces the whole tag -->
<application xdt:Transform="Replace" user="guest" type="web" >
<sub>new content</sub>
</application>
</system.web>
</configuration>
"@
$testExpectedXml = Invoke-XdtTransform -Base $replaceTestBaseXml.OuterXml -Transform $replaceTestTransformXml.OuterXml
$testActualXml = Merge-XdtTransform -Base $replaceTestBaseXml -Transform $replaceTestTransformXml
Assert-AreEqual -TestName "Test_Replace" -Expected $testExpectedXml.OuterXml -Actual $testActualXml.OuterXml > $null
[xml]$removeTestBaseXml = @"
<?xml version="1.0"?>
<configuration>
<appSettings>
<add key="IsStaging" value="True" />
<add key="Environment" value="Dev" />
<add key="Version" value="14" />
</appSettings>
</configuration>
"@
[xml]$removeTestTransformXml = @"
<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
<appSettings>
<add key="Environment"
xdt:Locator="Match(key)" xdt:Transform="Remove" />
</appSettings>
</configuration>
"@
$testExpectedXml = Invoke-XdtTransform -Base $removeTestBaseXml.Clone().OuterXml -Transform $removeTestTransformXml.OuterXml
$testActualXml = Merge-XdtTransform -Base $removeTestBaseXml -Transform $removeTestTransformXml
Assert-AreEqual -TestName "Test_Remove" -Expected $testExpectedXml.OuterXml -Actual $testActualXml.OuterXml > $null
[xml]$removeAllTestBaseXml = @"
<?xml version="1.0"?>
<configuration>
<appSettings>
<add key="IsStaging" value="True" />
<add key="Environment" value="Dev" />
<add key="Version" value="14" />
<object key="" />
</appSettings>
</configuration>
"@
[xml]$removeAllTestTransformXml = @"
<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
<appSettings>
<add xdt:Transform="RemoveAll" />
</appSettings>
</configuration>
"@
$testExpectedXml = Invoke-XdtTransform -Base $removeAllTestBaseXml.OuterXml -Transform $removeAllTestTransformXml.OuterXml
$testActualXml = Merge-XdtTransform -Base $removeAllTestBaseXml -Transform $removeAllTestTransformXml
Assert-AreEqual -TestName "Test_RemoveAll" -Expected $testExpectedXml.OuterXml -Actual $testActualXml.OuterXml > $null
[xml]$removeAllMatchTestBaseXml = @"
<?xml version="1.0"?>
<configuration>
<appSettings>
<add key="IsStaging" value="True" />
<add key="Environment" value="Dev" />
<add key="SiteStyle" value="dark" />
<add key="Environment" value="Prod" />
<add key="Version" value="14" />
<add key="Environment" value="Staging" />
</appSettings>
</configuration>
"@
[xml]$removeAllMatchTestTransformXml = @"
<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
<appSettings>
<add key="Environment"
xdt:Locator="Match(key)" xdt:Transform="RemoveAll"/>
</appSettings>
</configuration>
"@
$testExpectedXml = Invoke-XdtTransform -Base $removeAllMatchTestBaseXml.OuterXml -Transform $removeAllMatchTestTransformXml.OuterXml
$testActualXml = Merge-XdtTransform -Base $removeAllMatchTestBaseXml -Transform $removeAllMatchTestTransformXml
Assert-AreEqual -TestName "Test_RemoveAll_Match" -Expected $testExpectedXml.OuterXml -Actual $testActualXml.OuterXml > $null
[xml]$insertTestBaseXml = @"
<?xml version="1.0"?>
<configuration>
<appSettings>
<add key="IsStaging" value="True" />
<add key="Environment" value="Dev" />
</appSettings>
</configuration>
"@
[xml]$insertTestTransformXml = @"
<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
<appSettings>
<add key="SiteStyle" value="dark"
xdt:Locator="Match(key)" xdt:Transform="Insert" />
</appSettings>
</configuration>
"@
$testExpectedXml = Invoke-XdtTransform -Base $insertTestBaseXml.OuterXml -Transform $insertTestTransformXml.OuterXml
$testActualXml = Merge-XdtTransform -Base $insertTestBaseXml -Transform $insertTestTransformXml
Assert-AreEqual -TestName "Test_Insert" -Expected $testExpectedXml.OuterXml -Actual $testActualXml.OuterXml > $null
[xml]$insertIfMissingTestBaseXml = @"
<?xml version="1.0"?>
<configuration>
<appSettings>
<add key="IsStaging" value="True" />
<add key="Environment" value="Dev" />
</appSettings>
</configuration>
"@
[xml]$insertIfMissingTestTransformXml = @"
<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
<appSettings>
<!-- Will not be inserted since already existing in the source-->
<add key="Environment" value="Prod"
xdt:Locator="Match(key)" xdt:Transform="InsertIfMissing" />
<add key="SiteStyle" value="Dark"
xdt:Locator="Match(key)" xdt:Transform="InsertIfMissing" />
</appSettings>
</configuration>
"@
$testExpectedXml = Invoke-XdtTransform -Base $insertIfMissingTestBaseXml.OuterXml -Transform $insertIfMissingTestTransformXml.OuterXml
$testActualXml = Merge-XdtTransform -Base $insertIfMissingTestBaseXml -Transform $insertIfMissingTestTransformXml
Assert-AreEqual -TestName "Test_InsertIfMissing" -Expected $testExpectedXml.OuterXml -Actual $testActualXml.OuterXml > $null
[xml]$insertBeforeTestBaseXml = @"
<?xml version="1.0"?>
<configuration>
<appSettings>
<add key="IsStaging" value="True" />
<add key="Environment" value="Dev" />
<add key="Version" value="14" />
</appSettings>
</configuration>
"@
[xml]$insertBeforeTestTransformXml = @"
<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
<appSettings>
<add key="SiteStyle" value="dark"
xdt:Transform="InsertBefore(/configuration/appSettings/add[@key='Environment'])"/>
</appSettings>
</configuration>
"@
$testExpectedXml = Invoke-XdtTransform -Base $insertBeforeTestBaseXml.OuterXml -Transform $insertBeforeTestTransformXml.OuterXml
$testActualXml = Merge-XdtTransform -Base $insertBeforeTestBaseXml -Transform $insertBeforeTestTransformXml
Assert-AreEqual -TestName "Test_InsertBefore" -Expected $testExpectedXml.OuterXml -Actual $testActualXml.OuterXml > $null
[xml]$insertAfterTestBaseXml = @"
<?xml version="1.0"?>
<configuration>
<appSettings>
<add key="IsStaging" value="True" />
<add key="Environment" value="Dev" />
<add key="Version" value="14" />
</appSettings>
</configuration>
"@
[xml]$insertAfterTestTransformXml = @"
<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
<appSettings>
<add key="SiteStyle" value="dark"
xdt:Transform="InsertAfter(/configuration/appSettings/add[@key='Environment'])"/>
</appSettings>
</configuration>
"@
$testExpectedXml = Invoke-XdtTransform -Base $insertAfterTestBaseXml.OuterXml -Transform $insertAfterTestTransformXml.OuterXml
$testActualXml = Merge-XdtTransform -Base $insertAfterTestBaseXml -Transform $insertAfterTestTransformXml
Assert-AreEqual -TestName "Test_InsertAfter" -Expected $testExpectedXml.OuterXml -Actual $testActualXml.OuterXml > $null
[xml]$commentOutTestBaseXml = @"
<?xml version="1.0"?>
<configuration>
<appSettings>
<add key="IsStaging" value="True" />
<add key="Environment" value="Dev" />
<add key="Version" value="14" />
</appSettings>
</configuration>
"@
[xml]$commentOutTestTransformXml = @"
<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
<appSettings>
<add key="Environment"
xdt:Locator="Match(key)" xdt:Transform="CommentOut" />
</appSettings>
</configuration>
"@
$testExpectedXml = Invoke-XdtTransform -Base $commentOutTestBaseXml.OuterXml -Transform $commentOutTestTransformXml.OuterXml
$testActualXml = Merge-XdtTransform -Base $commentOutTestBaseXml -Transform $commentOutTestTransformXml
Assert-AreEqual -TestName "Test_CommentOut" -Expected $testExpectedXml.OuterXml -Actual $testActualXml.OuterXml > $null
[xml]$removeAttributesTestBaseXml = @"
<?xml version="1.0"?>
<configuration>
<system.web>
<application coldstart="true" user="root" />
<compilation debug="true" strict="true" batch="true" />
</system.web>
</configuration>
"@
[xml]$removeAttributesTestTransformXml = @"
<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
<system.web>
<!-- Remove all attributes-->
<application xdt:Transform="RemoveAttributes" />
<!-- Remove specific attributes-->
<compilation xdt:Transform="RemoveAttributes(debug,strict)" />
</system.web>
</configuration>
"@
$testExpectedXml = Invoke-XdtTransform -Base $removeAttributesTestBaseXml.OuterXml -Transform $removeAttributesTestTransformXml.OuterXml
$testActualXml = Merge-XdtTransform -Base $removeAttributesTestBaseXml -Transform $removeAttributesTestTransformXml
Assert-AreEqual -TestName "Test_RemoveAttributes" -Expected $testExpectedXml.OuterXml -Actual $testActualXml.OuterXml > $null
[xml]$setAttributesTestBaseXml = @"
<?xml version="1.0"?>
<configuration>
<system.web>
<application coldstart="true" user="root" />
<compilation debug="true" strict="true" batch="true" />
</system.web>
</configuration>
"@
[xml]$setAttributesTestTransformXml = @"
<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
<system.web>
<!-- Add or updates all attributes-->
<application user="guest" newAttribute="value"
xdt:Transform="SetAttributes" />
<!-- Add or updates specific attributes-->
<compilation debug="false" strict="false" batch="false"
xdt:Transform="SetAttributes(debug,strict)" />
</system.web>
</configuration>
"@
$testExpectedXml = Invoke-XdtTransform -Base $setAttributesTestBaseXml.OuterXml -Transform $setAttributesTestTransformXml.OuterXml
$testActualXml = Merge-XdtTransform -Base $setAttributesTestBaseXml -Transform $setAttributesTestTransformXml
Assert-AreEqual -TestName "SetAttributes_Element" -Expected $testExpectedXml.OuterXml -Actual $testActualXml.OuterXml > $null
[xml]$setAttributesMatchTestBaseXml = @"
<?xml version="1.0"?>
<configuration>
<appSettings>
<add key="IsStaging" value="True" />
<add key="Environment" value="Dev" />
<add key="SiteStyle" value="dark" />
</appSettings>
</configuration>
"@
[xml]$setAttributesMatchTestTransformXml = @"
<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
<appSettings>
<add key="Environment" value="Prod"
xdt:Locator="Match(key)" xdt:Transform="SetAttributes"/>
</appSettings>
</configuration>
"@
$testExpectedXml = Invoke-XdtTransform -Base $setAttributesMatchTestBaseXml.OuterXml -Transform $setAttributesMatchTestTransformXml.OuterXml
$testActualXml = Merge-XdtTransform -Base $setAttributesMatchTestBaseXml -Transform $setAttributesMatchTestTransformXml
Assert-AreEqual -TestName "Test_SetAttributesMatch" -Expected $testExpectedXml.OuterXml -Actual $testActualXml.OuterXml > $null
# TODO: Add tests for Condition and XPath
# Test_InsertBefore_CrossSection
# Reproduces Bug 1: the first //add in the document is under <connectionStrings>, but the
# InsertBefore XPath targets a node under <appSettings>. Without the fix this throws because
# <connectionStrings> is used as $parentNode while the reference node lives under <appSettings>.
[xml]$insertBeforeCrossSectionBaseXml = @"
<?xml version="1.0"?>
<configuration>
<connectionStrings>
<add name="Db" connectionString="" />
</connectionStrings>
<appSettings>
<add key="Env" value="Dev" />
<add key="Version" value="1" />
</appSettings>
</configuration>
"@
[xml]$insertBeforeCrossSectionTransformXml = @"
<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
<appSettings>
<add key="NewSetting" value="X"
xdt:Transform="InsertBefore(/configuration/appSettings/add[@key='Version'])"/>
</appSettings>
</configuration>
"@
$testExpectedXml = Invoke-XdtTransform -Base $insertBeforeCrossSectionBaseXml.OuterXml -Transform $insertBeforeCrossSectionTransformXml.OuterXml
$testActualXml = Merge-XdtTransform -Base $insertBeforeCrossSectionBaseXml -Transform $insertBeforeCrossSectionTransformXml
Assert-AreEqual -TestName "Test_InsertBefore_CrossSection" -Expected $testExpectedXml.OuterXml -Actual $testActualXml.OuterXml > $null
# Test_InsertAfter_CrossSection
# Same cross-section parent mismatch scenario for InsertAfter (Bug 2).
[xml]$insertAfterCrossSectionBaseXml = @"
<?xml version="1.0"?>
<configuration>
<connectionStrings>
<add name="Db" connectionString="" />
</connectionStrings>
<appSettings>
<add key="Env" value="Dev" />
<add key="Version" value="1" />
</appSettings>
</configuration>
"@
[xml]$insertAfterCrossSectionTransformXml = @"
<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
<appSettings>
<add key="NewSetting" value="X"
xdt:Transform="InsertAfter(/configuration/appSettings/add[@key='Env'])"/>
</appSettings>
</configuration>
"@
$testExpectedXml = Invoke-XdtTransform -Base $insertAfterCrossSectionBaseXml.OuterXml -Transform $insertAfterCrossSectionTransformXml.OuterXml
$testActualXml = Merge-XdtTransform -Base $insertAfterCrossSectionBaseXml -Transform $insertAfterCrossSectionTransformXml
Assert-AreEqual -TestName "Test_InsertAfter_CrossSection" -Expected $testExpectedXml.OuterXml -Actual $testActualXml.OuterXml > $null
# Test_RemoveAttributes_All
# Reproduces Bugs 3 & 4: RemoveAttributes with no argument list must call RemoveAll()
# on the correct node. The old code never reached RemoveAll() because Split(",") on an
# empty string produces @("") which is truthy, and it also referenced $nodesToModify instead
# of $nodeToModify.
[xml]$removeAttributesAllBaseXml = @"
<?xml version="1.0"?>
<configuration>
<system.web>
<compilation debug="true" strict="true" batch="true" />
</system.web>
</configuration>
"@
[xml]$removeAttributesAllTransformXml = @"
<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
<system.web>
<compilation xdt:Transform="RemoveAttributes" />
</system.web>
</configuration>
"@
$testExpectedXml = Invoke-XdtTransform -Base $removeAttributesAllBaseXml.OuterXml -Transform $removeAttributesAllTransformXml.OuterXml
$testActualXml = Merge-XdtTransform -Base $removeAttributesAllBaseXml -Transform $removeAttributesAllTransformXml
Assert-AreEqual -TestName "Test_RemoveAttributes_All" -Expected $testExpectedXml.OuterXml -Actual $testActualXml.OuterXml > $null
# Test_InsertBefore_MissingRef
# When the reference node in InsertBefore(...) does not exist in the base document the
# operation should fall back to AppendChild on the declared parent element.
[xml]$insertBeforeMissingRefBaseXml = @"
<?xml version="1.0"?>
<configuration>
<appSettings>
<add key="Env" value="Dev" />
</appSettings>
</configuration>
"@
[xml]$insertBeforeMissingRefTransformXml = @"
<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
<appSettings>
<add key="NewSetting" value="X"
xdt:Transform="InsertBefore(/configuration/appSettings/add[@key='DoesNotExist'])"/>
</appSettings>
</configuration>
"@
$testExpectedXml = Invoke-XdtTransform -Base $insertBeforeMissingRefBaseXml.OuterXml -Transform $insertBeforeMissingRefTransformXml.OuterXml
$testActualXml = Merge-XdtTransform -Base $insertBeforeMissingRefBaseXml -Transform $insertBeforeMissingRefTransformXml
Assert-AreEqual -TestName "Test_InsertBefore_MissingRef" -Expected $testExpectedXml.OuterXml -Actual $testActualXml.OuterXml > $null
# Test_InsertBefore_FunctionXPath
# Reproduces Bug 5: the old Replace(")","") strips ALL closing parentheses, corrupting any
# XPath that contains a function call such as contains(). The regex fix captures only the
# outermost group so inner parentheses are preserved.
[xml]$insertBeforeFunctionXPathBaseXml = @"
<?xml version="1.0"?>
<configuration>
<appSettings>
<add key="Env-Dev" value="1" />
<add key="Env-Prod" value="2" />
</appSettings>
</configuration>
"@
[xml]$insertBeforeFunctionXPathTransformXml = @"
<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
<appSettings>
<add key="NewKey" value="X"
xdt:Transform="InsertBefore(/configuration/appSettings/add[contains(@key,'Prod')])"/>
</appSettings>
</configuration>
"@
$testExpectedXml = Invoke-XdtTransform -Base $insertBeforeFunctionXPathBaseXml.OuterXml -Transform $insertBeforeFunctionXPathTransformXml.OuterXml
$testActualXml = Merge-XdtTransform -Base $insertBeforeFunctionXPathBaseXml -Transform $insertBeforeFunctionXPathTransformXml
Assert-AreEqual -TestName "Test_InsertBefore_FunctionXPath" -Expected $testExpectedXml.OuterXml -Actual $testActualXml.OuterXml > $null
@michaellwest
Copy link
Copy Markdown
Author

Found this neat website to perform transforms in the browser. Convenient to compare with results of this script.

Xdt Playground

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment