class Foo {}
class Bar {
[Foo]$Foo
[string]$FooStr
}
$bar = [Bar]::new()
$bar.Foo = [Foo]::new()
$bar.FooStr = 'abc'
function Fn {
[CmdletBinding()]
param(
[Parameter(ValueFromPipelineByPropertyName, Mandatory)]
[Alias('FooStr')]
[string] $Foo
)
return $Foo
}
$bar | Fn
Output abc
Output: Foo
Having an arbitrary class instance be coerced to a string of the class name is unexpected. I'd expect a type error if no other alias matches, and in the case where there is an alias defined that actually matches a property with the correct type, it should prefer that.
Concrete real world case: PowerGit Get-GitBranch returns a LibGit2Sharp.Branch object, which has a Repository property of type LibGit2Sharp.Repository, and a PowerShell ScriptProperty RepositoryName of type string which is parsed out of the remote URL.
PSGitHub cmdlets like Get-GitHubPullRequest have a mandatory -Repository parameter of type string, which is annotated with ValueFromPipelineByPropertyName and aliased to RepositoryName. I would expect to be able to pipe the branch object to this cmdlet and PowerShell to bind the RepositoryName property, instead it coerces the Repository property to the class name "LibGit2Sharp.Repository" and passes that, which obviously does not make sense. PSGitHub then fails because it cannot find a GitHub repository with that name.
Name Value
---- -----
PSVersion 6.2.1
PSEdition Core
GitCommitId 6.2.1
OS Darwin 18.6.0 Darwin Kernel Version 18.6.0: Thu Apr 25 23:16:27 PDT 2019; root:xnu-4903.261.4~2/RELEASE_X86_64
Platform Unix
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0鈥
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
WSManStackVersion 3.0
Trace-Command ParameterBinding logs:
> Trace-Command -Name ParameterBinding -Expression { $bar | Fn } -PSHost
DEBUG: ParameterBinding Information: 0 : BIND NAMED cmd line args [Fn]
DEBUG: ParameterBinding Information: 0 : BIND POSITIONAL cmd line args [Fn]
DEBUG: ParameterBinding Information: 0 : MANDATORY PARAMETER CHECK on cmdlet [Fn]
DEBUG: ParameterBinding Information: 0 : BIND arg [] to parameter [foo]
DEBUG: ParameterBinding Information: 0 : Executing DATA GENERATION metadata: [System.Management.Automation.ArgumentTypeConverterAttribute]
DEBUG: ParameterBinding Information: 0 : result returned from DATA GENERATION:
DEBUG: ParameterBinding Information: 0 : COERCE arg to [System.String]
DEBUG: ParameterBinding Information: 0 : Parameter and arg types the same, no coercion is needed.
DEBUG: ParameterBinding Information: 0 : BIND arg [] to param [foo] SUCCESSFUL
DEBUG: ParameterBinding Information: 0 : CALLING BeginProcessing
DEBUG: ParameterBinding Information: 0 : BIND PIPELINE object to parameters: [Fn]
DEBUG: ParameterBinding Information: 0 : PIPELINE object TYPE = [Bar]
DEBUG: ParameterBinding Information: 0 : RESTORING pipeline parameter's original values
DEBUG: ParameterBinding Information: 0 : Parameter [foo] PIPELINE INPUT ValueFromPipelineByPropertyName NO COERCION
DEBUG: ParameterBinding Information: 0 : BIND arg [Foo] to parameter [foo]
DEBUG: ParameterBinding Information: 0 : Executing DATA GENERATION metadata: [System.Management.Automation.ArgumentTypeConverterAttribute]
DEBUG: ParameterBinding Information: 0 : result returned from DATA GENERATION: Foo
DEBUG: ParameterBinding Information: 0 : BIND arg [Foo] to param [foo] SKIPPED
DEBUG: ParameterBinding Information: 0 : Parameter [foo] PIPELINE INPUT ValueFromPipelineByPropertyName WITH COERCION
DEBUG: ParameterBinding Information: 0 : BIND arg [Foo] to parameter [foo]
DEBUG: ParameterBinding Information: 0 : Executing DATA GENERATION metadata: [System.Management.Automation.ArgumentTypeConverterAttribute]
DEBUG: ParameterBinding Information: 0 : result returned from DATA GENERATION: Foo
DEBUG: ParameterBinding Information: 0 : COERCE arg to [System.String]
DEBUG: ParameterBinding Information: 0 : Parameter and arg types the same, no coercion is needed.
DEBUG: ParameterBinding Information: 0 : BIND arg [Foo] to param [foo] SUCCESSFUL
DEBUG: ParameterBinding Information: 0 : MANDATORY PARAMETER CHECK on cmdlet [Fn]
DEBUG: ParameterBinding Information: 0 : CALLING EndProcessing
Foo
I'd almost call this expected behaviour. The default .ToString() behaviour is to return the class name, which is what will happen here in this binding...
I do agree that if there's a better type match under an alias name that it should still be selected though. 馃憤
Not sure if it qualifies as a bug, but the handling here could absolutely be improved. 馃槃
I'd say it's not expected, because what's the point of a [string] type annotation if _anything_ goes (because _everything_ can be ToString()ed).
Even if you say that it should be coerced, you can't even force the string to not get chosen through ValidatePattern() - the parameter will still be bound and then fail validation with an error, instead of picking the other property that would pass.
Yeah, that's fair. The logic probably wasn't coded with the expectation that there would be multiple properties that could possibly match, some of which having different types than required.
Something similar has hit me, and I think it's more likely than at first indicated by the OP. Consider:
function ProcessString {[cmdletbinding()]Param([Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)] [string]$p) process { write-host "p value <$p>, type $($p.gettype())"}}
function ProcessInt {[cmdletbinding()]Param([Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)] [int]$p) process { write-host "p value <$p>, type $($p.gettype())"}}
function ProcessObject {[cmdletbinding()]Param([Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)] $p) process { write-host "p value <$p>, type $($p.gettype())"}}
Then we'll feed those by pipeline value and propertyname:
> '123', 123, [pscustomobject]@{p=123}, [pscustomobject]@{p='123'} | ProcessObject
p value <123>, type string
p value <123>, type int
p value <@{p=123}>, type System.Management.Automation.PSCustomObject
p value <@{p=123}>, type System.Management.Automation.PSCustomObject
That's expected, but a small gotcha to remember that the whole custom object can be coerced to string and value bound. There's a way out of course by typing the argument:
> '123', 123, [pscustomobject]@{p=123}, [pscustomobject]@{p='123'} | ProcessInt
p value <123>, type int
p value <123>, type int
p value <123>, type int
p value <123>, type int
That's expected by type coercion and selection of the propertyname
> '123', 123, [pscustomobject]@{p=123}, [pscustomobject]@{p='123'} | ProcessString
p value <123>, type string
p value <123>, type string
p value <@{p=123}>, type string
p value <123>, type string
That's surprising. The difference between the by value binding and the by property name binding seems wrong. There's been an "accidental" string coercion of the whole pscustomobject before the parameter binder spotted the potential property binding.
In the end I had to write a ArgumentTransformationAttribute to beat the awkward coercion. That cast the [object] via [string][int] which allowed:
function ProcessId {[cmdletbinding()]Param([Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)] [Id()] $p) process { write-host "p value <$p>, type $($p.gettype())"}}
> '123', 123, [pscustomobject]@{p=123}, [pscustomobject]@{p='123'} | ProcessId
p value <123>, type string
p value <123>, type string
p value <123>, type string
p value <123>, type string
(This did have a side-advantage that I could extract any int-like value from an incoming string-like object, so all was not lost)