Inserting new elements into XML files


The Goal:

Insert nodes into a specific place in XML config files

The Motivation:

I had a coworker a while back working with App Fabric. He needed to insert a particular chunk of XML into a specific spot inside of the config file. This had to be done on a bunch of different machines, but the kicker was that the config files might look different on all of them.

We knew the node that needed to come before our new node, but that might be in a different spot in each file.

When the configSections node ended ( </configSections> ) we needed to insert:

<appSettings>
    <add key="backgroundGC" value="true"/>
</appSettings>

This is the perfect place for PowerShell automation! We can write a script that searches for that node, inserts our new node in place and once we know it works we can just remote it out to all the servers that need it.

here is the file we are working with (MSDN wouldn't let me upload it directly)


<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <!-- Microsoft.ApplicationServer.Caching.Core assembly name is hard-coded -->
    
<section name="dataCacheConfig" type="Microsoft.ApplicationServer.Caching.DataCacheConfigSection, Microsoft.ApplicationServer.Caching.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    
<section name="fabric" type="Microsoft.Fabric.Common.ConfigFile, Microsoft.WindowsFabric.Common, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" allowLocation="true" allowDefinition="Everywhere" />
    
<section name="dataCache" type="Microsoft.ApplicationServer.Caching.DataCacheSection, Microsoft.ApplicationServer.Caching.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    
<section name="uri" type="System.Configuration.UriSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
  </configSections>
  <dataCacheConfig cacheHostName="AppFabricCachingService">
    <log location="C:\ProgramData\Microsoft\AppFabric\Runtime" logLevel="-1" />
    <clusterConfig provider="SPDistributedCacheClusterProvider" connectionString="Data Source=SQL;Initial Catalog=SharePoint_Config;Integrated Security=True;Enlist=False" />
  </dataCacheConfig>
  <fabric>
    
<section name="param" path="">
      <key name="VersionInfoClass" value="Microsoft.ApplicationServer.Caching.ServerVersionInfo, Microsoft.ApplicationServer.Caching.Server" />
      <key name="DroppedReplicaKeepDuration" value="0" />
      <key name="ClusterStableNodeUpInterval" value="10" />
      <key name="RPFederationCloseTimeout" value="15" />
      <key name="ReplicationQueueCapacity" value="128" />
      <key name="CopyQueueCapacity" value="2" />
      <key name="ReplicationTempListCapacity" value="1024" />
      <key name="ReplicationTempListInitialSize" value="128" />
      <key name="ReplicationRetryInterval" value="12" />
      <key name="ThrowOnAssert" value="true" />
      <key name="KeepOperationOnSecondary" value="false" />
      <key name="ExternalRingStateUpdateTimeout" value="480" />
      <key name="ExternalStoreUpdateRetry" value="8" />
    </section>

  </fabric>
  <dataCache size="Small">
    <hosts>
      <host replicationPort="22236" arbitrationPort="22235" clusterPort="22234" hostId="1739552749" size="1228" leadHost="true" account="NT AUTHORITY\NETWORK SERVICE" name="localhost" cacheHostName="AppFabricCachingService" cachePort="22233" />
    </hosts>
  </dataCache>
  <uri>
    <iriParsing enabled="true" />
  </uri>
  <runtime>
    <gcServer enabled="true" />
  </runtime>
  <startup>
    <supportedRuntime version="v4.0.30319" />
    <supportedRuntime version="v4.0" />
  </startup>
</configuration>

The Adventure:

There are a lot of different ways we could approach this problem, but I had seen type casting to [XML] before so I figured that was the best place to start.

$pathToConfig = "$PSScriptRoot\DistributedCacheService.exe.config" #put your path here

#force the config into an XML
$xml = (get-content $pathToConfig)

$xml | GM
   TypeName: System.Xml.XmlDocument

Name                        MemberType            Definition                   
----                        ----------            ----------                   
ToString                    CodeMethod            static string XmlNode(psob...
AppendChild                 Method                System.Xml.XmlNode AppendC...
Clone                       Method                System.Xml.XmlNode Clone()...
CloneNode                   Method                System.Xml.XmlNode CloneNo...
...
ImportNode                  Method                System.Xml.XmlNode ImportN...
InsertAfter                 Method                System.Xml.XmlNode InsertA...
InsertBefore                Method                System.Xml.XmlNode InsertB...
Load                        Method                void Load(string filename)...
LoadXml                     Method                void LoadXml(string xml)     
...
Item                        ParameterizedProperty System.Xml.XmlElement Item...
configuration               Property              System.Xml.XmlElement conf...
xml                         Property              string xml {get;set;}        

Here I noticed two really interesting things:

  1. The InsertAfter() method sounds like it will do exactly what I need if I can first find the <ConfigSections> node.
  2. The "configuration" property seemed a little odd. When viewing the actual XML file, it became clearer that it managed to make the <configuration> node into this property.
 <configSections>
    <!-- Microsoft.ApplicationServer.Caching.Core assembly name is hard-coded -->
    
<section name="dataCacheConfig" type="Microsoft.ApplicationServer.Caching.DataCacheConfigSection, Microsoft.ApplicationServer.Caching.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    
<section name="fabric" type="Microsoft.Fabric.Common.ConfigFile, Microsoft.WindowsFabric.Common, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" allowLocation="true" allowDefinition="Everywhere" />
    
<section name="dataCache" type="Microsoft.ApplicationServer.Caching.DataCacheSection, Microsoft.ApplicationServer.Caching.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    
<section name="uri" type="System.Configuration.UriSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
  </configSections>

So playing around a bit more I decided to try to dig down into that configuration element and try to locate the children.

$xml.configuration.configsections
#comment                                                               section          
--------                                                               -------          
 Microsoft.ApplicationServer.Caching.Core assembly name is hard-coded  {dataCacheConf...

Looking at the node in the file I can see a comment with that text and I can see a bunch of sections, the first being the truncated one. So now I know I've got the right element.

InsertAfter() takes in the new child, and the reference child, so I built the new node and tried it:

$newNode = @"
<appSettings>
    <add key="backgroundGC" value="true"/>
</appSettings>
"@

#add new node AFTER the configsections node

$xml.configuration.InsertAfter($newNode,$foundNode)
Exception calling "InsertAfter" with "2" argument(s): "The specified node cannot be 

inserted as the valid child of this node, because the specified node is the wrong type."

Doing a little searching on this lead me to find similar issues from C#, which are caused because the node you're trying to insert is "from a different document". It doesn't already live in the file, and it needs to be "Imported" into the XML document before it can be inserted. I looked around in the documentation and found ImportNode() which needs a node and a bool for deep copy.

$xml.ImportNode($newNode,$true)
Exception calling "ImportNode" with "2" argument(s): "Cannot import nodes of type document" 

Closer, but apparently [XML] made my $newNode a document, luckily we can just grab the root element though. Since ImportNode() returns the node it creates we also need to grab a reference to it.

$newNode = $xml.ImportNode($newNode.appSettings,$true) 

$xml.configuration.InsertAfter($newNode,$foundNode)

This seems to work, but gives some annoying output, so we'll take care of that and then save the file and take a look

$xml.configuration.InsertAfter($newNode,$foundNode) |out-null

$xml.Save("$pathToConfig.CHANGED.XML")

we did it!

The Treasure:

$pathToConfig = "$PSScriptRoot\DistributedCacheService.exe.config" #put your path here
 
#force the config into an XML
$xml = (get-content $pathToConfig)
 
#find the node to insert after
$foundNode = $xml.configuration.configsections
 
#build new node by hand and force it to be an XML object
$newNode = @"
<appSettings>
    <add key="backgroundGC" value="true"/>
</appSettings>
"@
 
#add new node AFTER the configsections node
$newNode = $xml.ImportNode($newNode.appSettings,$true) 
$xml.configuration.InsertAfter($newNode,$foundNode) |out-null
 
#save file
$xml.Save("$pathToConfig.CHANGED.XML")

Here is a slightly shorter version as well:

$pathToConfig = "$PSScriptRoot\DistributedCacheService.exe.config" #put your path here

#force the config into an XML
$xml = (get-content $pathToConfig)

#build new node by hand and force it to be an XML object
$newNode = @"
<appSettings>
    <add key="backgroundGC" value="true"/>
</appSettings>
"@

#add new node AFTER the configsections node
$xml.configuration.InsertAfter($xml.ImportNode($newNode.appSettings, $true), $xml.configuration.configsections) | out-null

#save file
$xml.Save($pathToConfig)

 

Here is the project on GitHub.

That's all for now, hopefully this was valuable for you to follow along with and see how I approach these kinds of problems. There are a lot of powerful tools built right in for us to take advantage of! If you enjoyed this content don't forget to like, rate and share 🙂

Comments (2)

  1. Kevin6565 says:

    It was valuable, thnx!

    1. Kory T says:

      Glad you liked it!

Skip to main content