diff --git a/src/System.Text.Json/src/System/Text/Json/Serialization/ClassMaterializer.cs b/src/System.Text.Json/src/System/Text/Json/Serialization/ClassMaterializer.cs index 5f786400051e..70fa56c8684b 100644 --- a/src/System.Text.Json/src/System/Text/Json/Serialization/ClassMaterializer.cs +++ b/src/System.Text.Json/src/System/Text/Json/Serialization/ClassMaterializer.cs @@ -2,11 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Collections; -using System.Collections.Generic; using System.Diagnostics; using System.Reflection; -using System.Text.Json.Serialization.Converters; namespace System.Text.Json.Serialization { @@ -14,25 +11,50 @@ internal abstract class ClassMaterializer { public abstract JsonClassInfo.ConstructorDelegate CreateConstructor(Type classType); - public abstract object ImmutableCreateRange(Type constructingType, Type elementType); + public abstract object ImmutableCollectionCreateRange(Type constructingType, Type elementType); + public abstract object ImmutableDictionaryCreateRange(Type constructingType, Type elementType); - protected MethodInfo ImmutableCreateRangeMethod(Type constructingType, Type elementType) + protected MethodInfo ImmutableCollectionCreateRangeMethod(Type constructingType, Type elementType) + { + MethodInfo createRangeMethod = FindImmutableCreateRangeMethod(constructingType); + + if (createRangeMethod == null) + { + return null; + } + + return createRangeMethod.MakeGenericMethod(elementType); + } + + protected MethodInfo ImmutableDictionaryCreateRangeMethod(Type constructingType, Type elementType) + { + MethodInfo createRangeMethod = FindImmutableCreateRangeMethod(constructingType); + + if (createRangeMethod == null) + { + return null; + } + + return createRangeMethod.MakeGenericMethod(typeof(string), elementType); + } + + private MethodInfo FindImmutableCreateRangeMethod(Type constructingType) { MethodInfo[] constructingTypeMethods = constructingType.GetMethods(); - MethodInfo createRange = null; foreach (MethodInfo method in constructingTypeMethods) { if (method.Name == "CreateRange" && method.GetParameters().Length == 1) { - createRange = method; - break; + return method; } } - Debug.Assert(createRange != null); - - return createRange.MakeGenericMethod(elementType); + // This shouldn't happen because constructingType should be an immutable type with + // a CreateRange method. `null` being returned here will cause a JsonException to be + // thrown when the desired CreateRange delegate is about to be invoked. + Debug.Fail("Could not create the appropriate CreateRange method."); + return null; } } } diff --git a/src/System.Text.Json/src/System/Text/Json/Serialization/ClassType.cs b/src/System.Text.Json/src/System/Text/Json/Serialization/ClassType.cs index f5fe6ce2c730..9ce97683ede6 100644 --- a/src/System.Text.Json/src/System/Text/Json/Serialization/ClassType.cs +++ b/src/System.Text.Json/src/System/Text/Json/Serialization/ClassType.cs @@ -14,5 +14,6 @@ internal enum ClassType Value = 2, // Data type with single value Enumerable = 3, // IEnumerable Dictionary = 4, // IDictionary + ImmutableDictionary = 5, // Immutable Dictionary } } diff --git a/src/System.Text.Json/src/System/Text/Json/Serialization/Converters/DefaultIEnumerableConstructibleConverter.cs b/src/System.Text.Json/src/System/Text/Json/Serialization/Converters/DefaultIEnumerableConstructibleConverter.cs index 8aa82ed1b6f7..31066a1eb8e3 100644 --- a/src/System.Text.Json/src/System/Text/Json/Serialization/Converters/DefaultIEnumerableConstructibleConverter.cs +++ b/src/System.Text.Json/src/System/Text/Json/Serialization/Converters/DefaultIEnumerableConstructibleConverter.cs @@ -10,33 +10,11 @@ namespace System.Text.Json.Serialization.Converters { internal sealed class DefaultIEnumerableConstructibleConverter : JsonEnumerableConverter { - public static ConcurrentDictionary s_objectJsonProperties = new ConcurrentDictionary(); - public override IEnumerable CreateFromList(ref ReadStack state, IList sourceList, JsonSerializerOptions options) { Type enumerableType = state.Current.JsonPropertyInfo.RuntimePropertyType; JsonClassInfo elementClassInfo = state.Current.JsonPropertyInfo.ElementClassInfo; - - JsonPropertyInfo propertyInfo; - if (elementClassInfo.ClassType == ClassType.Object) - { - Type objectType = elementClassInfo.Type; - - if (s_objectJsonProperties.ContainsKey(objectType)) - { - propertyInfo = s_objectJsonProperties[objectType]; - } - else - { - propertyInfo = JsonClassInfo.CreateProperty(objectType, objectType, null, typeof(object), options); - s_objectJsonProperties[objectType] = propertyInfo; - } - } - else - { - propertyInfo = elementClassInfo.GetPolicyProperty(); - } - + JsonPropertyInfo propertyInfo = options.GetJsonPropertyInfoFromClassInfo(elementClassInfo, options); return propertyInfo.CreateIEnumerableConstructibleType(enumerableType, sourceList); } } diff --git a/src/System.Text.Json/src/System/Text/Json/Serialization/Converters/DefaultImmutableConverter.cs b/src/System.Text.Json/src/System/Text/Json/Serialization/Converters/DefaultImmutableConverter.cs index 00b77ba795b2..d4a06b6bc5e0 100644 --- a/src/System.Text.Json/src/System/Text/Json/Serialization/Converters/DefaultImmutableConverter.cs +++ b/src/System.Text.Json/src/System/Text/Json/Serialization/Converters/DefaultImmutableConverter.cs @@ -34,11 +34,19 @@ internal sealed class DefaultImmutableConverter : JsonEnumerableConverter private const string ImmutableHashSetGenericTypeName = "System.Collections.Immutable.ImmutableHashSet`1"; private const string ImmutableSetGenericInterfaceTypeName = "System.Collections.Immutable.IImmutableSet`1"; + private const string ImmutableDictionaryTypeName = "System.Collections.Immutable.ImmutableDictionary"; + private const string ImmutableDictionaryGenericTypeName = "System.Collections.Immutable.ImmutableDictionary`2"; + private const string ImmutableDictionaryGenericInterfaceTypeName = "System.Collections.Immutable.IImmutableDictionary`2"; + + private const string ImmutableSortedDictionaryTypeName = "System.Collections.Immutable.ImmutableSortedDictionary"; + private const string ImmutableSortedDictionaryGenericTypeName = "System.Collections.Immutable.ImmutableSortedDictionary`2"; + internal delegate object ImmutableCreateRangeDelegate(IEnumerable items); + internal delegate object ImmutableDictCreateRangeDelegate(IEnumerable> items); - public static ConcurrentDictionary CreateRangeDelegates = new ConcurrentDictionary(); + private static ConcurrentDictionary s_createRangeDelegates = new ConcurrentDictionary(); - private string GetConstructingTypeName(string immutableCollectionTypeName) + private static string GetConstructingTypeName(string immutableCollectionTypeName) { switch (immutableCollectionTypeName) { @@ -47,22 +55,27 @@ private string GetConstructingTypeName(string immutableCollectionTypeName) return ImmutableListTypeName; case ImmutableStackGenericTypeName: case ImmutableStackGenericInterfaceTypeName: - return ImmutableStackTypeName; + return ImmutableStackTypeName; case ImmutableQueueGenericTypeName: case ImmutableQueueGenericInterfaceTypeName: - return ImmutableQueueTypeName; + return ImmutableQueueTypeName; case ImmutableSortedSetGenericTypeName: - return ImmutableSortedSetTypeName; + return ImmutableSortedSetTypeName; case ImmutableHashSetGenericTypeName: case ImmutableSetGenericInterfaceTypeName: - return ImmutableHashSetTypeName; + return ImmutableHashSetTypeName; + case ImmutableDictionaryGenericTypeName: + case ImmutableDictionaryGenericInterfaceTypeName: + return ImmutableDictionaryTypeName; + case ImmutableSortedDictionaryGenericTypeName: + return ImmutableSortedDictionaryTypeName; default: - // TODO: Refactor exception throw following serialization exception changes. + // TODO: Refactor exception throw following serialization exception changes. throw new NotSupportedException(SR.Format(SR.DeserializeTypeNotSupported, immutableCollectionTypeName)); } } - private string GetDelegateKey( + private static string GetDelegateKey( Type immutableCollectionType, Type elementType, out Type underlyingType, @@ -76,13 +89,58 @@ private string GetDelegateKey( return $"{constructingTypeName}:{elementType.FullName}"; } - public void RegisterImmutableCollectionType(Type immutableCollectionType, Type elementType, JsonSerializerOptions options) + internal static bool TypeIsImmutableDictionary(Type type) + { + if (!type.IsGenericType) + { + return false; + } + + switch (type.GetGenericTypeDefinition().FullName) + { + case ImmutableDictionaryGenericTypeName: + case ImmutableDictionaryGenericInterfaceTypeName: + case ImmutableSortedDictionaryGenericTypeName: + return true; + default: + return false; + } + } + + internal static bool TryGetCreateRangeDelegate(string delegateKey, out object createRangeDelegate) + { + return s_createRangeDelegates.TryGetValue(delegateKey, out createRangeDelegate) && createRangeDelegate != null; + } + + internal static void RegisterImmutableCollection(Type immutableCollectionType, Type elementType, JsonSerializerOptions options) + { + // Get a unique identifier for a delegate which will point to the appropiate CreateRange method. + string delegateKey = GetDelegateKey(immutableCollectionType, elementType, out Type underlyingType, out string constructingTypeName); + + // Exit if we have registered this immutable collection type. + if (s_createRangeDelegates.ContainsKey(delegateKey)) + { + return; + } + + // Get the constructing type. + Type constructingType = underlyingType.Assembly.GetType(constructingTypeName); + + // Create a delegate which will point to the CreateRange method. + object createRangeDelegate; + createRangeDelegate = options.ClassMaterializerStrategy.ImmutableCollectionCreateRange(constructingType, elementType); + + // Cache the delegate + s_createRangeDelegates.TryAdd(delegateKey, createRangeDelegate); + } + + internal static void RegisterImmutableDictionary(Type immutableCollectionType, Type elementType, JsonSerializerOptions options) { // Get a unique identifier for a delegate which will point to the appropiate CreateRange method. string delegateKey = GetDelegateKey(immutableCollectionType, elementType, out Type underlyingType, out string constructingTypeName); // Exit if we have registered this immutable collection type. - if (CreateRangeDelegates.ContainsKey(delegateKey)) + if (s_createRangeDelegates.ContainsKey(delegateKey)) { return; } @@ -91,11 +149,11 @@ public void RegisterImmutableCollectionType(Type immutableCollectionType, Type e Type constructingType = underlyingType.Assembly.GetType(constructingTypeName); // Create a delegate which will point to the CreateRange method. - object createRangeDelegate = options.ClassMaterializerStrategy.ImmutableCreateRange(constructingType, elementType); - Debug.Assert(createRangeDelegate != null); + object createRangeDelegate; + createRangeDelegate = options.ClassMaterializerStrategy.ImmutableDictionaryCreateRange(constructingType, elementType); // Cache the delegate - CreateRangeDelegates.TryAdd(delegateKey, createRangeDelegate); + s_createRangeDelegates.TryAdd(delegateKey, createRangeDelegate); } public override IEnumerable CreateFromList(ref ReadStack state, IList sourceList, JsonSerializerOptions options) @@ -104,31 +162,24 @@ public override IEnumerable CreateFromList(ref ReadStack state, IList sourceList Type elementType = state.Current.GetElementType(); string delegateKey = GetDelegateKey(immutableCollectionType, elementType, out _, out _); - Debug.Assert(CreateRangeDelegates.ContainsKey(delegateKey)); + Debug.Assert(s_createRangeDelegates.ContainsKey(delegateKey)); JsonClassInfo elementClassInfo = state.Current.JsonPropertyInfo.ElementClassInfo; + JsonPropertyInfo propertyInfo = options.GetJsonPropertyInfoFromClassInfo(elementClassInfo, options); + return propertyInfo.CreateImmutableCollectionFromList(immutableCollectionType, delegateKey, sourceList, state.JsonPath); + } - JsonPropertyInfo propertyInfo; - if (elementClassInfo.ClassType == ClassType.Object) - { - Type objectType = elementClassInfo.Type; - - if (DefaultIEnumerableConstructibleConverter.s_objectJsonProperties.ContainsKey(objectType)) - { - propertyInfo = DefaultIEnumerableConstructibleConverter.s_objectJsonProperties[objectType]; - } - else - { - propertyInfo = JsonClassInfo.CreateProperty(objectType, objectType, null, typeof(object), options); - DefaultIEnumerableConstructibleConverter.s_objectJsonProperties[objectType] = propertyInfo; - } - } - else - { - propertyInfo = elementClassInfo.GetPolicyProperty(); - } + internal IDictionary CreateFromDictionary(ref ReadStack state, IDictionary sourceDictionary, JsonSerializerOptions options) + { + Type immutableCollectionType = state.Current.JsonPropertyInfo.RuntimePropertyType; + Type elementType = state.Current.GetElementType(); - return propertyInfo.CreateImmutableCollectionFromList(delegateKey, sourceList); + string delegateKey = GetDelegateKey(immutableCollectionType, elementType, out _, out _); + Debug.Assert(s_createRangeDelegates.ContainsKey(delegateKey)); + + JsonClassInfo elementClassInfo = state.Current.JsonPropertyInfo.ElementClassInfo; + JsonPropertyInfo propertyInfo = options.GetJsonPropertyInfoFromClassInfo(elementClassInfo, options); + return propertyInfo.CreateImmutableCollectionFromDictionary(immutableCollectionType, delegateKey, sourceDictionary, state.JsonPath); } } } diff --git a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.AddProperty.cs b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.AddProperty.cs index c7578485d6c6..82316f5bcad4 100644 --- a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.AddProperty.cs +++ b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.AddProperty.cs @@ -2,8 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Diagnostics; using System.Reflection; +using System.Text.Json.Serialization.Converters; namespace System.Text.Json.Serialization { @@ -25,7 +25,7 @@ private JsonPropertyInfo AddProperty(Type propertyType, PropertyInfo propertyInf { JsonPropertyInfo jsonInfo = CreateProperty(propertyType, propertyType, propertyInfo, classType, options); - // Convert interfaces to concrete types. + // Convert non-immutable dictionary interfaces to concrete types. if (propertyType.IsInterface && jsonInfo.ClassType == ClassType.Dictionary) { // If a polymorphic case, we have to wait until run-time values are processed. @@ -65,6 +65,7 @@ internal static JsonPropertyInfo CreateProperty(Type declaredPropertyType, Type { case ClassType.Enumerable: case ClassType.Dictionary: + case ClassType.ImmutableDictionary: case ClassType.Unknown: collectionElementType = GetElementType(runtimePropertyType, parentClassType, propertyInfo); break; diff --git a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs index d993307846e6..3962c81a3c84 100644 --- a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs +++ b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs @@ -117,18 +117,33 @@ internal JsonClassInfo(Type type, JsonSerializerOptions options) DetermineExtensionDataProperty(); break; - case ClassType.Enumerable: case ClassType.Dictionary: - // Add a single property that maps to the class type so we can have policies applied. - JsonPropertyInfo policyProperty = AddPolicyProperty(type, options); + { + // Add a single property that maps to the class type so we can have policies applied. + JsonPropertyInfo policyProperty = AddPolicyProperty(type, options); - // Use the type from the property policy to get any late-bound concrete types (from an interface like IDictionary). - CreateObject = options.ClassMaterializerStrategy.CreateConstructor(policyProperty.RuntimePropertyType); + // Use the type from the property policy to get any late-bound concrete types (from an interface like IDictionary). + CreateObject = options.ClassMaterializerStrategy.CreateConstructor(policyProperty.RuntimePropertyType); - // Create a ClassInfo that maps to the element type which is used for (de)serialization and policies. - Type elementType = GetElementType(type, parentType : null, memberInfo: null); - ElementClassInfo = options.GetOrAddClass(elementType); + // Create a ClassInfo that maps to the element type which is used for (de)serialization and policies. + Type elementType = GetElementType(type, parentType: null, memberInfo: null); + ElementClassInfo = options.GetOrAddClass(elementType); + } + break; + case ClassType.ImmutableDictionary: + { + // Add a single property that maps to the class type so we can have policies applied. + AddPolicyProperty(type, options); + + Type elementType = GetElementType(type, parentType: null, memberInfo: null); + + CreateObject = options.ClassMaterializerStrategy.CreateConstructor( + typeof(Dictionary<,>).MakeGenericType(typeof(string), elementType)); + + // Create a ClassInfo that maps to the element type which is used for (de)serialization and policies. + ElementClassInfo = options.GetOrAddClass(elementType); + } break; case ClassType.Value: case ClassType.Unknown: @@ -373,7 +388,7 @@ public static Type GetElementType(Type propertyType, Type parentType, MemberInfo Type[] args = propertyType.GetGenericArguments(); ClassType classType = GetClassType(propertyType); - if (classType == ClassType.Dictionary && + if ((classType == ClassType.Dictionary || classType == ClassType.ImmutableDictionary) && args.Length >= 2 && // It is >= 2 in case there is a IDictionary. args[0].UnderlyingSystemType == typeof(string)) { @@ -403,9 +418,14 @@ internal static ClassType GetClassType(Type type) return ClassType.Value; } + if (DefaultImmutableConverter.TypeIsImmutableDictionary(type)) + { + return ClassType.ImmutableDictionary; + } + if (typeof(IDictionary).IsAssignableFrom(type) || - (type.IsGenericType && (type.GetGenericTypeDefinition() == typeof(IDictionary<,>) - || type.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>)))) + (type.IsGenericType && (type.GetGenericTypeDefinition() == typeof(IDictionary<,>) || + type.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>)))) { return ClassType.Dictionary; } diff --git a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs index df4b80f14e9e..9016ba959670 100644 --- a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs +++ b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs @@ -76,7 +76,7 @@ public JsonClassInfo ElementClassInfo { if (_elementClassInfo == null && _elementType != null) { - Debug.Assert(ClassType == ClassType.Enumerable || ClassType == ClassType.Dictionary); + Debug.Assert(ClassType == ClassType.Enumerable || ClassType == ClassType.Dictionary || ClassType == ClassType.ImmutableDictionary); _elementClassInfo = Options.GetOrAddClass(_elementType); } @@ -179,7 +179,7 @@ private void DeterminePropertyName() private void DetermineSerializationCapabilities() { - if (ClassType != ClassType.Enumerable && ClassType != ClassType.Dictionary) + if (ClassType != ClassType.Enumerable && ClassType != ClassType.Dictionary && ClassType != ClassType.ImmutableDictionary) { // We serialize if there is a getter + not ignoring readonly properties. ShouldSerialize = HasGetter && (HasSetter || !Options.IgnoreReadOnlyProperties); @@ -214,6 +214,12 @@ private void DetermineSerializationCapabilities() { EnumerableConverter = s_jsonArrayConverter; } + else if (ClassType == ClassType.ImmutableDictionary) + { + DefaultImmutableConverter.RegisterImmutableDictionary( + RuntimePropertyType, JsonClassInfo.GetElementType(RuntimePropertyType, parentType: null, memberInfo: null), Options); + EnumerableConverter = s_jsonImmutableConverter; + } else if (typeof(IEnumerable).IsAssignableFrom(RuntimePropertyType)) { Type elementType = JsonClassInfo.GetElementType(RuntimePropertyType, ParentClassType, PropertyInfo); @@ -236,8 +242,8 @@ private void DetermineSerializationCapabilities() RuntimePropertyType.FullName.StartsWith(DefaultImmutableConverter.ImmutableNamespace) && RuntimePropertyType.GetGenericArguments().Length == 1) { + DefaultImmutableConverter.RegisterImmutableCollection(RuntimePropertyType, elementType, Options); EnumerableConverter = s_jsonImmutableConverter; - ((DefaultImmutableConverter)EnumerableConverter).RegisterImmutableCollectionType(RuntimePropertyType, elementType, Options); } } } @@ -285,7 +291,9 @@ public static TAttribute GetAttribute(PropertyInfo propertyInfo) whe return (TAttribute)propertyInfo?.GetCustomAttribute(typeof(TAttribute), inherit: false); } - public abstract IEnumerable CreateImmutableCollectionFromList(string delegateKey, IList sourceList); + public abstract IEnumerable CreateImmutableCollectionFromList(Type collectionType, string delegateKey, IList sourceList, string propertyPath); + + public abstract IDictionary CreateImmutableCollectionFromDictionary(Type collectionType, string delegateKey, IDictionary sourceDictionary, string propertyPath); public abstract IEnumerable CreateIEnumerableConstructibleType(Type enumerableType, IList sourceList); diff --git a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoCommon.cs b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoCommon.cs index 22be588e0fa0..43e94e67a6fa 100644 --- a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoCommon.cs +++ b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoCommon.cs @@ -94,19 +94,38 @@ public override Type GetDictionaryConcreteType() return typeof(Dictionary); } - // Creates an IEnumerable and populates it with the items in the, + // Creates an IEnumerable and populates it with the items in the // sourceList argument then uses the delegateKey argument to identify the appropriate cached // CreateRange method to create and return the desired immutable collection type. - public override IEnumerable CreateImmutableCollectionFromList(string delegateKey, IList sourceList) + public override IEnumerable CreateImmutableCollectionFromList(Type collectionType, string delegateKey, IList sourceList, string propertyPath) { - Debug.Assert(DefaultImmutableConverter.CreateRangeDelegates.ContainsKey(delegateKey)); + if (!DefaultImmutableConverter.TryGetCreateRangeDelegate(delegateKey, out object createRangeDelegateObj)) + { + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(collectionType, propertyPath); + } DefaultImmutableConverter.ImmutableCreateRangeDelegate createRangeDelegate = ( - (DefaultImmutableConverter.ImmutableCreateRangeDelegate)DefaultImmutableConverter.CreateRangeDelegates[delegateKey]); + (DefaultImmutableConverter.ImmutableCreateRangeDelegate)createRangeDelegateObj); return (IEnumerable)createRangeDelegate.Invoke(CreateGenericIEnumerableFromList(sourceList)); } + // Creates an IEnumerable and populates it with the items in the + // sourceList argument then uses the delegateKey argument to identify the appropriate cached + // CreateRange method to create and return the desired immutable collection type. + public override IDictionary CreateImmutableCollectionFromDictionary(Type collectionType, string delegateKey, IDictionary sourceDictionary, string propertyPath) + { + if (!DefaultImmutableConverter.TryGetCreateRangeDelegate(delegateKey, out object createRangeDelegateObj)) + { + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(collectionType, propertyPath); + } + + DefaultImmutableConverter.ImmutableDictCreateRangeDelegate createRangeDelegate = ( + (DefaultImmutableConverter.ImmutableDictCreateRangeDelegate)createRangeDelegateObj); + + return (IDictionary)createRangeDelegate.Invoke(CreateGenericIEnumerableFromDictionary(sourceDictionary)); + } + public override IEnumerable CreateIEnumerableConstructibleType(Type enumerableType, IList sourceList) { return (IEnumerable)Activator.CreateInstance(enumerableType, CreateGenericIEnumerableFromList(sourceList)); @@ -119,5 +138,13 @@ private IEnumerable CreateGenericIEnumerableFromList(IList sou yield return (TRuntimeProperty)item; } } + + private IEnumerable> CreateGenericIEnumerableFromDictionary(IDictionary sourceDictionary) + { + foreach (DictionaryEntry item in sourceDictionary) + { + yield return new KeyValuePair((string)item.Key, (TRuntimeProperty)item.Value); + } + } } } diff --git a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleArray.cs b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleArray.cs index 20163bff4f50..cd5668e47c08 100644 --- a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleArray.cs +++ b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleArray.cs @@ -128,7 +128,7 @@ private static bool HandleEndArray( state.Current.ReturnValue = value; return true; } - else if (state.Current.IsEnumerable || state.Current.IsDictionary) + else if (state.Current.IsEnumerable || state.Current.IsDictionary || state.Current.IsImmutableDictionary) { // Returning a non-converted list. return true; @@ -202,6 +202,22 @@ internal static void ApplyObjectToEnumerable( ThrowHelper.ThrowJsonException_DeserializeDuplicateKey(key, reader, state.JsonPath); } } + else if (state.Current.IsImmutableDictionary || (state.Current.IsImmutableDictionaryProperty && !setPropertyDirectly)) + { + Debug.Assert(state.Current.TempDictionaryValues != null); + IDictionary dictionary = (IDictionary)state.Current.JsonPropertyInfo.GetValueAsObject(state.Current.TempDictionaryValues); + + string key = state.Current.KeyName; + Debug.Assert(!string.IsNullOrEmpty(key)); + if (!dictionary.Contains(key)) + { + dictionary.Add(key, value); + } + else + { + ThrowHelper.ThrowJsonException_DeserializeDuplicateKey(key, reader, state.JsonPath); + } + } else { Debug.Assert(state.Current.JsonPropertyInfo != null); @@ -265,6 +281,22 @@ internal static void ApplyValueToEnumerable( ThrowHelper.ThrowJsonException_DeserializeDuplicateKey(key, reader, state.JsonPath); } } + else if (state.Current.IsProcessingImmutableDictionary) + { + Debug.Assert(state.Current.TempDictionaryValues != null); + IDictionary dictionary = (IDictionary)state.Current.TempDictionaryValues; + + string key = state.Current.KeyName; + Debug.Assert(!string.IsNullOrEmpty(key)); + if (!dictionary.ContainsKey(key)) // The IDictionary.TryAdd extension method is not available in netstandard. + { + dictionary.Add(key, value); + } + else + { + ThrowHelper.ThrowJsonException_DeserializeDuplicateKey(key, reader, state.JsonPath); + } + } else { Debug.Assert(state.Current.JsonPropertyInfo != null); diff --git a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleDictionary.cs b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleDictionary.cs index ef9f66a0e91b..2871933f024c 100644 --- a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleDictionary.cs +++ b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleDictionary.cs @@ -4,6 +4,7 @@ using System.Collections; using System.Diagnostics; +using System.Text.Json.Serialization.Converters; namespace System.Text.Json.Serialization { @@ -24,7 +25,6 @@ private static void HandleStartDictionary(JsonSerializerOptions options, ref Utf // A nested object or dictionary so push new frame. if (state.Current.PropertyInitialized) { - JsonClassInfo classInfoTemp = jsonPropertyInfo.RuntimeClassInfo; state.Push(); state.Current.JsonClassInfo = jsonPropertyInfo.ElementClassInfo; state.Current.InitializeJsonPropertyInfo(); @@ -39,18 +39,32 @@ private static void HandleStartDictionary(JsonSerializerOptions options, ref Utf } JsonClassInfo classInfo = state.Current.JsonClassInfo; - state.Current.ReturnValue = classInfo.CreateObject(); + + if (state.Current.IsProcessingImmutableDictionary) + { + state.Current.TempDictionaryValues = (IDictionary)classInfo.CreateObject(); + } + else + { + state.Current.ReturnValue = classInfo.CreateObject(); + } return; } state.Current.PropertyInitialized = true; - // If current property is already set (from a constructor, for example) leave as-is. - if (jsonPropertyInfo.GetValueAsObject(state.Current.ReturnValue) == null) + if (state.Current.IsProcessingImmutableDictionary) + { + JsonClassInfo dictionaryClassInfo = options.GetOrAddClass(jsonPropertyInfo.RuntimePropertyType); + state.Current.TempDictionaryValues = (IDictionary)dictionaryClassInfo.CreateObject(); + } + // Else if current property is already set (from a constructor, for example), leave as-is. + else if (jsonPropertyInfo.GetValueAsObject(state.Current.ReturnValue) == null) { // Create the dictionary. JsonClassInfo dictionaryClassInfo = jsonPropertyInfo.RuntimeClassInfo; IDictionary value = (IDictionary)dictionaryClassInfo.CreateObject(); + if (value != null) { if (state.Current.ReturnValue != null) @@ -73,9 +87,23 @@ private static void HandleEndDictionary(JsonSerializerOptions options, ref Utf8J // We added the items to the dictionary already. state.Current.ResetProperty(); } + else if (state.Current.IsImmutableDictionaryProperty) + { + Debug.Assert(state.Current.TempDictionaryValues != null); + state.Current.JsonPropertyInfo.SetValueAsObject(state.Current.ReturnValue, CreateImmutableDictionaryFromTempValues(ref state, options)); + state.Current.ResetProperty(); + } else { - object value = state.Current.ReturnValue; + object value; + if (state.Current.TempDictionaryValues != null) + { + value = CreateImmutableDictionaryFromTempValues(ref state, options); + } + else + { + value = state.Current.ReturnValue; + } if (state.IsLastFrame) { @@ -90,5 +118,15 @@ private static void HandleEndDictionary(JsonSerializerOptions options, ref Utf8J } } } + + private static IDictionary CreateImmutableDictionaryFromTempValues(ref ReadStack state, JsonSerializerOptions options) + { + Debug.Assert(state.Current.IsProcessingImmutableDictionary); + + DefaultImmutableConverter converter = (DefaultImmutableConverter)state.Current.JsonPropertyInfo.EnumerableConverter; + Debug.Assert(converter != null); + + return converter.CreateFromDictionary(ref state, state.Current.TempDictionaryValues, options); + } } } diff --git a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleNull.cs b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleNull.cs index 4faefed6e9ba..32a16de8d871 100644 --- a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleNull.cs +++ b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleNull.cs @@ -29,13 +29,13 @@ private static bool HandleNull(ref Utf8JsonReader reader, ref ReadStack state, J ThrowHelper.ThrowJsonException_DeserializeCannotBeNull(reader, state.JsonPath); } - if (state.Current.IsEnumerable || state.Current.IsDictionary) + if (state.Current.IsEnumerable || state.Current.IsDictionary || state.Current.IsImmutableDictionary) { ApplyObjectToEnumerable(null, ref state, ref reader); return false; } - if (state.Current.IsEnumerableProperty || state.Current.IsDictionaryProperty) + if (state.Current.IsEnumerableProperty || state.Current.IsDictionaryProperty || state.Current.IsImmutableDictionaryProperty) { bool setPropertyToNull = !state.Current.PropertyInitialized; ApplyObjectToEnumerable(null, ref state, ref reader, setPropertyDirectly: setPropertyToNull); diff --git a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleObject.cs b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleObject.cs index 0672a311b8aa..b81e957deee6 100644 --- a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleObject.cs +++ b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleObject.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Collections; using System.Diagnostics; namespace System.Text.Json.Serialization @@ -10,7 +11,7 @@ public static partial class JsonSerializer { private static void HandleStartObject(JsonSerializerOptions options, ref Utf8JsonReader reader, ref ReadStack state) { - Debug.Assert(!state.Current.IsProcessingDictionary); + Debug.Assert(!state.Current.IsProcessingDictionary && !state.Current.IsProcessingImmutableDictionary); if (state.Current.IsProcessingEnumerable) { @@ -41,12 +42,19 @@ private static void HandleStartObject(JsonSerializerOptions options, ref Utf8Jso } } - state.Current.ReturnValue = classInfo.CreateObject(); + if (state.Current.IsProcessingImmutableDictionary) + { + state.Current.TempDictionaryValues = (IDictionary)classInfo.CreateObject(); + } + else + { + state.Current.ReturnValue = classInfo.CreateObject(); + } } private static void HandleEndObject(JsonSerializerOptions options, ref Utf8JsonReader reader, ref ReadStack state) { - Debug.Assert(!state.Current.IsProcessingDictionary); + Debug.Assert(!state.Current.IsProcessingDictionary && !state.Current.IsProcessingImmutableDictionary); state.Current.JsonClassInfo.UpdateSortedPropertyCache(ref state.Current); diff --git a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs index 044ef3048d55..8e8b4fa67bba 100644 --- a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs +++ b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs @@ -20,10 +20,10 @@ private static void HandlePropertyName( return; } - Debug.Assert(state.Current.ReturnValue != default); + Debug.Assert(state.Current.ReturnValue != default || state.Current.TempDictionaryValues != default); Debug.Assert(state.Current.JsonClassInfo != default); - if (state.Current.IsProcessingDictionary) + if (state.Current.IsProcessingDictionary || state.Current.IsProcessingImmutableDictionary) { if (ReferenceEquals(state.Current.JsonClassInfo.DataExtensionProperty, state.Current.JsonPropertyInfo)) { @@ -47,13 +47,16 @@ private static void HandlePropertyName( keyName = options.DictionaryKeyPolicy.ConvertName(keyName); } - if (state.Current.IsDictionary) + if (state.Current.IsDictionary || state.Current.IsImmutableDictionary) { state.Current.JsonPropertyInfo = state.Current.JsonClassInfo.GetPolicyProperty(); } - Debug.Assert(state.Current.IsDictionary || - (state.Current.IsDictionaryProperty && state.Current.JsonPropertyInfo != null)); + Debug.Assert( + state.Current.IsDictionary || + (state.Current.IsDictionaryProperty && state.Current.JsonPropertyInfo != null) || + state.Current.IsImmutableDictionary || + (state.Current.IsImmutableDictionaryProperty && state.Current.JsonPropertyInfo != null)); state.Current.KeyName = keyName; } diff --git a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.cs b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.cs index 4d96348b0394..69bdf40ac305 100644 --- a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.cs +++ b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.cs @@ -66,7 +66,7 @@ private static void ReadCore( break; } } - else if (readStack.Current.IsProcessingDictionary) + else if (readStack.Current.IsProcessingDictionary || readStack.Current.IsProcessingImmutableDictionary) { HandleStartDictionary(options, ref reader, ref readStack); } @@ -81,7 +81,7 @@ private static void ReadCore( { readStack.Pop(); } - else if (readStack.Current.IsProcessingDictionary) + else if (readStack.Current.IsProcessingDictionary || readStack.Current.IsProcessingImmutableDictionary) { HandleEndDictionary(options, ref reader, ref readStack); } diff --git a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleDictionary.cs b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleDictionary.cs index 468aee8bf0b5..5d7196a36929 100644 --- a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleDictionary.cs +++ b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleDictionary.cs @@ -31,7 +31,7 @@ private static bool HandleDictionary( return true; } - state.Current.Enumerator = enumerable.GetEnumerator(); + state.Current.Enumerator = ((IDictionary)enumerable).GetEnumerator(); state.Current.WriteObjectOrArrayStart(ClassType.Dictionary, writer); } @@ -113,6 +113,11 @@ internal static void WriteDictionary( value = (TProperty)polymorphicEnumerator.Current.Value; key = polymorphicEnumerator.Current.Key; } + else if (current.IsImmutableDictionary || current.IsImmutableDictionaryProperty) + { + value = (TProperty)((DictionaryEntry)current.Enumerator.Current).Value; + key = (string)((DictionaryEntry)current.Enumerator.Current).Key; + } else { // Todo: support non-generic Dictionary here (IDictionaryEnumerator) diff --git a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleObject.cs b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleObject.cs index 3dc24aab0990..e0fb37189b88 100644 --- a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleObject.cs +++ b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleObject.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Diagnostics; +using System.Text.Json.Serialization.Converters; namespace System.Text.Json.Serialization { @@ -103,6 +104,20 @@ private static bool HandleObject( return endOfEnumerable; } + // A property that returns an immutable dictionary keeps the same stack frame. + if (jsonPropertyInfo.ClassType == ClassType.ImmutableDictionary) + { + state.Current.IsImmutableDictionaryProperty = true; + + bool endOfEnumerable = HandleDictionary(jsonPropertyInfo.ElementClassInfo, options, writer, ref state); + if (endOfEnumerable) + { + state.Current.NextProperty(); + } + + return endOfEnumerable; + } + // A property that returns an object. if (!obtainedValue) { diff --git a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.cs b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.cs index c9a9e4a762bb..40c1ce53bdba 100644 --- a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.cs +++ b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.cs @@ -38,6 +38,7 @@ private static bool Write( finishedSerializing = WriteObject(options, writer, ref state); break; case ClassType.Dictionary: + case ClassType.ImmutableDictionary: finishedSerializing = HandleDictionary(current.JsonClassInfo.ElementClassInfo, options, writer, ref state); break; default: diff --git a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs index 00e21f572a08..9d072b204197 100644 --- a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs +++ b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs @@ -17,6 +17,7 @@ public sealed class JsonSerializerOptions internal static readonly JsonSerializerOptions s_defaultOptions = new JsonSerializerOptions(); private readonly ConcurrentDictionary _classes = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _objectJsonProperties = new ConcurrentDictionary(); private ClassMaterializer _classMaterializerStrategy; private JsonNamingPolicy _dictionayKeyPolicy; private JsonNamingPolicy _jsonPropertyNamingPolicy; @@ -304,6 +305,24 @@ internal JsonWriterOptions GetWriterOptions() }; } + internal JsonPropertyInfo GetJsonPropertyInfoFromClassInfo(JsonClassInfo classInfo, JsonSerializerOptions options) + { + if (classInfo.ClassType != ClassType.Object) + { + return classInfo.GetPolicyProperty(); + } + + Type objectType = classInfo.Type; + + if (!_objectJsonProperties.TryGetValue(objectType, out JsonPropertyInfo propertyInfo)) + { + propertyInfo = JsonClassInfo.CreateProperty(objectType, objectType, null, typeof(object), options); + _objectJsonProperties[objectType] = propertyInfo; + } + + return propertyInfo; + } + private void VerifyMutable() { // The default options are hidden and thus should be immutable. diff --git a/src/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs b/src/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs index 999cae10cde6..6380a0c879e2 100644 --- a/src/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs +++ b/src/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; +using System.Text.Json.Serialization.Converters; namespace System.Text.Json.Serialization { @@ -31,6 +32,9 @@ internal struct ReadStackFrame // Has an array or dictionary property been initialized. public bool PropertyInitialized; + // Support Immutable dictionary types. + public IDictionary TempDictionaryValues; + // For performance, we order the properties by the first deserialize and PropertyIndex helps find the right slot quicker. public int PropertyIndex; public List PropertyRefCache; @@ -38,11 +42,14 @@ internal struct ReadStackFrame // The current JSON data for a property does not match a given POCO, so ignore the property (recursively). public bool Drain; + public bool IsImmutableDictionary => JsonClassInfo.ClassType == ClassType.ImmutableDictionary; public bool IsDictionary => JsonClassInfo.ClassType == ClassType.Dictionary; public bool IsDictionaryProperty => JsonPropertyInfo != null && !JsonPropertyInfo.IsPropertyPolicy && JsonPropertyInfo.ClassType == ClassType.Dictionary; + public bool IsImmutableDictionaryProperty => JsonPropertyInfo != null && + !JsonPropertyInfo.IsPropertyPolicy && (JsonPropertyInfo.ClassType == ClassType.ImmutableDictionary); public bool IsEnumerable => JsonClassInfo.ClassType == ClassType.Enumerable; @@ -51,8 +58,9 @@ internal struct ReadStackFrame !JsonPropertyInfo.IsPropertyPolicy && JsonPropertyInfo.ClassType == ClassType.Enumerable; - public bool IsProcessingEnumerableOrDictionary => IsProcessingEnumerable || IsProcessingDictionary; + public bool IsProcessingEnumerableOrDictionary => IsProcessingEnumerable || IsProcessingDictionary || IsProcessingImmutableDictionary; public bool IsProcessingDictionary => IsDictionary || IsDictionaryProperty; + public bool IsProcessingImmutableDictionary => IsImmutableDictionary || IsImmutableDictionaryProperty; public bool IsProcessingEnumerable => IsEnumerable || IsEnumerableProperty; public bool IsProcessingValue @@ -71,7 +79,9 @@ public bool IsProcessingValue return type == ClassType.Value || type == ClassType.Unknown || KeyName != null && ( (IsDictionary && JsonClassInfo.ElementClassInfo.ClassType == ClassType.Unknown) || - (IsDictionaryProperty && JsonPropertyInfo.ElementClassInfo.ClassType == ClassType.Unknown) + (IsDictionaryProperty && JsonPropertyInfo.ElementClassInfo.ClassType == ClassType.Unknown) || + (IsImmutableDictionary && JsonClassInfo.ElementClassInfo.ClassType == ClassType.Unknown) || + (IsImmutableDictionaryProperty && JsonPropertyInfo.ElementClassInfo.ClassType == ClassType.Unknown) ); } } @@ -84,7 +94,10 @@ public void Initialize(Type type, JsonSerializerOptions options) public void InitializeJsonPropertyInfo() { - if (JsonClassInfo.ClassType == ClassType.Value || JsonClassInfo.ClassType == ClassType.Enumerable || JsonClassInfo.ClassType == ClassType.Dictionary) + if (JsonClassInfo.ClassType == ClassType.Value || + JsonClassInfo.ClassType == ClassType.Enumerable || + JsonClassInfo.ClassType == ClassType.Dictionary || + JsonClassInfo.ClassType == ClassType.ImmutableDictionary) { JsonPropertyInfo = JsonClassInfo.GetPolicyProperty(); } @@ -105,6 +118,7 @@ public void ResetProperty() PropertyInitialized = false; JsonPropertyInfo = null; TempEnumerableValues = null; + TempDictionaryValues = null; JsonPropertyName = null; KeyName = null; } @@ -154,12 +168,12 @@ public static object CreateEnumerableValue(ref Utf8JsonReader reader, ref ReadSt public Type GetElementType() { - if (IsEnumerableProperty || IsDictionaryProperty) + if (IsEnumerableProperty || IsDictionaryProperty || IsImmutableDictionaryProperty) { return JsonPropertyInfo.ElementClassInfo.Type; } - if (IsEnumerable || IsDictionary) + if (IsEnumerable || IsDictionary || IsImmutableDictionary) { return JsonClassInfo.ElementClassInfo.Type; } diff --git a/src/System.Text.Json/src/System/Text/Json/Serialization/ReflectionEmitMaterializer.cs b/src/System.Text.Json/src/System/Text/Json/Serialization/ReflectionEmitMaterializer.cs index 7af144a96aac..f7fbd48c8920 100644 --- a/src/System.Text.Json/src/System/Text/Json/Serialization/ReflectionEmitMaterializer.cs +++ b/src/System.Text.Json/src/System/Text/Json/Serialization/ReflectionEmitMaterializer.cs @@ -50,13 +50,31 @@ public override JsonClassInfo.ConstructorDelegate CreateConstructor(Type type) return (JsonClassInfo.ConstructorDelegate)dynamicMethod.CreateDelegate(typeof(JsonClassInfo.ConstructorDelegate)); } - public override object ImmutableCreateRange(Type constructingType, Type elementType) + public override object ImmutableCollectionCreateRange(Type constructingType, Type elementType) { - MethodInfo createRange = ImmutableCreateRangeMethod(constructingType, elementType); + MethodInfo createRange = ImmutableCollectionCreateRangeMethod(constructingType, elementType); + + if (createRange == null) + { + return null; + } return createRange.CreateDelegate( typeof(DefaultImmutableConverter.ImmutableCreateRangeDelegate<>).MakeGenericType(elementType), null); } + + public override object ImmutableDictionaryCreateRange(Type constructingType, Type elementType) + { + MethodInfo createRange = ImmutableDictionaryCreateRangeMethod(constructingType, elementType); + + if (createRange == null) + { + return null; + } + + return createRange.CreateDelegate( + typeof(DefaultImmutableConverter.ImmutableDictCreateRangeDelegate<,>).MakeGenericType(typeof(string), elementType), null); + } } } #endif diff --git a/src/System.Text.Json/src/System/Text/Json/Serialization/ReflectionMaterializer.cs b/src/System.Text.Json/src/System/Text/Json/Serialization/ReflectionMaterializer.cs index c22f951084d6..c7c98952680c 100644 --- a/src/System.Text.Json/src/System/Text/Json/Serialization/ReflectionMaterializer.cs +++ b/src/System.Text.Json/src/System/Text/Json/Serialization/ReflectionMaterializer.cs @@ -23,12 +23,30 @@ public override JsonClassInfo.ConstructorDelegate CreateConstructor(Type type) return () => Activator.CreateInstance(type); } - public override object ImmutableCreateRange(Type constructingType, Type elementType) + public override object ImmutableCollectionCreateRange(Type constructingType, Type elementType) { - MethodInfo createRange = ImmutableCreateRangeMethod(constructingType, elementType); + MethodInfo createRange = ImmutableCollectionCreateRangeMethod(constructingType, elementType); + + if (createRange == null) + { + return null; + } return createRange.CreateDelegate( typeof(DefaultImmutableConverter.ImmutableCreateRangeDelegate<>).MakeGenericType(elementType), null); } + + public override object ImmutableDictionaryCreateRange(Type constructingType, Type elementType) + { + MethodInfo createRange = ImmutableDictionaryCreateRangeMethod(constructingType, elementType); + + if (createRange == null) + { + return null; + } + + return createRange.CreateDelegate( + typeof(DefaultImmutableConverter.ImmutableDictCreateRangeDelegate<,>).MakeGenericType(typeof(string), elementType), null); + } } } diff --git a/src/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs b/src/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs index 5cc3453eeec0..a97da5b517a0 100644 --- a/src/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs +++ b/src/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Text.Json.Serialization.Converters; namespace System.Text.Json.Serialization { @@ -51,6 +52,13 @@ public void Push(JsonClassInfo nextClassInfo, object nextValue) Current.PopStackOnEnd = true; Current.JsonPropertyInfo = Current.JsonClassInfo.GetPolicyProperty(); } + else if (classType == ClassType.ImmutableDictionary) + { + Current.PopStackOnEnd = true; + Current.JsonPropertyInfo = Current.JsonClassInfo.GetPolicyProperty(); + + Current.IsImmutableDictionary = true; + } else { Debug.Assert(nextClassInfo.ClassType == ClassType.Object || nextClassInfo.ClassType == ClassType.Unknown); diff --git a/src/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs b/src/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs index f77bf4cca922..42bd1122acc1 100644 --- a/src/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs +++ b/src/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs @@ -5,6 +5,7 @@ using System.Buffers; using System.Collections; using System.Diagnostics; +using System.Text.Json.Serialization.Converters; namespace System.Text.Json.Serialization { @@ -17,6 +18,10 @@ internal struct WriteStackFrame // Support Dictionary keys. public string KeyName; + // Whether the current object is an immutable dictionary. + public bool IsImmutableDictionary; + public bool IsImmutableDictionaryProperty; + // The current enumerator for the IEnumerable or IDictionary. public IEnumerator Enumerator; @@ -42,6 +47,11 @@ public void Initialize(Type type, JsonSerializerOptions options) { JsonPropertyInfo = JsonClassInfo.GetPolicyProperty(); } + else if (JsonClassInfo.ClassType == ClassType.ImmutableDictionary) + { + JsonPropertyInfo = JsonClassInfo.GetPolicyProperty(); + IsImmutableDictionary = true; + } } public void WriteObjectOrArrayStart(ClassType classType, Utf8JsonWriter writer, bool writeNull = false) @@ -60,7 +70,7 @@ public void WriteObjectOrArrayStart(ClassType classType, Utf8JsonWriter writer, Debug.Assert(writeNull == false); // Write start without a property name. - if (classType == ClassType.Object || classType == ClassType.Dictionary) + if (classType == ClassType.Object || classType == ClassType.Dictionary || classType == ClassType.ImmutableDictionary) { writer.WriteStartObject(); StartObjectWritten = true; @@ -79,7 +89,9 @@ private void WriteObjectOrArrayStart(ClassType classType, JsonEncodedText proper { writer.WriteNull(propertyName); } - else if (classType == ClassType.Object || classType == ClassType.Dictionary) + else if (classType == ClassType.Object || + classType == ClassType.Dictionary || + classType == ClassType.ImmutableDictionary) { writer.WriteStartObject(propertyName); StartObjectWritten = true; @@ -99,6 +111,7 @@ public void Reset() JsonClassInfo = null; JsonPropertyInfo = null; PropertyIndex = 0; + IsImmutableDictionary = false; PopStackOnEndObject = false; PopStackOnEnd = false; StartObjectWritten = false; @@ -108,6 +121,7 @@ public void EndObject() { PropertyIndex = 0; PopStackOnEndObject = false; + IsImmutableDictionaryProperty = false; JsonPropertyInfo = null; } diff --git a/src/System.Text.Json/tests/Serialization/DictionaryTests.cs b/src/System.Text.Json/tests/Serialization/DictionaryTests.cs index 5d6d11428ad1..4bd3612c2d8d 100644 --- a/src/System.Text.Json/tests/Serialization/DictionaryTests.cs +++ b/src/System.Text.Json/tests/Serialization/DictionaryTests.cs @@ -4,6 +4,7 @@ using System.Collections; using System.Collections.Generic; +using System.Collections.Immutable; using Xunit; namespace System.Text.Json.Serialization.Tests @@ -14,6 +15,7 @@ public static partial class DictionaryTests public static void DictionaryOfString() { const string JsonString = @"{""Hello"":""World"",""Hello2"":""World2""}"; + const string ReorderedJsonString = @"{""Hello2"":""World2"",""Hello"":""World""}"; { Dictionary obj = JsonSerializer.Parse>(JsonString); @@ -50,6 +52,42 @@ public static void DictionaryOfString() json = JsonSerializer.ToString(obj); Assert.Equal(JsonString, json); } + + { + ImmutableDictionary obj = JsonSerializer.Parse>(JsonString); + Assert.Equal("World", obj["Hello"]); + Assert.Equal("World2", obj["Hello2"]); + + string json = JsonSerializer.ToString(obj); + Assert.True(JsonString == json || ReorderedJsonString == json); + + json = JsonSerializer.ToString(obj); + Assert.True(JsonString == json || ReorderedJsonString == json); + } + + { + IImmutableDictionary obj = JsonSerializer.Parse>(JsonString); + Assert.Equal("World", obj["Hello"]); + Assert.Equal("World2", obj["Hello2"]); + + string json = JsonSerializer.ToString(obj); + Assert.True(JsonString == json || ReorderedJsonString == json); + + json = JsonSerializer.ToString(obj); + Assert.True(JsonString == json || ReorderedJsonString == json); + } + + { + ImmutableSortedDictionary obj = JsonSerializer.Parse>(JsonString); + Assert.Equal("World", obj["Hello"]); + Assert.Equal("World2", obj["Hello2"]); + + string json = JsonSerializer.ToString(obj); + Assert.True(JsonString == json); + + json = JsonSerializer.ToString(obj); + Assert.True(JsonString == json); + } } [Fact] @@ -103,7 +141,8 @@ class Poco [Fact] public static void FirstGenericArgNotStringFail() { - Assert.Throws(() => JsonSerializer.Parse>(@"{""Key1"":1}")); + Assert.Throws(() => JsonSerializer.Parse>(@"{1:1}")); + Assert.Throws(() => JsonSerializer.Parse>(@"{1:1}")); } [Fact] @@ -111,19 +150,53 @@ public static void DictionaryOfList() { const string JsonString = @"{""Key1"":[1,2],""Key2"":[3,4]}"; - IDictionary> obj = JsonSerializer.Parse>>(JsonString); + { + IDictionary> obj = JsonSerializer.Parse>>(JsonString); - Assert.Equal(2, obj.Count); - Assert.Equal(2, obj["Key1"].Count); - Assert.Equal(1, obj["Key1"][0]); - Assert.Equal(2, obj["Key1"][1]); - Assert.Equal(2, obj["Key2"].Count); - Assert.Equal(3, obj["Key2"][0]); - Assert.Equal(4, obj["Key2"][1]); + Assert.Equal(2, obj.Count); + Assert.Equal(2, obj["Key1"].Count); + Assert.Equal(1, obj["Key1"][0]); + Assert.Equal(2, obj["Key1"][1]); + Assert.Equal(2, obj["Key2"].Count); + Assert.Equal(3, obj["Key2"][0]); + Assert.Equal(4, obj["Key2"][1]); + string json = JsonSerializer.ToString(obj); + Assert.Equal(JsonString, json); + } - string json = JsonSerializer.ToString(obj); - Assert.Equal(JsonString, json); + { + ImmutableDictionary> obj = JsonSerializer.Parse>>(JsonString); + + Assert.Equal(2, obj.Count); + Assert.Equal(2, obj["Key1"].Count); + Assert.Equal(1, obj["Key1"][0]); + Assert.Equal(2, obj["Key1"][1]); + Assert.Equal(2, obj["Key2"].Count); + Assert.Equal(3, obj["Key2"][0]); + Assert.Equal(4, obj["Key2"][1]); + + string json = JsonSerializer.ToString(obj); + const string ReorderedJsonString = @"{""Key2"":[3,4],""Key1"":[1,2]}"; + Assert.True(JsonString == json || ReorderedJsonString == json); + } + + { + IImmutableDictionary> obj = JsonSerializer.Parse>>(JsonString); + + Assert.Equal(2, obj.Count); + Assert.Equal(2, obj["Key1"].Count); + Assert.Equal(1, obj["Key1"][0]); + Assert.Equal(2, obj["Key1"][1]); + Assert.Equal(2, obj["Key2"].Count); + Assert.Equal(3, obj["Key2"][0]); + Assert.Equal(4, obj["Key2"][1]); + + + string json = JsonSerializer.ToString(obj); + const string ReorderedJsonString = @"{""Key2"":[3,4],""Key1"":[1,2]}"; + Assert.True(JsonString == json || ReorderedJsonString == json); + } } [Fact] @@ -148,63 +221,125 @@ public static void DictionaryOfArray() public static void ListOfDictionary() { const string JsonString = @"[{""Key1"":1,""Key2"":2},{""Key1"":3,""Key2"":4}]"; - List> obj = JsonSerializer.Parse>>(JsonString); - Assert.Equal(2, obj.Count); - Assert.Equal(2, obj[0].Count); - Assert.Equal(1, obj[0]["Key1"]); - Assert.Equal(2, obj[0]["Key2"]); - Assert.Equal(2, obj[1].Count); - Assert.Equal(3, obj[1]["Key1"]); - Assert.Equal(4, obj[1]["Key2"]); + { + List> obj = JsonSerializer.Parse>>(JsonString); - string json = JsonSerializer.ToString(obj); - Assert.Equal(JsonString, json); + Assert.Equal(2, obj.Count); + Assert.Equal(2, obj[0].Count); + Assert.Equal(1, obj[0]["Key1"]); + Assert.Equal(2, obj[0]["Key2"]); + Assert.Equal(2, obj[1].Count); + Assert.Equal(3, obj[1]["Key1"]); + Assert.Equal(4, obj[1]["Key2"]); - json = JsonSerializer.ToString(obj); - Assert.Equal(JsonString, json); + string json = JsonSerializer.ToString(obj); + Assert.Equal(JsonString, json); + + json = JsonSerializer.ToString(obj); + Assert.Equal(JsonString, json); + } + { + List> obj = JsonSerializer.Parse>>(JsonString); + + Assert.Equal(2, obj.Count); + Assert.Equal(2, obj[0].Count); + Assert.Equal(1, obj[0]["Key1"]); + Assert.Equal(2, obj[0]["Key2"]); + Assert.Equal(2, obj[1].Count); + Assert.Equal(3, obj[1]["Key1"]); + Assert.Equal(4, obj[1]["Key2"]); + + string json = JsonSerializer.ToString(obj); + Assert.Equal(JsonString, json); + + json = JsonSerializer.ToString(obj); + Assert.Equal(JsonString, json); + } } [Fact] public static void ArrayOfDictionary() { const string JsonString = @"[{""Key1"":1,""Key2"":2},{""Key1"":3,""Key2"":4}]"; - Dictionary[] obj = JsonSerializer.Parse[]>(JsonString); - Assert.Equal(2, obj.Length); - Assert.Equal(2, obj[0].Count); - Assert.Equal(1, obj[0]["Key1"]); - Assert.Equal(2, obj[0]["Key2"]); - Assert.Equal(2, obj[1].Count); - Assert.Equal(3, obj[1]["Key1"]); - Assert.Equal(4, obj[1]["Key2"]); + { + Dictionary[] obj = JsonSerializer.Parse[]>(JsonString); - string json = JsonSerializer.ToString(obj); - Assert.Equal(JsonString, json); + Assert.Equal(2, obj.Length); + Assert.Equal(2, obj[0].Count); + Assert.Equal(1, obj[0]["Key1"]); + Assert.Equal(2, obj[0]["Key2"]); + Assert.Equal(2, obj[1].Count); + Assert.Equal(3, obj[1]["Key1"]); + Assert.Equal(4, obj[1]["Key2"]); - json = JsonSerializer.ToString(obj); - Assert.Equal(JsonString, json); + string json = JsonSerializer.ToString(obj); + Assert.Equal(JsonString, json); + + json = JsonSerializer.ToString(obj); + Assert.Equal(JsonString, json); + } + + { + ImmutableSortedDictionary[] obj = JsonSerializer.Parse[]>(JsonString); + + Assert.Equal(2, obj.Length); + Assert.Equal(2, obj[0].Count); + Assert.Equal(1, obj[0]["Key1"]); + Assert.Equal(2, obj[0]["Key2"]); + Assert.Equal(2, obj[1].Count); + Assert.Equal(3, obj[1]["Key1"]); + Assert.Equal(4, obj[1]["Key2"]); + + string json = JsonSerializer.ToString(obj); + Assert.Equal(JsonString, json); + + json = JsonSerializer.ToString(obj); + Assert.Equal(JsonString, json); + } } [Fact] public static void DictionaryOfDictionary() { const string JsonString = @"{""Key1"":{""Key1a"":1,""Key1b"":2},""Key2"":{""Key2a"":3,""Key2b"":4}}"; - Dictionary> obj = JsonSerializer.Parse>>(JsonString); - Assert.Equal(2, obj.Count); - Assert.Equal(2, obj["Key1"].Count); - Assert.Equal(1, obj["Key1"]["Key1a"]); - Assert.Equal(2, obj["Key1"]["Key1b"]); - Assert.Equal(2, obj["Key2"].Count); - Assert.Equal(3, obj["Key2"]["Key2a"]); - Assert.Equal(4, obj["Key2"]["Key2b"]); + { + Dictionary> obj = JsonSerializer.Parse>>(JsonString); - string json = JsonSerializer.ToString(obj); - Assert.Equal(JsonString, json); + Assert.Equal(2, obj.Count); + Assert.Equal(2, obj["Key1"].Count); + Assert.Equal(1, obj["Key1"]["Key1a"]); + Assert.Equal(2, obj["Key1"]["Key1b"]); + Assert.Equal(2, obj["Key2"].Count); + Assert.Equal(3, obj["Key2"]["Key2a"]); + Assert.Equal(4, obj["Key2"]["Key2b"]); - json = JsonSerializer.ToString(obj); - Assert.Equal(JsonString, json); + string json = JsonSerializer.ToString(obj); + Assert.Equal(JsonString, json); + + json = JsonSerializer.ToString(obj); + Assert.Equal(JsonString, json); + } + + { + ImmutableSortedDictionary> obj = JsonSerializer.Parse>>(JsonString); + + Assert.Equal(2, obj.Count); + Assert.Equal(2, obj["Key1"].Count); + Assert.Equal(1, obj["Key1"]["Key1a"]); + Assert.Equal(2, obj["Key1"]["Key1b"]); + Assert.Equal(2, obj["Key2"].Count); + Assert.Equal(3, obj["Key2"]["Key2a"]); + Assert.Equal(4, obj["Key2"]["Key2b"]); + + string json = JsonSerializer.ToString(obj); + Assert.Equal(JsonString, json); + + json = JsonSerializer.ToString(obj); + Assert.Equal(JsonString, json); + } } [Fact] @@ -276,32 +411,64 @@ public static void DictionaryOfArrayOfDictionary() [Fact] public static void DictionaryOfClasses() { - Dictionary obj; - { - string json = @"{""Key1"":" + SimpleTestClass.s_json + @",""Key2"":" + SimpleTestClass.s_json + "}"; - obj = JsonSerializer.Parse>(json); - Assert.Equal(2, obj.Count); - obj["Key1"].Verify(); - obj["Key2"].Verify(); + Dictionary obj; + + { + string json = @"{""Key1"":" + SimpleTestClass.s_json + @",""Key2"":" + SimpleTestClass.s_json + "}"; + obj = JsonSerializer.Parse>(json); + Assert.Equal(2, obj.Count); + obj["Key1"].Verify(); + obj["Key2"].Verify(); + } + + { + // We can't compare against the json string above because property ordering is not deterministic (based on reflection order) + // so just round-trip the json and compare. + string json = JsonSerializer.ToString(obj); + obj = JsonSerializer.Parse>(json); + Assert.Equal(2, obj.Count); + obj["Key1"].Verify(); + obj["Key2"].Verify(); + } + + { + string json = JsonSerializer.ToString(obj); + obj = JsonSerializer.Parse>(json); + Assert.Equal(2, obj.Count); + obj["Key1"].Verify(); + obj["Key2"].Verify(); + } } { - // We can't compare against the json string above because property ordering is not deterministic (based on reflection order) - // so just round-trip the json and compare. - string json = JsonSerializer.ToString(obj); - obj = JsonSerializer.Parse>(json); - Assert.Equal(2, obj.Count); - obj["Key1"].Verify(); - obj["Key2"].Verify(); - } - - { - string json = JsonSerializer.ToString(obj); - obj = JsonSerializer.Parse>(json); - Assert.Equal(2, obj.Count); - obj["Key1"].Verify(); - obj["Key2"].Verify(); + ImmutableSortedDictionary obj; + + { + string json = @"{""Key1"":" + SimpleTestClass.s_json + @",""Key2"":" + SimpleTestClass.s_json + "}"; + obj = JsonSerializer.Parse>(json); + Assert.Equal(2, obj.Count); + obj["Key1"].Verify(); + obj["Key2"].Verify(); + } + + { + // We can't compare against the json string above because property ordering is not deterministic (based on reflection order) + // so just round-trip the json and compare. + string json = JsonSerializer.ToString(obj); + obj = JsonSerializer.Parse>(json); + Assert.Equal(2, obj.Count); + obj["Key1"].Verify(); + obj["Key2"].Verify(); + } + + { + string json = JsonSerializer.ToString(obj); + obj = JsonSerializer.Parse>(json); + Assert.Equal(2, obj.Count); + obj["Key1"].Verify(); + obj["Key2"].Verify(); + } } } @@ -449,6 +616,13 @@ public static void DictionaryNotSupportedButIgnored() Assert.Null(obj.MyDictionary); } + [Fact] + public static void DeserializeUserDefinedDictionaryThrows() + { + string json = @"{""Hello"":1,""Hello2"":2}"; + Assert.Throws(() => JsonSerializer.Parse(json)); + } + public class ClassWithDictionaryButNoSetter { public Dictionary MyDictionary { get; } = new Dictionary(); @@ -463,5 +637,81 @@ public class ClassWithNotSupportedDictionaryButIgnored { [JsonIgnore] public Dictionary MyDictionary { get; set; } } + + public class UserDefinedImmutableDictionary : IImmutableDictionary + { + public int this[string key] => throw new NotImplementedException(); + + public IEnumerable Keys => throw new NotImplementedException(); + + public IEnumerable Values => throw new NotImplementedException(); + + public int Count => throw new NotImplementedException(); + + public IImmutableDictionary Add(string key, int value) + { + throw new NotImplementedException(); + } + + public IImmutableDictionary AddRange(IEnumerable> pairs) + { + throw new NotImplementedException(); + } + + public IImmutableDictionary Clear() + { + throw new NotImplementedException(); + } + + public bool Contains(KeyValuePair pair) + { + throw new NotImplementedException(); + } + + public bool ContainsKey(string key) + { + throw new NotImplementedException(); + } + + public IEnumerator> GetEnumerator() + { + throw new NotImplementedException(); + } + + public IImmutableDictionary Remove(string key) + { + throw new NotImplementedException(); + } + + public IImmutableDictionary RemoveRange(IEnumerable keys) + { + throw new NotImplementedException(); + } + + public IImmutableDictionary SetItem(string key, int value) + { + throw new NotImplementedException(); + } + + public IImmutableDictionary SetItems(IEnumerable> items) + { + throw new NotImplementedException(); + } + + public bool TryGetKey(string equalKey, out string actualKey) + { + throw new NotImplementedException(); + } + + public bool TryGetValue(string key, out int value) + { + throw new NotImplementedException(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + throw new NotImplementedException(); + } + } } } diff --git a/src/System.Text.Json/tests/Serialization/TestClasses.SimpleTestClass.cs b/src/System.Text.Json/tests/Serialization/TestClasses.SimpleTestClass.cs index ba69ea656bdf..5f9e654203ef 100644 --- a/src/System.Text.Json/tests/Serialization/TestClasses.SimpleTestClass.cs +++ b/src/System.Text.Json/tests/Serialization/TestClasses.SimpleTestClass.cs @@ -62,6 +62,9 @@ public class SimpleTestClass : ITestClass public Dictionary MyStringToStringDict { get; set; } public IDictionary MyStringToStringIDict { get; set; } public IReadOnlyDictionary MyStringToStringIReadOnlyDict { get; set; } + public ImmutableDictionary MyStringToStringImmutableDict { get; set; } + public IImmutableDictionary MyStringToStringIImmutableDict { get; set; } + public ImmutableSortedDictionary MyStringToStringImmutableSortedDict { get; set; } public Stack MyStringStackT { get; set; } public Queue MyStringQueueT { get; set; } public HashSet MyStringHashSetT { get; set; } @@ -102,7 +105,10 @@ public class SimpleTestClass : ITestClass @"""MyEnum"" : 2," + // int by default @"""MyStringToStringDict"" : {""key"" : ""value""}," + @"""MyStringToStringIDict"" : {""key"" : ""value""}," + - @"""MyStringToStringIReadOnlyDict"" : {""key"" : ""value""}"; + @"""MyStringToStringIReadOnlyDict"" : {""key"" : ""value""}," + + @"""MyStringToStringImmutableDict"" : {""key"" : ""value""}," + + @"""MyStringToStringIImmutableDict"" : {""key"" : ""value""}," + + @"""MyStringToStringImmutableSortedDict"" : {""key"" : ""value""}"; private const string s_partialJsonArrays = @"""MyInt16Array"" : [1]," + @@ -228,6 +234,10 @@ public void Initialize() MyStringToStringIDict = new Dictionary { { "key", "value" } }; MyStringToStringIReadOnlyDict = new Dictionary { { "key", "value" } }; + MyStringToStringImmutableDict = ImmutableDictionary.CreateRange(MyStringToStringDict); + MyStringToStringIImmutableDict = ImmutableDictionary.CreateRange(MyStringToStringDict); + MyStringToStringImmutableSortedDict = ImmutableSortedDictionary.CreateRange(MyStringToStringDict); + MyStringStackT = new Stack(new List() { "Hello", "World" } ); MyStringQueueT = new Queue(new List() { "Hello", "World" }); MyStringHashSetT = new HashSet(new List() { "Hello" }); @@ -326,6 +336,10 @@ public void Verify() Assert.Equal("value", MyStringToStringIDict["key"]); Assert.Equal("value", MyStringToStringIReadOnlyDict["key"]); + Assert.Equal("value", MyStringToStringImmutableDict["key"]); + Assert.Equal("value", MyStringToStringIImmutableDict["key"]); + Assert.Equal("value", MyStringToStringImmutableSortedDict["key"]); + Assert.Equal(2, MyStringStackT.Count); Assert.True(MyStringStackT.Contains("Hello")); Assert.True(MyStringStackT.Contains("World")); diff --git a/src/System.Text.Json/tests/Serialization/TestClasses.SimpleTestClassWithObject.cs b/src/System.Text.Json/tests/Serialization/TestClasses.SimpleTestClassWithObject.cs index 6ae05115e843..1417945e5de5 100644 --- a/src/System.Text.Json/tests/Serialization/TestClasses.SimpleTestClassWithObject.cs +++ b/src/System.Text.Json/tests/Serialization/TestClasses.SimpleTestClassWithObject.cs @@ -37,6 +37,9 @@ public class SimpleTestClassWithObject : SimpleTestClassWithSimpleObject public object MyStringToStringDict { get; set; } public object MyStringToStringIDict { get; set; } public object MyStringToStringIReadOnlyDict { get; set; } + public object MyStringToStringImmutableDict { get; set; } + public object MyStringToStringIImmutableDict { get; set; } + public object MyStringToStringImmutableSortedDict { get; set; } public object MyStringStackT { get; set; } public object MyStringQueueT { get; set; } public object MyStringHashSetT { get; set; } @@ -96,7 +99,10 @@ public class SimpleTestClassWithObject : SimpleTestClassWithSimpleObject @"""MyStringIReadOnlyListT"" : [""Hello""]," + @"""MyStringToStringDict"" : {""key"" : ""value""}," + @"""MyStringToStringIDict"" : {""key"" : ""value""}," + - @"""MyStringToStringIReadOnlyDict"" : {""key"" : ""value""}" + + @"""MyStringToStringIReadOnlyDict"" : {""key"" : ""value""}," + + @"""MyStringToStringImmutableDict"" : {""key"" : ""value""}," + + @"""MyStringToStringIImmutableDict"" : {""key"" : ""value""}," + + @"""MyStringToStringImmutableSortedDict"" : {""key"" : ""value""}," + @"""MyStringStackT"" : [""Hello"", ""World""]," + @"""MyStringQueueT"" : [""Hello"", ""World""]," + @"""MyStringHashSetT"" : [""Hello""]," + @@ -148,6 +154,10 @@ public override void Initialize() MyStringToStringIDict = new Dictionary { { "key", "value" } }; MyStringToStringIReadOnlyDict = new Dictionary { { "key", "value" } }; + MyStringToStringImmutableDict = ImmutableDictionary.CreateRange((Dictionary)MyStringToStringDict); + MyStringToStringIImmutableDict = ImmutableDictionary.CreateRange((Dictionary)MyStringToStringDict); + MyStringToStringImmutableSortedDict = ImmutableSortedDictionary.CreateRange((Dictionary)MyStringToStringDict); + MyStringStackT = new Stack(new List() { "Hello", "World" }); MyStringQueueT = new Queue(new List() { "Hello", "World" }); MyStringHashSetT = new HashSet(new List() { "Hello" }); @@ -198,6 +208,10 @@ public override void Verify() Assert.Equal("value", ((IDictionary)MyStringToStringIDict)["key"]); Assert.Equal("value", ((IReadOnlyDictionary)MyStringToStringIReadOnlyDict)["key"]); + Assert.Equal("value", ((ImmutableDictionary)MyStringToStringImmutableDict)["key"]); + Assert.Equal("value", ((IImmutableDictionary)MyStringToStringIImmutableDict)["key"]); + Assert.Equal("value", ((ImmutableSortedDictionary)MyStringToStringImmutableSortedDict)["key"]); + Assert.Equal(2, ((Stack)MyStringStackT).Count); Assert.True(((Stack)MyStringStackT).Contains("Hello")); Assert.True(((Stack)MyStringStackT).Contains("World")); diff --git a/src/System.Text.Json/tests/Serialization/Value.ReadTests.ImmutableCollections.cs b/src/System.Text.Json/tests/Serialization/Value.ReadTests.ImmutableCollections.cs index c1b923b01282..2cd3313efb81 100644 --- a/src/System.Text.Json/tests/Serialization/Value.ReadTests.ImmutableCollections.cs +++ b/src/System.Text.Json/tests/Serialization/Value.ReadTests.ImmutableCollections.cs @@ -544,5 +544,11 @@ public static void ReadPrimitiveImmutableSortedSetT() result = JsonSerializer.Parse>(Encoding.UTF8.GetBytes(@"[]")); Assert.Equal(0, result.Count()); } + + [Fact] + public static void ReadPrimitiveImmutableArrayThrows() + { + Assert.Throws(() => JsonSerializer.Parse>(Encoding.UTF8.GetBytes(@"[1,2]"))); + } } }