From 2b2393193236d3c7cfa7351bab2b0f1609b921fc Mon Sep 17 00:00:00 2001 From: DamianEdwards Date: Tue, 14 Sep 2021 12:36:21 -0700 Subject: [PATCH] Generate & cache fast property getter Fixes #10 --- ThirdPartyNotices.txt | 36 +++++ src/MinimalValidation/MinimalValidation.cs | 2 +- src/MinimalValidation/PropertyHelper.cs | 146 +++++++++++++++++++++ src/MinimalValidation/TypeDetailsCache.cs | 15 +-- 4 files changed, 190 insertions(+), 9 deletions(-) create mode 100644 ThirdPartyNotices.txt create mode 100644 src/MinimalValidation/PropertyHelper.cs diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt new file mode 100644 index 0000000..fb36677 --- /dev/null +++ b/ThirdPartyNotices.txt @@ -0,0 +1,36 @@ +MinimalValidation uses third-party libraries or other resources that may be +distributed under licenses different than the MinimalValidation software. + +In the event that I accidentally failed to list a required notice, please +bring it to my attention. Post an issue or email me: + + damian@damianedwards.com + +The attached notices are provided for information only. + +License notice for ASP.NET Core (https://github.com/dotnet/aspnetcore/blob/main/LICENSE.txt) +------------------------------------ + +The MIT License (MIT) + +Copyright (c) .NET Foundation and Contributors + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/src/MinimalValidation/MinimalValidation.cs b/src/MinimalValidation/MinimalValidation.cs index 10aa563..01a15e6 100644 --- a/src/MinimalValidation/MinimalValidation.cs +++ b/src/MinimalValidation/MinimalValidation.cs @@ -91,7 +91,7 @@ private static bool TryValidateImpl( var validationContext = new ValidationContext(target) { MemberName = property.Name }; var validationResults = new List(); var propertyValue = property.GetValue(target); - var propertyIsValid = Validator.TryValidateValue(propertyValue, validationContext, validationResults, property.ValidationAttributes); + var propertyIsValid = Validator.TryValidateValue(propertyValue!, validationContext, validationResults, property.ValidationAttributes); if (!propertyIsValid) { ProcessValidationResults(property.Name, validationResults, errors, prefix); diff --git a/src/MinimalValidation/PropertyHelper.cs b/src/MinimalValidation/PropertyHelper.cs new file mode 100644 index 0000000..e4b9c28 --- /dev/null +++ b/src/MinimalValidation/PropertyHelper.cs @@ -0,0 +1,146 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// Code in this file is taken from https://github.dev/dotnet/aspnetcore + +using System; +using System.Diagnostics; +using System.Reflection; + +namespace MinimalValidationLib +{ + internal static class PropertyHelper + { + private const BindingFlags DeclaredOnlyLookup = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly; + + // Delegate type for a by-ref property getter + private delegate TValue ByRefFunc(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)!; + + 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!.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; + } + + // 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); + } + } +} diff --git a/src/MinimalValidation/TypeDetailsCache.cs b/src/MinimalValidation/TypeDetailsCache.cs index 27b06a9..d8503ec 100644 --- a/src/MinimalValidation/TypeDetailsCache.cs +++ b/src/MinimalValidation/TypeDetailsCache.cs @@ -9,7 +9,7 @@ namespace MinimalValidationLib { internal class TypeDetailsCache { - private static readonly PropertyDetails[] _emptyPropertyDetails = new PropertyDetails[0]; + private static readonly PropertyDetails[] _emptyPropertyDetails = Array.Empty(); private readonly ConcurrentDictionary _cache = new(); public PropertyDetails[] Get(Type type) @@ -44,7 +44,7 @@ private void Visit(Type type, HashSet visited) var hasPropertiesOfOwnType = false; var hasValidatableProperties = false; - foreach (var property in type.GetProperties()) + foreach (var property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public)) { if (property.GetIndexParameters().Length > 0) { @@ -66,7 +66,7 @@ private void Visit(Type type, HashSet visited) if (type == property.PropertyType && !hasSkipRecursionOnProperty) { propertiesToValidate ??= new List(); - propertiesToValidate.Add(new (property.Name, property, validationAttributes.ToArray(), true, enumerableType)); + propertiesToValidate.Add(new (property.Name, property.PropertyType, PropertyHelper.MakeNullSafeFastPropertyGetter(property), validationAttributes.ToArray(), true, enumerableType)); hasPropertiesOfOwnType = true; continue; } @@ -81,7 +81,7 @@ private void Visit(Type type, HashSet visited) if (recurse || hasValidationOnProperty) { propertiesToValidate ??= new List(); - propertiesToValidate.Add(new(property.Name, property, validationAttributes.ToArray(), recurse, enumerableTypeHasProperties ? enumerableType : null)); + propertiesToValidate.Add(new(property.Name, property.PropertyType, PropertyHelper.MakeNullSafeFastPropertyGetter(property), validationAttributes.ToArray(), recurse, enumerableTypeHasProperties ? enumerableType : null)); hasValidatableProperties = true; } } @@ -95,7 +95,7 @@ private void Visit(Type type, HashSet visited) var enumerableTypeHasProperties = property.EnumerableType != null && _cache.TryGetValue(property.EnumerableType, out var enumProperties) && enumProperties.Length > 0; - var keepProperty = property.PropertyInfo.PropertyType != type || (hasValidatableProperties || enumerableTypeHasProperties); + var keepProperty = property.Type != type || (hasValidatableProperties || enumerableTypeHasProperties); if (!keepProperty) { propertiesToValidate.RemoveAt(i); @@ -126,10 +126,9 @@ private void Visit(Type type, HashSet visited) } } - internal record PropertyDetails(string Name, PropertyInfo PropertyInfo, ValidationAttribute[] ValidationAttributes, bool Recurse, Type? EnumerableType) + internal record PropertyDetails(string Name, Type Type, Func PropertyGetter, ValidationAttribute[] ValidationAttributes, bool Recurse, Type? EnumerableType) { - // TODO: Replace this with cached property getter (aka FastPropertyGetter) - public object? GetValue(object target) => PropertyInfo.GetValue(target); + public object? GetValue(object target) => PropertyGetter(target); public bool IsEnumerable => EnumerableType != null; public bool HasValidationAttributes => ValidationAttributes.Length > 0; }