From 0bb45642e046449375402563d74863e99b053085 Mon Sep 17 00:00:00 2001 From: cp Date: Tue, 4 Jan 2022 15:29:34 +0100 Subject: [PATCH 1/6] Add basic support to populate pre-existing C# objects when deserializing Add IDeserializer.PopulateObject() APIs Add currentValue parameter to INodeDeserializers and IValueDeserializers Add checks to only create a new instance if currentValue is empty Add ways to allow the user to configure population of pre-existing collections: DeserializerBuilder.WithCollectionPopulationOptions() Note: Pre-existing collection _items_ do _not_ get populated. Pre-existing collections can be re-used though, see DeserializerBuilder.WithCollectionPopulationOptions() Todo: Allow to populate pre-existing collection items. This needs some refactoring of the way the Array-, Collection- and DictionaryNodeDeserializer work. --- YamlDotNet/Serialization/Deserializer.cs | 31 +++++-- .../Serialization/DeserializerBuilder.cs | 20 +++++ YamlDotNet/Serialization/IDeserializer.cs | 13 +++ YamlDotNet/Serialization/INodeDeserializer.cs | 2 +- .../Serialization/IValueDeserializer.cs | 2 +- .../LazyComponentRegistrationList.cs | 15 +++- .../ArrayNodeDeserializer.cs | 21 ++++- .../CollectionNodeDeserializer.cs | 21 +++-- .../DictionaryNodeDeserializer.cs | 51 +++++++++--- .../EnumerableNodeDeserializer.cs | 4 +- .../NodeDeserializers/NullNodeDeserializer.cs | 2 +- .../ObjectNodeDeserializer.cs | 9 +- .../ScalarNodeDeserializer.cs | 2 +- .../TypeConverterNodeDeserializer.cs | 2 +- .../YamlConvertibleNodeDeserializer.cs | 4 +- .../YamlSerializableNodeDeserializer.cs | 2 +- .../Serialization/PopulationStrategies.cs | 83 +++++++++++++++++++ .../AliasValueDeserializer.cs | 4 +- .../NodeValueDeserializer.cs | 4 +- 19 files changed, 246 insertions(+), 46 deletions(-) create mode 100644 YamlDotNet/Serialization/PopulationStrategies.cs diff --git a/YamlDotNet/Serialization/Deserializer.cs b/YamlDotNet/Serialization/Deserializer.cs index 82acc4e3f..2ea2fab19 100644 --- a/YamlDotNet/Serialization/Deserializer.cs +++ b/YamlDotNet/Serialization/Deserializer.cs @@ -90,17 +90,17 @@ public T Deserialize(TextReader input) public object? Deserialize(TextReader input, Type type) { - return Deserialize(new Parser(input), type); + return Deserialize(new Parser(input), type, null); } public T Deserialize(IParser parser) { - return (T)Deserialize(parser, typeof(T))!; // We really want an exception if we are trying to deserialize null into a non-nullable type + return (T)Deserialize(parser, typeof(T), null)!; // We really want an exception if we are trying to deserialize null into a non-nullable type } public object? Deserialize(IParser parser) { - return Deserialize(parser, typeof(object)); + return Deserialize(parser, typeof(object), null); } /// @@ -110,6 +110,27 @@ public T Deserialize(IParser parser) /// The static type of the object to deserialize. /// Returns the deserialized object. public object? Deserialize(IParser parser, Type type) + { + return Deserialize(parser, type, null); + } + + public T PopulateObject(string input, T target) + { + using var reader = new StringReader(input); + return PopulateObject(reader, target); + } + + public T PopulateObject(TextReader input, T target) + { + return PopulateObject(new Parser(input), target); + } + + public T PopulateObject(IParser parser, T target) + { + return (T)Deserialize(parser, typeof(T), target)!; + } + + private object? Deserialize(IParser parser, Type type, object? target) { if (parser == null) { @@ -125,11 +146,11 @@ public T Deserialize(IParser parser) var hasDocumentStart = parser.TryConsume(out var _); - object? result = null; + object? result = target; if (!parser.Accept(out var _) && !parser.Accept(out var _)) { using var state = new SerializerState(); - result = valueDeserializer.DeserializeValue(parser, type, state, valueDeserializer); + result = valueDeserializer.DeserializeValue(parser, type, state, valueDeserializer, result); state.OnDeserialization(); } diff --git a/YamlDotNet/Serialization/DeserializerBuilder.cs b/YamlDotNet/Serialization/DeserializerBuilder.cs index 3ad041de6..b47ba12bf 100755 --- a/YamlDotNet/Serialization/DeserializerBuilder.cs +++ b/YamlDotNet/Serialization/DeserializerBuilder.cs @@ -20,6 +20,7 @@ // SOFTWARE. using System; +using System.Collections; using System.Collections.Generic; using YamlDotNet.Core; using YamlDotNet.Serialization.NamingConventions; @@ -363,6 +364,25 @@ public DeserializerBuilder IgnoreUnmatchedProperties() return this; } + /// + /// Configures how pre-existing collections are handled when using and overloads. + /// + /// + /// + /// + /// + public DeserializerBuilder WithCollectionPopulationOptions( + PreexistingArrayPopulationStrategy arrayStrategy = PreexistingArrayPopulationStrategy.CreateNew, + PreexistingCollectionPopulationStrategy collectionStrategy = PreexistingCollectionPopulationStrategy.CreateNew, + PreexistingDictionaryPopulationStrategy dictionaryStrategy = PreexistingDictionaryPopulationStrategy.CreateNew) + { + nodeDeserializerFactories.Replace(typeof(ArrayNodeDeserializer), _ => new ArrayNodeDeserializer(arrayStrategy)); + nodeDeserializerFactories.Replace(typeof(DictionaryNodeDeserializer), _ => new DictionaryNodeDeserializer(objectFactory.Value, dictionaryStrategy)); + nodeDeserializerFactories.Replace(typeof(CollectionNodeDeserializer), _ => new CollectionNodeDeserializer(objectFactory.Value, collectionStrategy)); + + return this; + } + /// /// Creates a new according to the current configuration. /// diff --git a/YamlDotNet/Serialization/IDeserializer.cs b/YamlDotNet/Serialization/IDeserializer.cs index c41066a5b..474224afb 100644 --- a/YamlDotNet/Serialization/IDeserializer.cs +++ b/YamlDotNet/Serialization/IDeserializer.cs @@ -42,5 +42,18 @@ public interface IDeserializer /// The static type of the object to deserialize. /// Returns the deserialized object. object? Deserialize(IParser parser, Type type); + + T PopulateObject(string input, T target); + T PopulateObject(TextReader input, T target); + + /// + /// Populates a pre-existing object. Values of fields/properties missing in the given YAML remain unchanged. + /// Use to configure how pre-existing collections are handled. + /// + /// The type of the target object. + /// The from where to deserialize the object. + /// The target object to be populated. + /// Returns the target object with values populated from the deserialized YAML. + T PopulateObject(IParser parser, T target); } } diff --git a/YamlDotNet/Serialization/INodeDeserializer.cs b/YamlDotNet/Serialization/INodeDeserializer.cs index ed3cb04f1..db32ee434 100644 --- a/YamlDotNet/Serialization/INodeDeserializer.cs +++ b/YamlDotNet/Serialization/INodeDeserializer.cs @@ -26,6 +26,6 @@ namespace YamlDotNet.Serialization { public interface INodeDeserializer { - bool Deserialize(IParser reader, Type expectedType, Func nestedObjectDeserializer, out object? value); + bool Deserialize(IParser reader, Type expectedType, Func nestedObjectDeserializer, out object? value, object? currentValue); } } diff --git a/YamlDotNet/Serialization/IValueDeserializer.cs b/YamlDotNet/Serialization/IValueDeserializer.cs index 8ff5fe32b..6281143e2 100644 --- a/YamlDotNet/Serialization/IValueDeserializer.cs +++ b/YamlDotNet/Serialization/IValueDeserializer.cs @@ -27,6 +27,6 @@ namespace YamlDotNet.Serialization { public interface IValueDeserializer { - object? DeserializeValue(IParser parser, Type expectedType, SerializerState state, IValueDeserializer nestedObjectDeserializer); + object? DeserializeValue(IParser parser, Type expectedType, SerializerState state, IValueDeserializer nestedObjectDeserializer, object? result); } } diff --git a/YamlDotNet/Serialization/LazyComponentRegistrationList.cs b/YamlDotNet/Serialization/LazyComponentRegistrationList.cs index 4692af08b..83a725eba 100644 --- a/YamlDotNet/Serialization/LazyComponentRegistrationList.cs +++ b/YamlDotNet/Serialization/LazyComponentRegistrationList.cs @@ -70,13 +70,24 @@ public void Add(Type componentType, Func factory) } public void Remove(Type componentType) + { + var index = IndexOf(componentType); + entries.RemoveAt(index); + } + + public void Replace(Type componentType, Func factory) + { + var index = IndexOf(componentType); + entries[index] = new LazyComponentRegistration(componentType, factory); + } + + private int IndexOf(Type componentType) { for (var i = 0; i < entries.Count; ++i) { if (entries[i].ComponentType == componentType) { - entries.RemoveAt(i); - return; + return i; } } diff --git a/YamlDotNet/Serialization/NodeDeserializers/ArrayNodeDeserializer.cs b/YamlDotNet/Serialization/NodeDeserializers/ArrayNodeDeserializer.cs index 1e62c17fb..6bc1798b9 100644 --- a/YamlDotNet/Serialization/NodeDeserializers/ArrayNodeDeserializer.cs +++ b/YamlDotNet/Serialization/NodeDeserializers/ArrayNodeDeserializer.cs @@ -27,7 +27,14 @@ namespace YamlDotNet.Serialization.NodeDeserializers { public sealed class ArrayNodeDeserializer : INodeDeserializer { - bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value) + private readonly PreexistingArrayPopulationStrategy populationStrategy; + + public ArrayNodeDeserializer(PreexistingArrayPopulationStrategy populationStrategy = PreexistingArrayPopulationStrategy.CreateNew) + { + this.populationStrategy = populationStrategy; + } + + bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value, object? currentValue) { if (!expectedType.IsArray) { @@ -40,8 +47,16 @@ bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value) + bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value, object? currentValue) { IList? list; var canUpdate = true; @@ -49,7 +51,8 @@ bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func)); canUpdate = genericListType != null; list = (IList?)Activator.CreateInstance(typeof(GenericCollectionToNonGenericAdapter<>).MakeGenericType(itemType), value); + + // TODO: How to handle pre-existing instance in this case? + if (populationStrategy != PreexistingCollectionPopulationStrategy.CreateNew) + { + throw new NotSupportedException($"Types implementing generic interface {typeof(IList<>).Name} but not non-generic interface {typeof(IList).Name} are not yet supported when using {nameof(Deserializer.PopulateObject)}() in combination with {populationStrategy}."); + } } } else if (typeof(IList).IsAssignableFrom(expectedType)) { itemType = typeof(object); - value = objectFactory.Create(expectedType); + value = (currentValue == null || populationStrategy == PreexistingCollectionPopulationStrategy.CreateNew) ? objectFactory.Create(expectedType) : currentValue; list = (IList)value; } else @@ -77,14 +86,14 @@ bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, IList result, bool canUpdate) + internal static void DeserializeHelper(Type tItem, IParser parser, Func nestedObjectDeserializer, IList result, bool canUpdate) { parser.Consume(); while (!parser.TryConsume(out var _)) { var current = parser.Current; - var value = nestedObjectDeserializer(parser, tItem); + var value = nestedObjectDeserializer(parser, tItem, null); if (value is IValuePromise promise) { if (canUpdate) diff --git a/YamlDotNet/Serialization/NodeDeserializers/DictionaryNodeDeserializer.cs b/YamlDotNet/Serialization/NodeDeserializers/DictionaryNodeDeserializer.cs index 5178eff77..f1f69f5ae 100644 --- a/YamlDotNet/Serialization/NodeDeserializers/DictionaryNodeDeserializer.cs +++ b/YamlDotNet/Serialization/NodeDeserializers/DictionaryNodeDeserializer.cs @@ -32,13 +32,15 @@ namespace YamlDotNet.Serialization.NodeDeserializers public sealed class DictionaryNodeDeserializer : INodeDeserializer { private readonly IObjectFactory objectFactory; + private readonly PreexistingDictionaryPopulationStrategy populationStrategy; - public DictionaryNodeDeserializer(IObjectFactory objectFactory) + public DictionaryNodeDeserializer(IObjectFactory objectFactory, PreexistingDictionaryPopulationStrategy populationStrategy = PreexistingDictionaryPopulationStrategy.CreateNew) { this.objectFactory = objectFactory ?? throw new ArgumentNullException(nameof(objectFactory)); + this.populationStrategy = populationStrategy; } - bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value) + bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value, object? currentValue) { IDictionary? dictionary; Type keyType, valueType; @@ -49,13 +51,18 @@ bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func but not IDictionary dictionary = (IDictionary?)Activator.CreateInstance(typeof(GenericDictionaryToNonGenericAdapter<,>).MakeGenericType(keyType, valueType), value); + + // TODO: How to handle pre-existing instance in this case? + if (populationStrategy != PreexistingDictionaryPopulationStrategy.CreateNew) + { + throw new NotSupportedException($"Types implementing generic interface {typeof(IDictionary<,>).Name} but not non-generic interface {typeof(IDictionary).Name} are not yet supported when using {nameof(Deserializer.PopulateObject)}() in combination with {populationStrategy}."); + } } } else if (typeof(IDictionary).IsAssignableFrom(expectedType)) @@ -63,7 +70,7 @@ bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, IDictionary result) + private void DeserializeHelper(Type tKey, Type tValue, IParser parser, Func nestedObjectDeserializer, IDictionary result) { parser.Consume(); while (!parser.TryConsume(out var _)) { - var key = nestedObjectDeserializer(parser, tKey); - var value = nestedObjectDeserializer(parser, tValue); + var key = nestedObjectDeserializer(parser, tKey, null); + var value = nestedObjectDeserializer(parser, tValue, null); var valuePromise = value as IValuePromise; if (key is IValuePromise keyPromise) @@ -91,7 +98,7 @@ private static void DeserializeHelper(Type tKey, Type tValue, IParser parser, Fu if (valuePromise == null) { // Key is pending, value is known - keyPromise.ValueAvailable += v => result[v!] = value!; + keyPromise.ValueAvailable += v => AddKeyValuePair(result, v!, value!); ; } else { @@ -102,7 +109,7 @@ private static void DeserializeHelper(Type tKey, Type tValue, IParser parser, Fu { if (hasFirstPart) { - result[v!] = value!; + AddKeyValuePair(result, v!, value!); } else { @@ -115,7 +122,7 @@ private static void DeserializeHelper(Type tKey, Type tValue, IParser parser, Fu { if (hasFirstPart) { - result[key] = v!; + AddKeyValuePair(result, key!, v!); } else { @@ -130,15 +137,33 @@ private static void DeserializeHelper(Type tKey, Type tValue, IParser parser, Fu if (valuePromise == null) { // Happy path: both key and value are known - result[key!] = value!; + AddKeyValuePair(result, key!, value!); } else { // Key is known, value is pending - valuePromise.ValueAvailable += v => result[key!] = v!; + valuePromise.ValueAvailable += v => AddKeyValuePair(result, key!, v!); } } } } + + private void AddKeyValuePair(IDictionary result, object key, object value) + { + switch (populationStrategy) + { + case PreexistingDictionaryPopulationStrategy.AddItemsThrowOnExistingKeys: + result.Add(key!, value!); + break; + + case PreexistingDictionaryPopulationStrategy.CreateNew: + case PreexistingDictionaryPopulationStrategy.AddItemsReplaceExistingKeys: + result[key!] = value!; + break; + + default: + throw new NotSupportedException(); + } + } } } diff --git a/YamlDotNet/Serialization/NodeDeserializers/EnumerableNodeDeserializer.cs b/YamlDotNet/Serialization/NodeDeserializers/EnumerableNodeDeserializer.cs index aed879d8e..f324a4515 100644 --- a/YamlDotNet/Serialization/NodeDeserializers/EnumerableNodeDeserializer.cs +++ b/YamlDotNet/Serialization/NodeDeserializers/EnumerableNodeDeserializer.cs @@ -29,7 +29,7 @@ namespace YamlDotNet.Serialization.NodeDeserializers { public sealed class EnumerableNodeDeserializer : INodeDeserializer { - bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value) + bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value, object? currentValue) { Type itemsType; if (expectedType == typeof(IEnumerable)) @@ -49,7 +49,7 @@ bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func).MakeGenericType(itemsType); - value = nestedObjectDeserializer(parser, collectionType); + value = nestedObjectDeserializer(parser, collectionType, null); return true; } } diff --git a/YamlDotNet/Serialization/NodeDeserializers/NullNodeDeserializer.cs b/YamlDotNet/Serialization/NodeDeserializers/NullNodeDeserializer.cs index 7f334fa0b..0bd8aac04 100644 --- a/YamlDotNet/Serialization/NodeDeserializers/NullNodeDeserializer.cs +++ b/YamlDotNet/Serialization/NodeDeserializers/NullNodeDeserializer.cs @@ -27,7 +27,7 @@ namespace YamlDotNet.Serialization.NodeDeserializers { public sealed class NullNodeDeserializer : INodeDeserializer { - bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value) + bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value, object? currentValue) { value = null; if (parser.Accept(out var evt)) diff --git a/YamlDotNet/Serialization/NodeDeserializers/ObjectNodeDeserializer.cs b/YamlDotNet/Serialization/NodeDeserializers/ObjectNodeDeserializer.cs index a401abf16..552c7122b 100644 --- a/YamlDotNet/Serialization/NodeDeserializers/ObjectNodeDeserializer.cs +++ b/YamlDotNet/Serialization/NodeDeserializers/ObjectNodeDeserializer.cs @@ -40,7 +40,7 @@ public ObjectNodeDeserializer(IObjectFactory objectFactory, ITypeInspector typeD this.ignoreUnmatched = ignoreUnmatched; } - bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value) + bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value, object? currentValue) { if (!parser.TryConsume(out var mapping)) { @@ -51,7 +51,8 @@ bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func(out var _)) { var propertyName = parser.Consume(); @@ -64,7 +65,9 @@ bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value) + bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value, object? currentValue) { if (!parser.TryConsume(out var scalar)) { diff --git a/YamlDotNet/Serialization/NodeDeserializers/TypeConverterNodeDeserializer.cs b/YamlDotNet/Serialization/NodeDeserializers/TypeConverterNodeDeserializer.cs index d00420229..68f8c103e 100644 --- a/YamlDotNet/Serialization/NodeDeserializers/TypeConverterNodeDeserializer.cs +++ b/YamlDotNet/Serialization/NodeDeserializers/TypeConverterNodeDeserializer.cs @@ -35,7 +35,7 @@ public TypeConverterNodeDeserializer(IEnumerable converters) this.converters = converters ?? throw new ArgumentNullException(nameof(converters)); } - bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value) + bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value, object? currentValue) { var converter = converters.FirstOrDefault(c => c.Accepts(expectedType)); if (converter == null) diff --git a/YamlDotNet/Serialization/NodeDeserializers/YamlConvertibleNodeDeserializer.cs b/YamlDotNet/Serialization/NodeDeserializers/YamlConvertibleNodeDeserializer.cs index 22fe9a26a..b9a83ac91 100644 --- a/YamlDotNet/Serialization/NodeDeserializers/YamlConvertibleNodeDeserializer.cs +++ b/YamlDotNet/Serialization/NodeDeserializers/YamlConvertibleNodeDeserializer.cs @@ -33,12 +33,12 @@ public YamlConvertibleNodeDeserializer(IObjectFactory objectFactory) this.objectFactory = objectFactory; } - public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value) + public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value, object? currentValue) { if (typeof(IYamlConvertible).IsAssignableFrom(expectedType)) { var convertible = (IYamlConvertible)objectFactory.Create(expectedType); - convertible.Read(parser, expectedType, type => nestedObjectDeserializer(parser, type)); + convertible.Read(parser, expectedType, type => nestedObjectDeserializer(parser, type, null)); value = convertible; return true; } diff --git a/YamlDotNet/Serialization/NodeDeserializers/YamlSerializableNodeDeserializer.cs b/YamlDotNet/Serialization/NodeDeserializers/YamlSerializableNodeDeserializer.cs index 44c17f66c..36715dd66 100644 --- a/YamlDotNet/Serialization/NodeDeserializers/YamlSerializableNodeDeserializer.cs +++ b/YamlDotNet/Serialization/NodeDeserializers/YamlSerializableNodeDeserializer.cs @@ -33,7 +33,7 @@ public YamlSerializableNodeDeserializer(IObjectFactory objectFactory) this.objectFactory = objectFactory; } - public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value) + public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value, object? currentValue) { #pragma warning disable 0618 // IYamlSerializable is obsolete if (typeof(IYamlSerializable).IsAssignableFrom(expectedType)) diff --git a/YamlDotNet/Serialization/PopulationStrategies.cs b/YamlDotNet/Serialization/PopulationStrategies.cs new file mode 100644 index 000000000..5fef87b3b --- /dev/null +++ b/YamlDotNet/Serialization/PopulationStrategies.cs @@ -0,0 +1,83 @@ +// 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; +using YamlDotNet.Core; + +namespace YamlDotNet.Serialization +{ + /// + /// Defines how arrays will be treated during deserialization when populating a pre-existing object (via and overloads) + /// + public enum PreexistingArrayPopulationStrategy + { + /// + /// Always create a new instance. + /// + CreateNew, + /// + /// Fill the pre-existing array, overwriting items. Pre-existing values that do not get overwritten will be kept. + /// If the pre-existing array does not have sufficient length, a will be thrown. + /// If the array does not exist yet, an instance will be created and populated. + /// + FillExisting + } + + /// + /// Defines how types inherting from and will be treated during deserialization when populating a pre-existing object (via and overloads) + /// + public enum PreexistingCollectionPopulationStrategy + { + /// + /// Always create a new instance. + /// + CreateNew, + /// + /// Add items to the pre-existing collection. + /// If the collection does not exist yet, an instance will be created and populated. + /// + AddItems + } + + /// + /// Defines how types inherting from and will be treated during deserialization when populating a pre-existing object (via and overloads) + /// + public enum PreexistingDictionaryPopulationStrategy + { + /// + /// Always create a new instance. + /// + CreateNew, + /// + /// Add items to the pre-existing dictionary. + /// If the key is already present in the pre-existing collection, an will be thrown. + /// If the dictionary does not exist yet, an instance will be created and populated. + /// + AddItemsThrowOnExistingKeys, + /// + /// Add items to the pre-existing dictionary, replacing values with pre-existing keys. + /// If the dictionary does not exist yet, an instance will be created and populated. + /// + AddItemsReplaceExistingKeys + } +} diff --git a/YamlDotNet/Serialization/ValueDeserializers/AliasValueDeserializer.cs b/YamlDotNet/Serialization/ValueDeserializers/AliasValueDeserializer.cs index 3a50d129a..0b1f689d8 100644 --- a/YamlDotNet/Serialization/ValueDeserializers/AliasValueDeserializer.cs +++ b/YamlDotNet/Serialization/ValueDeserializers/AliasValueDeserializer.cs @@ -96,7 +96,7 @@ public object? Value } } - public object? DeserializeValue(IParser parser, Type expectedType, SerializerState state, IValueDeserializer nestedObjectDeserializer) + public object? DeserializeValue(IParser parser, Type expectedType, SerializerState state, IValueDeserializer nestedObjectDeserializer, object? currentValue) { object? value; if (parser.TryConsume(out var alias)) @@ -121,7 +121,7 @@ public object? Value } } - value = innerDeserializer.DeserializeValue(parser, expectedType, state, nestedObjectDeserializer); + value = innerDeserializer.DeserializeValue(parser, expectedType, state, nestedObjectDeserializer, currentValue); if (!anchor.IsEmpty) { diff --git a/YamlDotNet/Serialization/ValueDeserializers/NodeValueDeserializer.cs b/YamlDotNet/Serialization/ValueDeserializers/NodeValueDeserializer.cs index 5bb9b8d59..73e09b64c 100644 --- a/YamlDotNet/Serialization/ValueDeserializers/NodeValueDeserializer.cs +++ b/YamlDotNet/Serialization/ValueDeserializers/NodeValueDeserializer.cs @@ -38,7 +38,7 @@ public NodeValueDeserializer(IList deserializers, IList(out var nodeEvent); var nodeType = GetTypeFromEvent(nodeEvent, expectedType); @@ -47,7 +47,7 @@ public NodeValueDeserializer(IList deserializers, IList nestedObjectDeserializer.DeserializeValue(r, t, state, nestedObjectDeserializer), out var value)) + if (deserializer.Deserialize(parser, nodeType, (r, t, o) => nestedObjectDeserializer.DeserializeValue(r, t, state, nestedObjectDeserializer, o), out var value, currentValue)) { return TypeConverter.ChangeType(value, expectedType); } From 1634abd497c231a33fba8654c90a94b2349dabda Mon Sep 17 00:00:00 2001 From: cp Date: Tue, 4 Jan 2022 15:29:56 +0100 Subject: [PATCH 2/6] Fix samples --- YamlDotNet.Samples/ValidatingDuringDeserialization.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/YamlDotNet.Samples/ValidatingDuringDeserialization.cs b/YamlDotNet.Samples/ValidatingDuringDeserialization.cs index 0d55fb3bf..5e7f50ab5 100644 --- a/YamlDotNet.Samples/ValidatingDuringDeserialization.cs +++ b/YamlDotNet.Samples/ValidatingDuringDeserialization.cs @@ -43,9 +43,9 @@ public ValidatingNodeDeserializer(INodeDeserializer nodeDeserializer) this.nodeDeserializer = nodeDeserializer; } - public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object value) + public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object value, object result) { - if (nodeDeserializer.Deserialize(parser, expectedType, nestedObjectDeserializer, out value)) + if (nodeDeserializer.Deserialize(parser, expectedType, nestedObjectDeserializer, out value, result)) { var context = new ValidationContext(value, null, null); Validator.ValidateObject(value, context, true); From 834fcc0c21944a6a3e897dd82876303b4fac7e85 Mon Sep 17 00:00:00 2001 From: cp Date: Tue, 4 Jan 2022 15:30:29 +0100 Subject: [PATCH 3/6] Add some specific tests --- .../Serialization/PopulateObjectTests.cs | 526 ++++++++++++++++++ 1 file changed, 526 insertions(+) create mode 100644 YamlDotNet.Test/Serialization/PopulateObjectTests.cs diff --git a/YamlDotNet.Test/Serialization/PopulateObjectTests.cs b/YamlDotNet.Test/Serialization/PopulateObjectTests.cs new file mode 100644 index 000000000..e2623a548 --- /dev/null +++ b/YamlDotNet.Test/Serialization/PopulateObjectTests.cs @@ -0,0 +1,526 @@ +// 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; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Text; +using FluentAssertions; +using Xunit; +using YamlDotNet.Core; +using YamlDotNet.Serialization; + +namespace YamlDotNet.Test.Serialization +{ + public class PopulateObjectTests : SerializationTestHelper + { + #region Simple objects + + class SimpleClass + { + public int Int { get; set; } + public string String { get; set; } + } + + class SimpleParentClass + { + public int Int { get; set; } + public string String { get; set; } + public SimpleClass Child { get; set; } + } + + [Fact] + public void PopulateSimpleObject() + { + var target = new SimpleClass { Int = 1, String = "one" }; + + var result = Deserializer.PopulateObject(@"Int: 2", target); + + Assert.Same(result, target); + + result.Int.Should().Be(2); + result.String.Should().Be("one"); + } + + [Fact] + public void PopulateSimpleObjectGraph() + { + var child = new SimpleClass { Int = 1, String = "one" }; + var target = new SimpleParentClass() + { + Int = 10, + String = "ten", + Child = child + }; + + var yaml = @" +Int: 20 +Child: + String: two"; + + var result = Deserializer.PopulateObject(yaml, target); + + result.Int.Should().Be(20); + + Assert.Same(result.Child, child); + + result.Child.Int.Should().Be(1); + result.Child.String.Should().Be("two"); + } + #endregion + + #region Simple structs + struct SimpleStruct + { + public int Int { get; set; } + public string String { get; set; } + } + + [Fact] + public void PopulateSimpleStruct() + { + var target = new SimpleStruct { Int = 1, String = "one" }; + + var result = Deserializer.PopulateObject(@"Int: 2", target); + + result.Int.Should().Be(2); + result.String.Should().Be("one"); + } + #endregion + + #region Populate collections + #region Populate collections: Arrays of value types + class IntArrayContainer + { + public int[] Ints { get; set; } + } + + [Fact] + public void PopulateArray_OfValueType_AsNestedNode_CreateNew() + { + var intArray = new int[] { 1, 2 }; + var container = new IntArrayContainer { Ints = intArray }; + + var yaml = @" +Ints: +- 10"; + + var result = DeserializerBuilder + .WithCollectionPopulationOptions(arrayStrategy: PreexistingArrayPopulationStrategy.CreateNew) + .Build() + .PopulateObject(yaml, container); + + // original array should remain unchanged + Assert.NotSame(result.Ints, intArray); + intArray.ShouldBeEquivalentTo(new[] { 1, 2 }); + + result.Ints.ShouldBeEquivalentTo(new[] { 10 }); + } + + [Fact] + public void PopulateArray_OfValueType_AsNestedNode_FillExisting() + { + var intArray = new int[] { 1, 2 }; + var container = new IntArrayContainer { Ints = intArray }; + + var yaml = @" +Ints: +- 10"; + + var result = DeserializerBuilder + .WithCollectionPopulationOptions(arrayStrategy: PreexistingArrayPopulationStrategy.FillExisting) + .Build() + .PopulateObject(yaml, container); + + Assert.Same(result.Ints, intArray); + + result.Ints.ShouldBeEquivalentTo(new[] { 10, 2 }); + } + + [Fact] + public void PopulateArray_OfValueType_AsNestedNode_FillExisting_WithInsufficientSize() + { + var intArray = new int[] { 1, 2 }; + var container = new IntArrayContainer { Ints = intArray }; + + var yaml = @" +Ints: +- 10 +- 20 +- 30"; + + Action action = () => DeserializerBuilder + .WithCollectionPopulationOptions(arrayStrategy: PreexistingArrayPopulationStrategy.FillExisting) + .Build() + .PopulateObject(yaml, container); + + action.ShouldThrow().Where(ex => ex.Message.Contains("exceed size")); + } + + [Fact] + public void PopulateArray_OfValueType_AsRootNode_CreateNew() + { + var intArray = new int[] { 1, 2 }; + + var yaml = @"- 10"; + + var result = DeserializerBuilder + .WithCollectionPopulationOptions(arrayStrategy: PreexistingArrayPopulationStrategy.CreateNew) + .Build() + .PopulateObject(yaml, intArray); + + // original array should remain unchanged + Assert.NotSame(result, intArray); + intArray.ShouldAllBeEquivalentTo(new[] { 1, 2 }); + + result.ShouldBeEquivalentTo(new[] { 10 }); + } + + [Fact] + public void PopulateArray_OfValueType_AsRootNode_FillExisting() + { + var intArray = new int[] { 1, 2 }; + + var yaml = @"- 10"; + + var result = DeserializerBuilder + .WithCollectionPopulationOptions(arrayStrategy: PreexistingArrayPopulationStrategy.FillExisting) + .Build() + .PopulateObject(yaml, intArray); + + Assert.Same(result, intArray); + + result.ShouldBeEquivalentTo(new[] { 10, 2 }); + } + #endregion + + #region Populate collections: Collections (lists) of value types + public class IntGenericCollectionContainer + { + public IList Ints { get; set; } + } + + [Fact] + public void PopulateCollection_OfValueType_AsNestedNode_CreateNew() + { + var intCollection = new List { 1, 2 }; + var container = new IntGenericCollectionContainer { Ints = intCollection }; + + var yaml = @" +Ints: +- 10"; + + var result = DeserializerBuilder + .WithCollectionPopulationOptions(collectionStrategy: PreexistingCollectionPopulationStrategy.CreateNew) + .Build() + .PopulateObject(yaml, container); + + Assert.NotSame(result.Ints, intCollection); + result.Ints.ShouldBeEquivalentTo(new[] { 10 }); + } + + [Fact] + public void PopulateCollection_OfValueType_AsNestedNode_AddItems() + { + var intCollection = new List { 1, 2 }; + var container = new IntGenericCollectionContainer { Ints = intCollection }; + + var yaml = @" +Ints: +- 10"; + + var result = DeserializerBuilder + .WithCollectionPopulationOptions(collectionStrategy: PreexistingCollectionPopulationStrategy.AddItems) + .Build() + .PopulateObject(yaml, container); + + Assert.Same(result.Ints, intCollection); + result.Ints.ShouldBeEquivalentTo(new[] { 1, 2, 10 }); + } + + [Fact] + public void PopulateCollection_OfValueType_AsRootNode_CreateNew() + { + var intCollection = new List { 1, 2 }; + + var yaml = @"- 10"; + + var result = DeserializerBuilder + .WithCollectionPopulationOptions(collectionStrategy: PreexistingCollectionPopulationStrategy.CreateNew) + .Build() + .PopulateObject(yaml, intCollection); + + Assert.NotSame(result, intCollection); + result.ShouldBeEquivalentTo(new[] { 10 }); + } + + [Fact] + public void PopulateCollection_OfValueType_AsRootNode_AddItems() + { + var intCollection = new List { 1, 2 }; + + var yaml = @"- 10"; + + var result = DeserializerBuilder + .WithCollectionPopulationOptions(collectionStrategy: PreexistingCollectionPopulationStrategy.AddItems) + .Build() + .PopulateObject(yaml, intCollection); + + Assert.Same(result, intCollection); + result.ShouldBeEquivalentTo(new[] { 1, 2, 10 }); + } + #endregion + + #region Populate collections: Dictionaries of value types + class StringIntDictionaryContainer + { + public Dictionary StringInts { get; set; } + } + + [Fact] + public void PopulateDictionary_OfValueTypes_AsNestedNode_CreateNew() + { + var stringInts = new Dictionary + { + { "one", 1 }, + { "two", 2 }, + }; + var container = new StringIntDictionaryContainer { StringInts = stringInts }; + + var yaml = @" +StringInts: + one: 10 + three: 30"; + + var result = DeserializerBuilder + .WithCollectionPopulationOptions(dictionaryStrategy: PreexistingDictionaryPopulationStrategy.CreateNew) + .Build() + .PopulateObject(yaml, container); + + Assert.NotSame(result.StringInts, stringInts); + container.StringInts.ShouldBeEquivalentTo(new Dictionary { + { "one", 10 }, + { "three", 30 } + }); + } + + [Fact] + public void PopulateDictionary_OfValueTypes_AsNestedNode_AddItemsReplaceExistingKeys() + { + var stringInts = new Dictionary + { + { "one", 1 }, + { "two", 2 }, + }; + var container = new StringIntDictionaryContainer { StringInts = stringInts }; + + var yaml = @" +StringInts: + one: 10 + three: 30"; + + var result = DeserializerBuilder + .WithCollectionPopulationOptions(dictionaryStrategy: PreexistingDictionaryPopulationStrategy.AddItemsReplaceExistingKeys) + .Build() + .PopulateObject(yaml, container); + + Assert.Same(result.StringInts, stringInts); + container.StringInts.ShouldBeEquivalentTo(new Dictionary { + { "one", 10 }, + { "two", 2 }, + { "three", 30 } + }); + } + + [Fact] + public void PopulateDictionary_OfValueTypes_AsNestedNode_AddItemsThrowOnExistingKeys() + { + var stringInts = new Dictionary + { + { "one", 1 }, + { "two", 2 }, + }; + var container = new StringIntDictionaryContainer { StringInts = stringInts }; + + var yaml = @" +StringInts: + one: 10 + three: 30"; + + Action action = () => DeserializerBuilder + .WithCollectionPopulationOptions(dictionaryStrategy: PreexistingDictionaryPopulationStrategy.AddItemsThrowOnExistingKeys) + .Build() + .PopulateObject(yaml, container); + + action.ShouldThrow().WithInnerException().Where(ex => ex.InnerException.Message.Contains("same key")); + } + + + [Fact] + public void PopulateDictionary_OfValueTypes_AsRootNode_CreateNew() + { + var stringInts = new Dictionary + { + { "one", 1 }, + { "two", 2 }, + }; + + var yaml = @" +one: 10 +three: 30"; + + var result = DeserializerBuilder + .WithCollectionPopulationOptions(dictionaryStrategy: PreexistingDictionaryPopulationStrategy.CreateNew) + .Build() + .PopulateObject(yaml, stringInts); + + Assert.NotSame(result, stringInts); + result.ShouldBeEquivalentTo(new Dictionary { + { "one", 10 }, + { "three", 30 } + }); + } + + [Fact] + public void PopulateDictionary_OfValueTypes_AsRootNode_AddItemsReplaceExistingKeys() + { + var stringInts = new Dictionary + { + { "one", 1 }, + { "two", 2 }, + }; + + var yaml = @" +one: 10 +three: 30"; + + var result = DeserializerBuilder + .WithCollectionPopulationOptions(dictionaryStrategy: PreexistingDictionaryPopulationStrategy.AddItemsReplaceExistingKeys) + .Build() + .PopulateObject(yaml, stringInts); + + Assert.Same(result, stringInts); + result.ShouldBeEquivalentTo(new Dictionary { + { "one", 10 }, + { "two", 2 }, + { "three", 30 } + }); + } + + class GenericDictionaryButNotNonGenericDictionary : IDictionary + { + public TValue this[TKey key] { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + public ICollection Keys => throw new NotImplementedException(); + + public ICollection Values => throw new NotImplementedException(); + + public int Count => throw new NotImplementedException(); + + public bool IsReadOnly => throw new NotImplementedException(); + + public void Add(TKey key, TValue value) + { + throw new NotImplementedException(); + } + + public void Add(KeyValuePair item) + { + throw new NotImplementedException(); + } + + public void Clear() + { + throw new NotImplementedException(); + } + + public bool Contains(KeyValuePair item) + { + throw new NotImplementedException(); + } + + public bool ContainsKey(TKey key) + { + throw new NotImplementedException(); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + throw new NotImplementedException(); + } + + public IEnumerator> GetEnumerator() + { + throw new NotImplementedException(); + } + + public bool Remove(TKey key) + { + throw new NotImplementedException(); + } + + public bool Remove(KeyValuePair item) + { + throw new NotImplementedException(); + } + + public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) + { + throw new NotImplementedException(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + throw new NotImplementedException(); + } + } + + [Fact] + public void PopulateDictionary_WithTypeNotSupportingPopulation_CreateNew() + { + var dictionary = new GenericDictionaryButNotNonGenericDictionary(); + + Action action = () => DeserializerBuilder + .WithCollectionPopulationOptions(dictionaryStrategy: PreexistingDictionaryPopulationStrategy.CreateNew) + .Build() + .PopulateObject("one: ten", dictionary); + + action.ShouldThrow().WithInnerException(); + } + + [Fact] + public void PopulateDictionary_WithTypeNotSupportingPopulation_AddItemsReplaceExistingKeys() + { + var dictionary = new GenericDictionaryButNotNonGenericDictionary(); + + Action action = () => DeserializerBuilder + .WithCollectionPopulationOptions(dictionaryStrategy: PreexistingDictionaryPopulationStrategy.AddItemsReplaceExistingKeys) + .Build() + .PopulateObject("one: ten", dictionary); + + action.ShouldThrow().WithInnerException().Where(ex => ex.InnerException.Message.Contains("Types implementing generic interface")); ; + } + #endregion + #endregion + } +} From 23db8009931f6cf11056bd8a3f093ce73d1d7d7c Mon Sep 17 00:00:00 2001 From: cp Date: Tue, 4 Jan 2022 17:02:23 +0100 Subject: [PATCH 4/6] Rename "population" to "populating" --- .../Serialization/PopulateObjectTests.cs | 39 +++++++++---------- .../Serialization/DeserializerBuilder.cs | 8 ++-- YamlDotNet/Serialization/IDeserializer.cs | 2 +- .../ArrayNodeDeserializer.cs | 10 ++--- .../CollectionNodeDeserializer.cs | 14 +++---- .../DictionaryNodeDeserializer.cs | 22 +++++------ ...nStrategies.cs => PopulatingStrategies.cs} | 6 +-- 7 files changed, 50 insertions(+), 51 deletions(-) rename YamlDotNet/Serialization/{PopulationStrategies.cs => PopulatingStrategies.cs} (93%) diff --git a/YamlDotNet.Test/Serialization/PopulateObjectTests.cs b/YamlDotNet.Test/Serialization/PopulateObjectTests.cs index e2623a548..af90d12ad 100644 --- a/YamlDotNet.Test/Serialization/PopulateObjectTests.cs +++ b/YamlDotNet.Test/Serialization/PopulateObjectTests.cs @@ -126,7 +126,7 @@ public void PopulateArray_OfValueType_AsNestedNode_CreateNew() - 10"; var result = DeserializerBuilder - .WithCollectionPopulationOptions(arrayStrategy: PreexistingArrayPopulationStrategy.CreateNew) + .WithPopulatingOptions(arrayStrategy: ArrayPopulatingStrategy.CreateNew) .Build() .PopulateObject(yaml, container); @@ -148,7 +148,7 @@ public void PopulateArray_OfValueType_AsNestedNode_FillExisting() - 10"; var result = DeserializerBuilder - .WithCollectionPopulationOptions(arrayStrategy: PreexistingArrayPopulationStrategy.FillExisting) + .WithPopulatingOptions(arrayStrategy: ArrayPopulatingStrategy.FillExisting) .Build() .PopulateObject(yaml, container); @@ -170,7 +170,7 @@ public void PopulateArray_OfValueType_AsNestedNode_FillExisting_WithInsufficient - 30"; Action action = () => DeserializerBuilder - .WithCollectionPopulationOptions(arrayStrategy: PreexistingArrayPopulationStrategy.FillExisting) + .WithPopulatingOptions(arrayStrategy: ArrayPopulatingStrategy.FillExisting) .Build() .PopulateObject(yaml, container); @@ -185,7 +185,7 @@ public void PopulateArray_OfValueType_AsRootNode_CreateNew() var yaml = @"- 10"; var result = DeserializerBuilder - .WithCollectionPopulationOptions(arrayStrategy: PreexistingArrayPopulationStrategy.CreateNew) + .WithPopulatingOptions(arrayStrategy: ArrayPopulatingStrategy.CreateNew) .Build() .PopulateObject(yaml, intArray); @@ -204,7 +204,7 @@ public void PopulateArray_OfValueType_AsRootNode_FillExisting() var yaml = @"- 10"; var result = DeserializerBuilder - .WithCollectionPopulationOptions(arrayStrategy: PreexistingArrayPopulationStrategy.FillExisting) + .WithPopulatingOptions(arrayStrategy: ArrayPopulatingStrategy.FillExisting) .Build() .PopulateObject(yaml, intArray); @@ -231,7 +231,7 @@ public void PopulateCollection_OfValueType_AsNestedNode_CreateNew() - 10"; var result = DeserializerBuilder - .WithCollectionPopulationOptions(collectionStrategy: PreexistingCollectionPopulationStrategy.CreateNew) + .WithPopulatingOptions(collectionStrategy: CollectionPopulatingStrategy.CreateNew) .Build() .PopulateObject(yaml, container); @@ -250,7 +250,7 @@ public void PopulateCollection_OfValueType_AsNestedNode_AddItems() - 10"; var result = DeserializerBuilder - .WithCollectionPopulationOptions(collectionStrategy: PreexistingCollectionPopulationStrategy.AddItems) + .WithPopulatingOptions(collectionStrategy: CollectionPopulatingStrategy.AddItems) .Build() .PopulateObject(yaml, container); @@ -266,7 +266,7 @@ public void PopulateCollection_OfValueType_AsRootNode_CreateNew() var yaml = @"- 10"; var result = DeserializerBuilder - .WithCollectionPopulationOptions(collectionStrategy: PreexistingCollectionPopulationStrategy.CreateNew) + .WithPopulatingOptions(collectionStrategy: CollectionPopulatingStrategy.CreateNew) .Build() .PopulateObject(yaml, intCollection); @@ -282,7 +282,7 @@ public void PopulateCollection_OfValueType_AsRootNode_AddItems() var yaml = @"- 10"; var result = DeserializerBuilder - .WithCollectionPopulationOptions(collectionStrategy: PreexistingCollectionPopulationStrategy.AddItems) + .WithPopulatingOptions(collectionStrategy: CollectionPopulatingStrategy.AddItems) .Build() .PopulateObject(yaml, intCollection); @@ -313,7 +313,7 @@ public void PopulateDictionary_OfValueTypes_AsNestedNode_CreateNew() three: 30"; var result = DeserializerBuilder - .WithCollectionPopulationOptions(dictionaryStrategy: PreexistingDictionaryPopulationStrategy.CreateNew) + .WithPopulatingOptions(dictionaryStrategy: DictionaryPopulatingStrategy.CreateNew) .Build() .PopulateObject(yaml, container); @@ -340,7 +340,7 @@ public void PopulateDictionary_OfValueTypes_AsNestedNode_AddItemsReplaceExisting three: 30"; var result = DeserializerBuilder - .WithCollectionPopulationOptions(dictionaryStrategy: PreexistingDictionaryPopulationStrategy.AddItemsReplaceExistingKeys) + .WithPopulatingOptions(dictionaryStrategy: DictionaryPopulatingStrategy.AddItemsReplaceExistingKeys) .Build() .PopulateObject(yaml, container); @@ -368,14 +368,13 @@ public void PopulateDictionary_OfValueTypes_AsNestedNode_AddItemsThrowOnExisting three: 30"; Action action = () => DeserializerBuilder - .WithCollectionPopulationOptions(dictionaryStrategy: PreexistingDictionaryPopulationStrategy.AddItemsThrowOnExistingKeys) + .WithPopulatingOptions(dictionaryStrategy: DictionaryPopulatingStrategy.AddItemsThrowOnExistingKeys) .Build() .PopulateObject(yaml, container); action.ShouldThrow().WithInnerException().Where(ex => ex.InnerException.Message.Contains("same key")); } - [Fact] public void PopulateDictionary_OfValueTypes_AsRootNode_CreateNew() { @@ -390,7 +389,7 @@ public void PopulateDictionary_OfValueTypes_AsRootNode_CreateNew() three: 30"; var result = DeserializerBuilder - .WithCollectionPopulationOptions(dictionaryStrategy: PreexistingDictionaryPopulationStrategy.CreateNew) + .WithPopulatingOptions(dictionaryStrategy: DictionaryPopulatingStrategy.CreateNew) .Build() .PopulateObject(yaml, stringInts); @@ -415,7 +414,7 @@ public void PopulateDictionary_OfValueTypes_AsRootNode_AddItemsReplaceExistingKe three: 30"; var result = DeserializerBuilder - .WithCollectionPopulationOptions(dictionaryStrategy: PreexistingDictionaryPopulationStrategy.AddItemsReplaceExistingKeys) + .WithPopulatingOptions(dictionaryStrategy: DictionaryPopulatingStrategy.AddItemsReplaceExistingKeys) .Build() .PopulateObject(yaml, stringInts); @@ -496,12 +495,12 @@ IEnumerator IEnumerable.GetEnumerator() } [Fact] - public void PopulateDictionary_WithTypeNotSupportingPopulation_CreateNew() + public void PopulateDictionary_WithTypeNotSupportedForPopulating_CreateNew() { var dictionary = new GenericDictionaryButNotNonGenericDictionary(); Action action = () => DeserializerBuilder - .WithCollectionPopulationOptions(dictionaryStrategy: PreexistingDictionaryPopulationStrategy.CreateNew) + .WithPopulatingOptions(dictionaryStrategy: DictionaryPopulatingStrategy.CreateNew) .Build() .PopulateObject("one: ten", dictionary); @@ -509,16 +508,16 @@ public void PopulateDictionary_WithTypeNotSupportingPopulation_CreateNew() } [Fact] - public void PopulateDictionary_WithTypeNotSupportingPopulation_AddItemsReplaceExistingKeys() + public void PopulateDictionary_WithTypeNotSupportedForPopulating_AddItemsReplaceExistingKeys() { var dictionary = new GenericDictionaryButNotNonGenericDictionary(); Action action = () => DeserializerBuilder - .WithCollectionPopulationOptions(dictionaryStrategy: PreexistingDictionaryPopulationStrategy.AddItemsReplaceExistingKeys) + .WithPopulatingOptions(dictionaryStrategy: DictionaryPopulatingStrategy.AddItemsReplaceExistingKeys) .Build() .PopulateObject("one: ten", dictionary); - action.ShouldThrow().WithInnerException().Where(ex => ex.InnerException.Message.Contains("Types implementing generic interface")); ; + action.ShouldThrow().WithInnerException(); } #endregion #endregion diff --git a/YamlDotNet/Serialization/DeserializerBuilder.cs b/YamlDotNet/Serialization/DeserializerBuilder.cs index b47ba12bf..59d67057b 100755 --- a/YamlDotNet/Serialization/DeserializerBuilder.cs +++ b/YamlDotNet/Serialization/DeserializerBuilder.cs @@ -371,10 +371,10 @@ public DeserializerBuilder IgnoreUnmatchedProperties() /// /// /// - public DeserializerBuilder WithCollectionPopulationOptions( - PreexistingArrayPopulationStrategy arrayStrategy = PreexistingArrayPopulationStrategy.CreateNew, - PreexistingCollectionPopulationStrategy collectionStrategy = PreexistingCollectionPopulationStrategy.CreateNew, - PreexistingDictionaryPopulationStrategy dictionaryStrategy = PreexistingDictionaryPopulationStrategy.CreateNew) + public DeserializerBuilder WithPopulatingOptions( + ArrayPopulatingStrategy arrayStrategy = ArrayPopulatingStrategy.CreateNew, + CollectionPopulatingStrategy collectionStrategy = CollectionPopulatingStrategy.CreateNew, + DictionaryPopulatingStrategy dictionaryStrategy = DictionaryPopulatingStrategy.CreateNew) { nodeDeserializerFactories.Replace(typeof(ArrayNodeDeserializer), _ => new ArrayNodeDeserializer(arrayStrategy)); nodeDeserializerFactories.Replace(typeof(DictionaryNodeDeserializer), _ => new DictionaryNodeDeserializer(objectFactory.Value, dictionaryStrategy)); diff --git a/YamlDotNet/Serialization/IDeserializer.cs b/YamlDotNet/Serialization/IDeserializer.cs index 474224afb..d75db8ffa 100644 --- a/YamlDotNet/Serialization/IDeserializer.cs +++ b/YamlDotNet/Serialization/IDeserializer.cs @@ -48,7 +48,7 @@ public interface IDeserializer /// /// Populates a pre-existing object. Values of fields/properties missing in the given YAML remain unchanged. - /// Use to configure how pre-existing collections are handled. + /// Use to configure how pre-existing collections are handled. /// /// The type of the target object. /// The from where to deserialize the object. diff --git a/YamlDotNet/Serialization/NodeDeserializers/ArrayNodeDeserializer.cs b/YamlDotNet/Serialization/NodeDeserializers/ArrayNodeDeserializer.cs index 6bc1798b9..483943c3c 100644 --- a/YamlDotNet/Serialization/NodeDeserializers/ArrayNodeDeserializer.cs +++ b/YamlDotNet/Serialization/NodeDeserializers/ArrayNodeDeserializer.cs @@ -27,11 +27,11 @@ namespace YamlDotNet.Serialization.NodeDeserializers { public sealed class ArrayNodeDeserializer : INodeDeserializer { - private readonly PreexistingArrayPopulationStrategy populationStrategy; + private readonly ArrayPopulatingStrategy populatingStrategy; - public ArrayNodeDeserializer(PreexistingArrayPopulationStrategy populationStrategy = PreexistingArrayPopulationStrategy.CreateNew) + public ArrayNodeDeserializer(ArrayPopulatingStrategy populatingStrategy = ArrayPopulatingStrategy.CreateNew) { - this.populationStrategy = populationStrategy; + this.populatingStrategy = populatingStrategy; } bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value, object? currentValue) @@ -47,7 +47,7 @@ bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value, object? currentValue) @@ -51,7 +51,7 @@ bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func).MakeGenericType(itemType), value); // TODO: How to handle pre-existing instance in this case? - if (populationStrategy != PreexistingCollectionPopulationStrategy.CreateNew) + if (populatingStrategy != CollectionPopulatingStrategy.CreateNew) { - throw new NotSupportedException($"Types implementing generic interface {typeof(IList<>).Name} but not non-generic interface {typeof(IList).Name} are not yet supported when using {nameof(Deserializer.PopulateObject)}() in combination with {populationStrategy}."); + throw new NotSupportedException($"Types implementing generic interface {typeof(IList<>).Name} but not non-generic interface {typeof(IList).Name} are not yet supported when using {nameof(Deserializer.PopulateObject)}() in combination with {populatingStrategy}."); } } } @@ -72,7 +72,7 @@ bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value, object? currentValue) @@ -51,7 +51,7 @@ bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func).MakeGenericType(keyType, valueType), value); // TODO: How to handle pre-existing instance in this case? - if (populationStrategy != PreexistingDictionaryPopulationStrategy.CreateNew) + if (populatingStrategy != DictionaryPopulatingStrategy.CreateNew) { - throw new NotSupportedException($"Types implementing generic interface {typeof(IDictionary<,>).Name} but not non-generic interface {typeof(IDictionary).Name} are not yet supported when using {nameof(Deserializer.PopulateObject)}() in combination with {populationStrategy}."); + throw new NotSupportedException($"Types implementing generic interface {typeof(IDictionary<,>).Name} but not non-generic interface {typeof(IDictionary).Name} are not yet supported when using {nameof(Deserializer.PopulateObject)}() in combination with {populatingStrategy}."); } } } @@ -70,7 +70,7 @@ bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func /// Defines how arrays will be treated during deserialization when populating a pre-existing object (via and overloads) /// - public enum PreexistingArrayPopulationStrategy + public enum ArrayPopulatingStrategy { /// /// Always create a new instance. @@ -46,7 +46,7 @@ public enum PreexistingArrayPopulationStrategy /// /// Defines how types inherting from and will be treated during deserialization when populating a pre-existing object (via and overloads) /// - public enum PreexistingCollectionPopulationStrategy + public enum CollectionPopulatingStrategy { /// /// Always create a new instance. @@ -62,7 +62,7 @@ public enum PreexistingCollectionPopulationStrategy /// /// Defines how types inherting from and will be treated during deserialization when populating a pre-existing object (via and overloads) /// - public enum PreexistingDictionaryPopulationStrategy + public enum DictionaryPopulatingStrategy { /// /// Always create a new instance. From ca1252af5954d17f14455e83a4031147bc5afd27 Mon Sep 17 00:00:00 2001 From: cp Date: Tue, 4 Jan 2022 17:02:57 +0100 Subject: [PATCH 5/6] Add test for list edge case as well --- .../Serialization/PopulateObjectTests.cs | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/YamlDotNet.Test/Serialization/PopulateObjectTests.cs b/YamlDotNet.Test/Serialization/PopulateObjectTests.cs index af90d12ad..903042e4e 100644 --- a/YamlDotNet.Test/Serialization/PopulateObjectTests.cs +++ b/YamlDotNet.Test/Serialization/PopulateObjectTests.cs @@ -425,6 +425,93 @@ public void PopulateDictionary_OfValueTypes_AsRootNode_AddItemsReplaceExistingKe { "three", 30 } }); } + #endregion + + #region Edge cases + class GenericListButNotNonGenericList : IList + { + public T this[int index] { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + public int Count => throw new NotImplementedException(); + + public bool IsReadOnly => throw new NotImplementedException(); + + public void Add(T item) + { + throw new NotImplementedException(); + } + + public void Clear() + { + throw new NotImplementedException(); + } + + public bool Contains(T item) + { + throw new NotImplementedException(); + } + + public void CopyTo(T[] array, int arrayIndex) + { + throw new NotImplementedException(); + } + + public IEnumerator GetEnumerator() + { + throw new NotImplementedException(); + } + + public int IndexOf(T item) + { + throw new NotImplementedException(); + } + + public void Insert(int index, T item) + { + throw new NotImplementedException(); + } + + public bool Remove(T item) + { + throw new NotImplementedException(); + } + + public void RemoveAt(int index) + { + throw new NotImplementedException(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + throw new NotImplementedException(); + } + } + + [Fact] + public void PopulateList_WithTypeNotSupportedForPopulating_CreateNew() + { + var list = new GenericListButNotNonGenericList(); + + Action action = () => DeserializerBuilder + .WithPopulatingOptions(collectionStrategy: CollectionPopulatingStrategy.CreateNew) + .Build() + .PopulateObject("- one", list); + + action.ShouldThrow().WithInnerException(); + } + + [Fact] + public void PopulateList_WithTypeNotSupportedForPopulating_AddItems() + { + var list = new GenericListButNotNonGenericList(); + + Action action = () => DeserializerBuilder + .WithPopulatingOptions(collectionStrategy: CollectionPopulatingStrategy.AddItems) + .Build() + .PopulateObject("- one", list); + + action.ShouldThrow().WithInnerException(); + } class GenericDictionaryButNotNonGenericDictionary : IDictionary { From 5712e20510a24f084b1d799136ccb3241ff11337 Mon Sep 17 00:00:00 2001 From: cp Date: Wed, 5 Jan 2022 17:15:06 +0100 Subject: [PATCH 6/6] Add base functionality to populate reference type items inside collections and arrays --- .../Serialization/PopulateObjectTests.cs | 300 +++++++++++++++++- .../ArrayNodeDeserializer.cs | 50 ++- .../CollectionNodeDeserializer.cs | 24 +- .../Serialization/PopulatingStrategies.cs | 11 +- 4 files changed, 365 insertions(+), 20 deletions(-) diff --git a/YamlDotNet.Test/Serialization/PopulateObjectTests.cs b/YamlDotNet.Test/Serialization/PopulateObjectTests.cs index 903042e4e..59d08f566 100644 --- a/YamlDotNet.Test/Serialization/PopulateObjectTests.cs +++ b/YamlDotNet.Test/Serialization/PopulateObjectTests.cs @@ -23,8 +23,6 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Text; using FluentAssertions; using Xunit; using YamlDotNet.Core; @@ -109,6 +107,304 @@ public void PopulateSimpleStruct() #endregion #region Populate collections + #region Populate collections: Reference types + [Fact] + public void PopulateArray_OfReferenceType_DeserializeSameSize() + { + var array = new[] + { + new SimpleClass { Int = 1, String = "one" }, + new SimpleClass { Int = 2, String = "two" } + }; + + var firstItem = array[0]; + + var yaml = @" +- + Int: 10 +- + String: TWO +"; + + var result = DeserializerBuilder + .WithPopulatingOptions(arrayStrategy: ArrayPopulatingStrategy.PopulateItems) + .Build() + .PopulateObject(yaml, array); + + Assert.Same(result, array); + Assert.Same(result[0], firstItem); + + result.Length.Should().Be(2); + + result[0].Int.Should().Be(10); + result[0].String.Should().Be("one"); + + result[1].Int.Should().Be(2); + result[1].String.Should().Be("TWO"); + } + + [Fact] + public void PopulateArray_OfReferenceType_DeserializeSmallerSize() + { + var array = new[] + { + new SimpleClass { Int = 1, String = "one" }, + new SimpleClass { Int = 2, String = "two" } + }; + + var firstItem = array[0]; + + var yaml = @" +- + Int: 10 +"; + + var result = DeserializerBuilder + .WithPopulatingOptions(arrayStrategy: ArrayPopulatingStrategy.PopulateItems) + .Build() + .PopulateObject(yaml, array); + + Assert.Same(result, array); + Assert.Same(result[0], firstItem); + + result.Length.Should().Be(2); + + result[0].Int.Should().Be(10); + result[0].String.Should().Be("one"); + + result[1].Int.Should().Be(2); + result[1].String.Should().Be("two"); + } + + [Fact] + public void PopulateArray_OfReferenceType_DeserializeBiggerSize() + { + var array = new[] + { + new SimpleClass { Int = 1, String = "one" }, + new SimpleClass { Int = 2, String = "two" } + }; + + var firstItem = array[0]; + + var yaml = @" +- + Int: 10 +- + String: TWO +- + Int: 30 + String: THREE +"; + + var result = DeserializerBuilder + .WithPopulatingOptions(arrayStrategy: ArrayPopulatingStrategy.PopulateItems) + .Build() + .PopulateObject(yaml, array); + + Assert.NotSame(result, array); + Assert.Same(result[0], firstItem); + + result.Length.Should().Be(3); + + result[0].Int.Should().Be(10); + result[0].String.Should().Be("one"); + + result[1].Int.Should().Be(2); + result[1].String.Should().Be("two"); + + result[2].Int.Should().Be(30); + result[2].String.Should().Be("THREE"); + } + + [Fact] + public void PopulateArray_OfReferenceType_DeserializeBiggerSize_PopulateItemsAllowGrowingArray() + { + var array = new[] + { + new SimpleClass { Int = 1, String = "one" }, + new SimpleClass { Int = 2, String = "two" } + }; + + var firstItem = array[0]; + + var yaml = @" +- + Int: 10 +- + String: TWO +- + Int: 30 + String: THREE +"; + + var result = DeserializerBuilder + .WithPopulatingOptions(arrayStrategy: ArrayPopulatingStrategy.PopulateItemsAllowGrowingArray) + .Build() + .PopulateObject(yaml, array); + + Assert.NotSame(result, array); + Assert.Same(result[0], firstItem); + + result.Length.Should().Be(3); + + result[0].Int.Should().Be(10); + result[0].String.Should().Be("one"); + + result[1].Int.Should().Be(2); + result[1].String.Should().Be("TWO"); + + result[2].Int.Should().Be(30); + result[2].String.Should().Be("THREE"); + } + + [Fact] + public void PopulateList_OfReferenceType_DeserializeBiggerSize_PopulateOrAddItems() + { + var list = new List(new[] + { + new SimpleClass { Int = 1, String = "one" }, + new SimpleClass { Int = 2, String = "two" } + }); + + var firstItem = list[0]; + + var yaml = @" +- + Int: 10 +- + String: TWO +- + Int: 30 + String: THREE +"; + + var result = DeserializerBuilder + .WithPopulatingOptions(collectionStrategy: CollectionPopulatingStrategy.PopulateOrAddItems) + .Build() + .PopulateObject(yaml, list); + + Assert.Same(result, list); + Assert.Same(result[0], firstItem); + + result.Count.Should().Be(3); + + result[0].Int.Should().Be(10); + result[0].String.Should().Be("one"); + + result[1].Int.Should().Be(2); + result[1].String.Should().Be("TWO"); + + result[2].Int.Should().Be(30); + result[2].String.Should().Be("THREE"); + } + + [Fact] + public void PopulateList_OfReferenceType_DeserializeEqualSize_PopulateOrAddItems() + { + var list = new List(new[] + { + new SimpleClass { Int = 1, String = "one" }, + new SimpleClass { Int = 2, String = "two" } + }); + + var firstItem = list[0]; + + var yaml = @" +- + Int: 10 +- + String: TWO +"; + + var result = DeserializerBuilder + .WithPopulatingOptions(collectionStrategy: CollectionPopulatingStrategy.PopulateOrAddItems) + .Build() + .PopulateObject(yaml, list); + + Assert.Same(result, list); + Assert.Same(result[0], firstItem); + + result.Count.Should().Be(2); + + result[0].Int.Should().Be(10); + result[0].String.Should().Be("one"); + + result[1].Int.Should().Be(2); + result[1].String.Should().Be("TWO"); + } + + [Fact] + public void PopulateList_OfReferenceType_DeserializeSmallerSize_PopulateOrAddItems() + { + var list = new List(new[] + { + new SimpleClass { Int = 1, String = "one" }, + new SimpleClass { Int = 2, String = "two" } + }); + + var firstItem = list[0]; + + var yaml = @" +- + Int: 10 +"; + + var result = DeserializerBuilder + .WithPopulatingOptions(collectionStrategy: CollectionPopulatingStrategy.PopulateOrAddItems) + .Build() + .PopulateObject(yaml, list); + + Assert.Same(result, list); + Assert.Same(result[0], firstItem); + + result.Count.Should().Be(2); + + result[0].Int.Should().Be(10); + result[0].String.Should().Be("one"); + + result[1].Int.Should().Be(2); + result[1].String.Should().Be("two"); + } + /* + [Fact] + public void PopulateReferenceTypeCollections() + { + var listOfDicts = new Dictionary>() + { + { "A", new Dictionary() + { + { "A1", "1" }, + { "A2", "2" } + } + } + }; + + var a = listOfDicts["A"]; + + var yaml = @" +A: + A1: 10 + C1: 30 +B: + B1: 100 +"; + + var result = DeserializerBuilder + .WithPopulatingOptions(ArrayPopulatingStrategy.PopulateItems) + .Build() + .PopulateObject(yaml, listOfDicts); + + Assert.Same(result, listOfDicts); + Assert.Same(result["A"], a); + result["A"].Count.Should().Be(3); + result["A"]["A1"].Should().Be("10"); + result.Count.Should().Be(2); + result["B"]["B1"].Should().Be("100"); + } + */ + #endregion + #region Populate collections: Arrays of value types class IntArrayContainer { diff --git a/YamlDotNet/Serialization/NodeDeserializers/ArrayNodeDeserializer.cs b/YamlDotNet/Serialization/NodeDeserializers/ArrayNodeDeserializer.cs index 483943c3c..5eabf6ea2 100644 --- a/YamlDotNet/Serialization/NodeDeserializers/ArrayNodeDeserializer.cs +++ b/YamlDotNet/Serialization/NodeDeserializers/ArrayNodeDeserializer.cs @@ -44,24 +44,53 @@ bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, IList result, bool canUpdate) + internal static void DeserializeHelper(Type tItem, IParser parser, Func nestedObjectDeserializer, IList result, bool canUpdate, CollectionPopulatingStrategy populatingStrategy = CollectionPopulatingStrategy.CreateNew) { parser.Consume(); + var i = 0; while (!parser.TryConsume(out var _)) { var current = parser.Current; - var value = nestedObjectDeserializer(parser, tItem, null); + var currentValue = populatingStrategy == CollectionPopulatingStrategy.PopulateOrAddItems ? (result.Count > i ? result[i] : null) : null; + var isPopulating = currentValue != null; + + var value = nestedObjectDeserializer(parser, tItem, currentValue); if (value is IValuePromise promise) { if (canUpdate) { - var index = result.Add(tItem.IsValueType() ? Activator.CreateInstance(tItem) : null); - promise.ValueAvailable += v => result[index] = TypeConverter.ChangeType(v, tItem); + if (isPopulating == false) + { + var index = result.Add(tItem.IsValueType() ? Activator.CreateInstance(tItem) : null); + promise.ValueAvailable += v => result[index] = TypeConverter.ChangeType(v, tItem); + } } else { @@ -112,8 +119,13 @@ internal static void DeserializeHelper(Type tItem, IParser parser, Func will be thrown. /// If the array does not exist yet, an instance will be created and populated. /// - FillExisting + FillExisting, + PopulateItems, + PopulateItemsAllowGrowingArray } /// @@ -56,7 +58,12 @@ public enum CollectionPopulatingStrategy /// Add items to the pre-existing collection. /// If the collection does not exist yet, an instance will be created and populated. /// - AddItems + AddItems, + /// + /// Populate pre-existing items, create and add new items where there is no pre-existing target. + /// If the collection does not exist yet, an instance will be created and populated. + /// + PopulateOrAddItems } ///