59
votes

Is it possible to use Microsoft's XML document transform, for preparing web.configs, outside of MSBuild? I would like to use PowerShell to do these transform without having to run this through the MSBuild engine. If Microsoft had used standard XSLT it would be easy to do in PowerShell. From what I can tell I have to use their C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\v10.0\Web\Microsoft.Web.Publishing.Tasks.dll which requires a build engine. Thanks

8

8 Answers

108
votes

I created a small function to handle Microsoft's XML Document Transform in PowerShell.

I copied the Microsoft.Web.XmlTransform.dll file from Visual Studio build folder to my script's path, but you can reference it from the source folder if you'd like.

function XmlDocTransform($xml, $xdt)
{
    if (!$xml -or !(Test-Path -path $xml -PathType Leaf)) {
        throw "File not found. $xml";
    }
    if (!$xdt -or !(Test-Path -path $xdt -PathType Leaf)) {
        throw "File not found. $xdt";
    }

    $scriptPath = (Get-Variable MyInvocation -Scope 1).Value.InvocationName | split-path -parent
    Add-Type -LiteralPath "$scriptPath\Microsoft.Web.XmlTransform.dll"

    $xmldoc = New-Object Microsoft.Web.XmlTransform.XmlTransformableDocument;
    $xmldoc.PreserveWhitespace = $true
    $xmldoc.Load($xml);

    $transf = New-Object Microsoft.Web.XmlTransform.XmlTransformation($xdt);
    if ($transf.Apply($xmldoc) -eq $false)
    {
        throw "Transformation failed."
    }
    $xmldoc.Save($xml);
}

To transform web.config using web.release.config:

XmlDocTransform -xml "Web.config" -xdt "Web.Release.config"

Alternatively, you can use Sayed's self-bootstraping Xml Transform script, which will take care of getting the Microsoft.Xml.Xdt.dll for you:

https://gist.github.com/sayedihashimi/f1fdc4bfba74d398ec5b

14
votes

The logic of the transformation is contained inside of the TransformXml task itself. If you want to call it from code you would have to use the MSBuild API with a mock engine and execute it. I have some code for this if you want.

In your case since you mentioned PowerShell the best thing for you to do is to just create a wrapper MSBuild file to invoke the TransformXml task. I say this because PowerShell is configured to run under .NET 2.0, but the TransformXml task requires .NET 4.0. In order to call it from a dummy MSBuild file you can check my blog at http://sedodream.com/2010/04/26/ConfigTransformationsOutsideOfWebAppBuilds.aspx, but I've also pasted a sample from that link below.

<Project ToolsVersion="4.0" DefaultTargets="Demo" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <UsingTask TaskName="TransformXml"
             AssemblyFile="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v10.0\Web\Microsoft.Web.Publishing.Tasks.dll"/>

    <Target Name="Demo">
        <TransformXml Source="app.config"
                      Transform="Transform.xml"
                      Destination="app.prod.config"/>
    </Target>
</Project>

For mono, this should work (tested on mono 6.4, macos, 2019) :

<Project DefaultTargets="TransformConfig" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <UsingTask TaskName="TransformXml"
    AssemblyFile="$(MSBuildSDKsPath)/Microsoft.NET.Sdk.Publish/tools/net46/Microsoft.NET.Sdk.Publish.Tasks.dll"/>
  <PropertyGroup>
    <TransformSource>Web.config</TransformSource>
    <Transformer>Web.Live.config</Transformer>
    <Destination>Output.Web.config</Destination>
  </PropertyGroup>
  <Target Name="TransformConfig">
    <Message Text="From TransformSource : $(TransformSource)" />
    <Message Text="Using Transform : $(Transformer)" />
    <Message Text="Output : $(Destination)" />
    <Message Text="MSBuildSDKsPath=$(MSBuildSDKsPath)" Condition="'$(MSBuildSDKsPath)' != ''" />
    <TransformXml Source="$(TransformSource)" Transform="$(Transformer)" Destination="$(Destination)"/>
  </Target>
</Project>

which you can run with just msbuild or supply parameters with

msbuild /p:TransformSource=... /p:Transformer=...
11
votes

Based on Michel's answer I wrote a C# function that will accomplish the same.

Of course you could invoke the resultant DLL with PowerShell, but I was actually looking for a fully programatic version, so here it is, in case anybody else is looking for similar solution:

using Microsoft.Web.XmlTransform;

...

public static void TransformConfig(string configFileName, string transformFileName)
{
     var document = new XmlTransformableDocument();
     document.PreserveWhitespace = true;
     document.Load(configFileName);

     var transformation = new XmlTransformation(transformFileName);
     if (!transformation.Apply(document))
     {
         throw new Exception("Transformation Failed");
     }
     document.Save(configFileName);
}

You will just need to include reference to following:

C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\v11.0\Web\Microsoft.Web.XmlTransform.dll

10
votes

Microsoft has posted XDT to codeplex http://xdt.codeplex.com and as a NuGet package https://www.nuget.org/packages/Microsoft.Web.Xdt/. I have also created a NuGet pig with an MSBuild task, TransformXml, and an .exe to invoke them https://www.nuget.org/packages/SlowCheetah.Xdt/1.1.6-beta.

For PowerShell I've created a self-bootstrapping script which you can use https://gist.github.com/sayedihashimi/f1fdc4bfba74d398ec5b.

More about self bootstrapping scripts at http://sedodream.com/2014/07/22/StopCheckinginBinariesInsteadCreateSelfbootstrappingScripts.aspx.

7
votes

I updated the script a bit to make it work with the latest version of powershell and make it a bit easier.

function XmlDocTransform($xml, $xdt)
{
      $scriptpath = $PSScriptRoot + "\"
      $xmlpath = $scriptpath + $xml
      $xdtpath = $scriptpath + $xdt

      if (!($xmlpath) -or !(Test-Path -path ($xmlpath) -PathType Leaf)) {
         throw "Base file not found. $xmlpath";
      }

      if (!($xdtpath) -or !(Test-Path -path ($xdtpath) -PathType Leaf)) {
         throw "Transform file not found. $xdtpath";
      }

      Add-Type -LiteralPath "$PSScriptRoot\Microsoft.Web.XmlTransform.dll"

      $xmldoc = New-Object   Microsoft.Web.XmlTransform.XmlTransformableDocument;
      $xmldoc.PreserveWhitespace = $true
      $xmldoc.Load($xmlpath);

      $transf = New-Object Microsoft.Web.XmlTransform.XmlTransformation($xdtpath);
      if ($transf.Apply($xmldoc) -eq $false)
      {
          throw "Transformation failed."
      }
      $xmldoc.Save($xmlpath);

      Write-Host "Transformation succeeded" -ForegroundColor Green
  }

And to invoke the function use

 XmlDocTransform "App.config" "App.acc.config"
5
votes

Take a looked at using MSDeploy since it has PowerShell scripting APIs that allow you to transform and deploy your package.

You can also look at XML-Document-Transform which if you wanted to you can write your own code to perform the Transform.

Here is a codeplex project that did something similar.XDT Transformation Tool

0
votes

So extended slightly to work recursively

    function XmlDocTransform($xml, $xdt)
    {
        if (!$xml -or !(Test-Path -path $xml -PathType Leaf)) {
            throw "File not found. $xml";
        }
        if (!$xdt -or !(Test-Path -path $xdt -PathType Leaf)) {
            throw "File not found. $xdt";
        }
        $scriptPath = (Get-Variable MyInvocation -Scope 1).Value.InvocationName | split-path -parent
        Add-Type -LiteralPath "$scriptPath\Microsoft.Web.XmlTransform.dll"
        $xmldoc = New-Object Microsoft.Web.XmlTransform.XmlTransformableDocument;
        $xmldoc.PreserveWhitespace = $true
        $xmldoc.Load($xml);
        $transf = New-Object Microsoft.Web.XmlTransform.XmlTransformation($xdt);
        if ($transf.Apply($xmldoc) -eq $false)
        {
            throw "Transformation failed."
        }
        $xmldoc.Save($xml);
    }
    function DoConfigTransform($webFolder, $environment)
    {
        $allConfigFiles = Get-ChildItem $webFolder -File -Filter *.config -Recurse
          $transformFiles = $allConfigFiles | Where-Object {$_.Name -like ("*." + $environment + ".config")} | %{$_.fullname}
          ForEach($item in $transformFiles)
          {
            $origFile = $item -replace("$environment.",'')
              XmlDocTransform -xml $origFile -xdt $origFile$item
              #Write-Output ("orig = " + $origFile + ", transform = " + $item)
          }
          cd C:\WebApplications\xxx\xxx\xxx\
          .\PostDeploy.ps1
    }
    DoConfigTransform -webFolder "C:\WebApplications\xxx\xxx\xxx" -environment "xx-xxx-xx"

So the DoConfigTransform logic goes:

  • Find all config files recursively
  • Find all transform templates for the environment we are in #passed in as a parameter
  • For each transform file, find the corresponding config
  • Then do transform
  • Code runs a postdeploy script to remove all unwanted transform files.
0
votes

I came to this post well after it original posting but it helped resolve my issue so I thought I'd put my solution (based on the solution above) here for anyone having the same problem with a current visual studio / msbuild install.

Currently in VS build time transforms are done using the SlowCheetah nuget pkg. If you can rely on this package in your project you can use the script I have placed below and it will automatically find the required assembly based on the installed cheetah version and perform your transform as needed.

Hope this helps someone.

param(  
  [ValidateScript({$(Test-Path $_) -eq $true})]
  [string] $NuGetRootPath,

  [ValidateScript({$(Test-Path $_) -eq $true})]
  [string] $BaseFile,

  [ValidateScript({$(Test-Path $_) -eq $true})]
  [string] $TransformFile,

  [string] $TargetPath
)

"[INFO] Creating Custom XML Transform..." | Out-Default   
"[INFO] ==> Source:    $BaseFile" | Out-Default   
"[INFO] ==> Transform: $TransformFile" | Out-Default   
"[INFO] ==> Target:    $TargetPath" | Out-Default   

$cheetahDir = Join-Path $NuGetRootPath *SlowCheetah* | Resolve-Path | Select-Object -Last 1 -ExpandProperty Path
$xformDll = Join-Path $cheetahDir "tools\Microsoft.Web.XmlTransform.dll" | Resolve-Path | Select-Object -ExpandProperty Path
Add-Type -LiteralPath $xformDll

$xmldoc = New-Object Microsoft.Web.XmlTransform.XmlTransformableDocument;
$xmldoc.PreserveWhitespace = $true
$xmldoc.Load($BaseFile);

"[INFO] Running Transform..." | Out-Default
$transf = New-Object Microsoft.Web.XmlTransform.XmlTransformation($TransformFile);
if ($transf.Apply($xmldoc) -eq $false) {
    throw "[ERROR] Transformation failed."
}

$xmldoc.Save($TargetPath);
"[INFO] Transformation Complete..." | Out-Default