Strange toString methods in System.ServiceProcess.ServiceController and Set-Content writing hashtables

Viewed 167

If I do $myobjects = Get-Service in PowerShell 5.1

  • Then $myobjects | Set-Content test.txt yields a list of fully qualified class names.
  • Whereas Set-Content test.txt $myobjects yields a list of the names of the services.
  • Set-Content test.txt $myobjects[0] gets a fully qualified class name.
  • Set-Content test.txt @($myobjects[0]) gets a name of the service.

I know, that Set-Content uses the .ToString() method, to represent complex objects in the file to write. Some classes have meaningful .ToString() methods, while other classes simply return their fully qualified class names. Fair enough.

I further know, that the pipeline makes Set-Content getting the elements of $myobjects one by one, and every time the .ToString() method of each element is called. Whereas in the second kind, Set-Content gets the array as a whole, and so, the .ToString() method of the array is called.

But: Doesn't the .ToString() method of the array fall back onto the .ToString() methods of each single element? So, where is the difference? According to the result of both commands, the array .ToString() method seems to be more "meaningful", than the .ToString() method of each single object of type [System.ServiceProcess.ServiceController]. But how can that be, if the array-toString just queries the .ToString() methods of the objects?

I think it might have something to do with the fact, that the .ToString() method of System.ServiceProcess.ServiceController is defined as ScriptMethod System.Object ToString() whereas normally the .ToString() method is defined as Method string ToString(). But where is the exact causality and what has that to do with arrays?

One additional question: Is it normal, that Set-Content automagically yields hashtable notations, if you try to write objects of the class [PSCustomObject] to a file, even though the .ToString() method of a PSCustomObject yields an empty string?

And the written hashtable notation is even wrong, because all quotations marks for the string literals are missing in the hashtable, what leads to reimport errors, if a string contains a comma or a semicolon, for example? This behavior seems nowhere described in the official documentation of the cmdlet.

1 Answers

After some investigation I guess the issue you have is a bug in the internal type converter implemenation. It's not using the ScriptMethod when a scalar object has been provided, but I'm not sure yet.

Background

PowerShell uses internally a type converter which is exposed to the user as the following method:

[System.Management.Automation.LanguagePrimitives]::ConvertTo()

You can also simply cast something to [string] or [string[]] to get the same effect, since it uses ConvertTo() behind the scenes, which answers your question about the pscustomobject/hashtable.

$newCustomObject = [PSCustomObject]@{
    Test = 'ABC'
}

[string]$newCustomObject

# returns:
# @{Test=ABC}

Overriden ToString()

As you already figured out, the method ToString() of System.ServiceProcess.ServiceController has been overriden by PowerShell's Extended Type System (ETS). It simply calls $this.ServiceName

$services[0].psobject.Methods.Match('toString') 

# returns:
# Script              : $this.ServiceName
# OverloadDefinitions : {System.Object ToString();}
# MemberType          : ScriptMethod
# TypeNameOfValue     : System.Object
# Value               : System.Object ToString();
# Name                : ToString

Due to this overriden method, $services[0].ToString() returns the name of the service, whereas $services[0].psbase.ToString() (the method of the underlying/raw object) returns the type name.

Reproduction

I can reproduce the conversion the following way:

[System.Management.Automation.LanguagePrimitives]::ConvertTo($services[0] , [string])

# returns:
# System.ServiceProcess.ServiceController
[System.Management.Automation.LanguagePrimitives]::ConvertTo([object[]]$services[0] , [string])

# returns:
# AarSvc_d165c2

Trace

Tracing the above two conversions return the following:

Trace-Command -PSHost -Name TypeConversion -Expression { 
    [System.Management.Automation.LanguagePrimitives]::ConvertTo([object[]]$services[0] , [string])
}

# returns:
# DEBUG: 2021-05-17 18:49:38.6355 TypeConversion Information: 0 : Value to convert is scalar.
# DEBUG: 2021-05-17 18:49:38.6358 TypeConversion Information: 0 : Converting "System.ServiceProcess.ServiceController" to "System.Object".
# DEBUG: 2021-05-17 18:49:38.6360 TypeConversion Information: 0 :     Result type is assignable from value to convert's type
# DEBUG: 2021-05-17 18:49:38.6362 TypeConversion Information: 0 : Converting "System.Object[]" to "System.String".
# DEBUG: 2021-05-17 18:49:38.6365 TypeConversion Information: 0 :     Converting object to string.
Trace-Command -PSHost -Name TypeConversion -Expression {
    [System.Management.Automation.LanguagePrimitives]::ConvertTo($services[0] , [string])
}

# returns
# DEBUG: 2021-05-17 18:48:52.3834 TypeConversion Information: 0 : Converting "System.ServiceProcess.ServiceController" to "System.String".
# DEBUG: 2021-05-17 18:48:52.3838 TypeConversion Information: 0 :     Converting object to string.

What's next

We should look into the source code of the languagePrimitives class to figure out what's going on behind the scenes between a scalar and a collection: https://github.com/PowerShell/PowerShell/blob/658837323599ab1c7a81fe66fcd43f7420e4402b/src/System.Management.Automation/engine/LanguagePrimitives.cs

More information

Some information about the ETS type converter:

ETS uses two basic types of type converters when a call is made to the LanguagePrimitives.ConvertTo(System.Object, System.Type) method. When this method is called, PowerShell attempts to perform the type conversion using its standard PowerShell language converters or a custom converter. If PowerShell cannot perform the conversion, it throws an PSInvalidCastException exception.

You can find more information about the ETS Type Conversion here:
https://docs.microsoft.com/en-us/powershell/scripting/developer/ets/typeconverters?view=powershell-7.1

Related