Skip to content

Commit

Permalink
Add support for private properties.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
willson556 committed Apr 24, 2020
1 parent 37964b2 commit a148ad3
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 15 deletions.
28 changes: 28 additions & 0 deletions YamlDotNet.Test/Serialization/SerializationTestHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/// <inheritdoc />
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";

/// <inheritdoc />
public override string ToString() => $"{Public},{Internal},{Protected},{Private}";
}
}
74 changes: 74 additions & 0 deletions YamlDotNet.Test/Serialization/SerializationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<NonPublicPropertiesExample>(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<NonPublicPropertiesExample>(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<NonPublicFieldsExample>(Yaml.ReaderForText(@"
Public: public2
Internal: internal2
Protected: protected2
Private: private2
"));

Assert.Equal("public2,internal,protected,private", deserialized.ToString());
}

[TypeConverter(typeof(DoublyConvertedTypeConverter))]
public class DoublyConverted
{
Expand Down
40 changes: 29 additions & 11 deletions YamlDotNet/Helpers/Portability.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,15 +182,22 @@ public static Type[] GetGenericArguments(this Type type)
return type.GetRuntimeProperty(name);
}

public static IEnumerable<PropertyInfo> GetPublicProperties(this Type type)
public static IEnumerable<PropertyInfo> GetProperties(this Type type, bool includeNonPublic)
{
var instancePublic = new Func<PropertyInfo, bool>(
p => !p.GetMethod.IsStatic && p.GetMethod.IsPublic);
var instancePublic
= new Func<PropertyInfo, bool>(
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<PropertyInfo> GetPublicProperties(this Type type)
{
return GetProperties(type, false);
}

public static IEnumerable<FieldInfo> GetPublicFields(this Type type)
Expand Down Expand Up @@ -225,7 +232,7 @@ public static IEnumerable<MethodInfo> 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;
}
Expand Down Expand Up @@ -324,12 +331,23 @@ public static TypeCode GetTypeCode(this Type type)

public static IEnumerable<PropertyInfo> GetPublicProperties(this Type type)
{
var instancePublic = BindingFlags.Instance | BindingFlags.Public;
return GetProperties(type, false);
}

public static IEnumerable<PropertyInfo> 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<FieldInfo> GetPublicFields(this Type type)
Expand Down
2 changes: 1 addition & 1 deletion YamlDotNet/RepresentationModel/YamlMappingNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
12 changes: 11 additions & 1 deletion YamlDotNet/Serialization/BuilderSkeleton.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public abstract class BuilderSkeleton<TBuilder>
internal readonly LazyComponentRegistrationList<Nothing, IYamlTypeConverter> typeConverterFactories;
internal readonly LazyComponentRegistrationList<ITypeInspector, ITypeInspector> typeInspectorFactories;
private bool ignoreFields;
private bool includeNonPublicProperties = false;

internal BuilderSkeleton(ITypeResolver typeResolver)
{
Expand All @@ -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(
Expand All @@ -81,6 +82,15 @@ public TBuilder IgnoreFields()
return Self;
}

/// <summary>
/// Allows serialization and deserialization of non-public properties.
/// </summary>
public TBuilder IncludeNonPublicProperties()
{
includeNonPublicProperties = true;
return Self;
}

/// <summary>
/// Sets the <see cref="INamingConvention" /> that will be used by the (de)serializer.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<IPropertyDescriptor> GetProperties(Type type, object? container)
{
return type
.GetPublicProperties()
.GetProperties(includeNonPublicProperties)
.Where(IsValidProperty)
.Select(p => (IPropertyDescriptor)new ReflectionPropertyDescriptor(p, typeResolver));
}
Expand Down

0 comments on commit a148ad3

Please sign in to comment.