From abe677b421f1bdc706c361c26c3b2e0c2d0ea1f1 Mon Sep 17 00:00:00 2001 From: Ivan Date: Mon, 5 Apr 2021 18:34:28 -0400 Subject: [PATCH 1/2] Added another option to deserialize interface types by specifying their concrete types using the builder.AddTypeMapping() api. --- YamlDotNet/Serialization/BuilderSkeleton.cs | 3 + .../Serialization/DeserializerBuilder.cs | 28 +++++++++ .../MappingNodeTypeResolver.cs | 58 +++++++++++++++++++ .../ObjectFactories/DefaultObjectFactory.cs | 17 ++++++ YamlDotNet/Serialization/SerializerBuilder.cs | 5 ++ 5 files changed, 111 insertions(+) create mode 100644 YamlDotNet/Serialization/NodeTypeResolvers/MappingNodeTypeResolver.cs diff --git a/YamlDotNet/Serialization/BuilderSkeleton.cs b/YamlDotNet/Serialization/BuilderSkeleton.cs index 137806af4..394c8f369 100755 --- a/YamlDotNet/Serialization/BuilderSkeleton.cs +++ b/YamlDotNet/Serialization/BuilderSkeleton.cs @@ -111,6 +111,9 @@ public TBuilder WithTypeResolver(ITypeResolver typeResolver) public abstract TBuilder WithTagMapping(TagName tag, Type type); + public abstract TBuilder WithTypeMapping() + where TConcrete : TInterface; + #if !NET20 /// /// Register an for a given property. diff --git a/YamlDotNet/Serialization/DeserializerBuilder.cs b/YamlDotNet/Serialization/DeserializerBuilder.cs index 82797263f..e032f21e1 100755 --- a/YamlDotNet/Serialization/DeserializerBuilder.cs +++ b/YamlDotNet/Serialization/DeserializerBuilder.cs @@ -45,6 +45,7 @@ public sealed class DeserializerBuilder : BuilderSkeleton private readonly LazyComponentRegistrationList nodeDeserializerFactories; private readonly LazyComponentRegistrationList nodeTypeResolverFactories; private readonly Dictionary tagMappings; + private readonly Dictionary typeMappings = new Dictionary(); private bool ignoreUnmatched; /// @@ -85,6 +86,7 @@ public DeserializerBuilder() nodeTypeResolverFactories = new LazyComponentRegistrationList { + { typeof(MappingNodeTypeResolver), _ => new MappingNodeTypeResolver(typeMappings) }, { typeof(YamlConvertibleTypeResolver), _ => new YamlConvertibleTypeResolver() }, { typeof(YamlSerializableTypeResolver), _ => new YamlSerializableTypeResolver() }, { typeof(TagNodeTypeResolver), _ => new TagNodeTypeResolver(tagMappings) }, @@ -301,6 +303,31 @@ public override DeserializerBuilder WithTagMapping(TagName tag, Type type) return this; } + /// + /// Registers a type mapping. + /// + public override DeserializerBuilder WithTypeMapping() + { + var interfaceType = typeof(TInterface); + var concreteType = typeof(TConcrete); + + if (!interfaceType.IsAssignableFrom(concreteType)) + { + throw new InvalidOperationException($"The type '{concreteType.Name}' does not implement interface '{interfaceType.Name}'."); + } + + if (typeMappings.ContainsKey(interfaceType)) + { + typeMappings[interfaceType] = concreteType; + } + else + { + typeMappings.Add(interfaceType, concreteType); + } + + return this; + } + /// /// Unregisters an existing tag mapping. /// @@ -332,6 +359,7 @@ public DeserializerBuilder IgnoreUnmatchedProperties() /// public IDeserializer Build() { + objectFactory = new DefaultObjectFactory(typeMappings); return Deserializer.FromValueDeserializer(BuildValueDeserializer()); } diff --git a/YamlDotNet/Serialization/NodeTypeResolvers/MappingNodeTypeResolver.cs b/YamlDotNet/Serialization/NodeTypeResolvers/MappingNodeTypeResolver.cs new file mode 100644 index 000000000..17960e872 --- /dev/null +++ b/YamlDotNet/Serialization/NodeTypeResolvers/MappingNodeTypeResolver.cs @@ -0,0 +1,58 @@ +// 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.Generic; +using YamlDotNet.Core.Events; + +namespace YamlDotNet.Serialization.NodeTypeResolvers +{ + public class MappingNodeTypeResolver : INodeTypeResolver + { + private readonly IDictionary _mappings; + + public MappingNodeTypeResolver(IDictionary mappings) + { + if (mappings == null) throw new ArgumentNullException(nameof(mappings)); + + foreach (var pair in mappings) + { + if (!pair.Key.IsAssignableFrom(pair.Value)) + { + throw new InvalidOperationException($"Type '{pair.Value}' does not implement type '{pair.Key}'."); + } + } + + _mappings = mappings; + } + + public bool Resolve(NodeEvent? nodeEvent, ref Type currentType) + { + if (_mappings.TryGetValue(currentType, out var concreteType)) + { + currentType = concreteType; + return true; + } + + return false; + } + } +} diff --git a/YamlDotNet/Serialization/ObjectFactories/DefaultObjectFactory.cs b/YamlDotNet/Serialization/ObjectFactories/DefaultObjectFactory.cs index db24b0163..a9d89b12b 100644 --- a/YamlDotNet/Serialization/ObjectFactories/DefaultObjectFactory.cs +++ b/YamlDotNet/Serialization/ObjectFactories/DefaultObjectFactory.cs @@ -46,6 +46,23 @@ public sealed class DefaultObjectFactory : IObjectFactory { typeof(IDictionary), typeof(Dictionary) } }; + public DefaultObjectFactory() + { + } + + public DefaultObjectFactory(IDictionary mappings) + { + foreach (var pair in mappings) + { + if (!pair.Key.IsAssignableFrom(pair.Value)) + { + throw new InvalidOperationException($"Type '{pair.Value}' does not implement type '{pair.Key}'."); + } + + DefaultNonGenericInterfaceImplementations.Add(pair.Key, pair.Value); + } + } + public object Create(Type type) { if (type.IsInterface()) diff --git a/YamlDotNet/Serialization/SerializerBuilder.cs b/YamlDotNet/Serialization/SerializerBuilder.cs index 68905b08e..7af485a73 100755 --- a/YamlDotNet/Serialization/SerializerBuilder.cs +++ b/YamlDotNet/Serialization/SerializerBuilder.cs @@ -543,6 +543,11 @@ public IValueSerializer BuildValueSerializer() ); } + public override SerializerBuilder WithTypeMapping() + { + throw new NotImplementedException("This is only used by the deserializer"); + } + private class ValueSerializer : IValueSerializer { private readonly IObjectGraphTraversalStrategy traversalStrategy; From 3219bbb8d7c7c9f6da60dd9a6dae378a3199244e Mon Sep 17 00:00:00 2001 From: Ivan Date: Tue, 6 Apr 2021 01:11:27 -0400 Subject: [PATCH 2/2] Removed abstract method AddTypeMapping from BuilderSkeleton as this only applies to DeserializerBuilder. Updated dictionaries to instance types as the static types caused concurrent unit tests to fail. Updated DefaultObjectFactory to use lazy loading to allow constructor initialization with type mappings. --- .../Serialization/DeserializerTest.cs | 131 ++++++++++++++++++ .../net20+net35+unitysubset3.5/Lazy.cs | 7 + YamlDotNet/Serialization/BuilderSkeleton.cs | 3 - .../Serialization/DeserializerBuilder.cs | 30 ++-- .../ObjectFactories/DefaultObjectFactory.cs | 6 +- YamlDotNet/Serialization/SerializerBuilder.cs | 5 - 6 files changed, 160 insertions(+), 22 deletions(-) create mode 100644 YamlDotNet.Test/Serialization/DeserializerTest.cs diff --git a/YamlDotNet.Test/Serialization/DeserializerTest.cs b/YamlDotNet.Test/Serialization/DeserializerTest.cs new file mode 100644 index 000000000..0657e188d --- /dev/null +++ b/YamlDotNet.Test/Serialization/DeserializerTest.cs @@ -0,0 +1,131 @@ +// 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.Generic; +using FluentAssertions; +using Xunit; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace YamlDotNet.Test.Serialization +{ + public class DeserializerTest + { + [Fact] + public void Deserialize_YamlWithInterfaceTypeAndMapping_ReturnsModel() + { + var yaml = @" +name: Jack +cars: +- name: Mercedes + year: 2018 +- name: Honda + year: 2021 +"; + + var sut = new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .WithTypeMapping() + .Build(); + + var person = sut.Deserialize(yaml); + person.Name.Should().Be("Jack"); + person.Cars.Should().HaveCount(2); + person.Cars[0].Name.Should().Be("Mercedes"); + person.Cars[0].Spec.Should().BeNull(); + person.Cars[1].Name.Should().Be("Honda"); + person.Cars[1].Spec.Should().BeNull(); + } + + [Fact] + public void Deserialize_YamlWithTwoInterfaceTypesAndMappings_ReturnsModel() + { + var yaml = @" +name: Jack +cars: +- name: Mercedes + year: 2018 + spec: + engineType: V6 + driveType: AWD +- name: Honda + year: 2021 + spec: + engineType: V4 + driveType: FWD +"; + + var sut = new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .WithTypeMapping() + .WithTypeMapping() + .Build(); + + var person = sut.Deserialize(yaml); + person.Name.Should().Be("Jack"); + person.Cars.Should().HaveCount(2); + person.Cars[0].Name.Should().Be("Mercedes"); + person.Cars[0].Spec.EngineType.Should().Be("V6"); + person.Cars[0].Spec.DriveType.Should().Be("AWD"); + person.Cars[1].Name.Should().Be("Honda"); + person.Cars[1].Spec.EngineType.Should().Be("V4"); + person.Cars[1].Spec.DriveType.Should().Be("FWD"); + } + + public class Person + { + public string Name { get; private set; } + + public IList Cars { get; private set; } + } + + public class Car : ICar + { + public string Name { get; private set; } + + public int Year { get; private set; } + + public IModelSpec Spec { get; private set; } + } + + public interface ICar + { + string Name { get; } + + int Year { get; } + IModelSpec Spec { get; } + } + + public class ModelSpec : IModelSpec + { + public string EngineType { get; private set; } + + public string DriveType { get; private set; } + } + + public interface IModelSpec + { + string EngineType { get; } + + string DriveType { get; } + } + } +} diff --git a/YamlDotNet/Helpers/Portability/net20+net35+unitysubset3.5/Lazy.cs b/YamlDotNet/Helpers/Portability/net20+net35+unitysubset3.5/Lazy.cs index 632fd0c4c..990a8c7ca 100644 --- a/YamlDotNet/Helpers/Portability/net20+net35+unitysubset3.5/Lazy.cs +++ b/YamlDotNet/Helpers/Portability/net20+net35+unitysubset3.5/Lazy.cs @@ -41,6 +41,13 @@ public Lazy(T value) valueState = ValueState.Created; } + public Lazy(Func valueFactory) + { + this.valueFactory = valueFactory; + this.isThreadSafe = false; + valueState = ValueState.NotCreated; + } + public Lazy(Func valueFactory, bool isThreadSafe) { this.valueFactory = valueFactory; diff --git a/YamlDotNet/Serialization/BuilderSkeleton.cs b/YamlDotNet/Serialization/BuilderSkeleton.cs index 394c8f369..137806af4 100755 --- a/YamlDotNet/Serialization/BuilderSkeleton.cs +++ b/YamlDotNet/Serialization/BuilderSkeleton.cs @@ -111,9 +111,6 @@ public TBuilder WithTypeResolver(ITypeResolver typeResolver) public abstract TBuilder WithTagMapping(TagName tag, Type type); - public abstract TBuilder WithTypeMapping() - where TConcrete : TInterface; - #if !NET20 /// /// Register an for a given property. diff --git a/YamlDotNet/Serialization/DeserializerBuilder.cs b/YamlDotNet/Serialization/DeserializerBuilder.cs index e032f21e1..3ad041de6 100755 --- a/YamlDotNet/Serialization/DeserializerBuilder.cs +++ b/YamlDotNet/Serialization/DeserializerBuilder.cs @@ -41,11 +41,11 @@ namespace YamlDotNet.Serialization /// public sealed class DeserializerBuilder : BuilderSkeleton { - private IObjectFactory objectFactory = new DefaultObjectFactory(); + private Lazy objectFactory; private readonly LazyComponentRegistrationList nodeDeserializerFactories; private readonly LazyComponentRegistrationList nodeTypeResolverFactories; private readonly Dictionary tagMappings; - private readonly Dictionary typeMappings = new Dictionary(); + private readonly Dictionary typeMappings; private bool ignoreUnmatched; /// @@ -54,6 +54,9 @@ public sealed class DeserializerBuilder : BuilderSkeleton public DeserializerBuilder() : base(new StaticTypeResolver()) { + typeMappings = new Dictionary(); + objectFactory = new Lazy(() => new DefaultObjectFactory(typeMappings), true); + tagMappings = new Dictionary { { FailsafeSchema.Tags.Map, typeof(Dictionary) }, @@ -72,16 +75,16 @@ public DeserializerBuilder() nodeDeserializerFactories = new LazyComponentRegistrationList { - { typeof(YamlConvertibleNodeDeserializer), _ => new YamlConvertibleNodeDeserializer(objectFactory) }, - { typeof(YamlSerializableNodeDeserializer), _ => new YamlSerializableNodeDeserializer(objectFactory) }, + { typeof(YamlConvertibleNodeDeserializer), _ => new YamlConvertibleNodeDeserializer(objectFactory.Value) }, + { typeof(YamlSerializableNodeDeserializer), _ => new YamlSerializableNodeDeserializer(objectFactory.Value) }, { typeof(TypeConverterNodeDeserializer), _ => new TypeConverterNodeDeserializer(BuildTypeConverters()) }, { typeof(NullNodeDeserializer), _ => new NullNodeDeserializer() }, { typeof(ScalarNodeDeserializer), _ => new ScalarNodeDeserializer() }, { typeof(ArrayNodeDeserializer), _ => new ArrayNodeDeserializer() }, - { typeof(DictionaryNodeDeserializer), _ => new DictionaryNodeDeserializer(objectFactory) }, - { typeof(CollectionNodeDeserializer), _ => new CollectionNodeDeserializer(objectFactory) }, + { typeof(DictionaryNodeDeserializer), _ => new DictionaryNodeDeserializer(objectFactory.Value) }, + { typeof(CollectionNodeDeserializer), _ => new CollectionNodeDeserializer(objectFactory.Value) }, { typeof(EnumerableNodeDeserializer), _ => new EnumerableNodeDeserializer() }, - { typeof(ObjectNodeDeserializer), _ => new ObjectNodeDeserializer(objectFactory, BuildTypeInspector(), ignoreUnmatched) } + { typeof(ObjectNodeDeserializer), _ => new ObjectNodeDeserializer(objectFactory.Value, BuildTypeInspector(), ignoreUnmatched) } }; nodeTypeResolverFactories = new LazyComponentRegistrationList @@ -102,7 +105,12 @@ public DeserializerBuilder() /// public DeserializerBuilder WithObjectFactory(IObjectFactory objectFactory) { - this.objectFactory = objectFactory ?? throw new ArgumentNullException(nameof(objectFactory)); + if (objectFactory == null) + { + throw new ArgumentNullException(nameof(objectFactory)); + } + + this.objectFactory = new Lazy(() => objectFactory, true); return this; } @@ -304,9 +312,10 @@ public override DeserializerBuilder WithTagMapping(TagName tag, Type type) } /// - /// Registers a type mapping. + /// Registers a type mapping using the default object factory. /// - public override DeserializerBuilder WithTypeMapping() + public DeserializerBuilder WithTypeMapping() + where TConcrete : TInterface { var interfaceType = typeof(TInterface); var concreteType = typeof(TConcrete); @@ -359,7 +368,6 @@ public DeserializerBuilder IgnoreUnmatchedProperties() /// public IDeserializer Build() { - objectFactory = new DefaultObjectFactory(typeMappings); return Deserializer.FromValueDeserializer(BuildValueDeserializer()); } diff --git a/YamlDotNet/Serialization/ObjectFactories/DefaultObjectFactory.cs b/YamlDotNet/Serialization/ObjectFactories/DefaultObjectFactory.cs index a9d89b12b..097fd4265 100644 --- a/YamlDotNet/Serialization/ObjectFactories/DefaultObjectFactory.cs +++ b/YamlDotNet/Serialization/ObjectFactories/DefaultObjectFactory.cs @@ -30,7 +30,7 @@ namespace YamlDotNet.Serialization.ObjectFactories /// public sealed class DefaultObjectFactory : IObjectFactory { - private static readonly Dictionary DefaultGenericInterfaceImplementations = new Dictionary + private readonly Dictionary DefaultGenericInterfaceImplementations = new Dictionary { { typeof(IEnumerable<>), typeof(List<>) }, { typeof(ICollection<>), typeof(List<>) }, @@ -38,7 +38,7 @@ public sealed class DefaultObjectFactory : IObjectFactory { typeof(IDictionary<,>), typeof(Dictionary<,>) } }; - private static readonly Dictionary DefaultNonGenericInterfaceImplementations = new Dictionary + private readonly Dictionary DefaultNonGenericInterfaceImplementations = new Dictionary { { typeof(IEnumerable), typeof(List) }, { typeof(ICollection), typeof(List) }, @@ -54,7 +54,7 @@ public DefaultObjectFactory(IDictionary mappings) { foreach (var pair in mappings) { - if (!pair.Key.IsAssignableFrom(pair.Value)) + if (!pair.Key.IsAssignableFrom(pair.Value)) { throw new InvalidOperationException($"Type '{pair.Value}' does not implement type '{pair.Key}'."); } diff --git a/YamlDotNet/Serialization/SerializerBuilder.cs b/YamlDotNet/Serialization/SerializerBuilder.cs index 7af485a73..68905b08e 100755 --- a/YamlDotNet/Serialization/SerializerBuilder.cs +++ b/YamlDotNet/Serialization/SerializerBuilder.cs @@ -543,11 +543,6 @@ public IValueSerializer BuildValueSerializer() ); } - public override SerializerBuilder WithTypeMapping() - { - throw new NotImplementedException("This is only used by the deserializer"); - } - private class ValueSerializer : IValueSerializer { private readonly IObjectGraphTraversalStrategy traversalStrategy;