2

Environment: Windows Server 2022 21H2, Powershell 7.2 (running as administrator)

I have a script that implements ShouldProcess, which works fine in Windows PowerShell 5. However, in PowerShell 7, the script invariably throws the error Cannot find an overload for "ShouldProcess" and the argument count: "1". ShouldProcess at MSDoc says that the one-argument overload for $PSCmdlet.ShouldProcess() exists and should work.

It's failing, as above. Why?

The script in question is pasted below; it's in a script module:

function Remove-DomainUserProfile {
<#
#Comment-based help removed for space considerations
#>

    [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact="High")]

    param(
        [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
        [Parameter(ParameterSetName='SpecificProfile')]
        [Parameter(ParameterSetName='ByAge')]
        [Parameter(ParameterSetName='AllProfiles')]
        [String[]]$ComputerName = $env:ComputerName,

        [Parameter(Mandatory=$true,ParameterSetName='SpecificProfile')]
        [Parameter(ParameterSetName='ByAge')]
        [Alias("UserName","sAMAccountName")]
        [String]$Identity,

        [Parameter(ParameterSetName='ByAge')]
        [Parameter(ParameterSetName='AllProfiles')]
        [Switch]$DomainOnly,

        [Parameter(ParameterSetName='SpecificProfile')]
        [Parameter(ParameterSetName='ByAge')]
        [Int]$Age,

        [Parameter(Mandatory=$true,ParameterSetName='AllProfiles')]
        [Switch]$All
    )

    BEGIN {
        if (-NOT (Test-IsAdmin)) {
            Write-Output "This function requires being run in an Administrator session! Please start a PowerShell
session with Run As Administrator and try running this command again."
            return
        }
        $NoSystemAccounts = "SID!='S-1-5-18' AND SID!='S-1-5-19' AND SID!='S-1-5-20' AND NOT SID LIKE 'S-1-5-%-500' "
# Don't even bother with the system or administrator accounts.
        if ($DomainOnly) {
            $SIDQuery = "SID LIKE '$((Get-ADDomain).DomainSID)%' "                     # All domain account SIDs begin
with the domain SID
        } elseif ($Identity.Length -ne 0) {
            $SIDQuery = "SID LIKE '$(Get-UserSID -AccountName $Identity)' "
        }
        $CutoffDate = (Get-Date).AddDays(-$Age)
        $Query = "SELECT * FROM Win32_UserProfile "
    }

    PROCESS{
        ForEach ($Computer in $ComputerName) {
            Write-Verbose "Processing Computer $Computer..."
            if ($SIDQuery) {
                $Query += "WHERE " + $SIDQuery
            } else {
                $Query += "WHERE " + $NoSystemAccounts
            }
            if ($All) {
                Write-Verbose "Querying WMI using '$Query'"
                $UserProfiles = Get-WMIObject -ComputerName $Computer -Query $Query
            } else {
                Write-Verbose "Querying WMI using '$Query' and filtering for profiles last used before $CutoffDate ..."
                $UserProfiles = Get-WMIObject -ComputerName $Computer -Query $Query | Where-Object {
[Management.ManagementDateTimeConverter]::ToDateTime($_.LastUseTime) -lt $CutoffDate }
            }
            ForEach ($UserProfile in $UserProfiles) {
                if ($PSCmdlet.ShouldProcess($UserProfile)) {
                    Write-Verbose "Deleting profile object $UserProfile ($(Get-SIDUser $UserProfile.SID))..."
                    $UserProfile.Delete()
                }
            }
        }
    }

    END {}
}
Jeff Zeitlin
  • 9,773
  • 2
  • 21
  • 33
  • 1
    I'm unable to reproduce this with 7.2.1, can you share the script (or a sample script that reproduces the issue) along with the exact version you're using? – Mathias R. Jessen Mar 11 '22 at 12:57
  • @MathiasR.Jessen - Script added. – Jeff Zeitlin Mar 11 '22 at 13:02
  • @SantiagoSquarzon - It works as written with Windows PowerShell 5.1 on the same server. The documentation doesn't imply that I should need to select a property. That's also not what the error message suggests; it's specifically calling out the overload, not a bad parameter value/type. – Jeff Zeitlin Mar 11 '22 at 13:31
  • 1
    Give `$PSCmdlet.ShouldProcess("$UserProfile")` a try – Mathias R. Jessen Mar 11 '22 at 13:33
  • @MathiasR.Jessen - That got past the `$PSCmdlet.ShouldProcess()` error; thanks - post it (or a more detailed explanation; you do good explanations) as an answer. It doesn't make the script work under PS7; I now have a problem with the UserProfile object being deserialized and therefore without a `Delete()` method, but that's not relevant to this question. – Jeff Zeitlin Mar 11 '22 at 13:38

2 Answers2

2

To complement Santiago Squarzon's excellent analysis:

  • The behavior, present up to at least PowerShell 7.2.1, should be considered a bug, because any object should be auto-convertible to a string in a .NET method call.

    • There is no reason for [pscustomobject] a.k.a [psobject] instances to act differently than instances of any other type (irrespective of whether implicit stringification makes sense in a given situation); to give a simple example:

      • If (42).ToString((Get-Item /)) works, ...
      • ... there's no reason why (42).ToString(([pscustomobject] @{ foo=1 })) shouldn't.
      • Note that implicit stringification in the context of cmdlets / functions / script is not affected; e.g., Get-Date -Format ([pscustomobject] @{ foo=1 }) doesn't cause an error.
    • See GitHub issue #16988.

  • The reason that the serialization infrastructure is involved at all is that the obsolete WMI cmdlets such as Get-WmiObject aren't natively available in PowerShell (Core) v6+ anymore, and using them implicitly makes use of the Windows PowerShell Compatibility feature:

    • This entails using a hidden powershell.exe child process, communication with which requires use of serialization, during which most non-primitive types lose their type identity and are emulated with method-less [psobject] instances that contain copies of the original object's properties.

    • In PowerShell v3 and above, and especially in PowerShell (Core) v6+, use the CIM cmdlets instead, such as Get-CimInstance, instead:

      • While similar to the WMI cmdlets in many respects, an important difference is that objects returned from CIM cmdlets have no methods; instead, methods must be called via Invoke-CimMethod.
    • See this answer for more information.

mklement0
  • 382,024
  • 64
  • 607
  • 775
1

For reference, this error can be reproduced on both PowerShell versions 5.1 and Core. The steps to reproduce is passing a System.Management.Automation.PSObject as argument to the .ShouldProcess(String) overload. It makes sense, by looking at your comment mentioning a serialized object. In below example, if the System.Diagnostics.Process object is not serialized it works properly on both versions.

function Test-ShouldProcess {
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = "High")]
    param()

    $obj = [System.Management.Automation.PSSerializer]::Deserialize(
        [System.Management.Automation.PSSerializer]::Serialize((Get-Process)[0])
    )

    # will throw
    if ($PSCmdlet.ShouldProcess($obj)) { 'hello' }
}

Test-ShouldProcess
Santiago Squarzon
  • 41,465
  • 5
  • 14
  • 37
  • 1
    This suggests that WMI objects returned under Windows Powershell 5.1 are not serialized by default, and under PowerShell 7 they are (and apparently, CIM Instances are always serialized). Which will make some of our migration to PS7 a little more complex. – Jeff Zeitlin Mar 11 '22 at 15:40
  • @JeffZeitlin I'm still wondering why in this example it would take an object of type `Process` but not a `PSObject` (assumed but, with a `PSCustomObject` it would also fail too) – Santiago Squarzon Mar 11 '22 at 16:20
  • I suspect that if the object has a "built-in" method to "stringify" it, Windows PowerShell 5 would apply that automatically when it's passed to a parameter of type string. – Jeff Zeitlin Mar 11 '22 at 16:25
  • @JeffZeitlin yes, for me in PS Core, it passes `(Get-Process)[0].ToString()` to the constructor and works properly but we can also call `.ToString()` to the object after serializing it without problem so it's a bit odd right? – Santiago Squarzon Mar 11 '22 at 16:26
  • That's not actually clear - I think I've seen somewhere that serialization doesn't include methods, only properties, and deserialization therefore doesn't reconstruct any methods - so the `.ToString()` method may not actually be sensical. – Jeff Zeitlin Mar 11 '22 at 16:29
  • @JeffZeitlin yes, that's from [Remote Output](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_remote_output?view=powershell-7.2#deserialized-objects) doc. – Santiago Squarzon Mar 11 '22 at 16:30