Conversion of [datetime] property in class does not work
Prerequisites
- [X] Write a descriptive title.
- [X] Make sure you are able to repro it on the latest released version
- [X] Search the existing issues.
- [X] Refer to the FAQ.
- [X] Refer to Differences between Windows PowerShell 5.1 and PowerShell.
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
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.
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.
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!
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.
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
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
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...