File: JsonSchemaExporter\JsonSchemaExporter.ReflectionHelpers.cs
Web Access
Project: src\src\Shared\Shared.csproj (Shared)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#if !NET9_0_OR_GREATER
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
#if !NET
using System.Linq;
#endif
using System.Reflection;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using Microsoft.Shared.Diagnostics;
 
#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
 
namespace System.Text.Json.Schema;
 
internal static partial class JsonSchemaExporter
{
    private static class ReflectionHelpers
    {
        private const BindingFlags AllInstance = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
        private static PropertyInfo? _jsonTypeInfo_ElementType;
        private static PropertyInfo? _jsonPropertyInfo_MemberName;
        private static FieldInfo? _nullableConverter_ElementConverter_Generic;
        private static FieldInfo? _enumConverter_Options_Generic;
        private static FieldInfo? _enumConverter_NamingPolicy_Generic;
 
        public static bool IsBuiltInConverter(JsonConverter converter) =>
            converter.GetType().Assembly == typeof(JsonConverter).Assembly;
 
        public static bool CanBeNull(Type type) => !type.IsValueType || Nullable.GetUnderlyingType(type) is not null;
 
        public static Type GetElementType(JsonTypeInfo typeInfo)
        {
            Debug.Assert(typeInfo.Kind is JsonTypeInfoKind.Enumerable or JsonTypeInfoKind.Dictionary, "TypeInfo must be of collection type");
 
            // Uses reflection to access the element type encapsulated by a JsonTypeInfo.
            if (_jsonTypeInfo_ElementType is null)
            {
                PropertyInfo? elementTypeProperty = typeof(JsonTypeInfo).GetProperty("ElementType", AllInstance);
                _jsonTypeInfo_ElementType = Throw.IfNull(elementTypeProperty);
            }
 
            return (Type)_jsonTypeInfo_ElementType.GetValue(typeInfo)!;
        }
 
        public static string? GetMemberName(JsonPropertyInfo propertyInfo)
        {
            // Uses reflection to the member name encapsulated by a JsonPropertyInfo.
            if (_jsonPropertyInfo_MemberName is null)
            {
                PropertyInfo? memberName = typeof(JsonPropertyInfo).GetProperty("MemberName", AllInstance);
                _jsonPropertyInfo_MemberName = Throw.IfNull(memberName);
            }
 
            return (string?)_jsonPropertyInfo_MemberName.GetValue(propertyInfo);
        }
 
        public static JsonConverter GetElementConverter(JsonConverter nullableConverter)
        {
            // Uses reflection to access the element converter encapsulated by a nullable converter.
            if (_nullableConverter_ElementConverter_Generic is null)
            {
                FieldInfo? genericFieldInfo = Type
                    .GetType("System.Text.Json.Serialization.Converters.NullableConverter`1, System.Text.Json")!
                    .GetField("_elementConverter", AllInstance);
 
                _nullableConverter_ElementConverter_Generic = Throw.IfNull(genericFieldInfo);
            }
 
            Type converterType = nullableConverter.GetType();
            var thisFieldInfo = (FieldInfo)converterType.GetMemberWithSameMetadataDefinitionAs(_nullableConverter_ElementConverter_Generic);
            return (JsonConverter)thisFieldInfo.GetValue(nullableConverter)!;
        }
 
        public static void GetEnumConverterConfig(JsonConverter enumConverter, out JsonNamingPolicy? namingPolicy, out bool allowString)
        {
            // Uses reflection to access configuration encapsulated by an enum converter.
            if (_enumConverter_Options_Generic is null)
            {
                FieldInfo? genericFieldInfo = Type
                    .GetType("System.Text.Json.Serialization.Converters.EnumConverter`1, System.Text.Json")!
                    .GetField("_converterOptions", AllInstance);
 
                _enumConverter_Options_Generic = Throw.IfNull(genericFieldInfo);
            }
 
            if (_enumConverter_NamingPolicy_Generic is null)
            {
                FieldInfo? genericFieldInfo = Type
                    .GetType("System.Text.Json.Serialization.Converters.EnumConverter`1, System.Text.Json")!
                    .GetField("_namingPolicy", AllInstance);
 
                _enumConverter_NamingPolicy_Generic = Throw.IfNull(genericFieldInfo);
            }
 
            const int EnumConverterOptionsAllowStrings = 1;
            Type converterType = enumConverter.GetType();
            var converterOptionsField = (FieldInfo)converterType.GetMemberWithSameMetadataDefinitionAs(_enumConverter_Options_Generic);
            var namingPolicyField = (FieldInfo)converterType.GetMemberWithSameMetadataDefinitionAs(_enumConverter_NamingPolicy_Generic);
 
            namingPolicy = (JsonNamingPolicy?)namingPolicyField.GetValue(enumConverter);
            int converterOptions = (int)converterOptionsField.GetValue(enumConverter)!;
            allowString = (converterOptions & EnumConverterOptionsAllowStrings) != 0;
        }
 
        // The .NET 8 source generator doesn't populate attribute providers for properties
        // cf. https://github.com/dotnet/runtime/issues/100095
        // Work around the issue by running a query for the relevant MemberInfo using the internal MemberName property
        // https://github.com/dotnet/runtime/blob/de774ff9ee1a2c06663ab35be34b755cd8d29731/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs#L206
        public static ICustomAttributeProvider? ResolveAttributeProvider(
            [DynamicallyAccessedMembers(
            DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties |
            DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.NonPublicFields)]
            Type? declaringType,
            JsonPropertyInfo? propertyInfo)
        {
            if (declaringType is null || propertyInfo is null)
            {
                return null;
            }
 
            if (propertyInfo.AttributeProvider is { } provider)
            {
                return provider;
            }
 
            string? memberName = ReflectionHelpers.GetMemberName(propertyInfo);
            if (memberName is not null)
            {
                return (MemberInfo?)declaringType.GetProperty(memberName, AllInstance) ??
                                    declaringType.GetField(memberName, AllInstance);
            }
 
            return null;
        }
 
        // Resolves the parameters of the deserialization constructor for a type, if they exist.
        public static Func<JsonPropertyInfo, ParameterInfo?>? ResolveJsonConstructorParameterMapper(
            [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)]
            Type type,
            JsonTypeInfo typeInfo)
        {
            Debug.Assert(type == typeInfo.Type, "The declaring type must match the typeInfo type.");
            Debug.Assert(typeInfo.Kind is JsonTypeInfoKind.Object, "Should only be passed object JSON kinds.");
 
            if (typeInfo.Properties.Count > 0 &&
                typeInfo.CreateObject is null && // Ensure that a default constructor isn't being used
                TryGetDeserializationConstructor(type, useDefaultCtorInAnnotatedStructs: true, out ConstructorInfo? ctor))
            {
                ParameterInfo[]? parameters = ctor?.GetParameters();
                if (parameters?.Length > 0)
                {
                    Dictionary<ParameterLookupKey, ParameterInfo> dict = new(parameters.Length);
                    foreach (ParameterInfo parameter in parameters)
                    {
                        if (parameter.Name is not null)
                        {
                            // We don't care about null parameter names or conflicts since they
                            // would have already been rejected by JsonTypeInfo exporterOptions.
                            dict[new(parameter.Name, parameter.ParameterType)] = parameter;
                        }
                    }
 
                    return prop => dict.TryGetValue(new(prop.Name, prop.PropertyType), out ParameterInfo? parameter) ? parameter : null;
                }
            }
 
            return null;
        }
 
        // Resolves the nullable reference type annotations for a property or field,
        // additionally addressing a few known bugs of the NullabilityInfo pre .NET 9.
        public static NullabilityInfo GetMemberNullability(NullabilityInfoContext context, MemberInfo memberInfo)
        {
            Debug.Assert(memberInfo is PropertyInfo or FieldInfo, "Member must be property or field.");
            return memberInfo is PropertyInfo prop
                ? context.Create(prop)
                : context.Create((FieldInfo)memberInfo);
        }
 
        public static NullabilityState GetParameterNullability(NullabilityInfoContext context, ParameterInfo parameterInfo)
        {
#if NET8_0
            // Workaround for https://github.com/dotnet/runtime/issues/92487
            // The fix has been incorporated into .NET 9 (and the polyfilled implementations in netfx).
            // Should be removed once .NET 8 support is dropped.
            if (GetGenericParameterDefinition(parameterInfo) is { ParameterType: { IsGenericParameter: true } typeParam })
            {
                // Step 1. Look for nullable annotations on the type parameter.
                if (GetNullableFlags(typeParam) is byte[] flags)
                {
                    return TranslateByte(flags[0]);
                }
 
                // Step 2. Look for nullable annotations on the generic method declaration.
                if (typeParam.DeclaringMethod != null && GetNullableContextFlag(typeParam.DeclaringMethod) is byte flag)
                {
                    return TranslateByte(flag);
                }
 
                // Step 3. Look for nullable annotations on the generic method declaration.
                if (GetNullableContextFlag(typeParam.DeclaringType!) is byte flag2)
                {
                    return TranslateByte(flag2);
                }
 
                // Default to nullable.
                return NullabilityState.Nullable;
 
                static byte[]? GetNullableFlags(MemberInfo member)
                {
                    foreach (CustomAttributeData attr in member.GetCustomAttributesData())
                    {
                        Type attrType = attr.AttributeType;
                        if (attrType.Name == "NullableAttribute" && attrType.Namespace == "System.Runtime.CompilerServices")
                        {
                            foreach (CustomAttributeTypedArgument ctorArg in attr.ConstructorArguments)
                            {
                                switch (ctorArg.Value)
                                {
                                    case byte flag:
                                        return [flag];
                                    case byte[] flags:
                                        return flags;
                                }
                            }
                        }
                    }
 
                    return null;
                }
 
                static byte? GetNullableContextFlag(MemberInfo member)
                {
                    foreach (CustomAttributeData attr in member.GetCustomAttributesData())
                    {
                        Type attrType = attr.AttributeType;
                        if (attrType.Name == "NullableContextAttribute" && attrType.Namespace == "System.Runtime.CompilerServices")
                        {
                            foreach (CustomAttributeTypedArgument ctorArg in attr.ConstructorArguments)
                            {
                                if (ctorArg.Value is byte flag)
                                {
                                    return flag;
                                }
                            }
                        }
                    }
 
                    return null;
                }
 
#pragma warning disable S109 // Magic numbers should not be used
                static NullabilityState TranslateByte(byte b) => b switch
                {
                    1 => NullabilityState.NotNull,
                    2 => NullabilityState.Nullable,
                    _ => NullabilityState.Unknown
                };
#pragma warning restore S109 // Magic numbers should not be used
            }
 
            static ParameterInfo GetGenericParameterDefinition(ParameterInfo parameter)
            {
                if (parameter.Member is { DeclaringType.IsConstructedGenericType: true }
                                        or MethodInfo { IsGenericMethod: true, IsGenericMethodDefinition: false })
                {
                    var genericMethod = (MethodBase)GetGenericMemberDefinition(parameter.Member);
                    return genericMethod.GetParameters()[parameter.Position];
                }
 
                return parameter;
            }
 
            static MemberInfo GetGenericMemberDefinition(MemberInfo member)
            {
                if (member is Type type)
                {
                    return type.IsConstructedGenericType ? type.GetGenericTypeDefinition() : type;
                }
 
                if (member.DeclaringType?.IsConstructedGenericType is true)
                {
                    return member.DeclaringType.GetGenericTypeDefinition().GetMemberWithSameMetadataDefinitionAs(member);
                }
 
                if (member is MethodInfo { IsGenericMethod: true, IsGenericMethodDefinition: false } method)
                {
                    return method.GetGenericMethodDefinition();
                }
 
                return member;
            }
#endif
            return context.Create(parameterInfo).WriteState;
        }
 
        // Taken from https://github.com/dotnet/runtime/blob/903bc019427ca07080530751151ea636168ad334/src/libraries/System.Text.Json/Common/ReflectionExtensions.cs#L288-L317
        public static object? GetNormalizedDefaultValue(ParameterInfo parameterInfo)
        {
            Type parameterType = parameterInfo.ParameterType;
            object? defaultValue = parameterInfo.DefaultValue;
 
            if (defaultValue is null)
            {
                return null;
            }
 
            // DBNull.Value is sometimes used as the default value (returned by reflection) of nullable params in place of null.
            if (defaultValue == DBNull.Value && parameterType != typeof(DBNull))
            {
                return null;
            }
 
            // Default values of enums or nullable enums are represented using the underlying type and need to be cast explicitly
            // cf. https://github.com/dotnet/runtime/issues/68647
            if (parameterType.IsEnum)
            {
                return Enum.ToObject(parameterType, defaultValue);
            }
 
            if (Nullable.GetUnderlyingType(parameterType) is Type underlyingType && underlyingType.IsEnum)
            {
                return Enum.ToObject(underlyingType, defaultValue);
            }
 
            return defaultValue;
        }
 
        // Resolves the deserialization constructor for a type using logic copied from
        // https://github.com/dotnet/runtime/blob/e12e2fa6cbdd1f4b0c8ad1b1e2d960a480c21703/src/libraries/System.Text.Json/Common/ReflectionExtensions.cs#L227-L286
        private static bool TryGetDeserializationConstructor(
            [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)]
            Type type,
            bool useDefaultCtorInAnnotatedStructs,
            out ConstructorInfo? deserializationCtor)
        {
            ConstructorInfo? ctorWithAttribute = null;
            ConstructorInfo? publicParameterlessCtor = null;
            ConstructorInfo? lonePublicCtor = null;
 
            ConstructorInfo[] constructors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance);
 
            if (constructors.Length == 1)
            {
                lonePublicCtor = constructors[0];
            }
 
            foreach (ConstructorInfo constructor in constructors)
            {
                if (HasJsonConstructorAttribute(constructor))
                {
                    if (ctorWithAttribute != null)
                    {
                        deserializationCtor = null;
                        return false;
                    }
 
                    ctorWithAttribute = constructor;
                }
                else if (constructor.GetParameters().Length == 0)
                {
                    publicParameterlessCtor = constructor;
                }
            }
 
            // Search for non-public ctors with [JsonConstructor].
            foreach (ConstructorInfo constructor in type.GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance))
            {
                if (HasJsonConstructorAttribute(constructor))
                {
                    if (ctorWithAttribute != null)
                    {
                        deserializationCtor = null;
                        return false;
                    }
 
                    ctorWithAttribute = constructor;
                }
            }
 
            // Structs will use default constructor if attribute isn't used.
            if (useDefaultCtorInAnnotatedStructs && type.IsValueType && ctorWithAttribute == null)
            {
                deserializationCtor = null;
                return true;
            }
 
            deserializationCtor = ctorWithAttribute ?? publicParameterlessCtor ?? lonePublicCtor;
            return true;
 
            static bool HasJsonConstructorAttribute(ConstructorInfo constructorInfo) =>
                constructorInfo.GetCustomAttribute<JsonConstructorAttribute>() != null;
        }
 
        // Parameter to property matching semantics as declared in
        // https://github.com/dotnet/runtime/blob/12d96ccfaed98e23c345188ee08f8cfe211c03e7/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs#L1007-L1030
        private readonly struct ParameterLookupKey : IEquatable<ParameterLookupKey>
        {
            public ParameterLookupKey(string name, Type type)
            {
                Name = name;
                Type = type;
            }
 
            public string Name { get; }
            public Type Type { get; }
 
            public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Name);
            public bool Equals(ParameterLookupKey other) => Type == other.Type && string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase);
            public override bool Equals(object? obj) => obj is ParameterLookupKey key && Equals(key);
        }
    }
 
#if !NET
    private static MemberInfo GetMemberWithSameMetadataDefinitionAs(this Type specializedType, MemberInfo member)
    {
        const BindingFlags All = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance;
        return specializedType.GetMember(member.Name, member.MemberType, All).First(m => m.MetadataToken == member.MetadataToken);
    }
#endif
}
#endif