From: Steve Harter Date: Fri, 18 Oct 2019 15:27:43 +0000 (-0500) Subject: Support IDictionary without IDictionaryEnumerator X-Git-Tag: submit/tizen/20210909.063632~11031^2~237^2~2 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=fed54008b5b6bd23fa856570144e0c227aef32a7;p=platform%2Fupstream%2Fdotnet%2Fruntime.git Support IDictionary without IDictionaryEnumerator Commit migrated from https://github.com/dotnet/corefx/commit/8124b88d3851c49a2cc09ce5de879b6622cd202e --- diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.AddProperty.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.AddProperty.cs index 66fa353..f7f85fb 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.AddProperty.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.AddProperty.cs @@ -197,14 +197,17 @@ namespace System.Text.Json return jsonPropertyInfo; } - internal JsonPropertyInfo CreateRootObject(JsonSerializerOptions options) + /// + /// Create a for a given Type. + /// + internal static JsonPropertyInfo CreateRootProperty(Type type, JsonSerializerOptions options) { return CreateProperty( - declaredPropertyType: Type, - runtimePropertyType: Type, - implementedPropertyType: Type, + declaredPropertyType: type, + runtimePropertyType: type, + implementedPropertyType: type, propertyInfo: null, - parentClassType: Type, + parentClassType: typeof(object), // a dummy value (not used) converter: null, options: options); } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs index 9604b4a..2cddbab 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs @@ -28,6 +28,8 @@ namespace System.Text.Json private JsonClassInfo _runtimeClassInfo; private JsonClassInfo _declaredTypeClassInfo; + private JsonPropertyInfo _dictionaryValuePropertyPolicy; + public bool CanBeNull { get; private set; } public ClassType ClassType; @@ -217,6 +219,31 @@ namespace System.Text.Json } /// + /// Return the JsonPropertyInfo for the TValue in IDictionary{string, TValue} when deserializing. + /// + /// + /// This should not be called during warm-up (initial creation of JsonPropertyInfos) to avoid recursive behavior + /// which could result in a StackOverflowException. + /// + public JsonPropertyInfo DictionaryValuePropertyPolicy + { + get + { + if (_dictionaryValuePropertyPolicy == null) + { + Debug.Assert(ClassType == ClassType.Dictionary || ClassType == ClassType.IDictionaryConstructible); + + Type dictionaryValueType = ElementType; + Debug.Assert(ElementType != null); + + _dictionaryValuePropertyPolicy = JsonClassInfo.CreateRootProperty(dictionaryValueType, Options); + } + + return _dictionaryValuePropertyPolicy; + } + } + + /// /// Return the JsonClassInfo for the element type, or null if the property is not an enumerable or dictionary. /// /// @@ -254,9 +281,29 @@ namespace System.Text.Json return (TAttribute)propertyInfo?.GetCustomAttribute(typeof(TAttribute), inherit: false); } + public abstract Type GetConcreteType(Type type); + public abstract Type GetDictionaryConcreteType(); - public abstract Type GetConcreteType(Type type); + public void GetDictionaryKeyAndValue(ref WriteStackFrame writeStackFrame, out string key, out object value) + { + Debug.Assert(ClassType == ClassType.Dictionary || + ClassType == ClassType.IDictionaryConstructible); + + if (writeStackFrame.CollectionEnumerator is IDictionaryEnumerator iDictionaryEnumerator) + { + // Since IDictionaryEnumerator is not based on generics we can obtain the value directly. + key = (string)iDictionaryEnumerator.Key; + value = iDictionaryEnumerator.Value; + } + else + { + // Forward to the generic dictionary. + DictionaryValuePropertyPolicy.GetDictionaryKeyAndValueFromGenericDictionary(ref writeStackFrame, out key, out value); + } + } + + public abstract void GetDictionaryKeyAndValueFromGenericDictionary(ref WriteStackFrame writeStackFrame, out string key, out object value); public virtual void GetPolicies() { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNotNullable.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNotNullable.cs index eb90e96..c961555 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNotNullable.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNotNullable.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.Collections.Generic; using System.Diagnostics; @@ -126,5 +127,21 @@ namespace System.Text.Json { return typeof(Dictionary); } + + public override void GetDictionaryKeyAndValueFromGenericDictionary(ref WriteStackFrame writeStackFrame, out string key, out object value) + { + if (writeStackFrame.CollectionEnumerator is IEnumerator> genericEnumerator) + { + key = genericEnumerator.Current.Key; + value = genericEnumerator.Current.Value; + } + else + { + throw ThrowHelper.GetNotSupportedException_SerializationNotSupportedCollection( + writeStackFrame.JsonPropertyInfo.DeclaredPropertyType, + writeStackFrame.JsonPropertyInfo.ParentClassType, + writeStackFrame.JsonPropertyInfo.PropertyInfo); + } + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNotNullableContravariant.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNotNullableContravariant.cs index 4216239..1a814a2 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNotNullableContravariant.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNotNullableContravariant.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.Collections.Generic; using System.Diagnostics; @@ -128,5 +129,21 @@ namespace System.Text.Json.Serialization { return typeof(Dictionary); } + + public override void GetDictionaryKeyAndValueFromGenericDictionary(ref WriteStackFrame writeStackFrame, out string key, out object value) + { + if (writeStackFrame.CollectionEnumerator is IEnumerator> genericEnumerator) + { + key = genericEnumerator.Current.Key; + value = genericEnumerator.Current.Value; + } + else + { + throw ThrowHelper.GetNotSupportedException_SerializationNotSupportedCollection( + writeStackFrame.JsonPropertyInfo.DeclaredPropertyType, + writeStackFrame.JsonPropertyInfo.ParentClassType, + writeStackFrame.JsonPropertyInfo.PropertyInfo); + } + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNullable.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNullable.cs index 724578e..0a4b047 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNullable.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNullable.cs @@ -152,5 +152,21 @@ namespace System.Text.Json { return typeof(Dictionary); } + + public override void GetDictionaryKeyAndValueFromGenericDictionary(ref WriteStackFrame writeStackFrame, out string key, out object value) + { + if (writeStackFrame.CollectionEnumerator is IEnumerator> genericEnumerator) + { + key = genericEnumerator.Current.Key; + value = genericEnumerator.Current.Value; + } + else + { + throw ThrowHelper.GetNotSupportedException_SerializationNotSupportedCollection( + writeStackFrame.JsonPropertyInfo.DeclaredPropertyType, + writeStackFrame.JsonPropertyInfo.ParentClassType, + writeStackFrame.JsonPropertyInfo.PropertyInfo); + } + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleArray.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleArray.cs index 2d56b20..8475f32 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleArray.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleArray.cs @@ -27,7 +27,7 @@ namespace System.Text.Json JsonPropertyInfo jsonPropertyInfo = state.Current.JsonPropertyInfo; if (jsonPropertyInfo == null) { - jsonPropertyInfo = state.Current.JsonClassInfo.CreateRootObject(options); + jsonPropertyInfo = JsonClassInfo.CreateRootProperty(state.Current.JsonClassInfo.Type, options); } else if (state.Current.JsonClassInfo.ClassType == ClassType.Unknown) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleDictionary.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleDictionary.cs index 8eef116..0b9bab6 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleDictionary.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleDictionary.cs @@ -17,7 +17,7 @@ namespace System.Text.Json JsonPropertyInfo jsonPropertyInfo = state.Current.JsonPropertyInfo; if (jsonPropertyInfo == null) { - jsonPropertyInfo = state.Current.JsonClassInfo.CreateRootObject(options); + jsonPropertyInfo = JsonClassInfo.CreateRootProperty(state.Current.JsonClassInfo.Type, options); } Debug.Assert(jsonPropertyInfo != null); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleValue.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleValue.cs index fb15f4e..4dc2052 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleValue.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleValue.cs @@ -20,7 +20,7 @@ namespace System.Text.Json JsonPropertyInfo jsonPropertyInfo = state.Current.JsonPropertyInfo; if (jsonPropertyInfo == null) { - jsonPropertyInfo = state.Current.JsonClassInfo.CreateRootObject(options); + jsonPropertyInfo = JsonClassInfo.CreateRootProperty(state.Current.JsonClassInfo.Type, options); } else if (state.Current.JsonClassInfo.ClassType == ClassType.Unknown) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleDictionary.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleDictionary.cs index 067e129..459ee71 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleDictionary.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleDictionary.cs @@ -41,14 +41,11 @@ namespace System.Text.Json return true; } - if (enumerable is IDictionary dictionary) - { - state.Current.CollectionEnumerator = dictionary.GetEnumerator(); - } - else - { - state.Current.CollectionEnumerator = enumerable.GetEnumerator(); - } + // Let the dictionary return the default IEnumerator from its IEnumerable.GetEnumerator(). + // For IDictionary-derived classes this is normally be IDictionaryEnumerator. + // For IDictionary-derived classes this is normally IDictionaryEnumerator as well + // but may be IEnumerable> if the dictionary only supports generics. + state.Current.CollectionEnumerator = enumerable.GetEnumerator(); if (state.Current.ExtensionDataStatus != ExtensionDataWriteStatus.Writing) { @@ -58,28 +55,35 @@ namespace System.Text.Json if (state.Current.CollectionEnumerator.MoveNext()) { + // A dictionary should not have a null KeyValuePair. + Debug.Assert(state.Current.CollectionEnumerator.Current != null); + + bool obtainedValues = false; + string key = default; + object value = default; + // Check for polymorphism. if (elementClassInfo.ClassType == ClassType.Unknown) { - object currentValue = ((IDictionaryEnumerator)state.Current.CollectionEnumerator).Entry.Value; - GetRuntimeClassInfo(currentValue, ref elementClassInfo, options); + jsonPropertyInfo.GetDictionaryKeyAndValue(ref state.Current, out key, out value); + GetRuntimeClassInfo(value, ref elementClassInfo, options); + obtainedValues = true; } if (elementClassInfo.ClassType == ClassType.Value) { elementClassInfo.PolicyProperty.WriteDictionary(ref state, writer); } - else if (state.Current.CollectionEnumerator.Current == null) - { - writer.WriteNull(jsonPropertyInfo.Name); - } else { + if (!obtainedValues) + { + jsonPropertyInfo.GetDictionaryKeyAndValue(ref state.Current, out key, out value); + } + // An object or another enumerator requires a new stack frame. - var enumerator = (IDictionaryEnumerator)state.Current.CollectionEnumerator; - object value = enumerator.Value; state.Push(elementClassInfo, value); - state.Current.KeyName = (string)enumerator.Key; + state.Current.KeyName = key; } return false; @@ -127,15 +131,13 @@ namespace System.Text.Json key = polymorphicEnumerator.Current.Key; value = (TProperty)polymorphicEnumerator.Current.Value; } - else if (current.IsIDictionaryConstructible || current.IsIDictionaryConstructibleProperty) + else if (current.CollectionEnumerator is IDictionaryEnumerator iDictionaryEnumerator) { - key = (string)((DictionaryEntry)current.CollectionEnumerator.Current).Key; - value = (TProperty)((DictionaryEntry)current.CollectionEnumerator.Current).Value; + key = (string)iDictionaryEnumerator.Key; + value = (TProperty)iDictionaryEnumerator.Value; } else { - // Todo: support non-generic Dictionary here (IDictionaryEnumerator) - // https://github.com/dotnet/corefx/issues/41034 throw ThrowHelper.GetNotSupportedException_SerializationNotSupportedCollection( current.JsonPropertyInfo.DeclaredPropertyType, current.JsonPropertyInfo.ParentClassType, diff --git a/src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.cs b/src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.cs index 5be02db..16dcd93 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.cs @@ -883,13 +883,23 @@ namespace System.Text.Json.Serialization.Tests } [Fact] - public static void HashtableFail() + public static void Hashtable() { - { - IDictionary ht = new Hashtable(); - ht.Add("Key", "Value"); - Assert.Throws(() => JsonSerializer.Serialize(ht)); - } + const string Json = @"{""Key"":""Value""}"; + + IDictionary ht = new Hashtable(); + ht.Add("Key", "Value"); + string json = JsonSerializer.Serialize(ht); + + Assert.Equal(Json, json); + + ht = JsonSerializer.Deserialize(json); + Assert.IsType(ht["Key"]); + Assert.Equal("Value", ((JsonElement)ht["Key"]).GetString()); + + // Verify round-tripped JSON. + json = JsonSerializer.Serialize(ht); + Assert.Equal(Json, json); } [Fact] @@ -1496,5 +1506,252 @@ namespace System.Text.Json.Serialization.Tests public Dictionary> ObjectDictVals { get; set; } public Dictionary ClassVals { get; set; } } + + public class DictionaryThatOnlyImplementsIDictionaryOfStringTValue : IDictionary + { + IDictionary _inner = new Dictionary(); + + public TValue this[string key] + { + get + { + return _inner[key]; + } + set + { + _inner[key] = value; + } + } + + public ICollection Keys => _inner.Keys; + + public ICollection Values => _inner.Values; + + public int Count => _inner.Count; + + public bool IsReadOnly => _inner.IsReadOnly; + + public void Add(string key, TValue value) + { + _inner.Add(key, value); + } + + public void Add(KeyValuePair item) + { + _inner.Add(item); + } + + public void Clear() + { + throw new NotImplementedException(); + } + + public bool Contains(KeyValuePair item) + { + return _inner.Contains(item); + } + + public bool ContainsKey(string key) + { + return _inner.ContainsKey(key); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + // CopyTo should not be called. + throw new NotImplementedException(); + } + + public IEnumerator> GetEnumerator() + { + // Don't return results directly from _inner since that will return an enumerator that returns + // IDictionaryEnumerator which should not require. + foreach (KeyValuePair keyValuePair in _inner) + { + yield return keyValuePair; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public bool Remove(string key) + { + // Remove should not be called. + throw new NotImplementedException(); + } + + public bool Remove(KeyValuePair item) + { + // Remove should not be called. + throw new NotImplementedException(); + } + + public bool TryGetValue(string key, out TValue value) + { + return _inner.TryGetValue(key, out value); + } + } + + [Fact] + public static void DictionaryOfTOnlyWithStringTValueAsInt() + { + const string Json = @"{""One"":1,""Two"":2}"; + + DictionaryThatOnlyImplementsIDictionaryOfStringTValue dictionary; + + dictionary = JsonSerializer.Deserialize>(Json); + Assert.Equal(1, dictionary["One"]); + Assert.Equal(2, dictionary["Two"]); + + string json = JsonSerializer.Serialize(dictionary); + Assert.Equal(Json, json); + } + + [Fact] + public static void DictionaryOfTOnlyWithStringTValueAsPoco() + { + const string Json = @"{""One"":{""Id"":1},""Two"":{""Id"":2}}"; + + DictionaryThatOnlyImplementsIDictionaryOfStringTValue dictionary; + + dictionary = JsonSerializer.Deserialize>(Json); + Assert.Equal(1, dictionary["One"].Id); + Assert.Equal(2, dictionary["Two"].Id); + + string json = JsonSerializer.Serialize(dictionary); + Assert.Equal(Json, json); + } + + public class DictionaryThatOnlyImplementsIDictionaryOfStringPoco : DictionaryThatOnlyImplementsIDictionaryOfStringTValue + { + } + + [Fact] + public static void DictionaryOfTOnlyWithStringPoco() + { + const string Json = @"{""One"":{""Id"":1},""Two"":{""Id"":2}}"; + + DictionaryThatOnlyImplementsIDictionaryOfStringPoco dictionary; + + dictionary = JsonSerializer.Deserialize(Json); + Assert.Equal(1, dictionary["One"].Id); + Assert.Equal(2, dictionary["Two"].Id); + + string json = JsonSerializer.Serialize(dictionary); + Assert.Equal(Json, json); + } + + public class DictionaryThatHasIncomatibleEnumerator : IDictionary + { + IDictionary _inner = new Dictionary(); + + public TValue this[string key] + { + get + { + return _inner[key]; + } + set + { + _inner[key] = value; + } + } + + public ICollection Keys => _inner.Keys; + + public ICollection Values => _inner.Values; + + public int Count => _inner.Count; + + public bool IsReadOnly => _inner.IsReadOnly; + + public void Add(string key, TValue value) + { + _inner.Add(key, value); + } + + public void Add(KeyValuePair item) + { + _inner.Add(item); + } + + public void Clear() + { + throw new NotImplementedException(); + } + + public bool Contains(KeyValuePair item) + { + return _inner.Contains(item); + } + + public bool ContainsKey(string key) + { + return _inner.ContainsKey(key); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + // CopyTo should not be called. + throw new NotImplementedException(); + } + + public IEnumerator> GetEnumerator() + { + // The generic GetEnumerator() should not be called for this test. + throw new NotImplementedException(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + // Create an incompatible converter. + return new int[] {100,200 }.GetEnumerator(); + } + + public bool Remove(string key) + { + // Remove should not be called. + throw new NotImplementedException(); + } + + public bool Remove(KeyValuePair item) + { + // Remove should not be called. + throw new NotImplementedException(); + } + + public bool TryGetValue(string key, out TValue value) + { + return _inner.TryGetValue(key, out value); + } + } + + [Fact] + public static void VerifyDictionaryThatHasIncomatibleEnumeratorWithInt() + { + const string Json = @"{""One"":1,""Two"":2}"; + + DictionaryThatHasIncomatibleEnumerator dictionary; + dictionary = JsonSerializer.Deserialize>(Json); + Assert.Equal(1, dictionary["One"]); + Assert.Equal(2, dictionary["Two"]); + Assert.Throws(() => JsonSerializer.Serialize(dictionary)); + } + + + [Fact] + public static void VerifyDictionaryThatHasIncomatibleEnumeratorWithPoco() + { + const string Json = @"{""One"":{""Id"":1},""Two"":{""Id"":2}}"; + + DictionaryThatHasIncomatibleEnumerator dictionary; + dictionary = JsonSerializer.Deserialize>(Json); + Assert.Equal(1, dictionary["One"].Id); + Assert.Equal(2, dictionary["Two"].Id); + Assert.Throws(() => JsonSerializer.Serialize(dictionary)); + } } }