I strongly suggest to avoid working with raw XML strings and instead build the whole XML document element by element, using .NET API. This way you can just write any data as-is and the API makes sure of the proper XML encoding.
In general there are two kind of API for building XML:
- DOM-based, e. g. using
XmlDocument
(type accelerator [xml]
). This one is easiest to use, but is comparatively slow and stores the whole document in memory, which can be an issue for really large documents.
- Stream-based, e. g. using
XmlWriter
. This is the fastest way and has the lowest memory footprint. It is more cumbersome to use, as you have to take care that elements are properly closed. Also you can't create the elements out of order, they are written in the order you call the API.
DOM-based solution
$myitems = @(
[pscustomobject] @{AssertName="Joe";TestPass=$true}
[pscustomobject] @{AssertName="Sue";TestPass=$false}
[pscustomobject] @{AssertName="Cat";TestPass=$true}
)
$xml = [xml]::new()
$null = $xml.AppendChild( $xml.CreateXmlDeclaration('1.0', 'utf-8', $null) )
$root = $xml.AppendChild( $xml.CreateElement('testsuites') )
foreach ($item in $myitems )
{
$testSuite = $root.AppendChild( $xml.CreateElement('testsuite') )
$testSuite.SetAttribute('errors', 0)
$testSuite.SetAttribute('failures', 0)
$testSuite.SetAttribute('id', 0)
$testSuite.SetAttribute('name', $item.AssertName)
$testSuite.SetAttribute('tests', 1)
$testCase = $testSuite.AppendChild( $xml.CreateElement('testcase') )
$testCase.SetAttribute('classname', 'some.class.name')
$testCase.SetAttribute('name', 'Test1')
$testCase.SetAttribute('time', '123.345000')
}
$xml.Save( "$PSScriptRoot\test.xml" )
Output:
<?xml version="1.0" encoding="utf-8"?>
<testsuites>
<testsuite errors="0" failures="0" id="0" name="Joe" tests="1">
<testcase classname="some.class.name" name="Test1" time="123.345000" />
</testsuite>
<testsuite errors="0" failures="0" id="0" name="Sue" tests="1">
<testcase classname="some.class.name" name="Test1" time="123.345000" />
</testsuite>
<testsuite errors="0" failures="0" id="0" name="Cat" tests="1">
<testcase classname="some.class.name" name="Test1" time="123.345000" />
</testsuite>
</testsuites>
Stream-based solution
$path = "$PSScriptRoot\test.xml"
$myitems = @(
[pscustomobject] @{AssertName="Joe";TestPass=$true}
[pscustomobject] @{AssertName="Sue";TestPass=$false}
[pscustomobject] @{AssertName="Cat";TestPass=$true}
)
$writerSettings = [Xml.XmlWriterSettings] @{
Encoding = [Text.Encoding]::UTF8
Indent = $true
IndentChars = "`t"
WriteEndDocumentOnClose = $true # Write document end tag automatically
}
$writer = [xml.XmlWriter]::Create( $path, $writerSettings )
$writer.WriteStartDocument() # writes the XML declaration
$writer.WriteStartElement('testsuites')
foreach ($item in $myitems )
{
# Indentation is used to show the nesting of the XML elements
$writer.WriteStartElement('testsuite')
$writer.WriteAttributeString('errors', 0)
$writer.WriteAttributeString('failures', 0)
$writer.WriteAttributeString('id', 0)
$writer.WriteAttributeString('name', $item.AssertName)
$writer.WriteAttributeString('tests', 1)
$writer.WriteStartElement('testcase')
$writer.WriteAttributeString('classname', 'some.class.name')
$writer.WriteAttributeString('name', 'Test1')
$writer.WriteAttributeString('time', '123.345000')
$writer.WriteEndElement()
$writer.WriteEndElement()
}
# Very important - writes document end tag and closes the file
$writer.Dispose()
Output:
<?xml version="1.0" encoding="utf-8"?>
<testsuites>
<testsuite errors="0" failures="0" id="0" name="Joe" tests="1">
<testcase classname="some.class.name" name="Test1" time="123.345000" />
</testsuite>
<testsuite errors="0" failures="0" id="0" name="Sue" tests="1">
<testcase classname="some.class.name" name="Test1" time="123.345000" />
</testsuite>
<testsuite errors="0" failures="0" id="0" name="Cat" tests="1">
<testcase classname="some.class.name" name="Test1" time="123.345000" />
</testsuite>
</testsuites>