diff --git a/YamlDotNet.Test/Analyzers/StaticGenerator/HandleTargetInvocationExceptionTests.cs b/YamlDotNet.Test/Analyzers/StaticGenerator/HandleTargetInvocationExceptionTests.cs new file mode 100644 index 000000000..84d5cb52a --- /dev/null +++ b/YamlDotNet.Test/Analyzers/StaticGenerator/HandleTargetInvocationExceptionTests.cs @@ -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(() => serializer.Serialize(writer, obj)); + } + } + + [YamlSerializable] + public class ThrowingPropertyExample + { + public string Value => throw new InvalidOperationException(); + } +} diff --git a/YamlDotNet.Test/Serialization/SerializationTestHelper.cs b/YamlDotNet.Test/Serialization/SerializationTestHelper.cs index 2ff42fea4..4f70fd372 100644 --- a/YamlDotNet.Test/Serialization/SerializationTestHelper.cs +++ b/YamlDotNet.Test/Serialization/SerializationTestHelper.cs @@ -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 { private readonly Dictionary dictionary = new Dictionary(); diff --git a/YamlDotNet.Test/Serialization/SerializationTests.cs b/YamlDotNet.Test/Serialization/SerializationTests.cs index f89f8c05c..fef5a0c90 100644 --- a/YamlDotNet.Test/Serialization/SerializationTests.cs +++ b/YamlDotNet.Test/Serialization/SerializationTests.cs @@ -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 { @@ -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(() => Serializer.Serialize(writer, obj)); + } + [Fact] public void SerializingAGenericDictionaryShouldNotThrowTargetException() { diff --git a/YamlDotNet/Serialization/PropertyDescriptor.cs b/YamlDotNet/Serialization/PropertyDescriptor.cs index 32b82cceb..d4aea6f50 100644 --- a/YamlDotNet/Serialization/PropertyDescriptor.cs +++ b/YamlDotNet/Serialization/PropertyDescriptor.cs @@ -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); @@ -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); + } } } } diff --git a/YamlDotNet/Serialization/SerializerBuilder.cs b/YamlDotNet/Serialization/SerializerBuilder.cs index 74b927f69..bd36752e6 100755 --- a/YamlDotNet/Serialization/SerializerBuilder.cs +++ b/YamlDotNet/Serialization/SerializerBuilder.cs @@ -62,6 +62,7 @@ public sealed class SerializerBuilder : BuilderSkeleton private ScalarStyle defaultScalarStyle = ScalarStyle.Any; private bool quoteNecessaryStrings; private bool quoteYaml1_1Strings; + private bool handleTargetInvocationExceptions; public SerializerBuilder() : base(new DynamicTypeResolver()) @@ -129,6 +130,15 @@ public SerializerBuilder WithQuotingNecessaryStrings(bool quoteYaml1_1Strings = return this; } + /// + /// Enables handling TargetInvocationExceptions thrown by a property so that information about exception is serialized as string value of the property. + /// + public SerializerBuilder WithTargetInvocationExceptionsHandling() + { + handleTargetInvocationExceptions = true; + return this; + } + /// /// Sets the default quoting style for scalar values. The default value is /// @@ -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) { diff --git a/YamlDotNet/Serialization/StaticSerializerBuilder.cs b/YamlDotNet/Serialization/StaticSerializerBuilder.cs index a0de9755c..0f29d42fc 100644 --- a/YamlDotNet/Serialization/StaticSerializerBuilder.cs +++ b/YamlDotNet/Serialization/StaticSerializerBuilder.cs @@ -126,6 +126,17 @@ public StaticSerializerBuilder WithQuotingNecessaryStrings(bool quoteYaml1_1Stri return this; } + /// + /// Enables handling TargetInvocationExceptions thrown by a property so that information about exception is serialized as string value of the property. + /// + public StaticSerializerBuilder WithTargetInvocationExceptionsHandling() + { + typeInspectorFactories.Add( + typeof(HandleTargetInvocationExceptionsTypeInspector), + inner => new HandleTargetInvocationExceptionsTypeInspector(inner)); + return this; + } + /// /// 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. /// diff --git a/YamlDotNet/Serialization/TypeInspectors/HandleTargetInvocationExceptionsTypeInspector.cs b/YamlDotNet/Serialization/TypeInspectors/HandleTargetInvocationExceptionsTypeInspector.cs new file mode 100644 index 000000000..945c42ef5 --- /dev/null +++ b/YamlDotNet/Serialization/TypeInspectors/HandleTargetInvocationExceptionsTypeInspector.cs @@ -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 +{ + /// + /// Sets HandleTargetInvocationExceptions to true for all property descriptors. + /// + public sealed class HandleTargetInvocationExceptionsTypeInspector : TypeInspectorSkeleton + { + private readonly ITypeInspector innerTypeDescriptor; + + public HandleTargetInvocationExceptionsTypeInspector(ITypeInspector innerTypeDescriptor) + { + this.innerTypeDescriptor = innerTypeDescriptor; + } + + public override IEnumerable GetProperties(Type type, object? container) + { + return innerTypeDescriptor.GetProperties(type, container) + .Select(p => + { + var descriptor = new PropertyDescriptor(p) { HandleTargetInvocationExceptions = true }; + return (IPropertyDescriptor)descriptor; + }); + } + } +} diff --git a/YamlDotNet/Serialization/TypeInspectors/ReadablePropertiesTypeInspector.cs b/YamlDotNet/Serialization/TypeInspectors/ReadablePropertiesTypeInspector.cs index 90c3f2482..8a55be800 100644 --- a/YamlDotNet/Serialization/TypeInspectors/ReadablePropertiesTypeInspector.cs +++ b/YamlDotNet/Serialization/TypeInspectors/ReadablePropertiesTypeInspector.cs @@ -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) @@ -57,18 +62,23 @@ public override IEnumerable 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; } @@ -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); }