Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 46 additions & 4 deletions src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,17 +104,59 @@ private static bool HasCustomAttributeWithName(this MemberInfo memberInfo, strin
#if NET
return ctorInfo.Invoke(BindingFlags.DoNotWrapExceptions, null, parameters, null);
#else
object? result = null;
try
{
result = ctorInfo.Invoke(parameters);
return ctorInfo.Invoke(parameters);
}
catch (TargetInvocationException ex)
catch (TargetInvocationException ex) when (ex.InnerException is not null)
{
ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
throw; // unreachable
}
#endif
}

return result;
/// <summary>
/// Invokes <paramref name="methodInfo"/> without wrapping any exception thrown by the
/// target method in a <see cref="TargetInvocationException"/>. This matches the behavior of
/// the Reflection.Emit-based accessor, which emits direct calls into user code.
/// </summary>
public static object? InvokeNoWrapExceptions(this MethodInfo methodInfo, object? obj, object?[]? parameters)
{
#if NET
return methodInfo.Invoke(obj, BindingFlags.DoNotWrapExceptions, binder: null, parameters, culture: null);
#else
try
{
return methodInfo.Invoke(obj, parameters);
}
catch (TargetInvocationException ex) when (ex.InnerException is not null)
{
ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
throw; // unreachable
}
#endif
}

/// <summary>
/// Invokes <paramref name="constructorInfo"/> without wrapping any exception thrown by the
/// constructor in a <see cref="TargetInvocationException"/>. This matches the behavior of
/// the Reflection.Emit-based accessor, which emits direct calls into user code.
/// </summary>
public static object InvokeNoWrapExceptions(this ConstructorInfo constructorInfo, object?[]? parameters)
{
#if NET
return constructorInfo.Invoke(BindingFlags.DoNotWrapExceptions, binder: null, parameters, culture: null);
#else
try
{
return constructorInfo.Invoke(parameters);
}
catch (TargetInvocationException ex) when (ex.InnerException is not null)
{
ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
throw; // unreachable
}
#endif
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,13 @@ static MemberAccessor Initialize()
{
MemberAccessor value =
#if NET
// if dynamic code isn't supported, fallback to reflection
RuntimeFeature.IsDynamicCodeSupported ?
// On platforms where dynamic code is supported but not compiled to native code
// (e.g. the Mono interpreter, WASM and iOS), the IL emitted by the Reflection.Emit
// based accessor is only interpreted, offering no throughput benefit over plain
// reflection while still pulling in the Reflection.Emit stack. Gating on
// IsDynamicCodeCompiled (rather than IsDynamicCodeSupported) keeps Reflection.Emit
// on JIT-backed runtimes but lets the trimmer remove it everywhere else.
RuntimeFeature.IsDynamicCodeCompiled ?
new ReflectionEmitCachingMemberAccessor() :
new ReflectionMemberAccessor();
#elif NETFRAMEWORK
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text.Json.Reflection;

namespace System.Text.Json.Serialization.Metadata
{
Expand Down Expand Up @@ -33,7 +34,7 @@ public ReflectionMemberAccessor()
: null;
}

return () => ctorInfo.Invoke(null);
return () => ctorInfo.InvokeNoWrapExceptions(null);
}

public override Func<object[], T> CreateParameterizedConstructor<T>(ConstructorInfo constructor)
Expand All @@ -56,17 +57,10 @@ public override Func<object[], T> CreateParameterizedConstructor<T>(ConstructorI
argsToPass[i] = arguments[i];
}

try
{
return (T)constructor.Invoke(argsToPass);
}
catch (TargetInvocationException e)
{
// Plumb ArgumentException through for tuples with more than 7 generic parameters, e.g.
// System.ArgumentException : The last element of an eight element tuple must be a Tuple.
// This doesn't apply to the method below as it supports a max of 4 constructor params.
throw e.InnerException ?? e;
}
// Not wrapping in TargetInvocationException also plumbs ArgumentException through for
// tuples with more than 7 generic parameters, e.g.
// System.ArgumentException : The last element of an eight element tuple must be a Tuple.
return (T)constructor.InvokeNoWrapExceptions(argsToPass);
};
}

Expand Down Expand Up @@ -108,7 +102,7 @@ public override JsonTypeInfo.ParameterizedConstructorDelegate<T, TArg0, TArg1, T
}
}

return (T)constructor.Invoke(arguments);
return (T)constructor.InvokeNoWrapExceptions(arguments);
};
}

Expand All @@ -122,14 +116,7 @@ public override JsonTypeInfo.ParameterizedConstructorDelegate<T, TArg0, TArg1, T

return value =>
{
try
{
return (T)constructor.Invoke(new object?[] { value });
}
catch (TargetInvocationException e)
{
throw e.InnerException ?? e;
}
return (T)constructor.InvokeNoWrapExceptions(new object?[] { value });
};
}

Expand All @@ -143,7 +130,7 @@ public override JsonTypeInfo.ParameterizedConstructorDelegate<T, TArg0, TArg1, T

return delegate (TCollection collection, object? element)
{
addMethod.Invoke(collection, new object[] { element! });
addMethod.InvokeNoWrapExceptions(collection, new object[] { element! });
};
}

Expand All @@ -167,7 +154,7 @@ public override Func<object, TProperty> CreatePropertyGetter<TProperty>(Property

return delegate (object obj)
{
return (TProperty)getMethodInfo.Invoke(obj, null)!;
return (TProperty)getMethodInfo.InvokeNoWrapExceptions(obj, null)!;
};
}

Expand All @@ -177,7 +164,7 @@ public override Func<TDeclaringType, TProperty> CreatePropertyGetter<TDeclaringT

return delegate (TDeclaringType obj)
{
return (TProperty)getMethodInfo.Invoke(obj, null)!;
return (TProperty)getMethodInfo.InvokeNoWrapExceptions(obj, null)!;
};
}

Expand All @@ -187,7 +174,7 @@ public override Action<object, TProperty> CreatePropertySetter<TProperty>(Proper

return delegate (object obj, TProperty value)
{
setMethodInfo.Invoke(obj, new object[] { value! });
setMethodInfo.InvokeNoWrapExceptions(obj, new object[] { value! });
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@

using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using System.Text.Encodings.Web;
using Microsoft.DotNet.RemoteExecutor;
using Xunit;

namespace System.Text.Json.Serialization.Tests
Expand Down Expand Up @@ -632,5 +634,70 @@ public class PocoConverterThrowingCustomJsonException : JsonConverter<PocoUsingC
public override void Write(Utf8JsonWriter writer, PocoUsingCustomConverterThrowingJsonException value, JsonSerializerOptions options)
=> throw new JsonException(ExceptionMessage, ExceptionPath, 0, 0);
}

[Fact]
public static void ThrowingMembers_PropagateOriginalException()
{
// Exercises the default member accessor (Reflection.Emit on runtimes that compile dynamic code).
AssertThrowingMembersPropagateOriginalException();
}

[SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)]
[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
public static void ThrowingMembers_PropagateOriginalException_WithReflectionMemberAccessor()
{
// Disabling dynamic code support routes serialization through the reflection-based member
// accessor (as happens on WASM, iOS and the Mono interpreter) instead of the Reflection.Emit
// accessor. Exceptions thrown by user getters, setters and constructors must still propagate
// unwrapped rather than being surfaced as a TargetInvocationException.
var options = new RemoteInvokeOptions
{
RuntimeConfigurationOptions =
{
["System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported"] = false
}
};

RemoteExecutor.Invoke(static () =>
{
Assert.False(RuntimeFeature.IsDynamicCodeCompiled);
AssertThrowingMembersPropagateOriginalException();
}, options).Dispose();
}

private static void AssertThrowingMembersPropagateOriginalException()
{
InvalidOperationException ex;

ex = Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize(new ClassWithThrowingGetter()));
Assert.Equal(ThrowingMember_ExceptionMessage, ex.Message);

ex = Assert.Throws<InvalidOperationException>(() => JsonSerializer.Deserialize<ClassWithThrowingSetter>("""{"Value":1}"""));
Assert.Equal(ThrowingMember_ExceptionMessage, ex.Message);

ex = Assert.Throws<InvalidOperationException>(() => JsonSerializer.Deserialize<ClassWithThrowingConstructor>("{}"));
Assert.Equal(ThrowingMember_ExceptionMessage, ex.Message);
}

private const string ThrowingMember_ExceptionMessage = "Exception thrown from user code.";
Comment thread
eiriktsarpalis marked this conversation as resolved.

public class ClassWithThrowingGetter
{
public int Value => throw new InvalidOperationException(ThrowingMember_ExceptionMessage);
}

public class ClassWithThrowingSetter
{
public int Value { get => 0; set => throw new InvalidOperationException(ThrowingMember_ExceptionMessage); }
}

public class ClassWithThrowingConstructor
{
[JsonConstructor]
public ClassWithThrowingConstructor(int value)
=> throw new InvalidOperationException(ThrowingMember_ExceptionMessage);

public int Value { get; }
}
}
}
Loading