Working with XML? Don’t forget about XPath

.Net and ASP.Net applications use XML-based configuration files to store and manage their configuration data.  The files are typically stored with the application files and aptly named *.exe.config or web.config respectively.  MS Exchange is largely written in managed .Net code and not surprisingly stores executable and web service configuration data in just such files.  In fact, the %ExchangeInstallPath%\bin folder has dozens of *.exe.config files presumably storing a great many configuration options.

While for the most part Exchange’s default configurations are fine and the standard advice is to avoid changes, modifications are sometimes needed.  Having excellent XML support, PowerShell is an ideal tool to deploy config file changes both initially and in build/update cycles.

This post will demonstrate/discuss techniques I use to modify an Exchange configuration file.  However, XML is a standard, therefore these techniques are by no means limited to the task at hand.  Effectively, this discussion is a single example, the principles of which can be reused to perform similar work on almost any XML document.

In this case, I need to modify Exchange BackPressure thresholds on all my servers.  The settings are controlled by the Edge.Transport.exe.config file partially documented here.  Among other characteristics .Net Config files typically store a series of <add ../> nodes with key/value pairs as attributes formatted similar to below. I’ll be working within the same <appSettings> element/level, but again these techniques should be relevant at any level of any XML document.

<appSettings>
  <add key="AgentLogEnabled" value="true" />
  <add key="ResolverRetryInterval" value="30" />
  <add key="DeliverMoveMailboxRetryInterval" value="2" />
  <add key="ResolverLogLevel" value="Disabled" />
  . . .
</appSettings>

Note: The terminology can be confusing. Hereafter, pay careful attention to the value of the key attribute versus the value attribute, versus the value of the value attribute.

Per the documentation I needed to set the following 4 keys and their respective values:

UsedVersionBuckets.LowToMedium
UsedVersionBuckets.MediumToHigh
UsedVersionBuckets.HighToMedium
UsedVersionBuckets.MediumToLow

Dealing with redundant XML node names is always a little tricky.  I’ll have to isolate the correct node before I can assert the value.  Complicating matters further these keys aren’t present in the file by default.  So, I have to ensure the nodes exist before I can isolate them to assert the value attribute.

First Revision:

$ConfigFile = Join-Path $env:ExchangeInstallPath "Bin\EdgeTransport.exe.config"
$Config     = [XML](Get-Content $ConfigFile)

$ConfigPairs = [Ordered]@{
    'UsedVersionBuckets.LowToMedium'  = "1998"
    'UsedVersionBuckets.MediumToHigh' = "3000"
    'UsedVersionBuckets.HighToMedium' = "2000"
    'UsedVersionBuckets.MediumToLow'  = "1600"
}

$appSettings = Select-XML -Xml $Config -XPath "/configuration/appSettings"

If( $appSettings.Count -eq 1 ) {    
    $appSettings  = $appSettings.Node # Reset appSettings to the returned node
    $ExistingKeys = $appSettings.ChildNodes.Key
    ForEach( $Key in $ConfigPairs.Keys )
    {
        If( $Key -notin $ExistingKeys ) {
            # Add the node:
            $Add = $Config.CreateElement('add')
            $Add.SetAttribute( 'key', $Key )
            $Add.SetAttribute( 'value', $ConfigPairs[$Key] )
            [Void]$appSettings.AppendChild( $Add )
        }
        Else {
            # Node already exist, just set:
            $ConfigKey = $appSettings.ChildNodes | Where-Object{ $_.Key -eq $Key }
            $ConfigKey.value = $ConfigPairs[$Key]
        }
    }
}
Else {
    Write-Host "appSettings node not found!"
}

$Config.Save($ConfigFile)

For ease of reference, I stored the key names and values in an ordered hash table.  Although not required, the ordered hash ensures the sequence of the resulting XML output. 

Note: The order of XML elements usually doesn’t matter, so the [Ordered]hash only ensures the order of new elements among themselves. New elements are appended to the given section. In this case, the 4 elements are new and will ultimately be the last 4 elements within the <appSettings> section. If an element already exists it will be modified in place.

By looping through the dictionary entries I can check if each key is present and create the nodes as needed. But, when the key is already present, I still need to isolate it using a where{} clause before I can set the value.  This code works fine and is certainly satisfactory for the task at hand.  Nevertheless, running Where{} several times across what could be dozens of elements is inefficient.  Performance isn’t a big concern for this type of project, but the code could be better! Furthermore, given the commonality of XML-related tasks refactoring was definitely worth my time.

WARNING: Pay close attention to casing. XML is case sensitive! In an earlier version of the code and this post I had inadvertently capitalized the value attribute.  That caused Exchange Transport Service stop/start failures.  It took me hours to spot the oversight. Furthermore, had this been fully deployed it could have effected organization-wide mail flow.

Second Revision:

$ConfigFile = Join-Path $env:ExchangeInstallPath "Bin\EdgeTransport.exe.config"
$Config     = [XML](Get-Content $ConfigFile)

$ConfigPairs = [Ordered]@{
    'UsedVersionBuckets.LowToMedium'  = "1998"
    'UsedVersionBuckets.MediumToHigh' = "3000"
    'UsedVersionBuckets.HighToMedium' = "2000"
    'UsedVersionBuckets.MediumToLow'  = "1600"
}

$appSettingsPath = '/configuration/appSettings'

ForEach( $Key in $ConfigPairs.Keys )
{
    $Node = Select-XML -Xml $Config -XPath "$appSettingsPath/add[@key='$Key']"
    If( $Node ) {
        # Element exists:
        $Node = $Node.Node
        $Node.value = $ConfigPairs[$Key]
    }
    Else {
        $appSettings = Select-XML -Xml $Config -XPath $appSettingsPath
        If( $appSettings ) {
             # Create the node:
            $appSettings = $appSettings.Node
            $Add = $Config.CreateElement('add')            
            $Add.SetAttribute( 'key', $Key )
            $Add.SetAttribute( 'value', $ConfigPairs[$Key] )
            [Void]$appSettings.AppendChild( $Add )
        }
        Else {
            Write-Host -ForegroundColor Red "appSettings node doesn't exist!"
        }
    }
}

$Config.Save($ConfigFile)

Note: I reversed the logic because after initial deployment, I expect the nodes will exist more often than not.

Note: Although they are case sensitive, the .SelectSingleNode() and .SelectNodes() methods can be used in place of the Select-Xml cmdlet.

The second example leverages XPath more aggressively, obviating the need for a Where{} clause by directly checking for the key’s presence. Each loop iteration evaluates an expanding string to substitute the current value of $Key into the XPath query executed by Select-Xml.  Then, conditioned on the presence or lack of a result, the code either asserts the value attribute or creates the needed element.

XPath is succinct but somewhat opaque. I use it enough to have it in my toolbox, but too infrequently to be fluent.  To help craft the queries I sometimes use the XML Tools extension for VSCode.

The add-in has a neat feature that reveals the XPath query respective to the cursor position in the document. Simply position the cursor on the element you’re interested in, invoke the command pallet (Ctrl + Alt + P), and start typing “XML Tools: Get Current XPath”.

Hit enter and the XPath query string /configuration/appSettings/add[70]/@key is returned.  As partly indicated by the common array index syntax […], the pseudo-code translation can be read as a directive to return the value of the key attribute at the 70th instance of the <add> element within the <appSettings> section. This feature is granular enough to reveal the XPath statement down to the node or attribute level, depending on where you position the cursor.  While the returned string isn’t exactly what’s needed, it is a head start to crafting the more specific query below:

/configuration/appSettings/add[@key='UsedVersionBuckets.LowToMedium']

Adjusted per each iteration, the above XPath query is a directive to return the <add> element where the key attribute is present and has the sought-after value.

Conclusion:

XPath is a powerful path syntax and query language for selecting nodes from XML data.  Properly crafted XPath queries can be used to simplify and streamline your code.  Combined with other PowerShell features and techniques we can create concise, efficient and reusable code patterns for many different scenarios.  In this case, I used an ordered hash table to store the desired configuration elements making it very easy to check for, create and/or assert values in a short easy to comprehend set of statements.

Additional Resources:

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s