Skip to content

Commit

Permalink
Handle a property invocation exception as an error message string
Browse files Browse the repository at this point in the history
  • Loading branch information
alexeykuptsov committed Apr 10, 2024
1 parent 7ae209b commit efc6b10
Show file tree
Hide file tree
Showing 8 changed files with 218 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// 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.IO;
using FluentAssertions;
using Xunit;
using YamlDotNet.Serialization;

namespace YamlDotNet.Test.Analyzers.StaticGenerator
{
public class HandleTargetInvocationExceptionTests
{
[Fact]
public void StaticSerializationHandlesTargetInvocationException()
{
var serializer = new StaticSerializerBuilder(new StaticContext())
.WithTargetInvocationExceptionsHandling()
.Build();
var writer = new StringWriter();
var obj = new ThrowingPropertyExample();

serializer.Serialize(writer, obj);
var serialized = writer.ToString();

serialized.Should()
.Be("Value: Exception of type System.InvalidOperationException was thrown\r\n".NormalizeNewLines());
}

[Fact]
public void StaticSerializationDoesntHandleTargetInvocationExceptionByDefault()
{
var serializer = new StaticSerializerBuilder(new StaticContext()).Build();
var writer = new StringWriter();
var obj = new ThrowingPropertyExample();

Assert.Throws<InvalidOperationException>(() => serializer.Serialize(writer, obj));
}
}

[YamlSerializable]
public class ThrowingPropertyExample
{
public string Value => throw new InvalidOperationException();
}
}
5 changes: 5 additions & 0 deletions YamlDotNet.Test/Serialization/SerializationTestHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,11 @@ public class DefaultsExample
public string Value { get; set; }
}

public class ThrowingPropertyExample
{
public string Value => throw new InvalidOperationException();
}

public class CustomGenericDictionary : IDictionary<string, string>
{
private readonly Dictionary<string, string> dictionary = new Dictionary<string, string>();
Expand Down
24 changes: 24 additions & 0 deletions YamlDotNet.Test/Serialization/SerializationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
using YamlDotNet.Serialization.Callbacks;
using YamlDotNet.Serialization.NamingConventions;
using YamlDotNet.Serialization.ObjectFactories;
using YamlDotNet.Test.Analyzers.StaticGenerator;

namespace YamlDotNet.Test.Serialization
{
Expand Down Expand Up @@ -1117,6 +1118,29 @@ public void SerializationEmitsPropertyWhenValueDifferFromDefaultValueAttribute()
serialized.Should().Contain("Value");
}

[Fact]
public void SerializationHandlesTargetInvocationException()
{
var serializer = new SerializerBuilder().WithTargetInvocationExceptionsHandling().Build();
var writer = new StringWriter();
var obj = new ThrowingPropertyExample();

serializer.Serialize(writer, obj);
var serialized = writer.ToString();

serialized.Should()
.Be("Value: Exception of type System.InvalidOperationException was thrown\r\n".NormalizeNewLines());
}

[Fact]
public void SerializationDoesntHandleTargetInvocationExceptionByDefault()
{
var writer = new StringWriter();
var obj = new ThrowingPropertyExample();

Assert.Throws<TargetInvocationException>(() => Serializer.Serialize(writer, obj));
}

[Fact]
public void SerializingAGenericDictionaryShouldNotThrowTargetException()
{
Expand Down
22 changes: 21 additions & 1 deletion YamlDotNet/Serialization/PropertyDescriptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ public bool CanWrite
get { return baseDescriptor.CanWrite; }
}

public bool HandleTargetInvocationExceptions
{
get;
set;
}

public void Write(object target, object? value)
{
baseDescriptor.Write(target, value);
Expand All @@ -69,7 +75,21 @@ public void Write(object target, object? value)

public IObjectDescriptor Read(object target)
{
return baseDescriptor.Read(target);
if (!HandleTargetInvocationExceptions)
{
return baseDescriptor.Read(target);
}

try
{
return baseDescriptor.Read(target);
}
catch (Exception e)
{
return new ObjectDescriptor(
$"Exception of type {e.GetType().FullName} was thrown",
typeof(string), typeof(string), ScalarStyle.Any);
}
}
}
}
12 changes: 11 additions & 1 deletion YamlDotNet/Serialization/SerializerBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public sealed class SerializerBuilder : BuilderSkeleton<SerializerBuilder>
private ScalarStyle defaultScalarStyle = ScalarStyle.Any;
private bool quoteNecessaryStrings;
private bool quoteYaml1_1Strings;
private bool handleTargetInvocationExceptions;

public SerializerBuilder()
: base(new DynamicTypeResolver())
Expand Down Expand Up @@ -129,6 +130,15 @@ public SerializerBuilder WithQuotingNecessaryStrings(bool quoteYaml1_1Strings =
return this;
}

/// <summary>
/// Enables handling TargetInvocationExceptions thrown by a property so that information about exception is serialized as string value of the property.
/// </summary>
public SerializerBuilder WithTargetInvocationExceptionsHandling()
{
handleTargetInvocationExceptions = true;
return this;
}

/// <summary>
/// Sets the default quoting style for scalar values. The default value is <see cref="ScalarStyle.Any"/>
/// </summary>
Expand Down Expand Up @@ -695,7 +705,7 @@ public IValueSerializer BuildValueSerializer()

internal ITypeInspector BuildTypeInspector()
{
ITypeInspector innerInspector = new ReadablePropertiesTypeInspector(typeResolver, includeNonPublicProperties);
ITypeInspector innerInspector = new ReadablePropertiesTypeInspector(typeResolver, includeNonPublicProperties, handleTargetInvocationExceptions);

if (!ignoreFields)
{
Expand Down
11 changes: 11 additions & 0 deletions YamlDotNet/Serialization/StaticSerializerBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,17 @@ public StaticSerializerBuilder WithQuotingNecessaryStrings(bool quoteYaml1_1Stri
return this;
}

/// <summary>
/// Enables handling TargetInvocationExceptions thrown by a property so that information about exception is serialized as string value of the property.
/// </summary>
public StaticSerializerBuilder WithTargetInvocationExceptionsHandling()
{
typeInspectorFactories.Add(
typeof(HandleTargetInvocationExceptionsTypeInspector),
inner => new HandleTargetInvocationExceptionsTypeInspector(inner));
return this;
}

/// <summary>
/// Put double quotes around strings that need it, for example Null, True, False, a number. This should be called before any other "With" methods if you want this feature enabled.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// 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 System.Linq;

namespace YamlDotNet.Serialization.TypeInspectors
{
/// <summary>
/// Sets HandleTargetInvocationExceptions to true for all property descriptors.
/// </summary>
public sealed class HandleTargetInvocationExceptionsTypeInspector : TypeInspectorSkeleton
{
private readonly ITypeInspector innerTypeDescriptor;

public HandleTargetInvocationExceptionsTypeInspector(ITypeInspector innerTypeDescriptor)
{
this.innerTypeDescriptor = innerTypeDescriptor;
}

public override IEnumerable<IPropertyDescriptor> GetProperties(Type type, object? container)
{
return innerTypeDescriptor.GetProperties(type, container)
.Select(p =>
{
var descriptor = new PropertyDescriptor(p) { HandleTargetInvocationExceptions = true };
return (IPropertyDescriptor)descriptor;
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,21 @@ public sealed class ReadablePropertiesTypeInspector : TypeInspectorSkeleton
{
private readonly ITypeResolver typeResolver;
private readonly bool includeNonPublicProperties;
private readonly bool handleTargetInvocationExceptions;

public ReadablePropertiesTypeInspector(ITypeResolver typeResolver)
: this(typeResolver, false)
{
}

public ReadablePropertiesTypeInspector(ITypeResolver typeResolver, bool includeNonPublicProperties)
public ReadablePropertiesTypeInspector(
ITypeResolver typeResolver,
bool includeNonPublicProperties,
bool handleTargetInvocationExceptions = false)
{
this.typeResolver = typeResolver ?? throw new ArgumentNullException(nameof(typeResolver));
this.includeNonPublicProperties = includeNonPublicProperties;
this.handleTargetInvocationExceptions = handleTargetInvocationExceptions;
}

private static bool IsValidProperty(PropertyInfo property)
Expand All @@ -57,18 +62,23 @@ public override IEnumerable<IPropertyDescriptor> GetProperties(Type type, object
return type
.GetProperties(includeNonPublicProperties)
.Where(IsValidProperty)
.Select(p => (IPropertyDescriptor)new ReflectionPropertyDescriptor(p, typeResolver));
.Select(p => (IPropertyDescriptor)new ReflectionPropertyDescriptor(p, typeResolver, handleTargetInvocationExceptions));
}

private sealed class ReflectionPropertyDescriptor : IPropertyDescriptor
{
private readonly PropertyInfo propertyInfo;
private readonly ITypeResolver typeResolver;
private readonly bool handleTargetInvocationExceptions;

public ReflectionPropertyDescriptor(PropertyInfo propertyInfo, ITypeResolver typeResolver)
public ReflectionPropertyDescriptor(
PropertyInfo propertyInfo,
ITypeResolver typeResolver,
bool handleTargetInvocationExceptions)
{
this.propertyInfo = propertyInfo ?? throw new ArgumentNullException(nameof(propertyInfo));
this.typeResolver = typeResolver ?? throw new ArgumentNullException(nameof(typeResolver));
this.handleTargetInvocationExceptions = handleTargetInvocationExceptions;
ScalarStyle = ScalarStyle.Any;
}

Expand All @@ -92,7 +102,25 @@ public void Write(object target, object? value)

public IObjectDescriptor Read(object target)
{
var propertyValue = propertyInfo.ReadValue(target);
object? propertyValue;
if (handleTargetInvocationExceptions)
{
try
{
propertyValue = propertyInfo.ReadValue(target);
}
catch (TargetInvocationException e)
{
return new ObjectDescriptor(
$"Exception of type {e.InnerException!.GetType().FullName} was thrown",
typeof(string), typeof(string), ScalarStyle.Any);
}
}
else
{
propertyValue = propertyInfo.ReadValue(target);
}

var actualType = TypeOverride ?? typeResolver.Resolve(Type, propertyValue);
return new ObjectDescriptor(propertyValue, actualType, Type, ScalarStyle);
}
Expand Down

0 comments on commit efc6b10

Please sign in to comment.