ValueFromPipelineByPropertyName not providing the correct value?

This topic contains 11 replies, has 3 voices, and was last updated by  Dave Wyatt 3 years, 7 months ago.

  • Author
    Posts
  • #12409

    Charles Downing
    Participant

    My problem is with this code:

    function Get-SCCMCollectionContainerNode {
    	[CmdletBinding()]
    	Param(
    		[Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true)][string]$ContainerNodeID,
    		[Parameter(ValueFromPipeline=$false,ValueFromPipelineByPropertyName=$false,Mandatory=$true)][string]$SiteCode,
    		[Parameter(ValueFromPipeline=$false,ValueFromPipelineByPropertyName=$false,Mandatory=$true)][string]$SiteServer
    	)
    
    	BEGIN {
    	}
    	PROCESS {
    		Write-Debug -Message "ContainerNodeID: $ContainerNodeID"
    		$WMIQuery = "SELECT * FROM SMS_ObjectContainerNode WHERE ContainerNodeID = '$ContainerNodeID'"
    		Write-Debug -Message "WMIQuery: $WMIQuery"
    		Get-WmiObject -Namespace "root\sms\Site_$SiteCode" -Query $WMIQuery -ComputerName "$SiteServer"
    	}
    	END {
    	}
    }

    If I call the function by explicitly assigning all 3 params, everything works fine.

    PS >  Get-SCCMCollectionContainerNode -SiteCode CM1 -SiteServer Server1 -ContainerNodeID 16777220
    DEBUG: 16777220
    DEBUG: SELECT * FROM SMS_ObjectContainerNode WHERE ContainerNodeID = '16777220'
    
    
    __GENUS               : 2
    __CLASS               : SMS_ObjectContainerNode
    __SUPERCLASS          : SMS_BaseClass
    __DYNASTY             : SMS_BaseClass
    __RELPATH             : SMS_ObjectContainerNode.ContainerNodeID=16777220
    __PROPERTY_COUNT      : 11
    __DERIVATION          : {SMS_BaseClass}
    __SERVER              : Server1
    __NAMESPACE           : root\sms\Site_CM1
    __PATH                : \\Server1\root\sms\Site_CM1:SMS_ObjectContainerNode.ContainerNodeID=16777220
    ContainerNodeID       : 16777220
    FolderFlags           : 0
    FolderGuid            : 795E005E-3290-4223-AA94-E2669EE81999
    IsEmpty               : False
    Name                  : Net Test
    ObjectType            : 5000
    ObjectTypeName        : SMS_Collection_Device
    ParentContainerNodeID : 0
    SearchFolder          : False
    SearchString          :
    SourceSite            : CM1
    PSComputerName        : Server1

    If I call using the complementary function and only passing the ContainerNodeID attribute into the pipeline, everything works.

    PS >  (Get-SCCMCollectionContainerItem -SiteCode CM1 -SiteServer Server1 -CollectionID CM1000C8).ContainerNodeID | Get-SCCMCollectionContainerNode -SiteCode CM1 -SiteServer Server1
    DEBUG: SELECT * FROM SMS_ObjectContainerItem WHERE InstanceKey = 'CM1000C8'
    DEBUG: 16777220
    DEBUG: SELECT * FROM SMS_ObjectContainerNode WHERE ContainerNodeID = '16777220'
    
    
    __GENUS               : 2
    __CLASS               : SMS_ObjectContainerNode
    __SUPERCLASS          : SMS_BaseClass
    __DYNASTY             : SMS_BaseClass
    __RELPATH             : SMS_ObjectContainerNode.ContainerNodeID=16777220
    __PROPERTY_COUNT      : 11
    __DERIVATION          : {SMS_BaseClass}
    __SERVER              : Server1
    __NAMESPACE           : root\sms\Site_CM1
    __PATH                : \\Server1\root\sms\Site_CM1:SMS_ObjectContainerNode.ContainerNodeID=16777220
    ContainerNodeID       : 16777220
    FolderFlags           : 0
    FolderGuid            : 795E005E-3290-4223-AA94-E2669EE81999
    IsEmpty               : False
    Name                  : Net Test
    ObjectType            : 5000
    ObjectTypeName        : SMS_Collection_Device
    ParentContainerNodeID : 0
    SearchFolder          : False
    SearchString          :
    SourceSite            : CM1
    PSComputerName        : Server1

    Get-SCCMCollectionContainerItem function:

    function Get-SCCMCollectionContainerItem {
    	[CmdletBinding()]
    	Param(
    		[Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true)][string]$CollectionID,
    		[Parameter(ValueFromPipeline=$false,ValueFromPipelineByPropertyName=$false,Mandatory=$true)][string]$SiteCode,
    		[Parameter(ValueFromPipeline=$false,ValueFromPipelineByPropertyName=$false,Mandatory=$true)][string]$SiteServer
    	)
    
    	BEGIN {
    	}
    	PROCESS {
    		$WMIQuery = "SELECT * FROM SMS_ObjectContainerItem WHERE InstanceKey = '$collectionID'"
    		Write-Debug -Message $WMIQuery
    		Get-WmiObject -Namespace "root\sms\Site_$SiteCode" -Query $WMIQuery -ComputerName "$SiteServer"
    	}
    	END {
    	}
    }

    But, if I just call it using the complementary function without explicitly passing the ContainerNodeID attribute, I get the value of the attribute before ContainerNodeID, ("__PATH").

    PS >  Get-SCCMCollectionContainerItem -SiteCode CM1 -SiteServer Server1 -CollectionID CM1000C8
    
    DEBUG: SELECT * FROM SMS_ObjectContainerItem WHERE InstanceKey = 'CM1000C8'
    
    
    __GENUS          : 2
    __CLASS          : SMS_ObjectContainerItem
    __SUPERCLASS     : SMS_BaseClass
    __DYNASTY        : SMS_BaseClass
    __RELPATH        : SMS_ObjectContainerItem.MemberID=16777449
    __PROPERTY_COUNT : 7
    __DERIVATION     : {SMS_BaseClass}
    __SERVER         : Server1
    __NAMESPACE      : root\sms\Site_CM1
    __PATH           : \\Server1\root\sms\Site_CM1:SMS_ObjectContainerItem.MemberID=16777449
    ContainerNodeID  : 16777220
    InstanceKey      : CM1000C8
    MemberGuid       : 83919C0B-92B3-42A5-8668-4851163C4AB9
    MemberID         : 16777449
    ObjectType       : 5000
    ObjectTypeName   : SMS_Collection_Device
    SourceSite       :
    PSComputerName   : Server1
    
    
    
    PS >  Get-SCCMCollectionContainerItem -SiteCode CM1 -SiteServer Server1 -CollectionID CM1000C8
     | Get-SCCMCollectionContainerNode -SiteCode CM1 -SiteServer Server1
    DEBUG: SELECT * FROM SMS_ObjectContainerItem WHERE InstanceKey = 'CM1000C8'
    DEBUG: \\Server1\root\sms\Site_CM1:SMS_ObjectContainerItem.MemberID=16777449
    DEBUG: SELECT * FROM SMS_ObjectContainerNode WHERE ContainerNodeID =
    '\\Server1\root\sms\Site_CM1:SMS_ObjectContainerItem.MemberID=16777449'
    Get-WmiObject : Generic failure
    At F:\Storage\Scripts\Windows\Modules\SCCMToolbox\SCCMToolbox.psm1:72 char:3
    +         Get-WmiObject -Namespace "root\sms\Site_$SiteCode" -Query $WMIQuery -ComputerN ...
    +    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        + CategoryInfo          : InvalidOperation: (:) [Get-WmiObject], ManagementException
        + FullyQualifiedErrorId : GetWMIManagementException,Microsoft.PowerShell.Commands.GetWmiObjectCommand

    Any thoughts?

  • #12410

    Don Jones
    Keymaster

    So, as a note, you don't need to add the ValueFromPipeline=$false; that's the default.

    In your second example, you're passing ByValue – you could run this using Trace-Command and see that, I expect.

    Your third example SHOULD work. Unfortunately, it's a little tough to tell what PowerShell is doing from just that. You'd definitely need to run it in Trace-Command, using the options to output to PSHost and to capture parameter binding – there's an example of that in the help for Trace-Command. If you really sit and read that output, you can see what it's doing with the pipeline input.

    Should you need to post that trace here, please attach it as a TXT instead of pasting it. They get quite long.

  • #12412

    Dave Wyatt
    Moderator

    I've run into this same behavior recently. When you set both ValueFromPipeline and ValueFromPipelineByPropertyName on a parameter (and it's of type [string]), ValueFromPipeline takes precedence. You're getting the entire input object, casted to a string (which happens to match the value of the __PATH parameter, in this case.) I was under some time pressure when I came across this situation, so I just ditched ValueFromPipeline and only used ValueFromPipelineByPropertyName (which was how I intended to use the function anyway.)

    My initial impression is that this behavior may be a bug, but I haven't spent enough time with it to say that with any confidence. If the type of the input object doesn't exactly match the type of the parameter, it seems like the engine should try to bind by property name (if applicable) before it starts trying to coerce the input object to the parameter's type.

  • #12413

    Don Jones
    Keymaster

    Yeah, that may be what's happening – Trace-Command would tell you for sure. It isn't a bug; it's by design. If PowerShell CAN make ByValue work, it WILL make ByValue work, especially if it can do so through coercion. It's a bit "fuzzier," but then that's kind of the shell's motto. Not saying it's a *good* design, but it's definitely intentional.

  • #12424

    Charles Downing
    Participant

    Well, that is apparently exactly what is going on:

    ParameterBinding Information: 0 : BIND NAMED cmd line args [Get-SCCMCollectionContainerNode]
    ParameterBinding Information: 0 :     BIND arg [CM1] to parameter [SiteCode]
    ParameterBinding Information: 0 :         Executing DATA GENERATION metadata: [System.Management.Automation.ArgumentTypeConverterAttribute]
    ParameterBinding Information: 0 :             result returned from DATA GENERATION: CM1
    ParameterBinding Information: 0 :         COERCE arg to [System.String]
    ParameterBinding Information: 0 :             Parameter and arg types the same, no coercion is needed.
    ParameterBinding Information: 0 :         BIND arg [CM1] to param [SiteCode] SUCCESSFUL
    ParameterBinding Information: 0 :     BIND arg [jxsccm1] to parameter [SiteServer]
    ParameterBinding Information: 0 :         Executing DATA GENERATION metadata: [System.Management.Automation.ArgumentTypeConverterAttribute]
    ParameterBinding Information: 0 :             result returned from DATA GENERATION: jxsccm1
    ParameterBinding Information: 0 :         COERCE arg to [System.String]
    ParameterBinding Information: 0 :             Parameter and arg types the same, no coercion is needed.
    ParameterBinding Information: 0 :         BIND arg [jxsccm1] to param [SiteServer] SUCCESSFUL
    ParameterBinding Information: 0 : BIND POSITIONAL cmd line args [Get-SCCMCollectionContainerNode]
    ParameterBinding Information: 0 : MANDATORY PARAMETER CHECK on cmdlet [Get-SCCMCollectionContainerNode]
    ParameterBinding Information: 0 :     BIND arg [] to parameter [ContainerNodeID]
    ParameterBinding Information: 0 :         Executing DATA GENERATION metadata: [System.Management.Automation.ArgumentTypeConverterAttribute]
    ParameterBinding Information: 0 :             result returned from DATA GENERATION: 
    ParameterBinding Information: 0 :         COERCE arg to [System.String]
    ParameterBinding Information: 0 :             Parameter and arg types the same, no coercion is needed.
    ParameterBinding Information: 0 :         BIND arg [] to param [ContainerNodeID] SUCCESSFUL

    Can you explain to me why that is "by design", though, Don? Seems like it's converting a string array, (args), to a string, (ContainerNodeID), and saying they're the same thing.

    And Dave, I see what you're doing, but I don't see why I would need to do that... Sort of rewriting the book.

  • #12425

    Don Jones
    Keymaster

    I know that's the way it is *meant* to work. I don't know *why*. I didn't code it :).

    I can guess.

    Originally, ByValue got a lot more use, and it's always been "Plan A" for binding. Once PowerShell gives up on Plan A, and goes to Plan B (ByPropertyName), it NEVER goes back to Plan A. So Dave's suggestion of try strict ByValue, then ByPropertyName, then back to coerced ByValue, just isn't what it does. I don't disagree that maybe it should, but I don't have any insight into why they decided to do it they way they do.

    Come to the PowerShell Summit and ask one of the team members ;).

  • #12426

    Dave Wyatt
    Moderator

    [quote=12424]And Dave, I see what you're doing, but I don't see why I would need to do that… Sort of rewriting the book.
    [/quote]

    Yep, pretty much. But if you want to accept that parameter both by value and by property name (without falling into this default behavior problem where ByValue with coercion takes over), you're going to have to do something like that.

    Or you can just do what I did when I ran into this: pick one or the other (By Value or By Property Name), whichever is most important to how you intend to use the function.

  • #12427

    Don Jones
    Keymaster

    Another option is to add an -InputObject parameter that accepts, ByValue, the exact object type being output by your first cmdlet. That way, it should bind without coercion. You'd KNOW you were getting that whole object, so you could grab the appropriate property internally. Make that part of a separate parameter set so that folks can do one or the other, but not both.

    This is essentially how a lot of built-in cmdlets, like Stop-Service and Stop-Process, work.

  • #12428

    Dave Wyatt
    Moderator

    That's a good point. Here's the param block for Stop-Service, as an example:

        [CmdletBinding(DefaultParameterSetName='InputObject', SupportsShouldProcess=$true, ConfirmImpact='Medium', HelpUri='http://go.microsoft.com/fwlink/?LinkID=113414')]
        param(
            [switch]
            ${Force},
        
            [Parameter(ParameterSetName='Default', Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
            [Alias('ServiceName')]
            [string[]]
            ${Name},
        
            [Parameter(ParameterSetName='InputObject', Mandatory=$true, Position=0, ValueFromPipeline=$true)]
            [ValidateNotNullOrEmpty()]
            [System.ServiceProcess.ServiceController[]]
            ${InputObject},
        
            [switch]
            ${PassThru},
        
            [Parameter(ParameterSetName='DisplayName', Mandatory=$true)]
            [string[]]
            ${DisplayName},
        
            [ValidateNotNullOrEmpty()]
            [string[]]
            ${Include},
        
            [ValidateNotNullOrEmpty()]
            [string[]]
            ${Exclude})
    
  • #12429

    Charles Downing
    Participant

    So, it's annoying, (probably because there's just something there I don't understand), but that's what I ended up doing.

    function Get-SCCMCollectionContainerNode {
    	[CmdletBinding(DefaultParameterSetName="ContainerNodeID")]
    	Param(
    		[Parameter(ParameterSetName='InputObject',ValueFromPipeline=$true,Mandatory=$true)]$InputObject,
    		[Parameter(ParameterSetName='ContainerNodeID',ValueFromPipeline=$true,Mandatory=$true)][string[]]$ContainerNodeID,
    		[Parameter(Mandatory=$true)][string]$SiteCode,
    		[Parameter(Mandatory=$true)][string]$SiteServer
    	)
    
    	BEGIN {
    		switch ($PsCmdlet.ParameterSetName) {
    			"InputObject" {
    				Write-Debug "InputObject.ContainerNodeID: $($InputObject.ContainerNodeID)"
    				$ContainerNodeID = $InputObject.ContainerNodeID
    			}
    			"ContainerNodeID" {
    			}
    		}
    		Write-Debug -Message "ParameterSetName: $($PsCmdlet.ParameterSetName)"
    	}
    	PROCESS {
    		foreach ($containerID in $ContainerNodeID) {
    			Write-Debug -Message "ContainerNodeID: $ContainerNodeID"
    			$WMIQuery = "SELECT * FROM SMS_ObjectContainerNode WHERE ContainerNodeID = '$containerID'"
    			Write-Debug -Message "WMIQuery: $WMIQuery"
    			Get-WmiObject -Namespace "root\sms\Site_$SiteCode" -Query $WMIQuery -ComputerName "$SiteServer"
    		}
    	}
    	END {
    	}
    }

    So this still doesn't work:

    Get-SCCMCollectionContainerItem -SiteCode CM1 -SiteServer Server1 -CollectionID CM1000C8 | Get-SCCMCollectionContainerNode -SiteCode CM1 -SiteServer Server1

    But this does:

    PS >  Get-SCCMCollectionContainerNode -SiteCode CM1 -SiteServer Server1 -InputObject $(Get-SCCMCollection -SiteCode CM1 -SiteServer Server1 -CollectionName "IDEA" | Get-SCCMCollectionContaineritem -SiteCode CM1 -SiteServer Server1 )

    Just one of those things, I guess... Thanks for the help!

  • #12430

    Dave Wyatt
    Moderator

    You'd need to set your default parameter set to InputObject, and define a type for $InputObject (instead of leaving it at the default of [System.Object]). If you do that, and if the proper object type is piped in, you'll wind up using $InputObject. If anything else is piped in (and if it couldn't be successfully coerced to $InputObject's type), you'd wind up using the ContainerNodeID parameter set instead.

    Using some of the Stop-Service parameters I posted earlier as an example:

    function Test-PipelineInput
    {
        [CmdletBinding(DefaultParameterSetName='InputObject', SupportsShouldProcess=$true, ConfirmImpact='Medium', HelpUri='http://go.microsoft.com/fwlink/?LinkID=113414')]
        param(    
            [Parameter(ParameterSetName='Default', Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
            [Alias('ServiceName')]
            [string[]]
            ${Name},
            
            [Parameter(ParameterSetName='InputObject', Mandatory=$true, Position=0, ValueFromPipeline=$true)]
            [ValidateNotNullOrEmpty()]
            [System.ServiceProcess.ServiceController[]]
            ${InputObject}    
        )
    
        process
        {
            Write-Verbose "Parameter Set: $($PSCmdlet.ParameterSetName)"
    
            if ($PSCmdlet.ParameterSetName -eq 'InputObject')
            {
                Write-Verbose "InputObject.Name: $($InputObject.Name)"
            }
            else
            {
                Write-Verbose "Name: $Name"
            }
        }
    }
    
    "This is a test." | Test-PipelineInput -Verbose
    Get-Service | Select-Object -First 1 | Test-PipelineInput -Verbose
    
  • #12415

    Dave Wyatt
    Moderator

    It's a bit of a hassle, but you can sort of write your own parameter binding when you need to use a combination of ByValue / ByPropertyName:

    function Test-PipelineInput
    {
        [CmdletBinding()]
        param (
            [Parameter(ValueFromPipeline)]
            [Object]
            $ParameterName
        )
    
        process
        {
            # Simulating a combination of ByPropertyName / ByValue binding for a String parameter, but giving
            # ByPropertyName binding priority before attempting to coerce the input object.
    
            if ($ParameterName -is [string])
            {
                $parameterValue = $ParameterName
            }
            elseif ($null -ne $ParameterName -and $null -ne $ParameterName.PSObject.Properties['ParameterName'])
            {
                $parameterValue = [string]$ParameterName.ParameterName
            }
            else
            {
                $parameterValue = [string]$ParameterName
            }
    
            Write-Verbose "Parameter Value: $parameterValue"
        }
    }
    
    $testObject = New-Object psobject -Property @{ ParameterName = 'This is a test (By Property Name).' }
    $testObject2 = New-Object psobject -Property @{ SomeOtherProperty = 'Whatever.' }
    
    "This is a test." | Test-PipelineInput -Verbose
    $testObject | Test-PipelineInput -Verbose
    $testObject2 | Test-PipelineInput -Verbose
    
    < #
    Output:
    
    VERBOSE: Parameter Value: This is a test.
    VERBOSE: Parameter Value: This is a test (By Property Name).
    VERBOSE: Parameter Value: @{SomeOtherProperty=Whatever.}
    #>
    

You must be logged in to reply to this topic.