File: src\Shared\PropertyHelper\PropertyHelper.cs
Web Access
Project: src\src\Shared\test\Shared.Tests\Microsoft.AspNetCore.Shared.Tests.csproj (Microsoft.AspNetCore.Shared.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#nullable enable
 
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Reflection.Metadata;
using System.Runtime.CompilerServices;
 
[assembly: MetadataUpdateHandler(typeof(Microsoft.Extensions.Internal.PropertyHelper.MetadataUpdateHandler))]
 
namespace Microsoft.Extensions.Internal;
 
internal sealed class PropertyHelper
{
    private const BindingFlags DeclaredOnlyLookup = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly;
    private const BindingFlags Everything = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance;
 
    // Delegate type for a by-ref property getter
    private delegate TValue ByRefFunc<TDeclaringType, TValue>(ref TDeclaringType arg);
 
    private static readonly MethodInfo CallPropertyGetterOpenGenericMethod =
        typeof(PropertyHelper).GetMethod(nameof(CallPropertyGetter), DeclaredOnlyLookup)!;
 
    private static readonly MethodInfo CallPropertyGetterByReferenceOpenGenericMethod =
        typeof(PropertyHelper).GetMethod(nameof(CallPropertyGetterByReference), DeclaredOnlyLookup)!;
 
    private static readonly MethodInfo CallNullSafePropertyGetterOpenGenericMethod =
        typeof(PropertyHelper).GetMethod(nameof(CallNullSafePropertyGetter), DeclaredOnlyLookup)!;
 
    private static readonly MethodInfo CallNullSafePropertyGetterByReferenceOpenGenericMethod =
        typeof(PropertyHelper).GetMethod(nameof(CallNullSafePropertyGetterByReference), DeclaredOnlyLookup)!;
 
    private static readonly MethodInfo CallPropertySetterOpenGenericMethod =
        typeof(PropertyHelper).GetMethod(nameof(CallPropertySetter), DeclaredOnlyLookup)!;
 
    // Using an array rather than IEnumerable, as target will be called on the hot path numerous times.
    private static readonly ConcurrentDictionary<Type, PropertyHelper[]> PropertiesCache = new();
 
    private static readonly ConcurrentDictionary<Type, PropertyHelper[]> VisiblePropertiesCache = new();
 
    private Action<object, object?>? _valueSetter;
    private Func<object, object?>? _valueGetter;
 
    /// <summary>
    /// Initializes a fast <see cref="PropertyHelper"/>.
    /// This constructor does not cache the helper. For caching, use <see cref="GetProperties(Type)"/>.
    /// </summary>
    public PropertyHelper(PropertyInfo property)
    {
        Property = property ?? throw new ArgumentNullException(nameof(property));
        Name = property.Name;
    }
 
    /// <summary>
    /// Gets the backing <see cref="PropertyInfo"/>.
    /// </summary>
    public PropertyInfo Property { get; }
 
    /// <summary>
    /// Gets (or sets in derived types) the property name.
    /// </summary>
    public string Name { get; }
 
    /// <summary>
    /// Gets the property value getter.
    /// </summary>
    public Func<object, object?> ValueGetter
    {
        [RequiresUnreferencedCode("This API is not trim safe.")]
        get
        {
            return _valueGetter ??= MakeFastPropertyGetter(Property);
        }
    }
 
    /// <summary>
    /// Gets the property value setter.
    /// </summary>
    public Action<object, object?> ValueSetter
    {
        [RequiresUnreferencedCode("This API is not trim safe.")]
        get
        {
            return _valueSetter ??= MakeFastPropertySetter(Property);
        }
    }
 
    /// <summary>
    /// Returns the property value for the specified <paramref name="instance"/>.
    /// </summary>
    /// <param name="instance">The object whose property value will be returned.</param>
    /// <returns>The property value.</returns>
    [RequiresUnreferencedCode("This API is not trim safe.")]
    public object? GetValue(object instance)
    {
        return ValueGetter(instance);
    }
 
    /// <summary>
    /// Sets the property value for the specified <paramref name="instance" />.
    /// </summary>
    /// <param name="instance">The object whose property value will be set.</param>
    /// <param name="value">The property value.</param>
    [RequiresUnreferencedCode("This API is not trim safe.")]
    public void SetValue(object instance, object? value)
    {
        ValueSetter(instance, value);
    }
 
    /// <summary>
    /// Creates and caches fast property helpers that expose getters for every public get property on the
    /// specified type.
    /// </summary>
    /// <param name="type">The type to extract property accessors for.</param>
    /// <returns>A cached array of all public properties of the specified type.
    /// </returns>
    [RequiresUnreferencedCode("This API is not trim safe.")]
    public static PropertyHelper[] GetProperties(
        [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] Type type)
    {
        return GetProperties(type, PropertiesCache);
    }
 
    /// <summary>
    /// <para>
    /// Creates and caches fast property helpers that expose getters for every non-hidden get property
    /// on the specified type.
    /// </para>
    /// <para>
    /// <see cref="M:GetVisibleProperties"/> excludes properties defined on base types that have been
    /// hidden by definitions using the <c>new</c> keyword.
    /// </para>
    /// </summary>
    /// <param name="type">The type to extract property accessors for.</param>
    /// <returns>
    /// A cached array of all public properties of the specified type.
    /// </returns>
    [RequiresUnreferencedCode("This API is not trim safe.")]
    public static PropertyHelper[] GetVisibleProperties(
        [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] Type type)
    {
        return GetVisibleProperties(type, PropertiesCache, VisiblePropertiesCache);
    }
 
    /// <summary>
    /// Creates a single fast property getter. The result is not cached.
    /// </summary>
    /// <param name="propertyInfo">propertyInfo to extract the getter for.</param>
    /// <returns>a fast getter.</returns>
    /// <remarks>
    /// This method is more memory efficient than a dynamically compiled lambda, and about the
    /// same speed.
    /// </remarks>
    [RequiresUnreferencedCode("This API is not trimmer safe.")]
    public static Func<object, object?> MakeFastPropertyGetter(PropertyInfo propertyInfo)
    {
        Debug.Assert(propertyInfo != null);
 
        return MakeFastPropertyGetter(
            propertyInfo,
            CallPropertyGetterOpenGenericMethod,
            CallPropertyGetterByReferenceOpenGenericMethod);
    }
 
    /// <summary>
    /// Creates a single fast property getter which is safe for a null input object. The result is not cached.
    /// </summary>
    /// <param name="propertyInfo">propertyInfo to extract the getter for.</param>
    /// <returns>a fast getter.</returns>
    /// <remarks>
    /// This method is more memory efficient than a dynamically compiled lambda, and about the
    /// same speed.
    /// </remarks>
    [RequiresUnreferencedCode("This API is not trimmer safe.")]
    public static Func<object, object?> MakeNullSafeFastPropertyGetter(PropertyInfo propertyInfo)
    {
        Debug.Assert(propertyInfo != null);
 
        return MakeFastPropertyGetter(
            propertyInfo,
            CallNullSafePropertyGetterOpenGenericMethod,
            CallNullSafePropertyGetterByReferenceOpenGenericMethod);
    }
 
    [RequiresUnreferencedCode("This API is not trimmer safe.")]
    private static Func<object, object?> MakeFastPropertyGetter(
        PropertyInfo propertyInfo,
        MethodInfo propertyGetterWrapperMethod,
        MethodInfo propertyGetterByRefWrapperMethod)
    {
        Debug.Assert(propertyInfo != null);
 
        // Must be a generic method with a Func<,> parameter
        Debug.Assert(propertyGetterWrapperMethod != null);
        Debug.Assert(propertyGetterWrapperMethod.IsGenericMethodDefinition);
        Debug.Assert(propertyGetterWrapperMethod.GetParameters().Length == 2);
 
        // Must be a generic method with a ByRefFunc<,> parameter
        Debug.Assert(propertyGetterByRefWrapperMethod != null);
        Debug.Assert(propertyGetterByRefWrapperMethod.IsGenericMethodDefinition);
        Debug.Assert(propertyGetterByRefWrapperMethod.GetParameters().Length == 2);
 
        var getMethod = propertyInfo.GetMethod;
        Debug.Assert(getMethod != null);
        Debug.Assert(!getMethod.IsStatic);
        Debug.Assert(getMethod.GetParameters().Length == 0);
 
        // MakeGenericMethod + value type requires IsDynamicCodeSupported to be true.
        if (RuntimeFeature.IsDynamicCodeSupported)
        {
            // Instance methods in the CLR can be turned into static methods where the first parameter
            // is open over "target". This parameter is always passed by reference, so we have a code
            // path for value types and a code path for reference types.
            if (getMethod.DeclaringType!.IsValueType)
            {
                // Create a delegate (ref TDeclaringType) -> TValue
                return MakeFastPropertyGetter(
                    typeof(ByRefFunc<,>),
                    getMethod,
                    propertyGetterByRefWrapperMethod);
            }
            else
            {
                // Create a delegate TDeclaringType -> TValue
                return MakeFastPropertyGetter(
                    typeof(Func<,>),
                    getMethod,
                    propertyGetterWrapperMethod);
            }
        }
        else
        {
            return propertyInfo.GetValue;
        }
    }
 
    [RequiresUnreferencedCode("This API is not trimmer safe.")]
    [RequiresDynamicCode("This API requires dynamic code because it makes generic types which may be filled with ValueTypes.")]
    private static Func<object, object?> MakeFastPropertyGetter(
        Type openGenericDelegateType,
        MethodInfo propertyGetMethod,
        MethodInfo openGenericWrapperMethod)
    {
        var typeInput = propertyGetMethod.DeclaringType!;
        var typeOutput = propertyGetMethod.ReturnType;
 
        var delegateType = openGenericDelegateType.MakeGenericType(typeInput, typeOutput);
        var propertyGetterDelegate = propertyGetMethod.CreateDelegate(delegateType);
 
        var wrapperDelegateMethod = openGenericWrapperMethod.MakeGenericMethod(typeInput, typeOutput);
        var accessorDelegate = wrapperDelegateMethod.CreateDelegate(
            typeof(Func<object, object?>),
            propertyGetterDelegate);
 
        return (Func<object, object?>)accessorDelegate;
    }
 
    /// <summary>
    /// Creates a single fast property setter for reference types. The result is not cached.
    /// </summary>
    /// <param name="propertyInfo">propertyInfo to extract the setter for.</param>
    /// <returns>a fast getter.</returns>
    /// <remarks>
    /// This method is more memory efficient than a dynamically compiled lambda, and about the
    /// same speed. This only works for reference types.
    /// </remarks>
    [RequiresUnreferencedCode("This API is not trimmer safe.")]
    public static Action<object, object?> MakeFastPropertySetter(PropertyInfo propertyInfo)
    {
        Debug.Assert(propertyInfo != null);
        Debug.Assert(!propertyInfo.DeclaringType!.IsValueType);
 
        var setMethod = propertyInfo.SetMethod;
        Debug.Assert(setMethod != null);
        Debug.Assert(!setMethod.IsStatic);
        Debug.Assert(setMethod.ReturnType == typeof(void));
        var parameters = setMethod.GetParameters();
        Debug.Assert(parameters.Length == 1);
 
        // MakeGenericMethod + value type requires IsDynamicCodeSupported to be true.
        if (RuntimeFeature.IsDynamicCodeSupported)
        {
            // Instance methods in the CLR can be turned into static methods where the first parameter
            // is open over "target". This parameter is always passed by reference, so we have a code
            // path for value types and a code path for reference types.
            var typeInput = setMethod.DeclaringType!;
            var parameterType = parameters[0].ParameterType;
 
            // Create a delegate TDeclaringType -> { TDeclaringType.Property = TValue; }
            var propertySetterAsAction =
                setMethod.CreateDelegate(typeof(Action<,>).MakeGenericType(typeInput, parameterType));
            var callPropertySetterClosedGenericMethod =
                CallPropertySetterOpenGenericMethod.MakeGenericMethod(typeInput, parameterType);
            var callPropertySetterDelegate =
                callPropertySetterClosedGenericMethod.CreateDelegate(
                    typeof(Action<object, object?>), propertySetterAsAction);
 
            return (Action<object, object?>)callPropertySetterDelegate;
        }
        else
        {
            return propertyInfo.SetValue;
        }
    }
 
    /// <summary>
    /// Given an object, adds each instance property with a public get method as a key and its
    /// associated value to a dictionary.
    ///
    /// If the object is already an <see cref="IDictionary{String, Object}"/> instance, then a copy
    /// is returned.
    /// </summary>
    /// <remarks>
    /// The implementation of PropertyHelper will cache the property accessors per-type. This is
    /// faster when the same type is used multiple times with ObjectToDictionary.
    /// </remarks>
    [RequiresUnreferencedCode("Method uses reflection to generate the dictionary.")]
    public static IDictionary<string, object?> ObjectToDictionary(object? value)
    {
        if (value is IDictionary<string, object?> dictionary)
        {
            return new Dictionary<string, object?>(dictionary, StringComparer.OrdinalIgnoreCase);
        }
 
        dictionary = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
 
        if (value is not null)
        {
            foreach (var helper in GetProperties(value.GetType(), PropertiesCache))
            {
                dictionary[helper.Name] = helper.GetValue(value);
            }
        }
 
        return dictionary;
    }
 
    // Called via reflection
    private static object? CallPropertyGetter<TDeclaringType, TValue>(
        Func<TDeclaringType, TValue> getter,
        object target)
    {
        return getter((TDeclaringType)target);
    }
 
    // Called via reflection
    private static object? CallPropertyGetterByReference<TDeclaringType, TValue>(
        ByRefFunc<TDeclaringType, TValue> getter,
        object target)
    {
        var unboxed = (TDeclaringType)target;
        return getter(ref unboxed);
    }
 
    // Called via reflection
    private static object? CallNullSafePropertyGetter<TDeclaringType, TValue>(
        Func<TDeclaringType, TValue> getter,
        object target)
    {
        if (target == null)
        {
            return null;
        }
 
        return getter((TDeclaringType)target);
    }
 
    // Called via reflection
    private static object? CallNullSafePropertyGetterByReference<TDeclaringType, TValue>(
        ByRefFunc<TDeclaringType, TValue> getter,
        object target)
    {
        if (target == null)
        {
            return null;
        }
 
        var unboxed = (TDeclaringType)target;
        return getter(ref unboxed);
    }
 
    private static void CallPropertySetter<TDeclaringType, TValue>(
        Action<TDeclaringType, TValue> setter,
        object target,
        object value)
    {
        setter((TDeclaringType)target, (TValue)value);
    }
 
    /// <summary>
    /// <para>
    /// Creates and caches fast property helpers that expose getters for every non-hidden get property
    /// on the specified type.
    /// </para>
    /// <para>
    /// <see cref="M:GetVisibleProperties"/> excludes properties defined on base types that have been
    /// hidden by definitions using the <c>new</c> keyword.
    /// </para>
    /// </summary>
    /// <param name="type">The type to extract property accessors for.</param>
    /// <param name="allPropertiesCache">The cache to store results in. Use <see cref="PropertiesCache"/> to use the default cache. Use <see langword="null"/> to avoid caching.</param>
    /// <param name="visiblePropertiesCache">The cache to store results in. Use <see cref="VisiblePropertiesCache"/> if the calling type does not have its own independent cache. Use <see langword="null"/> to avoid caching.</param>
    /// <returns>
    /// A cached array of all public properties of the specified type.
    /// </returns>
    [RequiresUnreferencedCode("This API is not trim safe.")]
    public static PropertyHelper[] GetVisibleProperties(
        Type type,
        ConcurrentDictionary<Type, PropertyHelper[]>? allPropertiesCache,
        ConcurrentDictionary<Type, PropertyHelper[]>? visiblePropertiesCache)
    {
        if (visiblePropertiesCache is not null && visiblePropertiesCache.TryGetValue(type, out var result))
        {
            return result;
        }
 
        // The simple and common case, this is normal POCO object - no need to allocate.
        var allPropertiesDefinedOnType = true;
        var allProperties = GetProperties(type, allPropertiesCache);
        foreach (var propertyHelper in allProperties)
        {
            if (propertyHelper.Property.DeclaringType != type)
            {
                allPropertiesDefinedOnType = false;
                break;
            }
        }
 
        if (allPropertiesDefinedOnType)
        {
            result = allProperties;
            visiblePropertiesCache?.TryAdd(type, result);
            return result;
        }
 
        // There's some inherited properties here, so we need to check for hiding via 'new'.
        var filteredProperties = new List<PropertyHelper>(allProperties.Length);
        foreach (var propertyHelper in allProperties)
        {
            var declaringType = propertyHelper.Property.DeclaringType;
            if (declaringType == type)
            {
                filteredProperties.Add(propertyHelper);
                continue;
            }
 
            // If this property was declared on a base type then look for the definition closest to the
            // the type to see if we should include it.
            var ignoreProperty = false;
 
            // Walk up the hierarchy until we find the type that actually declares this
            // PropertyInfo.
            Type? currentType = type;
            while (currentType != null && currentType != declaringType)
            {
                // We've found a 'more proximal' public definition
                var declaredProperty = currentType.GetProperty(propertyHelper.Name, DeclaredOnlyLookup);
                if (declaredProperty != null)
                {
                    ignoreProperty = true;
                    break;
                }
 
                currentType = currentType.BaseType;
            }
 
            if (!ignoreProperty)
            {
                filteredProperties.Add(propertyHelper);
            }
        }
 
        result = filteredProperties.ToArray();
        visiblePropertiesCache?.TryAdd(type, result);
        return result;
    }
 
    /// <summary>
    /// Creates and caches fast property helpers that expose getters for every public get property on the
    /// specified type.
    /// </summary>
    /// <param name="type">The type to extract property accessors for.</param>
    /// <param name="cache">The cache to store results in. Use <see cref="PropertiesCache"/> to use the default cache. Use <see langword="null"/> to avoid caching.</param>
    /// <returns>A cached array of all public properties of the specified type.
    /// </returns>
    // There isn't a way to represent trimmability requirements since for type since we unwrap nullable types.
    [RequiresUnreferencedCode("This API is not trim safe.")]
    public static PropertyHelper[] GetProperties(
        Type type,
        ConcurrentDictionary<Type, PropertyHelper[]>? cache)
    {
        // Unwrap nullable types. This means Nullable<T>.Value and Nullable<T>.HasValue will not be
        // part of the sequence of properties returned by this method.
        type = Nullable.GetUnderlyingType(type) ?? type;
 
        if (cache is null || !cache.TryGetValue(type, out var result))
        {
            var propertyHelpers = new List<PropertyHelper>();
            // We avoid loading indexed properties using the Where statement.
            AddInterestingProperties(propertyHelpers, type);
 
            if (type.IsInterface)
            {
                // Reflection does not return information about inherited properties on the interface itself.
                foreach (var @interface in type.GetInterfaces())
                {
                    AddInterestingProperties(propertyHelpers, @interface);
                }
            }
 
            result = propertyHelpers.ToArray();
            cache?.TryAdd(type, result);
        }
 
        return result;
 
        static void AddInterestingProperties(
            List<PropertyHelper> propertyHelpers,
            [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] Type type)
        {
            foreach (var property in type.GetProperties(Everything))
            {
                if (!IsInterestingProperty(property))
                {
                    continue;
                }
 
                propertyHelpers.Add(new PropertyHelper(property));
            }
        }
    }
 
    private static bool IsInterestingProperty(PropertyInfo property)
    {
        // For improving application startup time, do not use GetIndexParameters() api early in this check as it
        // creates a copy of parameter array and also we would like to check for the presence of a get method
        // and short circuit asap.
        return
            property.GetMethod != null &&
            property.GetMethod.IsPublic &&
            !property.GetMethod.IsStatic &&
 
            // PropertyHelper can't really interact with ref-struct properties since they can't be
            // boxed and can't be used as generic types. We just ignore them.
            //
            // see: https://github.com/aspnet/Mvc/issues/8545
            !property.PropertyType.IsByRefLike &&
 
            // Indexed properties are not useful (or valid) for grabbing properties off an object.
            property.GetMethod.GetParameters().Length == 0;
    }
 
    internal static class MetadataUpdateHandler
    {
        /// <summary>
        /// Invoked as part of <see cref="MetadataUpdateHandlerAttribute" /> contract for hot reload.
        /// </summary>
        public static void ClearCache(Type[]? _)
        {
            PropertiesCache.Clear();
            VisiblePropertiesCache.Clear();
        }
    }
}