File: Schemas\JsonSchemaMapper\JsonSchemaMapper.ReflectionHelpers.cs
Web Access
Project: src\src\OpenApi\src\Microsoft.AspNetCore.OpenApi.csproj (Microsoft.AspNetCore.OpenApi)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
 
namespace JsonSchemaMapper;
 
#if EXPOSE_JSON_SCHEMA_MAPPER
public
#else
internal
#endif
    static partial class JsonSchemaMapper
{
    // Uses reflection to determine the element type of an enumerable or dictionary type
    // Workaround for https://github.com/dotnet/runtime/issues/77306#issuecomment-2007887560
    private static Type GetElementType(JsonTypeInfo typeInfo)
    {
        Debug.Assert(typeInfo.Kind is JsonTypeInfoKind.Enumerable or JsonTypeInfoKind.Dictionary);
        return (Type)typeof(JsonTypeInfo).GetProperty("ElementType", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(typeInfo)!;
    }
 
    // The source generator currently 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
#if NETCOREAPP
    [EditorBrowsable(EditorBrowsableState.Never)]
    [UnconditionalSuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.",
        Justification = "We're reading the internal JsonPropertyInfo.MemberName which cannot have been trimmed away.")]
    [UnconditionalSuppressMessage("Trimming", "IL2070:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The parameter of method does not have matching annotations.",
        Justification = "We're reading the member which is already accessed by the source generator.")]
#endif
    internal static ICustomAttributeProvider? ResolveAttributeProvider(Type? declaringType, JsonPropertyInfo? propertyInfo)
    {
        if (declaringType is null || propertyInfo is null)
        {
            return null;
        }
 
        if (propertyInfo.AttributeProvider is { } provider)
        {
            return provider;
        }
 
        s_memberNameProperty ??= typeof(JsonPropertyInfo).GetProperty("MemberName", BindingFlags.Instance | BindingFlags.NonPublic)!;
        var memberName = (string?)s_memberNameProperty.GetValue(propertyInfo);
        if (memberName is not null)
        {
            return declaringType.GetMember(memberName, MemberTypes.Property | MemberTypes.Field, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault();
        }
 
        return null;
    }
 
    private static PropertyInfo? s_memberNameProperty;
 
    // Uses reflection to determine any custom converters specified for the element of a nullable type.
#if NETCOREAPP
    [UnconditionalSuppressMessage("Trimming", "IL2026",
        Justification = "We're resolving private fields of the built-in Nullable converter which cannot have been trimmed away.")]
#endif
    private static JsonConverter? ExtractCustomNullableConverter(JsonConverter? converter)
    {
        Debug.Assert(converter is null || IsBuiltInConverter(converter));
 
        // There is unfortunately no way in which we can obtain the element converter from a nullable converter without resorting to private reflection
        // https://github.com/dotnet/runtime/blob/5fda47434cecc590095e9aef3c4e560b7b7ebb47/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverter.cs#L15-L17
        Type? converterType = converter?.GetType();
        if (converterType?.Name == "NullableConverter`1")
        {
            FieldInfo elementConverterField = converterType.GetPrivateFieldWithPotentiallyTrimmedMetadata("_elementConverter");
            return (JsonConverter)elementConverterField!.GetValue(converter)!;
        }
 
        return null;
    }
 
    // Uses reflection to determine serialization configuration for enum types
    // cf. https://github.com/dotnet/runtime/blob/5fda47434cecc590095e9aef3c4e560b7b7ebb47/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs#L23-L25
#if NETCOREAPP
    [UnconditionalSuppressMessage("Trimming", "IL2026",
        Justification = "We're resolving private fields of the built-in enum converter which cannot have been trimmed away.")]
#endif
    private static bool TryGetStringEnumConverterValues(JsonTypeInfo typeInfo, JsonConverter converter, out JsonArray? values)
    {
        Debug.Assert(typeInfo.Type.IsEnum && IsBuiltInConverter(converter));
 
        if (converter is JsonConverterFactory factory)
        {
            converter = factory.CreateConverter(typeInfo.Type, typeInfo.Options)!;
        }
 
        Type converterType = converter.GetType();
        FieldInfo converterOptionsField = converterType.GetPrivateFieldWithPotentiallyTrimmedMetadata("_converterOptions");
        FieldInfo namingPolicyField = converterType.GetPrivateFieldWithPotentiallyTrimmedMetadata("_namingPolicy");
 
        const int EnumConverterOptionsAllowStrings = 1;
        var converterOptions = (int)converterOptionsField!.GetValue(converter)!;
        if ((converterOptions & EnumConverterOptionsAllowStrings) != 0)
        {
            if (typeInfo.Type.GetCustomAttribute<FlagsAttribute>() is not null)
            {
                // For enums implemented as flags do not surface values in the JSON schema.
                values = null;
            }
            else
            {
                var namingPolicy = (JsonNamingPolicy?)namingPolicyField!.GetValue(converter)!;
                string[] names = Enum.GetNames(typeInfo.Type);
                values = new JsonArray();
                foreach (string name in names)
                {
                    string effectiveName = namingPolicy?.ConvertName(name) ?? name;
                    values.Add((JsonNode)effectiveName);
                }
            }
 
            return true;
        }
 
        values = null;
        return false;
    }
 
#if NETCOREAPP
    [RequiresUnreferencedCode("Resolves unreferenced member metadata.")]
#endif
    private static FieldInfo GetPrivateFieldWithPotentiallyTrimmedMetadata(this Type type, string fieldName)
    {
        FieldInfo? field = type.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
        if (field is null)
        {
            throw new InvalidOperationException(
                $"Could not resolve metadata for field '{fieldName}' in type '{type}'. " +
                "If running Native AOT ensure that the 'IlcTrimMetadata' property has been disabled.");
        }
 
        return field;
    }
 
    // Resolves the parameters of the deserialization constructor for a type, if they exist.
#if NETCOREAPP
    [UnconditionalSuppressMessage("Trimming", "IL2072:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.",
        Justification = "The deserialization constructor should have already been referenced by the source generator and therefore will not have been trimmed.")]
#endif
    private static Func<JsonPropertyInfo, ParameterInfo?> ResolveJsonConstructorParameterMapper(JsonTypeInfo typeInfo)
    {
        Debug.Assert(typeInfo.Kind is JsonTypeInfoKind.Object);
 
        if (typeInfo.Properties.Count > 0 &&
            typeInfo.CreateObject is null && // Ensure that a default constructor isn't being used
            typeInfo.Type.TryGetDeserializationConstructor(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 configuration.
                        dict[new(parameter.Name, parameter.ParameterType)] = parameter;
                    }
                }
 
                return prop => dict.TryGetValue(new(prop.Name, prop.PropertyType), out ParameterInfo? parameter) ? parameter : null;
            }
        }
 
        return static _ => 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);
    }
 
    // 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(
#if NETCOREAPP
        [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)]
#endif
        this 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;
    }
 
    private static bool IsBuiltInConverter(JsonConverter converter) =>
        converter.GetType().Assembly == typeof(JsonConverter).Assembly;
 
    // Resolves the nullable reference type annotations for a property or field,
    // additionally addressing a few known bugs of the NullabilityInfo pre .NET 9.
    private static NullabilityInfo GetMemberNullability(this NullabilityInfoContext context, MemberInfo memberInfo)
    {
        Debug.Assert(memberInfo is PropertyInfo or FieldInfo);
        return memberInfo is PropertyInfo prop
            ? context.Create(prop)
            : context.Create((FieldInfo)memberInfo);
    }
 
    private static NullabilityState GetParameterNullability(this NullabilityInfoContext context, ParameterInfo parameterInfo)
    {
        // Workaround for https://github.com/dotnet/runtime/issues/92487
        if (parameterInfo.GetGenericParameterDefinition() 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;
 
#if NETCOREAPP
            [UnconditionalSuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.",
                Justification = "We're resolving private fields of the built-in enum converter which cannot have been trimmed away.")]
#endif
            static byte[]? GetNullableFlags(MemberInfo member)
            {
                Attribute? attr = member.GetCustomAttributes().FirstOrDefault(attr =>
                {
                    Type attrType = attr.GetType();
                    return attrType.Namespace == "System.Runtime.CompilerServices" && attrType.Name == "NullableAttribute";
                });
 
                return (byte[])attr?.GetType().GetField("NullableFlags")?.GetValue(attr)!;
            }
 
#if NETCOREAPP
            [UnconditionalSuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.",
                Justification = "We're resolving private fields of the built-in enum converter which cannot have been trimmed away.")]
#endif
            static byte? GetNullableContextFlag(MemberInfo member)
            {
                Attribute? attr = member.GetCustomAttributes().FirstOrDefault(attr =>
                {
                    Type attrType = attr.GetType();
                    return attrType.Namespace == "System.Runtime.CompilerServices" && attrType.Name == "NullableContextAttribute";
                });
 
                return (byte?)attr?.GetType().GetField("Flag")?.GetValue(attr)!;
            }
 
            static NullabilityState TranslateByte(byte b) =>
                b switch
                {
                    1 => NullabilityState.NotNull,
                    2 => NullabilityState.Nullable,
                    _ => NullabilityState.Unknown
                };
        }
 
        return context.Create(parameterInfo).WriteState;
    }
 
    private static ParameterInfo GetGenericParameterDefinition(this ParameterInfo parameter)
    {
        if (parameter.Member is { DeclaringType.IsConstructedGenericType: true }
                             or MethodInfo { IsGenericMethod: true, IsGenericMethodDefinition: false })
        {
            var genericMethod = (MethodBase)parameter.Member.GetGenericMemberDefinition()!;
            return genericMethod.GetParameters()[parameter.Position];
        }
 
        return parameter;
    }
 
#if NETCOREAPP
    [UnconditionalSuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.",
        Justification = "Looking up the generic member definition of the provided member.")]
#endif
    private static MemberInfo GetGenericMemberDefinition(this MemberInfo member)
    {
        if (member is Type type)
        {
            return type.IsConstructedGenericType ? type.GetGenericTypeDefinition() : type;
        }
 
        if (member.DeclaringType!.IsConstructedGenericType)
        {
            const BindingFlags AllMemberFlags =
                BindingFlags.Static | BindingFlags.Instance |
                BindingFlags.Public | BindingFlags.NonPublic;
 
            return member.DeclaringType.GetGenericTypeDefinition()
                .GetMember(member.Name, AllMemberFlags)
                .First(m => m.MetadataToken == member.MetadataToken);
        }
 
        if (member is MethodInfo { IsGenericMethod: true, IsGenericMethodDefinition: false } method)
        {
            return method.GetGenericMethodDefinition();
        }
 
        return member;
    }
 
    // Taken from https://github.com/dotnet/runtime/blob/903bc019427ca07080530751151ea636168ad334/src/libraries/System.Text.Json/Common/ReflectionExtensions.cs#L288-L317
    private static object? GetNormalizedDefaultValue(this 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;
    }
}