diff --git a/YamlDotNet.Test/Serialization/GenericTestReadOnlyDictionary.cs b/YamlDotNet.Test/Serialization/GenericTestReadOnlyDictionary.cs new file mode 100644 index 00000000..84736e04 --- /dev/null +++ b/YamlDotNet.Test/Serialization/GenericTestReadOnlyDictionary.cs @@ -0,0 +1,85 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System.Collections; +using System.Collections.Generic; + +namespace YamlDotNet.Test.Serialization +{ + /// + /// Test Dictionary that implements , but not + /// nor . + /// + public class GenericTestReadOnlyDictionary : IReadOnlyDictionary + { + private readonly Dictionary dictionary; + + public GenericTestReadOnlyDictionary() + { + dictionary = new Dictionary(); + } + + public void Add(TKey key, TValue value) + { + dictionary.Add(key, value); + } + + public TValue this[TKey key] + { + get { return dictionary[key]; } + } + + public IEnumerable Keys + { + get { return dictionary.Keys; } + } + + public IEnumerable Values + { + get { return dictionary.Values; } + } + + public int Count + { + get { return dictionary.Count; } + } + + public bool ContainsKey(TKey key) + { + return dictionary.ContainsKey(key); + } + + public bool TryGetValue(TKey key, out TValue value) + { + return dictionary.TryGetValue(key, out value); + } + + public IEnumerator> GetEnumerator() + { + return dictionary.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return dictionary.GetEnumerator(); + } + } +} diff --git a/YamlDotNet.Test/Serialization/SerializationTests.cs b/YamlDotNet.Test/Serialization/SerializationTests.cs index e0ff5c29..2ccabc00 100644 --- a/YamlDotNet.Test/Serialization/SerializationTests.cs +++ b/YamlDotNet.Test/Serialization/SerializationTests.cs @@ -1577,6 +1577,29 @@ public void SerializeGenericDictionaryPropertyAndDoNotApplyNamingConvention() writer.ToString().Should().Contain("new_key_here: new_value"); } + [Fact] + public void SerializeGenericReadOnlyDictionary() + { + // A type that implements only IReadOnlyDictionary<,> must serialize as a + // mapping, the same as a regular dictionary, rather than as a sequence of + // key/value pairs. See https://github.com/aaubry/YamlDotNet/issues/606. + var readOnlyDictionary = new GenericTestReadOnlyDictionary(); + readOnlyDictionary.Add("key1", "value1"); + readOnlyDictionary.Add("key2", "value2"); + + var serializer = new SerializerBuilder().Build(); + + var actual = serializer.Serialize(readOnlyDictionary); + var expected = serializer.Serialize( + new Dictionary + { + ["key1"] = "value1", + ["key2"] = "value2", + }); + + actual.Should().Be(expected); + } + [Theory, MemberData(nameof(SpecialFloats))] public void SpecialFloatsAreHandledCorrectly(FloatTestCase testCase) { diff --git a/YamlDotNet/Helpers/GenericReadOnlyDictionaryToNonGenericAdapter.cs b/YamlDotNet/Helpers/GenericReadOnlyDictionaryToNonGenericAdapter.cs new file mode 100644 index 00000000..7f7099f9 --- /dev/null +++ b/YamlDotNet/Helpers/GenericReadOnlyDictionaryToNonGenericAdapter.cs @@ -0,0 +1,168 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace YamlDotNet.Helpers +{ + /// + /// Adapts an to + /// so that types which implement only the read-only interface are still traversed as mappings. + /// Only enumeration is supported, which is all that object graph traversal requires. + /// + internal sealed class GenericReadOnlyDictionaryToNonGenericAdapter : IDictionary + where TKey : notnull + { + private readonly IReadOnlyDictionary genericDictionary; + + public GenericReadOnlyDictionaryToNonGenericAdapter(IReadOnlyDictionary genericDictionary) + { + this.genericDictionary = genericDictionary ?? throw new ArgumentNullException(nameof(genericDictionary)); + } + + public void Add(object key, object? value) + { + throw new NotSupportedException(); + } + + public void Clear() + { + throw new NotSupportedException(); + } + + public bool Contains(object key) + { + throw new NotSupportedException(); + } + + public IDictionaryEnumerator GetEnumerator() + { + return new DictionaryEnumerator(genericDictionary.GetEnumerator()); + } + + public bool IsFixedSize + { + get { throw new NotSupportedException(); } + } + + public bool IsReadOnly + { + get { throw new NotSupportedException(); } + } + + public ICollection Keys + { + get { throw new NotSupportedException(); } + } + + public void Remove(object key) + { + throw new NotSupportedException(); + } + + public ICollection Values + { + get { throw new NotSupportedException(); } + } + + public object? this[object key] + { + get + { + throw new NotSupportedException(); + } + set + { + throw new NotSupportedException(); + } + } + + public void CopyTo(Array array, int index) + { + throw new NotSupportedException(); + } + + public int Count + { + get { throw new NotSupportedException(); } + } + + public bool IsSynchronized + { + get { throw new NotSupportedException(); } + } + + public object SyncRoot + { + get { throw new NotSupportedException(); } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + private class DictionaryEnumerator : IDictionaryEnumerator + { + private readonly IEnumerator> enumerator; + + public DictionaryEnumerator(IEnumerator> enumerator) + { + this.enumerator = enumerator; + } + + public DictionaryEntry Entry + { + get + { + return new DictionaryEntry(Key, Value); + } + } + + public object Key + { + get { return enumerator.Current.Key!; } + } + + public object? Value + { + get { return enumerator.Current.Value; } + } + + public object Current + { + get { return Entry; } + } + + public bool MoveNext() + { + return enumerator.MoveNext(); + } + + public void Reset() + { + enumerator.Reset(); + } + } + } +} diff --git a/YamlDotNet/Serialization/ObjectFactories/ObjectFactoryBase.cs b/YamlDotNet/Serialization/ObjectFactories/ObjectFactoryBase.cs index 3c363cee..fee3c1c7 100644 --- a/YamlDotNet/Serialization/ObjectFactories/ObjectFactoryBase.cs +++ b/YamlDotNet/Serialization/ObjectFactories/ObjectFactoryBase.cs @@ -63,6 +63,16 @@ public virtual bool GetDictionary(IObjectDescriptor descriptor, out IDictionary? dictionary = adaptedDictionary as IDictionary; return true; } + + var genericReadOnlyDictionaryType = descriptor.Type.GetImplementationOfOpenGenericInterface(typeof(IReadOnlyDictionary<,>)); + if (genericReadOnlyDictionaryType != null) + { + genericArguments = genericReadOnlyDictionaryType.GetGenericArguments(); + var adaptedDictionary = Activator.CreateInstance(typeof(GenericReadOnlyDictionaryToNonGenericAdapter<,>).MakeGenericType(genericArguments), descriptor.Value)!; + dictionary = adaptedDictionary as IDictionary; + return true; + } + genericArguments = null; dictionary = null; return false;