// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; namespace Microsoft.Extensions.Internal { internal class PropertyHelper { // Delegate type for a by-ref property getter private delegate TValue ByRefFunc(ref TDeclaringType arg); private static readonly MethodInfo CallPropertyGetterOpenGenericMethod = typeof(PropertyHelper).GetTypeInfo().GetDeclaredMethod(nameof(CallPropertyGetter)); private static readonly MethodInfo CallPropertyGetterByReferenceOpenGenericMethod = typeof(PropertyHelper).GetTypeInfo().GetDeclaredMethod(nameof(CallPropertyGetterByReference)); private static readonly MethodInfo CallNullSafePropertyGetterOpenGenericMethod = typeof(PropertyHelper).GetTypeInfo().GetDeclaredMethod(nameof(CallNullSafePropertyGetter)); private static readonly MethodInfo CallNullSafePropertyGetterByReferenceOpenGenericMethod = typeof(PropertyHelper).GetTypeInfo().GetDeclaredMethod(nameof(CallNullSafePropertyGetterByReference)); private static readonly MethodInfo CallPropertySetterOpenGenericMethod = typeof(PropertyHelper).GetTypeInfo().GetDeclaredMethod(nameof(CallPropertySetter)); // Using an array rather than IEnumerable, as target will be called on the hot path numerous times. private static readonly ConcurrentDictionary PropertiesCache = new ConcurrentDictionary(); private static readonly ConcurrentDictionary VisiblePropertiesCache = new ConcurrentDictionary(); private Action _valueSetter; private Func _valueGetter; /// /// Initializes a fast . /// This constructor does not cache the helper. For caching, use . /// public PropertyHelper(PropertyInfo property) { if (property == null) { throw new ArgumentNullException(nameof(property)); } Property = property; Name = property.Name; } /// /// Gets the backing . /// public PropertyInfo Property { get; } /// /// Gets (or sets in derived types) the property name. /// public virtual string Name { get; protected set; } /// /// Gets the property value getter. /// public Func ValueGetter { get { if (_valueGetter == null) { _valueGetter = MakeFastPropertyGetter(Property); } return _valueGetter; } } /// /// Gets the property value setter. /// public Action ValueSetter { get { if (_valueSetter == null) { _valueSetter = MakeFastPropertySetter(Property); } return _valueSetter; } } /// /// Returns the property value for the specified . /// /// The object whose property value will be returned. /// The property value. public object GetValue(object instance) { return ValueGetter(instance); } /// /// Sets the property value for the specified . /// /// The object whose property value will be set. /// The property value. public void SetValue(object instance, object value) { ValueSetter(instance, value); } /// /// Creates and caches fast property helpers that expose getters for every public get property on the /// underlying type. /// /// The type info to extract property accessors for. /// A cached array of all public properties of the specified type. /// public static PropertyHelper[] GetProperties(TypeInfo typeInfo) { return GetProperties(typeInfo.AsType()); } /// /// Creates and caches fast property helpers that expose getters for every public get property on the /// specified type. /// /// The type to extract property accessors for. /// A cached array of all public properties of the specified type. /// public static PropertyHelper[] GetProperties(Type type) { return GetProperties(type, CreateInstance, PropertiesCache); } /// /// /// Creates and caches fast property helpers that expose getters for every non-hidden get property /// on the specified type. /// /// /// excludes properties defined on base types that have been /// hidden by definitions using the new keyword. /// /// /// The type info to extract property accessors for. /// /// A cached array of all public properties of the specified type. /// public static PropertyHelper[] GetVisibleProperties(TypeInfo typeInfo) { return GetVisibleProperties(typeInfo.AsType(), CreateInstance, PropertiesCache, VisiblePropertiesCache); } /// /// /// Creates and caches fast property helpers that expose getters for every non-hidden get property /// on the specified type. /// /// /// excludes properties defined on base types that have been /// hidden by definitions using the new keyword. /// /// /// The type to extract property accessors for. /// /// A cached array of all public properties of the specified type. /// public static PropertyHelper[] GetVisibleProperties(Type type) { return GetVisibleProperties(type, CreateInstance, PropertiesCache, VisiblePropertiesCache); } /// /// Creates a single fast property getter. The result is not cached. /// /// propertyInfo to extract the getter for. /// a fast getter. /// /// This method is more memory efficient than a dynamically compiled lambda, and about the /// same speed. /// public static Func MakeFastPropertyGetter(PropertyInfo propertyInfo) { Debug.Assert(propertyInfo != null); return MakeFastPropertyGetter( propertyInfo, CallPropertyGetterOpenGenericMethod, CallPropertyGetterByReferenceOpenGenericMethod); } /// /// Creates a single fast property getter which is safe for a null input object. The result is not cached. /// /// propertyInfo to extract the getter for. /// a fast getter. /// /// This method is more memory efficient than a dynamically compiled lambda, and about the /// same speed. /// public static Func MakeNullSafeFastPropertyGetter(PropertyInfo propertyInfo) { Debug.Assert(propertyInfo != null); return MakeFastPropertyGetter( propertyInfo, CallNullSafePropertyGetterOpenGenericMethod, CallNullSafePropertyGetterByReferenceOpenGenericMethod); } private static Func 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); // 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.GetTypeInfo().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); } } private static Func 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), propertyGetterDelegate); return (Func)accessorDelegate; } /// /// Creates a single fast property setter for reference types. The result is not cached. /// /// propertyInfo to extract the setter for. /// a fast getter. /// /// This method is more memory efficient than a dynamically compiled lambda, and about the /// same speed. This only works for reference types. /// public static Action MakeFastPropertySetter(PropertyInfo propertyInfo) { Debug.Assert(propertyInfo != null); Debug.Assert(!propertyInfo.DeclaringType.GetTypeInfo().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); // 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), propertySetterAsAction); return (Action)callPropertySetterDelegate; } /// /// 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 instance, then a copy /// is returned. /// /// /// The implementation of PropertyHelper will cache the property accessors per-type. This is /// faster when the same type is used multiple times with ObjectToDictionary. /// public static IDictionary ObjectToDictionary(object value) { var dictionary = value as IDictionary; if (dictionary != null) { return new Dictionary(dictionary, StringComparer.OrdinalIgnoreCase); } dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); if (value != null) { foreach (var helper in GetProperties(value.GetType())) { dictionary[helper.Name] = helper.GetValue(value); } } return dictionary; } private static PropertyHelper CreateInstance(PropertyInfo property) { return new PropertyHelper(property); } // Called via reflection private static object CallPropertyGetter( Func getter, object target) { return getter((TDeclaringType)target); } // Called via reflection private static object CallPropertyGetterByReference( ByRefFunc getter, object target) { var unboxed = (TDeclaringType)target; return getter(ref unboxed); } // Called via reflection private static object CallNullSafePropertyGetter( Func getter, object target) { if (target == null) { return null; } return getter((TDeclaringType)target); } // Called via reflection private static object CallNullSafePropertyGetterByReference( ByRefFunc getter, object target) { if (target == null) { return null; } var unboxed = (TDeclaringType)target; return getter(ref unboxed); } private static void CallPropertySetter( Action setter, object target, object value) { setter((TDeclaringType)target, (TValue)value); } protected static PropertyHelper[] GetVisibleProperties( Type type, Func createPropertyHelper, ConcurrentDictionary allPropertiesCache, ConcurrentDictionary visiblePropertiesCache) { PropertyHelper[] result; if (visiblePropertiesCache.TryGetValue(type, out result)) { return result; } // The simple and common case, this is normal POCO object - no need to allocate. var allPropertiesDefinedOnType = true; var allProperties = GetProperties(type, createPropertyHelper, 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(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. var currentTypeInfo = type.GetTypeInfo(); var declaringTypeInfo = declaringType.GetTypeInfo(); while (currentTypeInfo != null && currentTypeInfo != declaringTypeInfo) { // We've found a 'more proximal' public definition var declaredProperty = currentTypeInfo.GetDeclaredProperty(propertyHelper.Name); if (declaredProperty != null) { ignoreProperty = true; break; } currentTypeInfo = currentTypeInfo.BaseType?.GetTypeInfo(); } if (!ignoreProperty) { filteredProperties.Add(propertyHelper); } } result = filteredProperties.ToArray(); visiblePropertiesCache.TryAdd(type, result); return result; } protected static PropertyHelper[] GetProperties( Type type, Func createPropertyHelper, ConcurrentDictionary cache) { // Unwrap nullable types. This means Nullable.Value and Nullable.HasValue will not be // part of the sequence of properties returned by this method. type = Nullable.GetUnderlyingType(type) ?? type; PropertyHelper[] helpers; if (!cache.TryGetValue(type, out helpers)) { // We avoid loading indexed properties using the Where statement. var properties = type.GetRuntimeProperties().Where(IsInterestingProperty); var typeInfo = type.GetTypeInfo(); if (typeInfo.IsInterface) { // Reflection does not return information about inherited properties on the interface itself. properties = properties.Concat(typeInfo.ImplementedInterfaces.SelectMany( interfaceType => interfaceType.GetRuntimeProperties().Where(IsInterestingProperty))); } helpers = properties.Select(p => createPropertyHelper(p)).ToArray(); cache.TryAdd(type, helpers); } return helpers; } // Indexed properties are not useful (or valid) for grabbing properties off an object. 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 && property.GetMethod.GetParameters().Length == 0; } } }