From 017b2f50bed2401a012d603d42ee3c23eabbd362 Mon Sep 17 00:00:00 2001 From: Theodore Tsirpanis Date: Sat, 11 Jul 2026 03:31:33 +0300 Subject: [PATCH 1/6] Fix nullability annotations of `MemberAccessor.CreateParameterizedConstructor`. --- .../System/Text/Json/Serialization/Metadata/MemberAccessor.cs | 2 +- .../Metadata/ReflectionEmitCachingMemberAccessor.cs | 2 +- .../Serialization/Metadata/ReflectionEmitMemberAccessor.cs | 4 ++-- .../Json/Serialization/Metadata/ReflectionMemberAccessor.cs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/MemberAccessor.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/MemberAccessor.cs index 9fbe468cebcced..d21125c268434f 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/MemberAccessor.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/MemberAccessor.cs @@ -54,7 +54,7 @@ static MemberAccessor Initialize() public abstract Func? CreateParameterlessConstructor(Type type, ConstructorInfo? constructorInfo); - public abstract Func CreateParameterizedConstructor(ConstructorInfo constructor); + public abstract Func CreateParameterizedConstructor(ConstructorInfo constructor); public abstract JsonTypeInfo.ParameterizedConstructorDelegate? CreateParameterizedConstructor(ConstructorInfo constructor); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionEmitCachingMemberAccessor.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionEmitCachingMemberAccessor.cs index 099c1fb0f93e54..0d80689e660a69 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionEmitCachingMemberAccessor.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionEmitCachingMemberAccessor.cs @@ -53,7 +53,7 @@ public override Func, TCollection> CreateImmutableEnumerab key: (nameof(CreateImmutableEnumerableCreateRangeDelegate), typeof((TCollection, TElement)), null), valueFactory: _ => _sourceAccessor.CreateImmutableEnumerableCreateRangeDelegate()); - public override Func CreateParameterizedConstructor(ConstructorInfo constructor) => + public override Func CreateParameterizedConstructor(ConstructorInfo constructor) => _cache.GetOrAdd( key: (nameof(CreateParameterizedConstructor), typeof(T), constructor), valueFactory: key => _sourceAccessor.CreateParameterizedConstructor((ConstructorInfo)key.member!)); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionEmitMemberAccessor.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionEmitMemberAccessor.cs index 5a9fde9c017305..836954b24872a3 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionEmitMemberAccessor.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionEmitMemberAccessor.cs @@ -68,8 +68,8 @@ public ReflectionEmitMemberAccessor() return CreateDelegate>(dynamicMethod); } - public override Func CreateParameterizedConstructor(ConstructorInfo constructor) => - CreateDelegate>(CreateParameterizedConstructor(constructor)); + public override Func CreateParameterizedConstructor(ConstructorInfo constructor) => + CreateDelegate>(CreateParameterizedConstructor(constructor)); private static DynamicMethod CreateParameterizedConstructor(ConstructorInfo constructor) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionMemberAccessor.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionMemberAccessor.cs index b0d9f2503f0840..069c121677d257 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionMemberAccessor.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionMemberAccessor.cs @@ -37,7 +37,7 @@ public ReflectionMemberAccessor() return () => ctorInfo.InvokeNoWrapExceptions(null); } - public override Func CreateParameterizedConstructor(ConstructorInfo constructor) + public override Func CreateParameterizedConstructor(ConstructorInfo constructor) { Type type = typeof(T); From c689d6c99dcb3b2b1e7bb138bc438e882b5719db Mon Sep 17 00:00:00 2001 From: Theodore Tsirpanis Date: Sat, 11 Jul 2026 04:16:04 +0300 Subject: [PATCH 2/6] Use constructor and method invokers in factory methods created by the reflection member accessor. --- .../Metadata/ReflectionMemberAccessor.cs | 122 +++++++++++------- 1 file changed, 75 insertions(+), 47 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionMemberAccessor.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionMemberAccessor.cs index 069c121677d257..70b30df54aff0b 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionMemberAccessor.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionMemberAccessor.cs @@ -34,7 +34,12 @@ public ReflectionMemberAccessor() : null; } +#if NET + ConstructorInvoker invoker = ConstructorInvoker.Create(ctorInfo); + return invoker.Invoke; +#else return () => ctorInfo.InvokeNoWrapExceptions(null); +#endif } public override Func CreateParameterizedConstructor(ConstructorInfo constructor) @@ -46,22 +51,24 @@ public ReflectionMemberAccessor() int parameterCount = constructor.GetParameters().Length; +#if NET + ConstructorInvoker invoker = ConstructorInvoker.Create(constructor); + return arguments => (T)invoker.Invoke(arguments.AsSpan(0, parameterCount)); +#else return (arguments) => { // The input array was rented from the shared ArrayPool, so its size is likely to be larger than the param count. // The emit equivalent of this method does not (need to) allocate here + transfer the objects. object[] argsToPass = new object[parameterCount]; - for (int i = 0; i < parameterCount; i++) - { - argsToPass[i] = arguments[i]; - } + Array.Copy(arguments, 0, argsToPass, 0, parameterCount); // Not wrapping in TargetInvocationException also plumbs ArgumentException through for // tuples with more than 7 generic parameters, e.g. // System.ArgumentException : The last element of an eight element tuple must be a Tuple. return (T)constructor.InvokeNoWrapExceptions(argsToPass); }; +#endif } public override JsonTypeInfo.ParameterizedConstructorDelegate? @@ -73,36 +80,55 @@ public override JsonTypeInfo.ParameterizedConstructorDelegate { - object[] arguments = new object[parameterCount]; +#if NET + switch (parameterCount) + { + case 0: + return (T)invoker.Invoke(); + case 1: + return (T)invoker.Invoke(arg0); + case 2: + return (T)invoker.Invoke(arg0, arg1); + case 3: + return (T)invoker.Invoke(arg0, arg1, arg2); + case 4: + return (T)invoker.Invoke(arg0, arg1, arg2, arg3); + default: + Debug.Fail("We shouldn't be here if there are more than 4 parameters."); + throw new InvalidOperationException(); + } +#else + object?[] arguments = new object?[parameterCount]; - for (int i = 0; i < parameterCount; i++) + switch (parameterCount) { - switch (i) - { - case 0: - arguments[0] = arg0!; - break; - case 1: - arguments[1] = arg1!; - break; - case 2: - arguments[2] = arg2!; - break; - case 3: - arguments[3] = arg3!; - break; - default: - Debug.Fail("We shouldn't be here if there are more than 4 parameters."); - throw new InvalidOperationException(); - } + case > 4: + Debug.Fail("We shouldn't be here if there are more than 4 parameters."); + throw new InvalidOperationException(); + case 4: + arguments[3] = arg3; + goto case 3; + case 3: + arguments[2] = arg2; + goto case 2; + case 2: + arguments[1] = arg1; + goto case 1; + case 1: + arguments[0] = arg0; + break; } return (T)constructor.InvokeNoWrapExceptions(arguments); +#endif }; } @@ -114,10 +140,12 @@ public override JsonTypeInfo.ParameterizedConstructorDelegate - { - return (T)constructor.InvokeNoWrapExceptions(new object?[] { value }); - }; +#if NET + ConstructorInvoker invoker = ConstructorInvoker.Create(constructor); + return value => (T)invoker.Invoke(value); +#else + return value => (T)constructor.InvokeNoWrapExceptions(new object?[] { value }); +#endif } public override Action CreateAddMethodDelegate<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TCollection>() @@ -127,11 +155,12 @@ public override JsonTypeInfo.ParameterizedConstructorDelegate invoker.Invoke(collection, element); +#else + return (collection, element) => addMethod.InvokeNoWrapExceptions(collection, new object[] { element }); +#endif } public override Func, TCollection> CreateImmutableEnumerableCreateRangeDelegate() @@ -150,32 +179,31 @@ public override Func>, TCollection> Creat public override Func CreatePropertyGetter(PropertyInfo propertyInfo) { - MethodInfo getMethodInfo = propertyInfo.GetMethod!; - - return delegate (object obj) - { - return (TProperty)getMethodInfo.InvokeNoWrapExceptions(obj, null)!; - }; + return CreatePropertyGetter(propertyInfo); } public override Func CreatePropertyGetter(PropertyInfo propertyInfo) { MethodInfo getMethodInfo = propertyInfo.GetMethod!; - return delegate (TDeclaringType obj) - { - return (TProperty)getMethodInfo.InvokeNoWrapExceptions(obj, null)!; - }; +#if NET + MethodInvoker invoker = MethodInvoker.Create(getMethodInfo); + return obj => (TProperty)invoker.Invoke(obj)!; +#else + return obj => (TProperty)getMethodInfo.InvokeNoWrapExceptions(obj, null)!; +#endif } public override Action CreatePropertySetter(PropertyInfo propertyInfo) { MethodInfo setMethodInfo = propertyInfo.SetMethod!; - return delegate (object obj, TProperty value) - { - setMethodInfo.InvokeNoWrapExceptions(obj, new object[] { value! }); - }; +#if NET + MethodInvoker invoker = MethodInvoker.Create(setMethodInfo); + return (obj, value) => invoker.Invoke(obj, value); +#else + return (obj, value) => setMethodInfo.InvokeNoWrapExceptions(obj, new object[] { value }); +#endif } public override Func CreateFieldGetter(FieldInfo fieldInfo) => From 0a7da8f7bd0ac924c3995305b1215c8d91fd5533 Mon Sep 17 00:00:00 2001 From: Theodore Tsirpanis Date: Sat, 11 Jul 2026 04:31:35 +0300 Subject: [PATCH 3/6] Use `IsDefined` to check for attribute's existence in a type. --- .../System.Text.Json/src/System/ReflectionExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs b/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs index 7d86bb22a98888..9e7d70d5d2e931 100644 --- a/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs +++ b/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs @@ -45,7 +45,7 @@ public static bool IsInSubtypeRelationshipWith(this Type type, Type other) => type.IsAssignableFromInternal(other) || other.IsAssignableFromInternal(type); private static bool HasJsonConstructorAttribute(ConstructorInfo constructorInfo) - => constructorInfo.GetCustomAttribute() != null; + => constructorInfo.IsDefined(typeof(JsonConstructorAttribute), inherit: false); public static bool HasRequiredMemberAttribute(this MemberInfo memberInfo) { From 8ddfc439e7526ebd450b465f4e2ee4a496996405 Mon Sep 17 00:00:00 2001 From: Theodore Tsirpanis Date: Sun, 19 Jul 2026 04:22:03 +0300 Subject: [PATCH 4/6] Create property getter with `CreateDelegate` if possible. --- .../src/System/ReflectionExtensions.cs | 5 +++++ .../Metadata/ReflectionMemberAccessor.cs | 14 +++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs b/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs index 9e7d70d5d2e931..43e0a5d679c91e 100644 --- a/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs +++ b/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs @@ -116,6 +116,11 @@ private static bool HasCustomAttributeWithName(this MemberInfo memberInfo, strin #endif } +#if !NET + public static T CreateDelegate(this MethodInfo methodInfo) where T : Delegate => + (T)methodInfo.CreateDelegate(typeof(T)); +#endif + /// /// Invokes without wrapping any exception thrown by the /// target method in a . This matches the behavior of diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionMemberAccessor.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionMemberAccessor.cs index 70b30df54aff0b..687f330e86916b 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionMemberAccessor.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionMemberAccessor.cs @@ -179,13 +179,25 @@ public override Func>, TCollection> Creat public override Func CreatePropertyGetter(PropertyInfo propertyInfo) { - return CreatePropertyGetter(propertyInfo); + MethodInfo getMethodInfo = propertyInfo.GetMethod!; +#if NET + MethodInvoker invoker = MethodInvoker.Create(getMethodInfo); + return obj => (TProperty)invoker.Invoke(obj)!; +#else + return obj => (TProperty)getMethodInfo.InvokeNoWrapExceptions(obj, null)!; +#endif } public override Func CreatePropertyGetter(PropertyInfo propertyInfo) { MethodInfo getMethodInfo = propertyInfo.GetMethod!; + // We can directly wrap a delegate over the getter only if the declaring type is a reference type. + if (!typeof(TDeclaringType).IsValueType) + { + return getMethodInfo.CreateDelegate>(); + } + #if NET MethodInvoker invoker = MethodInvoker.Create(getMethodInfo); return obj => (TProperty)invoker.Invoke(obj)!; From bfa81739dafb6e04b3922dd4ed3e5150958cdbd1 Mon Sep 17 00:00:00 2001 From: Theodore Tsirpanis Date: Sun, 19 Jul 2026 04:38:55 +0300 Subject: [PATCH 5/6] Use standard reflection polyfills. --- src/libraries/System.Text.Json/src/System.Text.Json.csproj | 1 + .../System.Text.Json/src/System/ReflectionExtensions.cs | 5 ----- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj index 0a80a0e73974c8..b588e933c2acba 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -21,6 +21,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET + diff --git a/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs b/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs index 43e0a5d679c91e..9e7d70d5d2e931 100644 --- a/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs +++ b/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs @@ -116,11 +116,6 @@ private static bool HasCustomAttributeWithName(this MemberInfo memberInfo, strin #endif } -#if !NET - public static T CreateDelegate(this MethodInfo methodInfo) where T : Delegate => - (T)methodInfo.CreateDelegate(typeof(T)); -#endif - /// /// Invokes without wrapping any exception thrown by the /// target method in a . This matches the behavior of From e94afd467676736c5cd4bdd64d1ce2fcf6d2b23f Mon Sep 17 00:00:00 2001 From: Theodore Tsirpanis Date: Sun, 19 Jul 2026 04:53:13 +0300 Subject: [PATCH 6/6] Use delegate that takes instance by reference, in value type property getters. --- .../Metadata/ReflectionMemberAccessor.cs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionMemberAccessor.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionMemberAccessor.cs index 687f330e86916b..775cb1323b07c3 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionMemberAccessor.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionMemberAccessor.cs @@ -188,22 +188,21 @@ public override Func CreatePropertyGetter(Property #endif } + private delegate TProperty ValueTypePropertyGetter(ref TDeclaringType obj); + public override Func CreatePropertyGetter(PropertyInfo propertyInfo) { MethodInfo getMethodInfo = propertyInfo.GetMethod!; - // We can directly wrap a delegate over the getter only if the declaring type is a reference type. - if (!typeof(TDeclaringType).IsValueType) + // If TDeclaringType is a value type, the instance is passed by reference, + // so we need to create a wrapper delegate to account for this. + if (typeof(TDeclaringType).IsValueType) { - return getMethodInfo.CreateDelegate>(); + ValueTypePropertyGetter f = getMethodInfo.CreateDelegate>(); + return obj => f(ref obj); } -#if NET - MethodInvoker invoker = MethodInvoker.Create(getMethodInfo); - return obj => (TProperty)invoker.Invoke(obj)!; -#else - return obj => (TProperty)getMethodInfo.InvokeNoWrapExceptions(obj, null)!; -#endif + return getMethodInfo.CreateDelegate>(); } public override Action CreatePropertySetter(PropertyInfo propertyInfo)