Support IDictionary<string,TValue> without IDictionaryEnumerator
authorSteve Harter <steveharter@users.noreply.github.com>
Fri, 18 Oct 2019 15:27:43 +0000 (10:27 -0500)
committerSteve Harter <steveharter@users.noreply.github.com>
Fri, 18 Oct 2019 19:06:55 +0000 (14:06 -0500)
Commit migrated from https://github.com/dotnet/corefx/commit/8124b88d3851c49a2cc09ce5de879b6622cd202e

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.AddProperty.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNotNullable.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNotNullableContravariant.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNullable.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleArray.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleDictionary.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleValue.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleDictionary.cs
src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.cs

index 66fa353..f7f85fb 100644 (file)
@@ -197,14 +197,17 @@ namespace System.Text.Json
             return jsonPropertyInfo;
         }
 
-        internal JsonPropertyInfo CreateRootObject(JsonSerializerOptions options)
+        /// <summary>
+        /// Create a <see cref="JsonPropertyInfo"/> for a given Type.
+        /// </summary>
+        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);
         }
index 9604b4a..2cddbab 100644 (file)
@@ -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
         }
 
         /// <summary>
+        /// Return the JsonPropertyInfo for the TValue in IDictionary{string, TValue} when deserializing.
+        /// </summary>
+        /// <remarks>
+        /// This should not be called during warm-up (initial creation of JsonPropertyInfos) to avoid recursive behavior
+        /// which could result in a StackOverflowException.
+        /// </remarks>
+        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;
+            }
+        }
+
+        /// <summary>
         /// Return the JsonClassInfo for the element type, or null if the property is not an enumerable or dictionary.
         /// </summary>
         /// <remarks>
@@ -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()
         {
index eb90e96..c961555 100644 (file)
@@ -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<string, TRuntimeProperty>);
         }
+
+        public override void GetDictionaryKeyAndValueFromGenericDictionary(ref WriteStackFrame writeStackFrame, out string key, out object value)
+        {
+            if (writeStackFrame.CollectionEnumerator is IEnumerator<KeyValuePair<string, TRuntimeProperty>> genericEnumerator)
+            {
+                key = genericEnumerator.Current.Key;
+                value = genericEnumerator.Current.Value;
+            }
+            else
+            {
+                throw ThrowHelper.GetNotSupportedException_SerializationNotSupportedCollection(
+                    writeStackFrame.JsonPropertyInfo.DeclaredPropertyType,
+                    writeStackFrame.JsonPropertyInfo.ParentClassType,
+                    writeStackFrame.JsonPropertyInfo.PropertyInfo);
+            }
+        }
     }
 }
index 4216239..1a814a2 100644 (file)
@@ -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<string, TRuntimeProperty>);
         }
+
+        public override void GetDictionaryKeyAndValueFromGenericDictionary(ref WriteStackFrame writeStackFrame, out string key, out object value)
+        {
+            if (writeStackFrame.CollectionEnumerator is IEnumerator<KeyValuePair<string, TRuntimeProperty>> genericEnumerator)
+            {
+                key = genericEnumerator.Current.Key;
+                value = genericEnumerator.Current.Value;
+            }
+            else
+            {
+                throw ThrowHelper.GetNotSupportedException_SerializationNotSupportedCollection(
+                    writeStackFrame.JsonPropertyInfo.DeclaredPropertyType,
+                    writeStackFrame.JsonPropertyInfo.ParentClassType,
+                    writeStackFrame.JsonPropertyInfo.PropertyInfo);
+            }
+        }
     }
 }
index 724578e..0a4b047 100644 (file)
@@ -152,5 +152,21 @@ namespace System.Text.Json
         {
             return typeof(Dictionary<string, TProperty?>);
         }
+
+        public override void GetDictionaryKeyAndValueFromGenericDictionary(ref WriteStackFrame writeStackFrame, out string key, out object value)
+        {
+            if (writeStackFrame.CollectionEnumerator is IEnumerator<KeyValuePair<string, TProperty?>> genericEnumerator)
+            {
+                key = genericEnumerator.Current.Key;
+                value = genericEnumerator.Current.Value;
+            }
+            else
+            {
+                throw ThrowHelper.GetNotSupportedException_SerializationNotSupportedCollection(
+                    writeStackFrame.JsonPropertyInfo.DeclaredPropertyType,
+                    writeStackFrame.JsonPropertyInfo.ParentClassType,
+                    writeStackFrame.JsonPropertyInfo.PropertyInfo);
+            }
+        }
     }
 }
index 2d56b20..8475f32 100644 (file)
@@ -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)
             {
index 8eef116..0b9bab6 100644 (file)
@@ -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);
index fb15f4e..4dc2052 100644 (file)
@@ -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)
             {
index 067e129..459ee71 100644 (file)
@@ -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<TKey, TVale>-derived classes this is normally IDictionaryEnumerator as well
+                // but may be IEnumerable<KeyValuePair<TKey, TValue>> 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,
index 5be02db..16dcd93 100644 (file)
@@ -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<NotSupportedException>(() => 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<IDictionary>(json);
+            Assert.IsType<JsonElement>(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<string, Dictionary<string, object>> ObjectDictVals { get; set; }
             public Dictionary<string, SimpleClassWithDictionaries> ClassVals { get; set; }
         }
+
+        public class DictionaryThatOnlyImplementsIDictionaryOfStringTValue<TValue> : IDictionary<string, TValue>
+        {
+            IDictionary<string, TValue> _inner = new Dictionary<string, TValue>();
+
+            public TValue this[string key]
+            {
+                get
+                {
+                    return _inner[key];
+                }
+                set
+                {
+                    _inner[key] = value;
+                }
+            }
+
+            public ICollection<string> Keys => _inner.Keys;
+
+            public ICollection<TValue> 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<string, TValue> item)
+            {
+                _inner.Add(item);
+            }
+
+            public void Clear()
+            {
+                throw new NotImplementedException();
+            }
+
+            public bool Contains(KeyValuePair<string, TValue> item)
+            {
+                return _inner.Contains(item);
+            }
+
+            public bool ContainsKey(string key)
+            {
+                return _inner.ContainsKey(key);
+            }
+
+            public void CopyTo(KeyValuePair<string, TValue>[] array, int arrayIndex)
+            {
+                // CopyTo should not be called.
+                throw new NotImplementedException();
+            }
+
+            public IEnumerator<KeyValuePair<string, TValue>> GetEnumerator()
+            {
+                // Don't return results directly from _inner since that will return an enumerator that returns
+                // IDictionaryEnumerator which should not require.
+                foreach (KeyValuePair<string, TValue> 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<string, TValue> 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<int> dictionary;
+
+            dictionary = JsonSerializer.Deserialize<DictionaryThatOnlyImplementsIDictionaryOfStringTValue<int>>(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<Poco> dictionary;
+
+            dictionary = JsonSerializer.Deserialize<DictionaryThatOnlyImplementsIDictionaryOfStringTValue<Poco>>(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<Poco>
+        {
+        }
+
+        [Fact]
+        public static void DictionaryOfTOnlyWithStringPoco()
+        {
+            const string Json = @"{""One"":{""Id"":1},""Two"":{""Id"":2}}";
+
+            DictionaryThatOnlyImplementsIDictionaryOfStringPoco dictionary;
+
+            dictionary = JsonSerializer.Deserialize<DictionaryThatOnlyImplementsIDictionaryOfStringPoco>(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<TValue> : IDictionary<string, TValue>
+        {
+            IDictionary<string, TValue> _inner = new Dictionary<string, TValue>();
+
+            public TValue this[string key]
+            {
+                get
+                {
+                    return _inner[key];
+                }
+                set
+                {
+                    _inner[key] = value;
+                }
+            }
+
+            public ICollection<string> Keys => _inner.Keys;
+
+            public ICollection<TValue> 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<string, TValue> item)
+            {
+                _inner.Add(item);
+            }
+
+            public void Clear()
+            {
+                throw new NotImplementedException();
+            }
+
+            public bool Contains(KeyValuePair<string, TValue> item)
+            {
+                return _inner.Contains(item);
+            }
+
+            public bool ContainsKey(string key)
+            {
+                return _inner.ContainsKey(key);
+            }
+
+            public void CopyTo(KeyValuePair<string, TValue>[] array, int arrayIndex)
+            {
+                // CopyTo should not be called.
+                throw new NotImplementedException();
+            }
+
+            public IEnumerator<KeyValuePair<string, TValue>> 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<string, TValue> 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<int> dictionary;
+            dictionary = JsonSerializer.Deserialize<DictionaryThatHasIncomatibleEnumerator<int>>(Json);
+            Assert.Equal(1, dictionary["One"]);
+            Assert.Equal(2, dictionary["Two"]);
+            Assert.Throws<NotSupportedException>(() => JsonSerializer.Serialize(dictionary));
+        }
+
+
+        [Fact]
+        public static void VerifyDictionaryThatHasIncomatibleEnumeratorWithPoco()
+        {
+            const string Json = @"{""One"":{""Id"":1},""Two"":{""Id"":2}}";
+
+            DictionaryThatHasIncomatibleEnumerator<Poco> dictionary;
+            dictionary = JsonSerializer.Deserialize<DictionaryThatHasIncomatibleEnumerator<Poco>>(Json);
+            Assert.Equal(1, dictionary["One"].Id);
+            Assert.Equal(2, dictionary["Two"].Id);
+            Assert.Throws<NotSupportedException>(() => JsonSerializer.Serialize(dictionary));
+        }
     }
 }