Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apply a variant of the visitor pattern for reflection-based generic type instantiation. #72373

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions src/libraries/System.Text.Json/src/System.Text.Json.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
<Compile Include="System\Text\Json\Serialization\Metadata\ReflectionEmitCachingMemberAccessor.cs" />
<Compile Include="System\Text\Json\Serialization\Metadata\ReflectionEmitMemberAccessor.cs" />
<Compile Include="System\Text\Json\Serialization\Metadata\ReflectionMemberAccessor.cs" />
<Compile Include="System\Text\Json\Serialization\Metadata\TypeWitness.cs" />
<Compile Include="System\Text\Json\Serialization\MetadataPropertyName.cs" />
<Compile Include="System\Text\Json\Serialization\PreserveReferenceHandler.cs" />
<Compile Include="System\Text\Json\Serialization\PreserveReferenceResolver.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Text.Json.Reflection;
using System.Threading;

namespace System.Text.Json.Serialization.Metadata
Expand Down Expand Up @@ -36,6 +35,10 @@ private DefaultJsonTypeInfoResolver(bool mutable)
}

/// <inheritdoc/>
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
Justification = "The ctor is marked RequiresUnreferencedCode.")]
[UnconditionalSuppressMessage("AotAnalysis", "IL3050:RequiresDynamicCode",
Justification = "The ctor is marked RequiresDynamicCode.")]
public virtual JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
{
if (type == null)
Expand Down Expand Up @@ -64,36 +67,23 @@ public virtual JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options
return typeInfo;
}

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
Justification = "The ctor is marked RequiresUnreferencedCode.")]
[UnconditionalSuppressMessage("AotAnalysis", "IL3050:RequiresDynamicCode",
Justification = "The ctor is marked RequiresDynamicCode.")]
private JsonTypeInfo CreateJsonTypeInfo(Type type, JsonSerializerOptions options)
[RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
[RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)]
private static JsonTypeInfo CreateJsonTypeInfo(Type type, JsonSerializerOptions options)
{
MethodInfo methodInfo = typeof(DefaultJsonTypeInfoResolver).GetMethod(nameof(CreateReflectionJsonTypeInfo), BindingFlags.NonPublic | BindingFlags.Static)!;
#if NETCOREAPP
return (JsonTypeInfo)methodInfo.MakeGenericMethod(type).Invoke(null, BindingFlags.NonPublic | BindingFlags.DoNotWrapExceptions, null, new[] { options }, null)!;
#else
try
{
return (JsonTypeInfo)methodInfo.MakeGenericMethod(type).Invoke(null, new[] { options })!;
}
catch (TargetInvocationException ex)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI there is BindingFlags.DoNotWrapExceptions that lets the raw exception throw.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not available in netstandard2.0, hence this extra code. That being said, it is evident from this PR that we haven't been applying the flag consistently.

{
// Some of the validation is done during construction (i.e. validity of JsonConverter, inner types etc.)
// therefore we need to unwrap TargetInvocationException for better user experience
ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
throw null!;
}
#endif
return ReflectionJsonTypeInfoBuilder.Instance.Visit(type, options);
}

[RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
[RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)]
private static JsonTypeInfo<T> CreateReflectionJsonTypeInfo<T>(JsonSerializerOptions options)
private sealed class ReflectionJsonTypeInfoBuilder : ITypeVisitor<JsonSerializerOptions, JsonTypeInfo>
{
JsonConverter converter = GetConverterForType(typeof(T), options);
return new ReflectionJsonTypeInfo<T>(converter, options);
public static ReflectionJsonTypeInfoBuilder Instance { get; } = new();
public JsonTypeInfo Visit<T>(JsonSerializerOptions options)
{
JsonConverter converter = GetConverterForType(typeof(T), options);
return new ReflectionJsonTypeInfo<T>(converter, options);
}
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text.Json.Reflection;
Expand Down Expand Up @@ -63,15 +62,13 @@ internal JsonPropertyInfo CreatePropertyUsingReflection(Type propertyType)
// If a JsonTypeInfo has already been cached for the property type,
// avoid reflection-based initialization by delegating construction
// of JsonPropertyInfo<T> construction to the property type metadata.
return jsonTypeInfo.CreateJsonPropertyInfo(declaringTypeInfo: this, Options);
jsonPropertyInfo = jsonTypeInfo.CreateJsonPropertyInfo(declaringTypeInfo: this, Options);
}
else
{
// Metadata for `propertyType` has not been registered yet.
// Use reflection to instantiate the correct JsonPropertyInfo<T>
s_createJsonPropertyInfo ??= typeof(JsonTypeInfo).GetMethod(nameof(CreateJsonPropertyInfo), BindingFlags.NonPublic | BindingFlags.Static)!;
MethodInfo factoryInfo = s_createJsonPropertyInfo.MakeGenericMethod(propertyType);
jsonPropertyInfo = (JsonPropertyInfo)factoryInfo.Invoke(null, new object[] { this, Options })!;
jsonPropertyInfo = JsonPropertyInfoBuilder.Instance.Visit(propertyType, (this, Options));
}

Debug.Assert(jsonPropertyInfo.PropertyType == propertyType);
Expand All @@ -83,10 +80,12 @@ internal JsonPropertyInfo CreatePropertyUsingReflection(Type propertyType)
/// </summary>
private protected abstract JsonPropertyInfo CreateJsonPropertyInfo(JsonTypeInfo declaringTypeInfo, JsonSerializerOptions options);

private static JsonPropertyInfo CreateJsonPropertyInfo<T>(JsonTypeInfo declaringTypeInfo, JsonSerializerOptions options)
=> new JsonPropertyInfo<T>(declaringTypeInfo.Type, declaringTypeInfo, options);

private static MethodInfo? s_createJsonPropertyInfo;
private sealed class JsonPropertyInfoBuilder : ITypeVisitor<(JsonTypeInfo declaringTypeInfo, JsonSerializerOptions options), JsonPropertyInfo>
{
public static JsonPropertyInfoBuilder Instance { get; } = new();
public JsonPropertyInfo Visit<T>((JsonTypeInfo declaringTypeInfo, JsonSerializerOptions options) state)
=> new JsonPropertyInfo<T>(state.declaringTypeInfo.Type, state.declaringTypeInfo, state.options);
}

// AggressiveInlining used although a large method it is only called from one location and is on a hot path.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using System.Text.Json.Reflection;
using System.Threading;

namespace System.Text.Json.Serialization.Metadata
Expand Down Expand Up @@ -533,11 +533,25 @@ public static JsonTypeInfo<T> CreateJsonTypeInfo<T>(JsonSerializerOptions option
ThrowHelper.ThrowArgumentNullException(nameof(options));
}

return CreateCustomJsonTypeInfo<T>(options);
}

[RequiresUnreferencedCode(MetadataFactoryRequiresUnreferencedCode)]
[RequiresDynamicCode(MetadataFactoryRequiresUnreferencedCode)]
private static JsonTypeInfo<T> CreateCustomJsonTypeInfo<T>(JsonSerializerOptions options)
{
JsonConverter converter = DefaultJsonTypeInfoResolver.GetConverterForType(typeof(T), options, resolveJsonConverterAttribute: false);
return new CustomJsonTypeInfo<T>(converter, options);
}

private static MethodInfo? s_createJsonTypeInfo;
[RequiresUnreferencedCode(MetadataFactoryRequiresUnreferencedCode)]
[RequiresDynamicCode(MetadataFactoryRequiresUnreferencedCode)]
private sealed class CustomJsonTypeInfoBuilder : ITypeVisitor<JsonSerializerOptions, JsonTypeInfo>
{
public static CustomJsonTypeInfoBuilder Instance { get; } = new();
public JsonTypeInfo Visit<T>(JsonSerializerOptions options)
=> CreateCustomJsonTypeInfo<T>(options);
}

/// <summary>
/// Creates a blank <see cref="JsonTypeInfo"/> instance.
Expand Down Expand Up @@ -576,9 +590,7 @@ public static JsonTypeInfo CreateJsonTypeInfo(Type type, JsonSerializerOptions o
ThrowHelper.ThrowArgumentException_CannotSerializeInvalidType(nameof(type), type, null, null);
}

s_createJsonTypeInfo ??= typeof(JsonTypeInfo).GetMethod(nameof(CreateJsonTypeInfo), new Type[] { typeof(JsonSerializerOptions) })!;
return (JsonTypeInfo)s_createJsonTypeInfo.MakeGenericMethod(type)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a note that this PR is changing the refactoring done in #67526.

In effect, replacing MethodInfo.MakeGenericMethod with Activor.CreateInstance()?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's using Type.MakeGenericType + Activator instead of MethodInfo.MakeGenericMethod, but everything is behind logic marked RUC so it shouldn't matter much.

.Invoke(null, new object[] { options })!;
return CustomJsonTypeInfoBuilder.Instance.Visit(type, options);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;

namespace System.Text.Json.Reflection
{
/// <summary>
/// Encapsulates a generic type parameter that can be unpacked on-demand using a generic visitor,
/// encoding an existential type. Can use reflection to instantiate type parameters that are not known at compile time.
/// Uses concepts taken from https://www.microsoft.com/en-us/research/publication/generalized-algebraic-data-types-and-object-oriented-programming/
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
internal abstract class TypeWitness
{
public abstract Type Type { get; }
public abstract TResult Accept<TState, TResult>(ITypeVisitor<TState, TResult> builder, TState state);

[RequiresUnreferencedCode("Uses Type.MakeGenericType to instantiate a generic TypeWitness<T> using reflection.")]
[RequiresDynamicCode("Uses Type.MakeGenericType to instantiate a generic TypeWitness<T> using reflection.")]
public static TypeWitness Create(Type type) => (TypeWitness)Activator.CreateInstance(typeof(TypeWitness<>).MakeGenericType(type))!;
}

internal sealed class TypeWitness<T> : TypeWitness
{
public override Type Type => typeof(T);
public override TResult Accept<TState, TResult>(ITypeVisitor<TState, TResult> builder, TState state)
=> builder.Visit<T>(state);
}

/// <summary>
/// Given a <typeparamref name="TState" /> input, visits the generic parameter of a
/// <see cref="TypeWitness{T}"/> and returns a <typeparamref name="TResult" /> output.
/// </summary>
internal interface ITypeVisitor<TState, TResult>
{
TResult Visit<T>(TState state);
}

internal static class TypeVisitorExtensions
{
[RequiresUnreferencedCode("Uses Type.MakeGenericType to instantiate a generic TypeWitness<T> using reflection.")]
[RequiresDynamicCode("Uses Type.MakeGenericType to instantiate a generic TypeWitness<T> using reflection.")]
public static TResult Visit<TState, TResult>(this ITypeVisitor<TState, TResult> visitor, Type type, TState state)
=> TypeWitness.Create(type).Accept(visitor, state);
}
}