From a148ad3ba0d6015815ad680aa11c88b122fe4f30 Mon Sep 17 00:00:00 2001 From: Thomas Willson Date: Thu, 23 Apr 2020 16:41:16 -0700 Subject: [PATCH] Add support for private properties. Extend the `ReadablePropertiesTypeInspector` to handle non-public properties. While it is possible for the user to provide custom type inspectors, it's not easy for these to surface additional properties. An alternative approach to the work here would be to make `ReflectionPropertyDescriptor` protected and unseal `ReadablePropertiesTypeInspector`. The `BuilderSkeleton` would also need to allow the root `ITypeInspector`(s) to be customized. --- .../Serialization/SerializationTestHelper.cs | 28 +++++++ .../Serialization/SerializationTests.cs | 74 +++++++++++++++++++ YamlDotNet/Helpers/Portability.cs | 40 +++++++--- .../RepresentationModel/YamlMappingNode.cs | 2 +- YamlDotNet/Serialization/BuilderSkeleton.cs | 12 ++- .../ReadablePropertiesTypeInspector.cs | 11 ++- 6 files changed, 152 insertions(+), 15 deletions(-) diff --git a/YamlDotNet.Test/Serialization/SerializationTestHelper.cs b/YamlDotNet.Test/Serialization/SerializationTestHelper.cs index 8b29d33a8..1be0e4767 100644 --- a/YamlDotNet.Test/Serialization/SerializationTestHelper.cs +++ b/YamlDotNet.Test/Serialization/SerializationTestHelper.cs @@ -539,4 +539,32 @@ public class NameConvention [YamlIgnore] public string fourthTest { get; set; } } + + public class NonPublicPropertiesExample + { + public string Public { get; set; } = "public"; + + internal string Internal { get; set; } = "internal"; + + protected string Protected { get; set; } = "protected"; + + private string Private { get; set; } = "private"; + + /// + public override string ToString() => $"{Public},{Internal},{Protected},{Private}"; + } + + public class NonPublicFieldsExample + { + public string Public = "public"; + + internal string Internal = "internal"; + + protected string Protected = "protected"; + + private string Private = "private"; + + /// + public override string ToString() => $"{Public},{Internal},{Protected},{Private}"; + } } \ No newline at end of file diff --git a/YamlDotNet.Test/Serialization/SerializationTests.cs b/YamlDotNet.Test/Serialization/SerializationTests.cs index 31325b2f3..d11c7c2e4 100644 --- a/YamlDotNet.Test/Serialization/SerializationTests.cs +++ b/YamlDotNet.Test/Serialization/SerializationTests.cs @@ -1764,6 +1764,80 @@ public void AnchorWithAllowedCharactersCanBeDeserialized() Assert.Equal("some value", deserialized["interpolated value"]); } + [Fact] + public void SerializationNonPublicPropertiesAreIgnored() + { + var sut = new SerializerBuilder().Build(); + var yaml = sut.Serialize(new NonPublicPropertiesExample()); + Assert.Equal("Public: public", yaml.TrimNewLines()); + } + + [Fact] + public void SerializationNonPublicPropertiesAreIncluded() + { + var sut = new SerializerBuilder().IncludeNonPublicProperties().Build(); + var yaml = sut.Serialize(new NonPublicPropertiesExample()); + + var expected = Yaml.Text(@" + Public: public + Internal: internal + Protected: protected + Private: private + "); + + Assert.Equal(expected.NormalizeNewLines(), yaml.NormalizeNewLines().TrimNewLines()); + } + + [Fact] + public void DeserializationNonPublicPropertiesAreIgnored() + { + var sut = new DeserializerBuilder().IgnoreUnmatchedProperties().Build(); + var deserialized = sut.Deserialize(Yaml.ReaderForText(@" + Public: public2 + Internal: internal2 + Protected: protected2 + Private: private2 + ")); + + Assert.Equal("public2,internal,protected,private", deserialized.ToString()); + } + + [Fact] + public void DeserializationNonPublicPropertiesAreIncluded() + { + var sut = new DeserializerBuilder().IncludeNonPublicProperties().Build(); + var deserialized = sut.Deserialize(Yaml.ReaderForText(@" + Public: public2 + Internal: internal2 + Protected: protected2 + Private: private2 + ")); + + Assert.Equal("public2,internal2,protected2,private2", deserialized.ToString()); + } + + [Fact] + public void SerializationNonPublicFieldsAreIgnored() + { + var sut = new SerializerBuilder().Build(); + var yaml = sut.Serialize(new NonPublicFieldsExample()); + Assert.Equal("Public: public", yaml.TrimNewLines()); + } + + [Fact] + public void DeserializationNonPublicFieldsAreIgnored() + { + var sut = new DeserializerBuilder().IgnoreUnmatchedProperties().Build(); + var deserialized = sut.Deserialize(Yaml.ReaderForText(@" + Public: public2 + Internal: internal2 + Protected: protected2 + Private: private2 + ")); + + Assert.Equal("public2,internal,protected,private", deserialized.ToString()); + } + [TypeConverter(typeof(DoublyConvertedTypeConverter))] public class DoublyConverted { diff --git a/YamlDotNet/Helpers/Portability.cs b/YamlDotNet/Helpers/Portability.cs index 329cf74cc..bdedccb85 100644 --- a/YamlDotNet/Helpers/Portability.cs +++ b/YamlDotNet/Helpers/Portability.cs @@ -182,15 +182,22 @@ public static Type[] GetGenericArguments(this Type type) return type.GetRuntimeProperty(name); } - public static IEnumerable GetPublicProperties(this Type type) + public static IEnumerable GetProperties(this Type type, bool includeNonPublic) { - var instancePublic = new Func( - p => !p.GetMethod.IsStatic && p.GetMethod.IsPublic); + var instancePublic + = new Func( + p => !p.GetMethod.IsStatic + && (p.GetMethod.IsPublic || includeNonPublic)); return type.IsInterface() - ? (new Type[] { type }) - .Concat(type.GetInterfaces()) - .SelectMany(i => i.GetRuntimeProperties().Where(instancePublic)) - : type.GetRuntimeProperties().Where(instancePublic); + ? (new Type[] { type }) + .Concat(type.GetInterfaces()) + .SelectMany(i => i.GetRuntimeProperties().Where(instancePublic)) + : type.GetRuntimeProperties().Where(instancePublic); + } + + public static IEnumerable GetPublicProperties(this Type type) + { + return GetProperties(type, false); } public static IEnumerable GetPublicFields(this Type type) @@ -225,7 +232,7 @@ public static IEnumerable GetPublicStaticMethods(this Type type) .FirstOrDefault(m => m.IsPublic && !m.IsStatic && m.Name.Equals(name)); } - public static MethodInfo GetGetMethod(this PropertyInfo property) + public static MethodInfo GetGetMethod(this PropertyInfo property, bool _) { return property.GetMethod; } @@ -324,12 +331,23 @@ public static TypeCode GetTypeCode(this Type type) public static IEnumerable GetPublicProperties(this Type type) { - var instancePublic = BindingFlags.Instance | BindingFlags.Public; + return GetProperties(type, false); + } + + public static IEnumerable GetProperties(this Type type, bool includeNonPublic) + { + var bindingFlags = BindingFlags.Instance | BindingFlags.Public; + + if (includeNonPublic) + { + bindingFlags |= BindingFlags.NonPublic; + } + return type.IsInterface ? (new Type[] { type }) .Concat(type.GetInterfaces()) - .SelectMany(i => i.GetProperties(instancePublic)) - : type.GetProperties(instancePublic); + .SelectMany(i => i.GetProperties(bindingFlags)) + : type.GetProperties(bindingFlags); } public static IEnumerable GetPublicFields(this Type type) diff --git a/YamlDotNet/RepresentationModel/YamlMappingNode.cs b/YamlDotNet/RepresentationModel/YamlMappingNode.cs index b686e2114..443792f0d 100644 --- a/YamlDotNet/RepresentationModel/YamlMappingNode.cs +++ b/YamlDotNet/RepresentationModel/YamlMappingNode.cs @@ -412,7 +412,7 @@ public static YamlMappingNode FromObject(object mapping) foreach (var property in mapping.GetType().GetPublicProperties()) { // CanRead == true => GetGetMethod() != null - if (property.CanRead && property.GetGetMethod()!.GetParameters().Length == 0) + if (property.CanRead && property.GetGetMethod(false)!.GetParameters().Length == 0) { var value = property.GetValue(mapping, null); if (!(value is YamlNode valueNode)) diff --git a/YamlDotNet/Serialization/BuilderSkeleton.cs b/YamlDotNet/Serialization/BuilderSkeleton.cs index 2ff24a361..04992cd3a 100755 --- a/YamlDotNet/Serialization/BuilderSkeleton.cs +++ b/YamlDotNet/Serialization/BuilderSkeleton.cs @@ -40,6 +40,7 @@ public abstract class BuilderSkeleton internal readonly LazyComponentRegistrationList typeConverterFactories; internal readonly LazyComponentRegistrationList typeInspectorFactories; private bool ignoreFields; + private bool includeNonPublicProperties = false; internal BuilderSkeleton(ITypeResolver typeResolver) { @@ -59,7 +60,7 @@ internal BuilderSkeleton(ITypeResolver typeResolver) internal ITypeInspector BuildTypeInspector() { - ITypeInspector innerInspector = new ReadablePropertiesTypeInspector(typeResolver); + ITypeInspector innerInspector = new ReadablePropertiesTypeInspector(typeResolver, includeNonPublicProperties); if (!ignoreFields) { innerInspector = new CompositeTypeInspector( @@ -81,6 +82,15 @@ public TBuilder IgnoreFields() return Self; } + /// + /// Allows serialization and deserialization of non-public properties. + /// + public TBuilder IncludeNonPublicProperties() + { + includeNonPublicProperties = true; + return Self; + } + /// /// Sets the that will be used by the (de)serializer. /// diff --git a/YamlDotNet/Serialization/TypeInspectors/ReadablePropertiesTypeInspector.cs b/YamlDotNet/Serialization/TypeInspectors/ReadablePropertiesTypeInspector.cs index 46932ab3c..6f193de94 100644 --- a/YamlDotNet/Serialization/TypeInspectors/ReadablePropertiesTypeInspector.cs +++ b/YamlDotNet/Serialization/TypeInspectors/ReadablePropertiesTypeInspector.cs @@ -33,22 +33,29 @@ namespace YamlDotNet.Serialization.TypeInspectors public sealed class ReadablePropertiesTypeInspector : TypeInspectorSkeleton { private readonly ITypeResolver typeResolver; + private readonly bool includeNonPublicProperties; public ReadablePropertiesTypeInspector(ITypeResolver typeResolver) + :this(typeResolver, false) + { + } + + public ReadablePropertiesTypeInspector(ITypeResolver typeResolver, bool includeNonPublicProperties) { this.typeResolver = typeResolver ?? throw new ArgumentNullException(nameof(typeResolver)); + this.includeNonPublicProperties = includeNonPublicProperties; } private static bool IsValidProperty(PropertyInfo property) { return property.CanRead - && property.GetGetMethod()!.GetParameters().Length == 0; + && property.GetGetMethod(true)!.GetParameters().Length == 0; } public override IEnumerable GetProperties(Type type, object? container) { return type - .GetPublicProperties() + .GetProperties(includeNonPublicProperties) .Where(IsValidProperty) .Select(p => (IPropertyDescriptor)new ReflectionPropertyDescriptor(p, typeResolver)); }