Chicanery! .foreach{} operator causing [XElement] obj to change output behavior?

I’m attempting to build an array of [XElement] objects as demonstrated in the .Net docs, and add them to an XMLTree using LINQ.

I’m a .foreach{} loop person, and being a for each loop person the way to do this seemed obvious:

[Array]$Array = (0..2).foreach{ [XElement]::new("test", "test") }
Boom, an array. (Typecast to an Array, otherwise, it's created as a generic collection)

All that was left was to add it to the XML tree

$Query = [XElement]::new('root', [XElement]::new("parameters", $Array))

$Query.ToString()

Output:
<root>
<parameters>&lt;test&gt;test&lt;/test&gt;&lt;test&gt;test&lt;/test&gt;&lt;test&gt;test&lt;/test&gt;</parameters>
</root>
Malformatted :( After a bunch of testing and dismay, I realized it was passing the Array as a string to the [XElement] constructor, which results in the encoding of the brackets. You can test this by using [XElement]::Parse() on the first index of the Array, and it outputs in the well-formatted and correct manner.
[XElement]::new("parameters",[XElement]::parse($Array[0])}
This is only an issue when using the .foreach() operator.

Any other variation of foreach does not have the issue:

$Array = foreach ($i in 0..2) { [XElement]::new("test", "test") }

$Query = [XElement]::new(‘root’, [XElement]::new(“parameters”, $Array)

)

$Query.ToString()

$Array = 0..2 | ForEach-Object { [XElement]::new("test", "test") }

$Query = [XElement]::new(‘root’, [XElement]::new(“parameters”, $Array)

)

$Query.ToString()


Output:

<root>
<parameters>
<test>test</test>
<test>test</test>
<test>test</test>
</parameters>
</root>

BUT…

If the array is instantiated first, and then each individual [XElement] obj is added to the array using += operator. It works as expected.

$Array = @()

(0…2).foreach{ $Array += [XElement]::new(“test”, “test”) }

$Query = [XElement]::new(‘root’, [XElement]::new(“parameters”, $Array)

)

$Query.ToString()

Output:
<root>
<parameters>
<test>test</test>
<test>test</test>
<test>test</test>
</parameters>
</root>
The only thing that looks different is that if you look at the BaseObject property of the psobject. You will see the following:
$Array = (0..2).foreach{ [XElement]::new("test", "test") }

$Array.psobject

BaseObject : {<test>test</test>, <test>test</test>, <test>test</test>}

String representation of [XElement] obj
$Array = foreach ($i in 0..2) { [XElement]::new("test", "test") }

$Array.psobject

BaseObject : {test, test, test}

Node Name of [XElement] obj

What is causing this behavior?

Please note that this behavior happens before the array of [XElements] are added to the Root XMLTree in the examples.

That’s weird. I went as far as to doing sanity checks after making your code cleaner.

$array = ('test','test','test').ForEach({
    Param(
        [ValidateScript({$_ -is [string]})]
        [parameter()]
        $in
    )
    [System.Xml.Linq.XElement]::new('NodeName',$in)},
    'test'
)
$Array1 = ,'test' * 3 | ForEach-Object {
    if($_ -is [string])
    {
        [System.Xml.Linq.XElement]::new('NodeName',$_)
    }
}

They seem to be identical except the baseobject like you showed…

$Array.psobject

BaseObject          : {<NodeName>test</NodeName>, <NodeName>test</NodeName>, <NodeName>test</NodeName>}

$Array1.psobject

BaseObject          : {NodeName, NodeName, NodeName}

But when you call it like this, they again look identical.

$Array.psobject.BaseObject

FirstAttribute : 
HasAttributes  : False
HasElements    : False
IsEmpty        : False
LastAttribute  : 
Name           : NodeName
NodeType       : Element
Value          : test
FirstNode      : test
LastNode       : test
NextNode       : 
PreviousNode   : 
BaseUri        : 
Document       : 
Parent         : 
LineNumber     : 0
LinePosition   : 0

FirstAttribute : 
HasAttributes  : False
HasElements    : False
IsEmpty        : False
LastAttribute  : 
Name           : NodeName
NodeType       : Element
Value          : test
FirstNode      : test
LastNode       : test
NextNode       : 
PreviousNode   : 
BaseUri        : 
Document       : 
Parent         : 
LineNumber     : 0
LinePosition   : 0

FirstAttribute : 
HasAttributes  : False
HasElements    : False
IsEmpty        : False
LastAttribute  : 
Name           : NodeName
NodeType       : Element
Value          : test
FirstNode      : test
LastNode       : test
NextNode       : 
PreviousNode   : 
BaseUri        : 
Document       : 
Parent         : 
LineNumber     : 0
LinePosition   : 0


$Array1.psobject.BaseObject

FirstAttribute : 
HasAttributes  : False
HasElements    : False
IsEmpty        : False
LastAttribute  : 
Name           : NodeName
NodeType       : Element
Value          : test
FirstNode      : test
LastNode       : test
NextNode       : 
PreviousNode   : 
BaseUri        : 
Document       : 
Parent         : 
LineNumber     : 0
LinePosition   : 0

FirstAttribute : 
HasAttributes  : False
HasElements    : False
IsEmpty        : False
LastAttribute  : 
Name           : NodeName
NodeType       : Element
Value          : test
FirstNode      : test
LastNode       : test
NextNode       : 
PreviousNode   : 
BaseUri        : 
Document       : 
Parent         : 
LineNumber     : 0
LinePosition   : 0

FirstAttribute : 
HasAttributes  : False
HasElements    : False
IsEmpty        : False
LastAttribute  : 
Name           : NodeName
NodeType       : Element
Value          : test
FirstNode      : test
LastNode       : test
NextNode       : 
PreviousNode   : 
BaseUri        : 
Document       : 
Parent         : 
LineNumber     : 0
LinePosition   : 0

Which brings me to my point, I have no idea why the heck this is happening nor have I found a fix. When doing the parse there is a “saveoptions” parameter you can use with tostring() but that didn’t have any effect on this example. I assume we will have to dig in the source code to find the differences in the way the foreach method wraps things up (or just as likely, unwraps?)

I would skip the method in this case as there are several other ways to do this that you’ve found works. I’ve had other weird issues with using nested foreach methods calls that I didn’t have time to figure out. Interesting find none-the-less, thanks for sharing. Maybe someone else is typing up an explanation at this very moment.

Take care.

I stumbled across this: https://stackoverflow.com/questions/20803345/preventing-powershell-from-wrapping-value-types-in-psobjects, which got me thinking:

$Array = (0..2) | foreach { [XElement]::new("Nodename", "Nodevalue") }

$Array.psobject

BaseObject          : {Nodename, Nodename, Nodename}

$($Array | foreach {$_}).psobject

BaseObject          : {<Nodename>Nodevalue</Nodename>, <Nodename>Nodevalue</Nodename>, <Nodename>Nodevalue</Nodename>}

$($Array | foreach {[XElement]$_}).psobject

BaseObject          : {Nodename, Nodename, Nodename}

So this is an example of a known method for preventing the issue of the text representation of the XElement obj, then using the same method to create another array, and running into the issue, then typing the pipeline variable while creating the array to prevent the issue.

The same technique fails when using .foreach{} to build the source array.

I think this demonstrates that a behavior with script blocks is causing the values to unwrap in some way? I dunno I’m butting up against the limits of my knowledge here. It’s like powershell implicitly understand it’s still of the same object type, so it’s displaying the correct properties to console when you print the Array, but when it’s apart of the pipeline it’s interpreting it as text because of something to do with script blocks??

Edit:

Look what a script block does:

$Array = {[XElement]::new("Nodename", "Nodevalue"), [XElement]::new("Nodename", "Nodevalue"), [XElement]::new("Nodename", "Nodevalue") }.Invoke()

$Array.psobject

BaseObject          : {<Nodename>Nodevalue</Nodename>, <Nodename>Nodevalue</Nodename>, <Nodename>Nodevalue</Nodename>}

The output is also of type [System.Collections.ObjectModel.Collection`1], just like when you use the .foreach{} method. With this I can expand my search to the behaviors of script blocks, which are better documented. I am so close to an answer.

At least I’m not crazy.

I went on a journey for this one.

TL;DR [XElement]$array=(0…2).foreach{[XElement]::new("Name","$_")} Specify the data type of the array using [XElement]. To use this with the .foreach() operator.

Here is what I've learned.
$DirectAssignment = {
[XElement]::new("Nodename","Nodevalue"),
[XElement]::new("Nodename","Nodevalue"),
[XElement]::new("Nodename","Nodevalue")
}.invoke()
Is that same as this.
$TestLoop = (0..2).foreach{ [XElement]::new("Name", "Value") }
At least in how the object appears when inspecting their members.
If you assign either to the value of [XElement](XName,Object[]), they will display malformed XML as demonstrated in the OP.
<root>
<parameters>&lt;test&gt;test&lt;/test&gt;&lt;test&gt;test&lt;/test&gt;&lt;test&gt;test&lt;/test&gt;</parameters>
</root>
However, if you extract each value of the collection, and enforce its type, it will interpret correctly, but this is only true if using ForEach-Object or foreach loop.
Good!
[System.Xml.Linq.XElement]::new("root", ($Array | foreach { [XElement]$_ })).tostring()

Bad!
[System.Xml.Linq.XElement]::new(“root”,($Array.foreach{[XElement]$_})).tostring()

From here I started playing around with the idea that the script block is causing the behavior. I stumbled across:
{[XElement]::new("Name", "Value") }.InvokeReturnAsIs()
This is when I realized
$invokereturnasis={
[XElement]::new("Name","Value"),
[XElement]::new("Name","Value"),
[XElement]::new("Name","Value")
}.InvokeReturnAsIs()
Creates an array like foreach and ForEach-Object
$invoke={
[XElement]::new("Name","Value"),
[XElement]::new("Name","Value"),
[XElement]::new("Name","Value")
}.Invoke()
Creates a Collection like the .foreach() operator and .invoke()
Some of this behavior can be explored here: https://powershell.one/tricks/performance/pipeline#how-it-works
It was here I was the cusp of going 'Too Deep", at the very least what I now understood was:
  1. The .foreach() operator behaves like a scriptblock that is executed with the .invoke() method
  2. You cannot force the .foreach() operator to use the .InvokeReturnAsIs() method.
  3. The objects of the collection, when cast individually using ForEach-Object as [XElement] objects work as expected.
Equipped with this knowledge I determined that the only way to use the .foreach() operator would be to somehow ensure
the values being assigned to the variable would be passed explicitly as the [XElement] type...the light bulb went off.
[XElement[]]$array=(0..2).foreach{[XElement]::new("Name","$_")}
et voila
[XElement]::new("Name", $Array).tostring()

Returns:
<Name>
<Name>0</Name>
<Name>1</Name>
<Name>2</Name>
</Name>

It works as expected when you explicitly type the collection to expect [XElement] objects.
(I actually thought of this a while ago but failed to recognize the [] syntax was necessary for doing this to arrays. :<)

I do not know what to call the behavior that is causing objects processed using .invoke() on a script block to behave as if they are strings when passed as parameters to a method, but that appears to be the core problem.

Ensuring that the elements of the collection are treated as their native type makes everything behave correctly. I believe the reason ForeEach-Object and foreach do not have this issue is because of how Powershell is handling the objects. They are assigning the objects as their native types without any ancillary behaviors.