Skip to content

-as operator does not recognize System.Management.Automation.PSCustomObject instances as such #4343

Closed

Description

Related: #5579

Steps to reproduce

# Create a [System.Management.Automation.PSCustomObject]
# Note that you cannot create a System.Management.Automation.PSCustomObject
# instance with `New-Object System.Management.Automation.PSCustomObject ...`.
# [pscustomobject] is actually [psobject] ([System.Management.Automation.PSObject])
$co = [pscustomobject] @{ foo = 'bar' }

$co.GetType().FullName

'---'

# This should be and is $True.
# Note that using `-is [pscustomobject]` is NOT equivalent and while it also returns $True,
# it does so for any object that has an (invisible) extra [psobject] wrapper, due to the identity
# of [pscustomobject] and [psobject]. All objects returned from *cmdlets* have that extra
# wrapper; e.g., `(Get-Item /) -is [pscustomobject]` yields $True also, while the seemingly
# equivalent  `[System.IO.DirectoryInfo]::new('/') -is [pscustomobject]`  doesn't - see #5579
$co -is [System.Management.Automation.PSCustomObject]

'---'

# The $co -as ... expression should effectively pass $co through, but doesn't.
# Note that using `-as [pscustomobject]` is NOT equivalent - and pointless, because 
# it passes *any* object through, given that it is the same as `-as [psobject]`, which  is what ALL 
# objects in PowerShell are (semi-secretly) wrapped in.
$null -ne ($co -as [System.Management.Automation.PSCustomObject])

Expected behavior

System.Management.Automation.PSCustomObject
---
True
---
True

Actual behavior

System.Management.Automation.PSCustomObject
---
True
---
False # !!

Environment data

PowerShell Core v6.0.0-beta.4 on macOS 10.12.5
PowerShell Core v6.0.0-beta.4 on Ubuntu 16.04.2 LTS
PowerShell Core v6.0.0-beta.4 on Microsoft Windows 10 Pro (64-bit; v10.0.15063)
Windows PowerShell v5.1.15063.413 on Microsoft Windows 10 Pro (64-bit; v10.0.15063)

Activity

  1. added
    WG-Enginecore PowerShell engine, interpreter, and runtime
    Issue-Discussionthe issue may not have a clear classification yet. The issue may generate an RFC or may be reclassif
    on Jul 25, 2017
  2. bgshacklett commented on Dec 1, 2017

    @bgshacklett

    I just ran into this recently. Has there been any discussion on this outside off the GitHub issue? I'm a bit surprised that there are no further comments here. Being able to use $foo -as [PSCustomObject] successfully on hashtables would be a big boost in usability as it fits in a pipeline where the alternative form does not. It's also quite unintuitive that [PSCustomObject]$foo functions differently from $foo -as [PSCustomObject], at least in where $foo is a hashtable.

  3. markekraus commented on Dec 1, 2017

    @markekraus
    Contributor

    Isn't this by design? PSCustomObject can't be used to cast objects. It doesn't have any public constructors.

    $object = [psobject]::new()
    [System.Management.Automation.PSCustomObject]$object

    Result:

    Cannot convert the "" value of type "System.Management.Automation.PSCustomObject" to type
    "System.Management.Automation.PSCustomObject".
    At line:1 char:1
    + [System.Management.Automation.PSCustomObject]$object
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        + CategoryInfo          : InvalidArgument: (:) [], RuntimeException
        + FullyQualifiedErrorId : ConvertToFinalInvalidCastException
    

    From https://docs.microsoft.com/en-us/dotnet/api/system.management.automation.pscustomobject?view=powershellsdk-1.1.0

    Serves as a placeholder BaseObject when PSObject's constructor with no parameters is used.

    If you can't cast an object as another type, -as returns false as it is intended to.

  4. mklement0 commented on Dec 2, 2017

    @mklement0
    ContributorAuthor

    Mark Kraus (@markekraus):

    Thanks for telling us how it's implemented, which is not necessarily the same as the design intent, however.

    If we consult the documentation (granted, historically speaking, that is unfortunately a somewhat shaky basis):

    The -as operator tries to convert the input object to the specified .NET Framework type

    Now, what would you expect if the input object already is of the specified type?

    I certainly would expect to pass the input object through, as is indeed the case for any type OTHER than [System.Management.Automation.PSCustomObject]:

    1 -as [int]  # passes integer 1 through
    (get-date) -as [datetime]  # passes the date output Get-Date through
    ...

    So, if the following returns $true (which it currently does):

    [pscustomobject] @{ foo = 'bar' } -is [System.Management.Automation.PSCustomObject]

    doesn't it stand to reason that the following - which currently returns $null - would pass the LHS through?

    [pscustomobject] @{ foo = 'bar' } -as [System.Management.Automation.PSCustomObject]

    The issue is further confused by the unfortunate identity of type accelerators [psobject] and [pscustomobject], both of which point to [System.Management.Automation.PSObject], so the use of [pscustomobject] on the RHS of -as is the same as [psobject] / [System.Management.Automation.PSObject], which passes anything through:

    > 1 -as [pscustomobject]   # same as: 1 -as [psobject]
    1

    (As an aside: This behavior is in itself debatable: while internally, every object is wrapped in a [psobject] instance, there's no reason to reflect that to the outside world.)

  5. markekraus commented on Dec 2, 2017

    @markekraus
    Contributor

    how it's implemented, which is not necessarily the same as the design intent, however.

    I don't believe that these are separate things. But that is a matter of opinion.

    Now, what what you expect if the input object already is of the specified type?

    I would expect conversion to fail if there is not a way to convert the object. Not all types can convert instances of their own type to their own type. PSCustomObject happens to be one of those. Kind of surprising, but true none the less.

    But, I can agree the documentation on this is lacking. Like several other parts of the documentation, there is a false assumption that the reader is familiar with .NET and C#. -as behaves pretty much like as in C#. The documentation on as is pretty good for this topic.

  6. mklement0 commented on Dec 3, 2017

    @mklement0
    ContributorAuthor

    I don't believe that these are separate things.

    An intent can have a flawed implementation. Q.E.D.

    But, I can agree the documentation on this is lacking.

    Yes, the documentation is lacking, but - more importantly - the actual behavior is lacking common sense:

    I would expect conversion to fail if there is not a way to convert the object. Not all types can convert instances of their own type to their own type. [...] -as behaves pretty much like as in C#.

    You're correct about C#'s behavior, but:

    • I would argue that C#'s behavior in this regard equally lacks common sense.
    • Conversely, PowerShell isn't C# and shouldn't be bound by C#'s behavior where it doesn't make sense.
      The fact that something like 1 -as [psobject] already passes 1 through while pretending that 1 is still System.Int32 ((1 -as [psobject]).GetType().FullName) shows you that PowerShell is already deviating from C#. (Yet, 1 -is [psobject] returns $False - another head-scratcher)).

    Of course, C# fixing the behavior of as would also solve the problem on the PowerShell side as well. (There may be a ton of reasons why this is not / no longer an option, but I'm blissfully ignorant of them.) (see below)


    For the record, the as documentation states:

    "

    expression as type  

    The code is equivalent to the following expression except that the expression variable is evaluated only one time.

    expression is type ? (type)expression : (type)null  
    Note that the as operator performs only reference conversions, nullable conversions, and boxing conversions. The `as` operator can't perform other conversions, such as user-defined conversions, which should instead be performed by using cast expressions.

    "

    As you correctly explain, using cast (System.Management.Automation.PSCustomObject) with anything - even an instance of that type itself - results in null, due to the lack of any public constructors.

  7. markekraus commented on Dec 3, 2017

    @markekraus
    Contributor

    An intent can have a flawed implementation. Q.E.D.

    I'm say that the intent, implementation, operation and everything about -as is perfectly fine, same with as.

    I would argue that C#'s behavior in this regard equally lacks common sense.

    I really don't think it does. It all made sense to me. the surprising thing is that 1) some classes cannot convert to themselves and 2) PSCustomObject is one of those. Everything else about -as/as makes sense.

    Conversely, PowerShell isn't C# and shouldn't be bound by C#'s behavior where it doesn't make sense.

    Agreed, however, i disagree that -as should be any different than as because they both make perfect sense in how they operate and making some silly exception for PSCustomObject would be ridiculous.

    The fact that something like 1 -as [psobject] already passes 1 through while pretending that 1 is still System.Int32 ((1 -as [psobject]).GetType().FullName) shows you that PowerShell is already deviating from C#

    Nope, not deviating at all. Acts exactly the same as System.Object does with as in C#:

    $Code = @'
    using System;
    
    namespace Test 
    {
        public static class AsObject
        {
            public static string GetResult()
            {
                Int32 testInt = 1;
                Object obj = testInt as Object;
                string result = obj.GetType().FullName;
                return result;
            }
        }
    }
    '@
    Add-Type -TypeDefinition $Code -Language CSharp 
    [Test.AsObject]::GetResult()

    Result:

    System.Int32
    
  8. mklement0 commented on Dec 3, 2017

    @mklement0
    ContributorAuthor

    the surprising thing is that 1) some classes cannot convert to themselves and 2) PSCustomObject is one of those. Everything else about -as/as makes sense.

    And it is that very surprise that is my point (but I appreciate that you introduced me to the subtler points of as).

    making some silly exception for PSCustomObject would be ridiculous.

    I'm not advocating an exception; I'm advocating that -as honor one fundamental expectation: if the LHS of -as already is of the RHS type - whatever that type may be - pass it through - which is in fact what C# does, after all:

    // Obtain a [System.Management.Automation.PSCustomObject] instance
    // and store it in a generic [System.Object] variable.
    // Note that the [System.Management.Automation.PSCustomObject] cannot be instantiated
    // directly - no public constructors - and must instead be created indirectly, via a 
    // [System.Management.Automation.PSObject] wrapper, whose `.BaseObject` property
    // contains the custom object.
    object o = new PSObject().BaseObject;
    
    // OK: Casting back to [System.Management.Automation.PSCustomObject] works.
    PSCustomObject co = (PSCustomObject) o;
    
    // OK with `as` too.
    PSCustomObject co_as = o as PSCustomObject;	

    So, if PowerShell tells me the following:

    > $co = [pscustomobject] @{ one = 1 }; $co.GetType().FullName; $co -is [System.Management.Automation.PSCustomObject]
    System.Management.Automation.PSCustomObject
    $True

    that is, if it treats the object as a bona fide [System.Management.Automation.PSCustomObject] instance in these contexts (which it should), then, of course, I'd expect

    $co -as [System.Management.Automation.PSCustomObject]

    to pass $co through.

    Presumably, the internal [psobject] wrapper gets in the way (I haven't looked).


    Acts exactly the same as System.Object does with as in C#:

    Except that every .NET type is indeed derived from System.Object - unlike from [psobject].

    The [psobject] wrapper is a PS implementation detail and that it surfaces prominently in some places (see #5551 and #5579) is unfortunate.

    Just as as System.Object is pointless in C# (anything is passed through), -as [psobject] is pointless in PowerShell - and behaves incorrectly to boot.

    The unfortunate identity of [psobject] and [pscustomobject] means that people will try something like $obj -as [pscustomobject] - and will be surprised to find that it passes anything through.

  9. mklement0 commented on Dec 3, 2017

    @mklement0
    ContributorAuthor

    To contrast the current -as behavior with that of -is:

    -is [pscustomobject] does return $True for [System.Management.Automation.PSCustomObject] instances and true [System.Management.Automation.PSObject] instances only,
    so it behaves more sensibly, but only accidentally:

    > $co = [pscustomobject] @{ one = 1 }; $co -is [pscustomobject]
    True
    
    > 1 -is [pscustomobject]
    False 

    [pscustomobject] @{ one = 1 } produces a [psobject]-wrapped [System.Management.Automation.PSCustomObject] instance, which is why testing with -is [pscustomobject] - which is the same as -is [psobject] - succeeds.

    Unfortunately, other types are situationally also (extra-)wrapped in [psobject] - see #5579 - which can yield false positives:

    > (Write-Output 1) -is [pscustomobject]
    True  # !! - the use of *command* output made the difference
  10. BrucePay commented on Mar 9, 2018

    @BrucePay
    Collaborator

    FYI: PSCustomObject is essentially a "typeless" object - a PSObject whose base object is itself. It's used when you need properties but don't have or need a type. For example - ... | select-object -property foo,bar,baz will extract the foo, bar and baz properties from the pipeline input object, then create a PSCustomObject and attach the extracted properties to that PSCustomObject. The resulting object has a fixed set of properties but it doesn't really have a type. You don't want to create a new concrete .NET type every time you run select-object, or import-json, or invoke a remote command or import CliXML, etc. So anywhere an object of no fixed type but with specific properties are needed, a PSCustomObject is used.

  11. markekraus commented on Mar 9, 2018

    @markekraus
    Contributor

    Bruce Payette (@BrucePay) That much is clear to me.

    I think the crux of this particular issue is whether -as and PSCustomObject are acting as by design. I believe that they are. I believe it may be surprising and somewhat confusing to the average PowerShell user, even though it makes perfect sense when you understand it in depth. If it is by design then the question becomes whether we consider special casing it somehow.

    My personal stance is that current behavior is by design and perfectly fine as is. But, it would great if someone with better understanding clarify that point.

  12. mklement0 commented on Mar 9, 2018

    @mklement0
    ContributorAuthor

    PowerShell tells white lies all the time, to shield us from the disconcerting complexities of the .NET framework and static typing.

    And that's fine and dandy: I loves me the free-flowing, carefree way that types can be thrown around in PowerShell - for the most part, it's a great experience.

    All I ask for is to be lied to consistently, so I can continue to find comfort in the illusion of a simpler world.

    If
    ([pscustomobject] @{ prop = 1 }).GetType().FullName
    yields System.Management.Automation.PSCustomObject, but
    ([pscustomobject] @{ prop = 1 }) -as [System.Management.Automation.PSCustomObject ] yields $null, my head starts spinning.

    Is a custom object mysteriously not itself? What's happening?

    Granted, -as [pscustomobject] does work as expected, but:

    (a) only because [pscustomobject] is really [psobject] (my head's RPM are increasing)

    (b) it is useless, because something like (Get-Item /) -as [pscustomobject] is passed through for any object (because, behind the scenes, everything is wrapped in [psobject]).

    No one except the architects / maintainers of the language should have to know any of the behind-the-scenes-but-not-fully intricacies mentioned here.

  13. mklement0 commented on Mar 9, 2018

    @mklement0
    ContributorAuthor

    Let me try to summarize the issue more succinctly:

    If a custom object:

    • self-reports as type [System.Management.Automation.PSCustomObject] via .GetType()

    • is correctly recognized by -is as that type

    why oh why should -as [System.Management.Automation.PSCustomObject] not work?

  14. mklement0 commented on Mar 9, 2018

    @mklement0
    ContributorAuthor

    If the -as [System.Management.Automation.PSCustomObject] use case strikes you as too exotic, let's recap the related head-spinners:

  15. bgshacklett commented on Mar 14, 2018

    @bgshacklett

    Mark Kraus (@markekraus) By design or not, the current behavior is unintuitive and likely to cause errors (as I experienced not too long ago). You said it yourself:

    I believe it may be surprising and somewhat confusing to the average PowerShell user.

    PowerShell functionality should be designed for its users, and leaking implementation details, no matter how much they make sense after one has been staring at the C# code, should be avoided wherever possible.

  16. markekraus commented on Mar 14, 2018

    @markekraus
    Contributor

    Brian G. Shacklett (@bgshacklett) The problem is that this would need to be special-cased. Where to we stop with the special-casing? There are a bunch of objects you would not be able to -as them to their own object type in PowerShell and .NET.

    It may be surprising, but so is the pipeline and objects and a million other aspects of PowerShell.

    I think this is better fixed with documentation, rather than risk breaking -as to special-case System.Management.Automation.PSCustomObject. If there were a large number of users tripped up by this regularly and the risk of regression to special-case it was low, I would champion the cause special-casing it. The risk for regression is high (I think, at least) and the number of users this trips up is low and if we documented it in the as operator documentation it could be remedied.

    I'm all for making the language easy for users where it makes sense. I just don't think this is a worthy cause.

  17. mklement0 commented on Mar 15, 2018

    @mklement0
    ContributorAuthor

    Where to we stop with the special-casing?

    All we need to address are leaky abstractions that cause confusion and/or repeated mistakes and/or require cumbersome workarounds.
    And, to be clear, that is not special-casing - that's fixing leaky abstractions.

    Incidentally, this is not just about -as, but also about -is, as I've hopefully clearly demonstrated.

    There are a bunch of objects you would not be able to -as them to their own object type in PowerShell and .NET.

    To recap the previous finding: any .NET instance is an instance of its type with respect to as - irrespective of the presence of public constructors, parameterless constructors, explicit conversions, ... - see below for an example.


    using System;
    
    public static class Program {
        public class Foo {
            // Private constructor
            private Foo() {}
             // Static method that creates an instance via the private constructor.
            public static Foo Create() { return new Foo(); }
        }
    
        public static void Main() {        
             // This outputs 'Foo', demonstrating that the Foo instance created with the
             // static .Create method indeed recognized itself as a Foo instance with `as`
            Console.WriteLine((Foo.Create() as Foo).GetType().Name);
        }
    }
  18. markekraus commented on Mar 15, 2018

    @markekraus
    Contributor

    And, to be clear, that is not special-casing - that's fixing leaky abstractions.

    This is special-casing. You cannot cast some objects as themselves. PSCustomObject is one of those. You want to make -as work for PSCustomObject when it would not normally work. That is the definition of special casing.

    $Code = @'
    using System;
    using System.Management.Automation;
    
    namespace Test 
    {
        public static class As
        {
            public static bool AsPSCustomObject(object InputObject)
            {
                PSCustomObject result = InputObject as PSCustomObject;
                if (null != result)
                {
                    return true;
                }
                else
                {
                    return false;
                }
            }
    
            public static bool AsPSObject(object InputObject)
            {
                PSObject result = InputObject as PSObject;
                if (null != result)
                {
                    return true;
                }
                else
                {
                    return false;
                }
            }
        }
    }
    '@
    Add-Type -TypeDefinition $Code
    $TestObject = [PSCustomObject]@{A = 'a'}
    [Test.As]::AsPSCustomObject($TestObject)
    [Test.As]::AsPSObject($TestObject)

    result

    False
    True
    

    You want to change that behavior from what would happen in .NET to something different in PowerShell. That would most absolutely be special-casing.

    Your test isn't exactly accurate. But I don't have the time to create an example or explain why.

  19. mklement0 commented on Mar 15, 2018

    @mklement0
    ContributorAuthor

    You cannot cast some objects as themselves.

    In C# you can, always. That is, if objects are cast to their true type (as your example demonstrates).

    That PowerShell pretends that a [psobject] (without a base object) is really a [System.Management.Automation.PSCustomObject] instance when you reflect on the type with .GetType() and use -is [System.Management.Automation.PSCustomObject], but then pretends that it isn't when you use -as is the true special-casing here: special-casing of common sense.

    -as in PowerShell looks like C#'s as and behaves like it in all cases but this.
    That it doesn't, is a leak in the implementation that needs to be plugged.
    Ditto for the related -is problems.

    (Whether special-casing behind the scenes is needed to plug this leak is immaterial.)

    The larger issue here is really [psobject] peeking from behind the curtain in several places, causing outright problems in this case and subtler variations in behavior in others.


    On a meta note:

    Your test isn't exactly accurate. But I don't have the time to create an example or explain why.

    Please don't post such comments. They add nothing to the discussion and only serve to antagonize.

  20. mklement0 commented on Mar 15, 2018

    @mklement0
    ContributorAuthor

    Bruce Payette (@BrucePay):

    That's great background information.

    Even though no two [System.Management.Automation.PSCustomObject] instances are therefore guaranteed to have the same members, it is still useful to have the ability to distinguish them from instances of regular types; e.g., you may want a function to accept a [pscustombject] instance as a mockup in lieu of a regular type and then detect that case in the function.

    With -is that works (though, regrettably, only with the full type name, due to [pscustombject] really being [psobject]) , but with -as it doesn't.

  21. markekraus commented on Mar 15, 2018

    @markekraus
    Contributor

    Please don't post such comments. They add nothing to the discussion and only serve to antagonize.

    Not my intent. It would just take more time to explain than I can give at the moment.

  22. microsoft-github-policy-service commented on Nov 16, 2023

    @microsoft-github-policy-service
    Contributor

    This issue has not had any activity in 6 months, if this is a bug please try to reproduce on the latest version of PowerShell and reopen a new issue and reference this issue if this is still a blocker for you.

  23. microsoft-github-policy-service commented on Nov 16, 2023

    @microsoft-github-policy-service
    Contributor

    This issue has not had any activity in 6 months, if this is a bug please try to reproduce on the latest version of PowerShell and reopen a new issue and reference this issue if this is still a blocker for you.

  24. microsoft-github-policy-service commented on Nov 16, 2023

    @microsoft-github-policy-service
    Contributor

    This issue has not had any activity in 6 months, if this is a bug please try to reproduce on the latest version of PowerShell and reopen a new issue and reference this issue if this is still a blocker for you.

  25. microsoft-github-policy-service commented on Nov 23, 2023

    @microsoft-github-policy-service
    Contributor

    This issue has been marked as "No Activity" as there has been no activity for 6 months. It has been closed for housekeeping purposes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Issue-Discussionthe issue may not have a clear classification yet. The issue may generate an RFC or may be reclassifResolution-No ActivityIssue has had no activity for 6 months or moreWG-Enginecore PowerShell engine, interpreter, and runtime

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions