PowerShell icon indicating copy to clipboard operation
PowerShell copied to clipboard

Conversion of [datetime] property in class does not work

Open eddie-zalar opened this issue 2 years ago • 7 comments

Prerequisites

Steps to reproduce

I implemented this class in file "statistics.ps1" and want to use the automatic cast/conversion utilities of PowerShell:

class Statistics {
	[ValidateNotNullOrEmpty()]
	[nullable[datetime]] $LastDownload

	[ValidateRange("Positive")]
	[int] $Downloads

	[ValidateNotNull()]
	[string] $DownloadedBy = "n/a"
}

Expected behavior

PS> . .\statistics.ps1
PS> $object = [pscustomobject]@{ LastDownload = (Get-Date); Downloads = 123; DownloadedBy = "Jesus!" }
PS> [StatisticsExp]$object

LastDownload        Downloads DownloadedBy
------------        --------- ------------
29.09.2023 15:04:40       123 Jesus!

Actual behavior

PS> . .\statistics.ps1
PS> $object = [pscustomobject]@{ LastDownload = (Get-Date); Downloads = 123; DownloadedBy = "Jesus!" }
PS> [StatisticsExp]$object
InvalidArgument: Cannot convert value "@{LastDownload=29.09.2023 15:04:40; Downloads=123; DownloadedBy=Jesus!}" to type "Statistics". Error: "Cannot convert value "29.09.2023 15:04:40" to type "System.Nullable`1[System.DateTime]". Error: "Cannot process argument because the value of argument "obj" is null. Change the value of argument "obj" to a non-null value.""

Error details

PS> Get-Error

Exception             : 
    Type           : System.Management.Automation.RuntimeException
    ErrorRecord    : 
        Exception             : 
            Type    : System.Management.Automation.ParentContainsErrorRecordException
            Message : Cannot convert value "@{LastDownload=29.09.2023 15:04:40; Downloads=123; DownloadedBy=Jesus!}" to type "Statistics". Error: "Cannot convert value "29.09.2023 15:04:40" to type "System.Nullable`1[System.DateTime]". Error: "Cannot process argument 
because the value of argument "obj" is null. Change the value of argument "obj" to a non-null value.""
            HResult : -2146233087
        CategoryInfo          : InvalidArgument: (:) [], ParentContainsErrorRecordException
        FullyQualifiedErrorId : InvalidCastConstructorException
        InvocationInfo        : 
            ScriptLineNumber : 1
            OffsetInLine     : 1
            HistoryId        : -1
            Line             : [Statistics]$object
            PositionMessage  : At line:1 char:1
                               + [Statistics]$object
                               + ~~~~~~~~~~~~~~~~~~~
            CommandOrigin    : Internal
        ScriptStackTrace      : at <ScriptBlock>, <No file>: line 1
    Message        : Cannot convert value "@{LastDownload=29.09.2023 15:04:40; Downloads=123; DownloadedBy=Jesus!}" to type "Statistics". Error: "Cannot convert value "29.09.2023 15:04:40" to type "System.Nullable`1[System.DateTime]". Error: "Cannot process argument  
because the value of argument "obj" is null. Change the value of argument "obj" to a non-null value.""
    InnerException : 
        Type           : System.Management.Automation.PSInvalidCastException
        ErrorRecord    : 
            Exception             : 
                Type    : System.Management.Automation.ParentContainsErrorRecordException
                Message : Cannot convert value "@{LastDownload=29.09.2023 15:04:40; Downloads=123; DownloadedBy=Jesus!}" to type "Statistics". Error: "Cannot convert value "29.09.2023 15:04:40" to type "System.Nullable`1[System.DateTime]". Error: "Cannot process      
argument because the value of argument "obj" is null. Change the value of argument "obj" to a non-null value.""
                HResult : -2146233087
            CategoryInfo          : InvalidArgument: (:) [], ParentContainsErrorRecordException
            FullyQualifiedErrorId : InvalidCastConstructorException
            InvocationInfo        : 
                ScriptLineNumber : 1
                OffsetInLine     : 1
                HistoryId        : -1
                Line             : [Statistics]$object
                PositionMessage  : At line:1 char:1
                                   + [Statistics]$object
                                   + ~~~~~~~~~~~~~~~~~~~
                CommandOrigin    : Internal
            ScriptStackTrace      : at <ScriptBlock>, <No file>: line 1
        TargetSite     : 
            Name          : Convert
            DeclaringType : System.Management.Automation.LanguagePrimitives+ConvertViaNoArgumentConstructor, System.Management.Automation, Version=7.3.7.500, Culture=neutral, PublicKeyToken=31bf3856ad364e35
            MemberType    : Method
            Module        : System.Management.Automation.dll
        Message        : Cannot convert value "@{LastDownload=29.09.2023 15:04:40; Downloads=123; DownloadedBy=Jesus!}" to type "Statistics". Error: "Cannot convert value "29.09.2023 15:04:40" to type "System.Nullable`1[System.DateTime]". Error: "Cannot process       
argument because the value of argument "obj" is null. Change the value of argument "obj" to a non-null value.""
        Data           : System.Collections.ListDictionaryInternal
        InnerException : 
            Type           : System.Management.Automation.PSInvalidCastException
            ErrorRecord    : 
                Exception             : 
                    Type    : System.Management.Automation.ParentContainsErrorRecordException
                    Message : Cannot convert value "29.09.2023 15:04:40" to type "System.Nullable`1[System.DateTime]". Error: "Cannot process argument because the value of argument "obj" is null. Change the value of argument "obj" to a non-null value."
                    HResult : -2146233087
                CategoryInfo          : InvalidArgument: (:) [], ParentContainsErrorRecordException
                FullyQualifiedErrorId : InvalidCastConstructorException
            TargetSite     : 
                Name          : Convert
                DeclaringType : System.Management.Automation.LanguagePrimitives+ConvertViaNoArgumentConstructor, System.Management.Automation, Version=7.3.7.500, Culture=neutral, PublicKeyToken=31bf3856ad364e35
                MemberType    : Method
                Module        : System.Management.Automation.dll
            Message        : Cannot convert value "29.09.2023 15:04:40" to type "System.Nullable`1[System.DateTime]". Error: "Cannot process argument because the value of argument "obj" is null. Change the value of argument "obj" to a non-null value."
            InnerException : 
                Type        : System.Management.Automation.PSArgumentNullException
                ErrorRecord : 
                    Exception             : 
                        Type    : System.Management.Automation.ParentContainsErrorRecordException
                        Message : Cannot process argument because the value of argument "obj" is null. Change the value of argument "obj" to a non-null value.
                        HResult : -2146233087
                    CategoryInfo          : InvalidArgument: (:) [], ParentContainsErrorRecordException
                    FullyQualifiedErrorId : ArgumentNull
                Message     : Cannot process argument because the value of argument "obj" is null. Change the value of argument "obj" to a non-null value.
                ParamName   : obj
                TargetSite  : 
                    Name          : AsPSObject
                    DeclaringType : psobject
                    MemberType    : Method
                    Module        : System.Management.Automation.dll
                Source      : System.Management.Automation
                HResult     : -2147467261
                StackTrace  : 
   at System.Management.Automation.PSObject.AsPSObject(Object obj, Boolean storeTypeNameAndInstanceMembersLocally)
   at System.Management.Automation.LanguagePrimitives.SetObjectProperties(Object o, IDictionary properties, Type resultType, MemberNotFoundError memberNotFoundErrorAction, MemberSetValueError memberSetValueErrorAction, Boolean enableMethodCall, IFormatProvider
formatProvider, Boolean recursion, Boolean ignoreUnknownMembers)
   at System.Management.Automation.LanguagePrimitives.ConvertViaNoArgumentConstructor.Convert(Object valueToConvert, Type resultType, Boolean recursion, PSObject originalValueToConvert, IFormatProvider formatProvider, TypeTable backupTable, Boolean ignoreUnknownMembers) 
            Source         : System.Management.Automation
            HResult        : -2147467262
            StackTrace     : 
   at System.Management.Automation.LanguagePrimitives.ConvertViaNoArgumentConstructor.Convert(Object valueToConvert, Type resultType, Boolean recursion, PSObject originalValueToConvert, IFormatProvider formatProvider, TypeTable backupTable, Boolean ignoreUnknownMembers) 
   at System.Management.Automation.LanguagePrimitives.SetObjectProperties(Object o, IDictionary properties, Type resultType, MemberNotFoundError memberNotFoundErrorAction, MemberSetValueError memberSetValueErrorAction, Boolean enableMethodCall, IFormatProvider
formatProvider, Boolean recursion, Boolean ignoreUnknownMembers)
   at System.Management.Automation.LanguagePrimitives.SetObjectProperties(Object o, PSObject psObject, Type resultType, MemberNotFoundError memberNotFoundErrorAction, MemberSetValueError memberSetValueErrorAction, IFormatProvider formatProvider, Boolean recursion,       
Boolean ignoreUnknownMembers)
   at System.Management.Automation.LanguagePrimitives.ConvertViaNoArgumentConstructor.Convert(Object valueToConvert, Type resultType, Boolean recursion, PSObject originalValueToConvert, IFormatProvider formatProvider, TypeTable backupTable, Boolean ignoreUnknownMembers) 
        Source         : System.Management.Automation
        HResult        : -2147467262
        StackTrace     : 
   at System.Management.Automation.LanguagePrimitives.ConvertViaNoArgumentConstructor.Convert(Object valueToConvert, Type resultType, Boolean recursion, PSObject originalValueToConvert, IFormatProvider formatProvider, TypeTable backupTable, Boolean ignoreUnknownMembers) 
   at System.Management.Automation.LanguagePrimitives.ConvertViaNoArgumentConstructor.Convert(Object valueToConvert, Type resultType, Boolean recursion, PSObject originalValueToConvert, IFormatProvider formatProvider, TypeTable backupTable)
   at CallSite.Target(Closure, CallSite, Object)
   at System.Dynamic.UpdateDelegates.UpdateAndExecute1[T0,TRet](CallSite site, T0 arg0)
   at System.Management.Automation.Interpreter.DynamicInstruction`2.Run(InterpretedFrame frame)
   at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)
    HResult        : -2146233087
CategoryInfo          : InvalidArgument: (:) [], RuntimeException
FullyQualifiedErrorId : InvalidCastConstructorException
InvocationInfo        : 
    ScriptLineNumber : 1
    OffsetInLine     : 1
    HistoryId        : -1
    Line             : [Statistics]$object
    PositionMessage  : At line:1 char:1
                       + [Statistics]$object
                       + ~~~~~~~~~~~~~~~~~~~
    CommandOrigin    : Internal
ScriptStackTrace      : at <ScriptBlock>, <No file>: line 1

Environment data

PS> $PSVersionTable

Name                           Value
----                           -----
PSVersion                      7.3.7
PSEdition                      Core
GitCommitId                    7.3.7
OS                             Microsoft Windows 10.0.19044
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

Visuals

No response

eddie-zalar avatar Sep 29 '23 13:09 eddie-zalar

I found a workaround for the problem by implementing the explicit operator:

class StatisticsExp {
	[ValidateNotNullOrEmpty()]
	[nullable[datetime]] $LastDownload

	[ValidateRange("Positive")]
	[int] $Downloads

	[ValidateNotNull()]
	[string] $DownloadedBy = "n/a"

	static [StatisticsExp] op_Explicit(
		$Other <# intentionally typeless to fulfill the interface definition #>
	) {
		[StatisticsExp] $converted = [StatisticsExp]::new()
		$converted.LastDownload = $Other.LastDownload
		$converted.Downloads = $Other.Downloads
		$converted.DownloadedBy = $Other.DownloadedBy
		return $converted
	}
}

Now it works as expected.

eddie-zalar avatar Sep 29 '23 13:09 eddie-zalar

It's odd - if you remove nullable the object is created without setting the date. Which makes me wonder if there are limitations on the property-types which transfer on an automatic cast.

jhoneill avatar Sep 29 '23 13:09 jhoneill

In the meantime I found out that it works with a hashtable as input - instead of a PSCustomObject:

PS > $ht = @{ LastDownload = (Get-Date); Downloads = 123; DownloadedBy = "Jesus!" }
PS > [Statistics]$ht    

LastDownload        Downloads DownloadedBy
------------        --------- ------------
29.09.2023 15:49:08       123 Jesus!

eddie-zalar avatar Sep 29 '23 14:09 eddie-zalar

From the call stack it may be that this line of code is the one responsible for the error message: https://github.com/PowerShell/PowerShell/blob/dab6ca28f545d2c15dc4cc9e5121daf3b24b47bd/src/System.Management.Automation/engine/LanguagePrimitives.cs#L4002

If I use a PSCustomObject as input, the properties evaluates to null because it does not implement IDictionary and the usage of the as cast operator. In case I pass a Hashtable as input, properties is not null because the Hashtable implements IDictionary and it works.

eddie-zalar avatar Sep 29 '23 14:09 eddie-zalar

Let me try to summarize:

  • The problem only affects initialization by [pscustomobject], not by hashtable, ...
  • ... and only occurs with invisibly [psobject]-wrapped property values, which happens when you use a cmdlet (Get-Date) as opposed to an expression ([datetime]::Now)
    • See also: #5579
  • It has two manifestations:
    • Binding to a nullable property fails
    • Binding to a non-nullable property is quietly ignored.

The following repro code demonstrates this:

class Statistics1 { [nullable[datetime]] $LastDownload } # nullable
class Statistics2 { [datetime] $LastDownload } # non-nullable

[pscustomobject] @{
  'Nullable, pscustomobject' = $(try { [Statistics1] [pscustomobject] @{ LastDownLoad = Get-Date } } catch { "ERROR: $_" }) | Out-String
  'Nullable, pscustomobject, no psobject wrapper' = $(try { [Statistics1] [pscustomobject] @{ LastDownLoad = [datetime]::Now } } catch { "ERROR: $_" }) | Out-String
  'Nullable, hashtable' =  [Statistics1] @{ LastDownLoad = Get-Date } | Out-String
  'Non-nullable, pscustomobject' =  [Statistics2] [pscustomobject] @{ LastDownLoad = Get-Date } | Out-String
  'Non-nullable, pscustomobject, no psobject wrapper' =  [Statistics2] [pscustomobject] @{ LastDownLoad = [datetime]::Now } | Out-String
  'Non-nullable, hashtable' =  [Statistics2] @{ LastDownLoad = Get-Date } | Out-String
} | Format-List

Output:

Nullable, pscustomobject                          : ERROR: Cannot convert value "@{LastDownLoad=9/29/2023 11:25:34 AM}" to type "Statistics1". Error: "Cannot convert value
                                                    "9/29/2023 11:25:34 AM" to type "System.Nullable`1[System.DateTime]". Error: "Cannot process argument because the value of
                                                    argument "obj" is null. Change the value of argument "obj" to a non-null value.""

Nullable, pscustomobject, no psobject wrapper     :
                                                    LastDownload
                                                    ------------
                                                    9/29/2023 11:25:34 AM


Nullable, hashtable                               :
                                                    LastDownload
                                                    ------------
                                                    9/29/2023 11:25:34 AM


Non-nullable, pscustomobject                      :
                                                    LastDownload
                                                    ------------
                                                    1/1/0001 12:00:00 AM


Non-nullable, pscustomobject, no psobject wrapper :
                                                    LastDownload
                                                    ------------
                                                    9/29/2023 11:25:34 AM


Non-nullable, hashtable                           :
                                                    LastDownload
                                                    ------------
                                                    9/29/2023 11:25:34 AM

Note how the use of non-[psobject]-wrapped property values works in all cases.


Conceptually related (in that initialization by [pscustomobject] behaves differently than initialization by hashtable):

  • #19552

mklement0 avatar Sep 29 '23 15:09 mklement0

The Engine WG discussed this and agree it's a bug

And also that most likely the cause of the bug is that the conversion path isn't handling when DateTime is wrapped in PSObject

SeeminglyScience avatar Nov 13 '23 19:11 SeeminglyScience

Resolved and closed by "no activity". OK, it is not such a big bug... But I wonder if this is the way how MS will implement the announced "security first initiative"? Just asking...

eddie-zalar avatar May 14 '24 15:05 eddie-zalar