dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.92k stars 4.64k forks source link

Deserializing DateTimeFormatInfo.AbbreviatedMonthNames from a JSON configuration file causes "ArgumentException: Length of the array must be 13" despite correct input #90022

Closed ondrejtucny closed 1 month ago

ondrejtucny commented 1 year ago

Description

The property DateTimeFormatInfo.AbbreviatedMonthNames requires a string[] with exactly 13 items. When deserializing a DateTimeFormatInfo from a configuration JSON object using IConfiguration.Get<T>(), the setter of the property causes an exception claiming that the length of the input array is not 13. However, in the input JSON, the array does have exactly 13 items.

When deserialized using JsonSerializer, it works correctly.

Reproduction Steps

This program demonstrates the problem:

using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Globalization;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.Json;
using System.IO;
using System.Text;

public class Program
{
    public static void Main()
    {
        string str1 = "{ \"AbbreviatedMonthNames\": [ \"I\", \"II\", \"III\", \"IV\", \"V\", \"VI\", \"VII\", \"VIII\", \"IX\", \"X\", \"XI\", \"XII\", \"--\" ] }";
        string str2 = "{ \"Format\": " + str1 + " }";

        // WORKS: deserialize string array
        var x = JsonSerializer.Deserialize<string[]>("[ \"I\", \"II\", \"III\", \"IV\", \"V\", \"VI\", \"VII\", \"VIII\", \"IX\", \"X\", \"XI\", \"XII\", \"\" ]");
        Console.WriteLine(string.Join(',', x));

        // WORKS: deserialize a DateTimeFormatInfo, including a string array with an expected fixed length
        var y = JsonSerializer.Deserialize<DateTimeFormatInfo>(str1);
        Console.WriteLine(string.Join(',', y.AbbreviatedMonthNames));

        // WORKS: deserialize a custom similar object from configuration, including a string array with an expected fixed length
        var config3 = GetConfigurationFromString(str2);
        var object3 = config3.Get<ConfigCustom>();
        Console.WriteLine(object3.Format.AbbreviatedMonthNames.Length);
        Console.WriteLine(string.Join(',', object3.Format.AbbreviatedMonthNames));

        // EXCEPTION: deserialize a DateTimeFormatInfo from configuration, including a string array with an expected fixed length
        // --> System.ArgumentException: Length of the array must be 13. (Parameter 'value')
        var config1 = GetConfigurationFromString(str1);
        var object1 = config1.Get<DateTimeFormatInfo>();
        Console.WriteLine(string.Join(',', object1.AbbreviatedMonthNames));

        // EXCEPTION: deserialize a wrapped DateTimeFormatInfo from configuration, including a string array with an expected fixed length
        // --> System.ArgumentException: Length of the array must be 13. (Parameter 'value')
        var config2 = GetConfigurationFromString(str2);
        var object2 = config2.Get<Config>();
        Console.WriteLine(string.Join(',', object2.Format.AbbreviatedMonthNames));  
    }

    public static IConfiguration GetConfigurationFromString(string jsonString)
    {
        var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(jsonString));
        return new ConfigurationBuilder()
            .AddJsonStream(memoryStream)
            .Build();
    }

    class Config 
    { 
        public DateTimeFormatInfo Format { get; set; } = (DateTimeFormatInfo)CultureInfo.InvariantCulture.DateTimeFormat.Clone(); 
    }   

    class FormatCustom
    {
        public string[] AbbreviatedMonthNames 
        { 
            get => _abbreviatedMonthNames; 
            set 
            {
                Console.WriteLine("setter: " + string.Join(',', value));
                if (value == null || value.Length != 13) throw new ArgumentException($"{value?.Length}");
                _abbreviatedMonthNames = value;
            }   
        }
        private string[] _abbreviatedMonthNames;
    }   

    class ConfigCustom
    { 
        public FormatCustom Format { get; set; } = new ();
    }   
}

Expected behavior

Be able to deserialize DateTimeFormatInfo instances from configuration, including the AbbreviatedMonthNames and other similar properties.

Actual behavior

The program used to reproduce the issue causes the following exception:

Unhandled exception. System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
 ---> System.ArgumentException: Length of the array must be 13. (Parameter 'value')
   at System.Globalization.DateTimeFormatInfo.set_AbbreviatedMonthNames(String[] value)
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
   at System.Reflection.MethodInvoker.Invoke(Object obj, IntPtr* args, BindingFlags invokeAttr)
   --- End of inner exception stack trace ---
   at System.Reflection.MethodInvoker.Invoke(Object obj, IntPtr* args, BindingFlags invokeAttr)
   at System.Reflection.RuntimeMethodInfo.InvokeOneParameter(Object obj, BindingFlags invokeAttr, Binder binder, Object parameter, CultureInfo culture)
   at System.Reflection.RuntimePropertyInfo.SetValue(Object obj, Object value, BindingFlags invokeAttr, Binder binder, Object[] index, CultureInfo culture)
   at System.Reflection.PropertyInfo.SetValue(Object obj, Object value)
   at Microsoft.Extensions.Configuration.ConfigurationBinder.BindProperties(Object instance, IConfiguration configuration, BinderOptions options)
   at Microsoft.Extensions.Configuration.ConfigurationBinder.BindInstance(Type type, BindingPoint bindingPoint, IConfiguration config, BinderOptions options)
   at Microsoft.Extensions.Configuration.ConfigurationBinder.Get(IConfiguration configuration, Type type, Action`1 configureOptions)
   at Microsoft.Extensions.Configuration.ConfigurationBinder.Get[T](IConfiguration configuration, Action`1 configureOptions)
   at Program.Main()

Regression?

Reproduced on .NET 5, 6, 7, 8 Preview 3 using DotNetFiddle.

Known Workarounds

When a custom class with a string[] AbbreviatedMonthNames property is used, the issue does not appear. In fact, the setter gets the expected 13-item array. This can be validated in the example code above.

Configuration

Originally spotted the issue on .NET SDK 6.0.313, running on Windows Server 2019.

Other information

No response

ghost commented 1 year ago

Tagging subscribers to this area: @dotnet/area-extensions-configuration See info in area-owners.md if you want to be subscribed.

Issue Details
### Description The property `DateTimeFormatInfo.AbbreviatedMonthNames` requires a `string[]` with exactly 13 items. When deserializing a `DateTimeFormatInfo` from a configuration JSON object using `IConfiguration.Get()`, the setter of the property causes an exception claiming that the length of the input array is not 13. However, in the input JSON, the array _does have_ exactly 13 items. When deserialized using `JsonSerializer`, it works correctly. ### Reproduction Steps This program demonstrates the problem: ``` using System; using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; using System.Globalization; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.Json; using System.IO; using System.Text; public class Program { public static void Main() { string str1 = "{ \"AbbreviatedMonthNames\": [ \"I\", \"II\", \"III\", \"IV\", \"V\", \"VI\", \"VII\", \"VIII\", \"IX\", \"X\", \"XI\", \"XII\", \"--\" ] }"; string str2 = "{ \"Format\": " + str1 + " }"; // WORKS: deserialize string array var x = JsonSerializer.Deserialize("[ \"I\", \"II\", \"III\", \"IV\", \"V\", \"VI\", \"VII\", \"VIII\", \"IX\", \"X\", \"XI\", \"XII\", \"\" ]"); Console.WriteLine(string.Join(',', x)); // WORKS: deserialize a DateTimeFormatInfo, including a string array with an expected fixed length var y = JsonSerializer.Deserialize(str1); Console.WriteLine(string.Join(',', y.AbbreviatedMonthNames)); // WORKS: deserialize a custom similar object from configuration, including a string array with an expected fixed length var config3 = GetConfigurationFromString(str2); var object3 = config3.Get(); Console.WriteLine(object3.Format.AbbreviatedMonthNames.Length); Console.WriteLine(string.Join(',', object3.Format.AbbreviatedMonthNames)); // EXCEPTION: deserialize a DateTimeFormatInfo from configuration, including a string array with an expected fixed length // --> System.ArgumentException: Length of the array must be 13. (Parameter 'value') var config1 = GetConfigurationFromString(str1); var object1 = config1.Get(); Console.WriteLine(string.Join(',', object1.AbbreviatedMonthNames)); // EXCEPTION: deserialize a wrapped DateTimeFormatInfo from configuration, including a string array with an expected fixed length // --> System.ArgumentException: Length of the array must be 13. (Parameter 'value') var config2 = GetConfigurationFromString(str2); var object2 = config2.Get(); Console.WriteLine(string.Join(',', object2.Format.AbbreviatedMonthNames)); } public static IConfiguration GetConfigurationFromString(string jsonString) { var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(jsonString)); return new ConfigurationBuilder() .AddJsonStream(memoryStream) .Build(); } class Config { public DateTimeFormatInfo Format { get; set; } = (DateTimeFormatInfo)CultureInfo.InvariantCulture.DateTimeFormat.Clone(); } class FormatCustom { public string[] AbbreviatedMonthNames { get => _abbreviatedMonthNames; set { Console.WriteLine("setter: " + string.Join(',', value)); if (value == null || value.Length != 13) throw new ArgumentException($"{value?.Length}"); _abbreviatedMonthNames = value; } } private string[] _abbreviatedMonthNames; } class ConfigCustom { public FormatCustom Format { get; set; } = new (); } } ``` ### Expected behavior Be able to deserialize `DateTimeFormatInfo` instances from configuration, including the `AbbreviatedMonthNames` and other similar properties. ### Actual behavior The program used to reproduce the issue causes the following exception: ``` Unhandled exception. System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.ArgumentException: Length of the array must be 13. (Parameter 'value') at System.Globalization.DateTimeFormatInfo.set_AbbreviatedMonthNames(String[] value) at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor) at System.Reflection.MethodInvoker.Invoke(Object obj, IntPtr* args, BindingFlags invokeAttr) --- End of inner exception stack trace --- at System.Reflection.MethodInvoker.Invoke(Object obj, IntPtr* args, BindingFlags invokeAttr) at System.Reflection.RuntimeMethodInfo.InvokeOneParameter(Object obj, BindingFlags invokeAttr, Binder binder, Object parameter, CultureInfo culture) at System.Reflection.RuntimePropertyInfo.SetValue(Object obj, Object value, BindingFlags invokeAttr, Binder binder, Object[] index, CultureInfo culture) at System.Reflection.PropertyInfo.SetValue(Object obj, Object value) at Microsoft.Extensions.Configuration.ConfigurationBinder.BindProperties(Object instance, IConfiguration configuration, BinderOptions options) at Microsoft.Extensions.Configuration.ConfigurationBinder.BindInstance(Type type, BindingPoint bindingPoint, IConfiguration config, BinderOptions options) at Microsoft.Extensions.Configuration.ConfigurationBinder.Get(IConfiguration configuration, Type type, Action`1 configureOptions) at Microsoft.Extensions.Configuration.ConfigurationBinder.Get[T](IConfiguration configuration, Action`1 configureOptions) at Program.Main() ```reproduce ### Regression? Reproduced on .NET 5, 6, 7, 8 Preview 3 using DotNetFiddle. ### Known Workarounds When a custom class with a `string[] AbbreviatedMonthNames` property is used, the issue does not appear. In fact, the setter gets the expected 13-item array. This can be validated in the example code above. ### Configuration Originally spotted the issue on .NET SDK 6.0.313, running on Windows Server 2019. ### Other information _No response_
Author: ondrejtucny
Assignees: -
Labels: `area-Extensions-Configuration`
Milestone: -
ericstj commented 1 year ago

Doesn't appear to be a regression. This is happening because the target property already has a value of

"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec",""

And configuration will append to this. So it ends up passing down.

"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", "", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII", "--"

Probably we'd need API to control this if we wanted different behavior. I think this is the same issue as https://github.com/dotnet/runtime/issues/36569

ericstj commented 1 month ago

Closing as this is by design.