Powershell: Method call behaves differently in PowerShell than in a compiled C# application

Created on 23 Aug 2019  路  5Comments  路  Source: PowerShell/PowerShell

Note: The [datetime]::ParseExact() method used below is the only one I've seen the problem with - I have no idea what underlies this symptom and how general the problem is.

If you run the code below in the following environments:

  • (a) as PowerShell code

  • (b) as C# code compiled on demand with Add-Type

  • (c) as C# code compiled to a .NET Core 2.1 / 3.0-preview8 console application

only (c) works as expected.

Steps to reproduce

The following code should yield 1921:

# Set the 2-digit-year threshold to 2020, so that '21' is interpreted as *19*21
($c = [CultureInfo]::InvariantCulture.Clone()).Calendar.TwoDigitYearMax = 2020; [datetime]::ParseExact('21', 'yy', $c).Year

# Run the equivalent C# code via Add-Type
Add-Type @'
    using System;
    using System.Globalization;

    public static class Program
    {
        public static void Main(string[] args)
        {
            var cc = (CultureInfo)CultureInfo.InvariantCulture.Clone();
            cc.Calendar.TwoDigitYearMax = 2020;
            Console.WriteLine(DateTime.ParseExact("21", "yy", cc).Year);
        }
    }
'@

[Program]::Main(@())

Expected behavior

1921
1921

Actual behavior

2021
2021

That is, the modified .TwoDigitYearMax property value was ignored.

Note:

  • If you run the exact same code as passed to Add-Type above as a .NET Core application, the result is as expected.

  • The problem may be related to calling .Clone() on the static [CultureInfo]::InvariantCulture property, because it goes away if you obtain the invariant culture with [CultureInfo] '' or [CultureInfo]::GetCultureInfo('') instead (while the resulting object _looks_ the same, there's no reference equality)

Environment data

PowerShell Core 7.0.0-preview.2
Windows PowerShell 5.1.17763.592
Issue-Question Resolution-External WG-Engine

All 5 comments

The reason for this behavior is that in reality ParseExact does not use CultureInfo.Calendar but CultureInfo.DateTimeFormat.Calendar property. In original object both calendars are the same reference but after cloning they still have the same values but become separate instances. You can quickly test this using following code:

$original = [cultureinfo]::InvariantCulture
$clone = [cultureinfo]$original.Clone()

[Object]::ReferenceEquals($original.Calendar, $original.DateTimeFormat.Calendar)
[Object]::ReferenceEquals($clone.Calendar, $clone.DateTimeFormat.Calendar)

If you set .DateTimeFormat.Calendar.TwoDigitYearMax = 2020 in your example then it will work as expected. I'm not sure if it's a bug or not but anyway it seems to have nothing to do with PowerShell itself. :)

Excellent analysis, @lpatalas, that narrows it down a lot:

Indeed, setting .DateTimeFormat.Calendar.TwoDigitYearMax directly makes the problem go away, in all scenarios.

but after cloning they still have the same values but become separate instances

That is true in _PowerShell_ - irrespective of whether you use PowerShell code or C# code via Add-Type - but not in _independently compiled C# code_.

That is, if you stick the content of the string passed to Add-Type _as-is_ into a netcoreapp3.0 console application compiled with SDK version 3.0.0-preview8-28405-07, cloning behaves correctly; that is, in the clone the reference equality between .Calendar and .DateTimeFormat.Calendar is _preserved_.

PowerShell's behavior amounts to what would be a _bug_ in the .Clone() method.

Here's code that shows the reference-inequality problem explicitly.
Again: running this _in PowerShell_ malfunctions, running the C# code passed to Add-Type as the source code of a _.NET Core console applications_ works correctly.

'--- PS code'

$ic = [cultureinfo]::InvariantCulture
$icc = $ic.Clone()
"Clone's .Calendar is same reference as its .DateTimeFormat.Calendar?: $([object]::ReferenceEquals($icc.Calendar, $icc.DateTimeFormat.Calendar))"
$icc.Calendar.TwoDigitYearMax = 2020
"21 parsed as: $([datetime]::ParseExact('21', 'yy', $icc).Year)"

'--- C# via Add-Type'

# Run the equivalent C# code via Add-Type
Add-Type @'
    using System;
    using System.Globalization;

    public static class Program
    {
        public static void Main(string[] args)
        {
            var ic = CultureInfo.InvariantCulture;
            var icc = (CultureInfo)ic.Clone();
            Console.WriteLine($"Clone's .Calendar is same reference as its .DateTimeFormat.Calendar?: {object.ReferenceEquals(icc.Calendar, icc.DateTimeFormat.Calendar)}");
            icc.Calendar.TwoDigitYearMax = 2020;
            Console.WriteLine($"21 parsed as: {DateTime.ParseExact("21", "yy", icc).Year}");
        }
    }
'@

[Program]::Main(@())

@lzybkr, any ideas?

I am also able to reproduce it in netcoreapp2.2 console app. The trick is that you have to access CultureInfo.Calendar property before cloning:

var orig = CultureInfo.InvariantCulture;
Console.WriteLine(ReferenceEquals(orig.Calendar, orig.DateTimeFormat.Calendar));

var clone = (CultureInfo)orig.Clone();
Console.WriteLine(ReferenceEquals(clone.Calendar, clone.DateTimeFormat.Calendar));

// Prints:
//   True
//   False

Because InvariantCulture is singleton it's enough to call it once to trigger that behaviour for the rest of the clones so that's probably a reason why we see it in PowerShell.

If I would have to guess it's caused by this line: https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/shared/System/Globalization/CultureInfo.cs#L1015. The _calendar field is null so it's not cloned until it's accessed for the first time. After that it's set and each subsequent Clone() call will create new Calendar instance.

GitHub
CoreCLR is the runtime for .NET Core. It includes the garbage collector, JIT compiler, primitive data types and low-level classes. - dotnet/coreclr

Thanks, @lpatalas - great stuff (it is accessing .DateTimeFormat.Calendar, not .Calendar, before cloning that triggers the bug).

I've created a CoreFx bug report at https://github.com/dotnet/corefx/issues/40953

So it sounds like PowerShell surfaces the bug accidentally, by virtue of the reflection it performs _behind the scenes_, correct?

I'm closing this, as it appears to be purely a CoreFx issue, even though PowerShell happens to surface it consistently.

You're right regarding the property - actually just accessing .DateTimeFormat is enough. So probably my guess regarding offending line is incorrect. :)

Was this page helpful?
0 / 5 - 0 ratings