Creating Custom XML from .NET and PowerShell

PowerShell-Text-Purple-hero
In my previous article, I walked through piecing together a custom XML file using ConvertTo-Xml. Obviously, this technique works, but it involves a lot of string parsing and manipulation and maybe even some regular expressions. In some ways, I find it to be too “busy.” So let me demonstrate another technique to assemble an XML document from scratch using the .NET Framework.
Before I jump in, I want to head off any comments from readers who are just jumping into the series — if you are intending to save output from a PowerShell expression to XML so that you can re-use it in another PowerShell session, then Export-CliXml is all you need. I covered that in previous articles.
Instead, let’s create an XML file with information pulled from PowerShell that you might need to use outside of PowerShell. You could still bring it back in a PowerShell session, it just requires a few more steps, which I’ll cover in another article. My goal in this article is to create the same type of XML file as I did in the last article. I’m going to get some system information from WMI using Get-CimInstance on a collection of servers.

$computers = Get-Content S:\myservers.txt | Where { Test-WSMan $_ -ErrorAction SilentlyContinue }
$os = Get-CimInstance Win32_Operatingsystem -ComputerName $computers |
Select @{Name="Computername";Expression={$_.PSComputername}},InstallDate,
Caption,Version,OSArchitecture
$cs = Get-Ciminstance Win32_Computersystem -ComputerName $computers |
Select PSComputername,TotalPhysicalMemory,HyperVisorPresent,NumberOfProcessors,
NumberofLogicalProcessors
$services = Get-Ciminstance Win32_Service -ComputerName $computers |
Select PSComputername,Name,Displayname,StartMode,State,StartName

The XML file will eventually contain this information in a structured format. The first step is to create a blank XML document object. The .NET Framework has a complete XML library under System.XML.  Here’s how to create the document.

[xml]$Doc = New-Object System.Xml.XmlDocument

This is now just another object, which means you can pipe it to Get-Member to learn more about it.
If you’ve been paying attention, you’ll notice that I keep referring to XML as being hierarchical.  In other words, the document has layers of information. These “layers” are referred to Nodes. Some nodes can be annotated with attributes that provide additional information or context. Within a node you might have one or more elements.

<?xml version="1.0" encoding="UTF-8"?>
<Root>
    <Node attribute1="value" attribute2="value">
        <Element>SomeValue</Element>
        <Element2>SomeOtherValue</Element2>
    </Node>
</Root>

When creating an XML document from scratch, you almost do it from the inside out. You create child and parent “containers” and then add each child to the parent as you move up the hierarchy.  Let me show you.
First, I want to add an XML declaration to the document. This will show the XML version number and encoding.

$dec = $Doc.CreateXmlDeclaration("1.0","UTF-8",$null)

This object needs to be added to the document.

$doc.AppendChild($dec)

I thought I would also add a comment to the document, which I’ll define in a here string.

$text = @"
Server Inventory Report
Generated $(Get-Date)
v1.0
"@

I could use the CreateComment() method from $Doc, save the result to a variable and then append it, or I can do it all in one line.

$doc.AppendChild($doc.CreateComment($text))

Here’s what it looks like thus far.

An XML Document
An XML Document (Image Credit: Jeff Hicks)

What’s missing is a root node. I’ll create a node with the name of Computers.

$root = $doc.CreateNode("element","Computers",$null)

Now I need to go through my data and create the necessary child nodes and elements. My goal is to have a Computer node for each server, so I’ll use a ForEach loop to process my list of servers. The first things I’ll do is create the child node.

foreach ($computer in $Computers) {
 $c = $doc.CreateNode("element","Computer",$null)

For the sake of education, I’m also going to create an attribute called Name, which will have the computername as a value.

$c.SetAttribute("Name",$computer)

Next, I will create a node for operating system information.

$osnode = $doc.CreateNode("element","OperatingSystem",$null)

Obviously, I need data, so I’ll get the related information from computername as the loop is currently processing.

$data = $os.where({$_.computername -eq $Computer})

I’m hoping that even though I’m using some new objects and methods, the syntax is familiar to you. Now we get to change gears a bit.

In the <OperatingSystem></OperatingSystem> node that I’ve defined, I want to create a set of elements based on the properties from my WMI query. I’ll do this by creating an element.

$e = $doc.CreateElement("Name")

The element can be called whatever you want. In my case, I am using Name and I intend the Caption property from WMI to be the value.

$e.InnerText = $data.Caption

Once created, I need to add it to the parent.

$osnode.AppendChild($e)

The remaining WMI properties don’t need any transformation. The property name can be the element name so I’ll loop through the properties and repeat the previous steps.

"Version","InstallDate","OSArchitecture" | foreach {
    $e = $doc.CreateElement($_)
    $e.InnerText = $data.$_
    $osnode.AppendChild($e)
 }

At this point the $osnode is complete and I need to add it to its parent.

$c.AppendChild($osnode)

The resulting XML will look like this:

XML result
the XML result (Image Credit: Jeff Hicks)

Once you understand the process, doing the same thing for computer system information is almost the same.

#create node for Computer system
 $csnode = $doc.CreateNode("element","ComputerSystem",$null)
 #this is using the original property name
 $data = $cs.where({$_.pscomputername -eq $Computer})
 #get a list of properties except PSComputername
 $props = ($cs[0] | Get-Member -MemberType Properties | where Name -ne 'PSComputername').Name
 #create elements for each property
 $props | Foreach {
    $e = $doc.CreateElement($_)
    $e.InnerText = $data.$_
    $csnode.AppendChild($e)
 }
 #add to parent
 $c.AppendChild($csnode)

Once we get to services, the first few steps are the same.

#create node for services
 $svcnode = $doc.CreateNode("element","Services",$null)
 #get a list of properties except PSComputername
 $props = ($services[0] | Get-Member -MemberType Properties | where Name -ne 'PSComputername').Name
 $data = $services.where({$_.pscomputername -eq $Computer})

But, each server has a different set of services. That’s what is in $data. Each item in $data needs to be its own element and I want each WMI property to be the value.

foreach ($item in $data) {
     #create a service node
     $s = $doc.CreateNode("element","Service",$null)
     #create elements for each property
     $props | Foreach {
        $e = $doc.CreateElement($_)
        $e.InnerText = $item.$_
        $s.AppendChild($e)
     }
     #add to parent
     $svcnode.AppendChild($s)
 }

This process will build up the $svcnode variable to contain elements describing each service. Of course, this node also needs to be appended to its parent, and then I need to append the entire computer node to the root.

#add to grandparent
 $c.AppendChild($svcnode)
 #append to root
 $root.AppendChild($c)

Almost done. I still need to append the root node to the document itself after processing all the computer names.

#add root to the document
$doc.AppendChild($root) | Out-Null

Keeping track of parent and child objects is the hardest part of creating an XML document from scratch.
The final step is to save the document.

$doc.save("d:\temp\custom.xml")

This looks like a lot of work, and frankly, some of it is. But the more you work with this, the easier it will become. Let me give you the final and complete code that I saved in a script file.

#requires -version 4.0
#Demo-ServerInventoryXML.ps1
Param($Path="C:\Work\MyInventory.xml")
Write-Host "Creating computer list" -ForegroundColor Green
#process list of computers filtering out those offline
$computers = Get-Content S:\myservers.txt | Where { Test-WSMan $_ -ErrorAction SilentlyContinue}
Write-Host "Getting Operating System information" -ForegroundColor Green
$os = Get-CimInstance Win32_Operatingsystem -ComputerName $computers |
Select @{Name="Computername";Expression={$_.PSComputername}},InstallDate,
Caption,Version,OSArchitecture
Write-Host "Getting Computer system information" -ForegroundColor Green
$cs = Get-Ciminstance Win32_Computersystem -ComputerName $computers |
Select PSComputername,TotalPhysicalMemory,HyperVisorPresent,NumberOfProcessors,
NumberofLogicalProcessors
Write-Host "Getting Services" -ForegroundColor Green
$services = Get-Ciminstance Win32_Service -ComputerName $computers |
Select PSComputername,Name,Displayname,StartMode,State,StartName
Write-Host "Initializing new XML document" -ForegroundColor Green
[xml]$Doc = New-Object System.Xml.XmlDocument
#create declaration
$dec = $Doc.CreateXmlDeclaration("1.0","UTF-8",$null)
#append to document
$doc.AppendChild($dec) | Out-Null
#create a comment and append it in one line
$text = @"
Server Inventory Report
Generated $(Get-Date)
v1.0
"@
$doc.AppendChild($doc.CreateComment($text)) | Out-Null
#create root Node
$root = $doc.CreateNode("element","Computers",$null)
#create a node for each computer
foreach ($computer in $Computers) {
 Write-Host "Adding inventory information for $computer" -ForegroundColor Green
 $c = $doc.CreateNode("element","Computer",$null)
 #add an attribute for the name
 $c.SetAttribute("Name",$computer) | Out-Null
 #create node for OS
 Write-Host "...OS" -ForegroundColor Green
 $osnode = $doc.CreateNode("element","OperatingSystem",$null)
 #get related data
 $data = $os.where({$_.computername -eq $Computer})
 #create an element
 $e = $doc.CreateElement("Name")
 #assign a value
 $e.InnerText = $data.Caption
 $osnode.AppendChild($e) | Out-Null
 #create elements for the remaining properties
 "Version","InstallDate","OSArchitecture" | foreach {
    $e = $doc.CreateElement($_)
    $e.InnerText = $data.$_
    $osnode.AppendChild($e) | Out-Null
 }
 #add to parent node
 $c.AppendChild($osnode) | Out-Null
 #create node for Computer system
 Write-Host "...ComputerSystem" -ForegroundColor Green
 $csnode = $doc.CreateNode("element","ComputerSystem",$null)
 #this is using the original property name
 $data = $cs.where({$_.pscomputername -eq $Computer})
 #get a list of properties except PSComputername
 $props = ($cs[0] | Get-Member -MemberType Properties | where Name -ne 'PSComputername').Name
 #create elements for each property
 $props | Foreach {
    $e = $doc.CreateElement($_)
    $e.InnerText = $data.$_
    $csnode.AppendChild($e) | Out-Null
 }
 #add to parent
 $c.AppendChild($csnode) | Out-Null
 #create node for services
 Write-Host "...Services" -ForegroundColor green
 $svcnode = $doc.CreateNode("element","Services",$null)
 #get a list of properties except PSComputername
 $props = ($services[0] | Get-Member -MemberType Properties | where Name -ne 'PSComputername').Name
 $data = $services.where({$_.pscomputername -eq $Computer})
 foreach ($item in $data) {
     #create a service node
     $s = $doc.CreateNode("element","Service",$null)
     #create elements for each property
     $props | Foreach {
        $e = $doc.CreateElement($_)
        $e.InnerText = $item.$_
        $s.AppendChild($e) | Out-Null
     }
     #add to parent
     $svcnode.AppendChild($s) | Out-Null
 }
 #add to grandparent
 $c.AppendChild($svcnode) | Out-Null
 #append to root
 $root.AppendChild($c) | Out-Null
} #foreach computer
#add root to the document
$doc.AppendChild($root) | Out-Null
#save file
Write-Host "Saving the XML document to $Path" -ForegroundColor Green
$doc.save($Path)
Write-Host "Finished!" -ForegroundColor green

As you look through this code, you’ll notice I’ve used Out-Null in many places. Many of the XML methods will write something to the pipeline. I didn’t want that output, so I’m sending the results to null.
When I run the script, I’ll get a final XML file like this:

The final XML
The final XML (Image Credit: Jeff Hicks)

My intention in this article isn’t to demonstrate how to create an inventory report, but rather how to use the different parts of the XML library from the .NET Framework. Use what I’ve demonstrated here as a starting point for your own scripting projects.

In the next article, we’ll look at how to bring all of this XML data back to life in PowerShell.