From e6826e92829c3d0e52ac97d18c192a387d6f2f9e Mon Sep 17 00:00:00 2001 From: Alexey Kuptsov Date: Wed, 10 Apr 2024 23:30:38 +0300 Subject: [PATCH] Add support of handling a property invocation exception by a custom handler that converts the exception to an error message string --- .../StaticGenerator/HandleExceptionTests.cs | 67 +++++++++++++++++++ .../Serialization/SerializationTestHelper.cs | 5 ++ .../Serialization/SerializationTests.cs | 29 ++++++++ .../Serialization/PropertyDescriptor.cs | 20 +++++- YamlDotNet/Serialization/SerializerBuilder.cs | 18 ++++- .../Serialization/StaticSerializerBuilder.cs | 16 +++++ .../ExceptionHandlerInspector.cs | 54 +++++++++++++++ .../ReadablePropertiesTypeInspector.cs | 38 +++++++++-- 8 files changed, 241 insertions(+), 6 deletions(-) create mode 100644 YamlDotNet.Test/Analyzers/StaticGenerator/HandleExceptionTests.cs create mode 100644 YamlDotNet/Serialization/TypeInspectors/ExceptionHandlerInspector.cs diff --git a/YamlDotNet.Test/Analyzers/StaticGenerator/HandleExceptionTests.cs b/YamlDotNet.Test/Analyzers/StaticGenerator/HandleExceptionTests.cs new file mode 100644 index 000000000..5c02b058b --- /dev/null +++ b/YamlDotNet.Test/Analyzers/StaticGenerator/HandleExceptionTests.cs @@ -0,0 +1,67 @@ +// 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 HandleExceptionTests + { + [Fact] + public void StaticSerializationHandlesTargetInvocationException() + { + var obj = new ThrowingPropertyExample(); + var serializer = new StaticSerializerBuilder(new StaticContext()) + .WithExceptionHandler((e, o, p) => + $"Exception of type {e.GetType().FullName} was thrown in property {p} " + + "of " + (ReferenceEquals(o, obj) ? "expected" : "unexpected") + " object") + .Build(); + var writer = new StringWriter(); + + serializer.Serialize(writer, obj); + var serialized = writer.ToString(); + + serialized.Should().Be( + "Value: Exception of type System.InvalidOperationException was thrown in property Value of expected object\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..fa6f956bc 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,34 @@ public void SerializationEmitsPropertyWhenValueDifferFromDefaultValueAttribute() serialized.Should().Contain("Value"); } + [Fact] + public void SerializationHandlesException() + { + var obj = new ThrowingPropertyExample(); + var serializer = new SerializerBuilder() + .WithExceptionHandler((e, o, p) => + $"Exception of type {e.GetType().FullName} was thrown in property {p} " + + "of " + (ReferenceEquals(o, obj) ? "expected" : "unexpected") + " object") + .Build(); + var writer = new StringWriter(); + + serializer.Serialize(writer, obj); + var serialized = writer.ToString(); + + serialized.Should().Be( + "Value: Exception of type System.InvalidOperationException was thrown in property Value of expected object\r\n" + .NormalizeNewLines()); + } + + [Fact] + public void SerializationDoesntHandleExceptionByDefault() + { + 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..473a97f97 100644 --- a/YamlDotNet/Serialization/PropertyDescriptor.cs +++ b/YamlDotNet/Serialization/PropertyDescriptor.cs @@ -57,6 +57,8 @@ public bool CanWrite get { return baseDescriptor.CanWrite; } } + public Func? ExceptionHandler { get; set; } + public void Write(object target, object? value) { baseDescriptor.Write(target, value); @@ -69,7 +71,23 @@ public void Write(object target, object? value) public IObjectDescriptor Read(object target) { - return baseDescriptor.Read(target); + if (ExceptionHandler == null) + { + return baseDescriptor.Read(target); + } + + try + { + return baseDescriptor.Read(target); + } + catch (Exception e) + { + return new ObjectDescriptor( + ExceptionHandler(e, target, Name), + typeof(string), + typeof(string), + ScalarStyle.Any); + } } } } diff --git a/YamlDotNet/Serialization/SerializerBuilder.cs b/YamlDotNet/Serialization/SerializerBuilder.cs index 74b927f69..68c74c61d 100755 --- a/YamlDotNet/Serialization/SerializerBuilder.cs +++ b/YamlDotNet/Serialization/SerializerBuilder.cs @@ -21,6 +21,7 @@ using System; using System.Collections.Generic; +using System.Reflection; #if NET7_0_OR_GREATER using System.Diagnostics.CodeAnalysis; #endif @@ -62,6 +63,7 @@ public sealed class SerializerBuilder : BuilderSkeleton private ScalarStyle defaultScalarStyle = ScalarStyle.Any; private bool quoteNecessaryStrings; private bool quoteYaml1_1Strings; + private Func? exceptionHandler; public SerializerBuilder() : base(new DynamicTypeResolver()) @@ -129,6 +131,19 @@ public SerializerBuilder WithQuotingNecessaryStrings(bool quoteYaml1_1Strings = return this; } + /// + /// Enables handling an exception thrown by a property so that information about exception is serialized as string value of the property. + /// + /// + /// A function that takes the caught exception, the object and the name of the object's property (from which the exception was thrown). + /// The string returned from the function is written instead of the property value by the serializer. + /// + public SerializerBuilder WithExceptionHandler(Func exceptionHandler) + { + this.exceptionHandler = exceptionHandler; + return this; + } + /// /// Sets the default quoting style for scalar values. The default value is /// @@ -695,7 +710,8 @@ public IValueSerializer BuildValueSerializer() internal ITypeInspector BuildTypeInspector() { - ITypeInspector innerInspector = new ReadablePropertiesTypeInspector(typeResolver, includeNonPublicProperties); + ITypeInspector innerInspector = new ReadablePropertiesTypeInspector( + typeResolver, includeNonPublicProperties, exceptionHandler); if (!ignoreFields) { diff --git a/YamlDotNet/Serialization/StaticSerializerBuilder.cs b/YamlDotNet/Serialization/StaticSerializerBuilder.cs index a0de9755c..4c8b8b8ad 100644 --- a/YamlDotNet/Serialization/StaticSerializerBuilder.cs +++ b/YamlDotNet/Serialization/StaticSerializerBuilder.cs @@ -21,6 +21,7 @@ using System; using System.Collections.Generic; +using System.Reflection; #if NET7_0_OR_GREATER using System.Diagnostics.CodeAnalysis; #endif @@ -126,6 +127,21 @@ public StaticSerializerBuilder WithQuotingNecessaryStrings(bool quoteYaml1_1Stri return this; } + /// + /// Enables handling an exception thrown by a property so that information about exception is serialized as string value of the property. + /// + /// + /// A function that takes the caught exception, the object and the name of the object's property (from which the exception was thrown). + /// The string returned from the function is written instead of the property value by the serializer. + /// + public StaticSerializerBuilder WithExceptionHandler(Func exceptionHandler) + { + typeInspectorFactories.Add( + typeof(ExceptionHandlerInspector), + inner => new ExceptionHandlerInspector(inner, exceptionHandler)); + 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/ExceptionHandlerInspector.cs b/YamlDotNet/Serialization/TypeInspectors/ExceptionHandlerInspector.cs new file mode 100644 index 000000000..9e89356bc --- /dev/null +++ b/YamlDotNet/Serialization/TypeInspectors/ExceptionHandlerInspector.cs @@ -0,0 +1,54 @@ +// 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; +using System.Reflection; + +namespace YamlDotNet.Serialization.TypeInspectors +{ + /// + /// Sets HandleTargetInvocationExceptions to true for all property descriptors. + /// + public sealed class ExceptionHandlerInspector : TypeInspectorSkeleton + { + private readonly ITypeInspector innerTypeDescriptor; + private readonly Func exceptionHandler; + + public ExceptionHandlerInspector( + ITypeInspector innerTypeDescriptor, Func exceptionHandler) + { + this.innerTypeDescriptor = innerTypeDescriptor; + this.exceptionHandler = exceptionHandler; + } + + public override IEnumerable GetProperties(Type type, object? container) + { + return innerTypeDescriptor.GetProperties(type, container) + .Select(p => + { + var descriptor = new PropertyDescriptor(p) { ExceptionHandler = exceptionHandler }; + return (IPropertyDescriptor)descriptor; + }); + } + } +} diff --git a/YamlDotNet/Serialization/TypeInspectors/ReadablePropertiesTypeInspector.cs b/YamlDotNet/Serialization/TypeInspectors/ReadablePropertiesTypeInspector.cs index 90c3f2482..50c89aa4f 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 Func? exceptionHandler; public ReadablePropertiesTypeInspector(ITypeResolver typeResolver) : this(typeResolver, false) { } - public ReadablePropertiesTypeInspector(ITypeResolver typeResolver, bool includeNonPublicProperties) + public ReadablePropertiesTypeInspector( + ITypeResolver typeResolver, + bool includeNonPublicProperties, + Func? exceptionHandler = null) { this.typeResolver = typeResolver ?? throw new ArgumentNullException(nameof(typeResolver)); this.includeNonPublicProperties = includeNonPublicProperties; + this.exceptionHandler = exceptionHandler; } 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, exceptionHandler)); } private sealed class ReflectionPropertyDescriptor : IPropertyDescriptor { private readonly PropertyInfo propertyInfo; private readonly ITypeResolver typeResolver; + private readonly Func? exceptionHandler; - public ReflectionPropertyDescriptor(PropertyInfo propertyInfo, ITypeResolver typeResolver) + public ReflectionPropertyDescriptor( + PropertyInfo propertyInfo, + ITypeResolver typeResolver, + Func? exceptionHandler) { this.propertyInfo = propertyInfo ?? throw new ArgumentNullException(nameof(propertyInfo)); this.typeResolver = typeResolver ?? throw new ArgumentNullException(nameof(typeResolver)); + this.exceptionHandler = exceptionHandler; ScalarStyle = ScalarStyle.Any; } @@ -92,7 +102,27 @@ public void Write(object target, object? value) public IObjectDescriptor Read(object target) { - var propertyValue = propertyInfo.ReadValue(target); + object? propertyValue; + if (exceptionHandler != null) + { + try + { + propertyValue = propertyInfo.ReadValue(target); + } + catch (TargetInvocationException e) + { + return new ObjectDescriptor( + exceptionHandler(e.InnerException!, target, propertyInfo.Name), + 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); }