From a3a57bc176e2ad7598268cd7d1f31f33b2fb1e20 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Mon, 24 Oct 2022 10:47:10 +0200 Subject: [PATCH] Improved Input Object Type inference and formatter compilation (#5498) --- .../Core/src/Types/SchemaBuilder.Setup.cs | 5 + .../Types/Types/Contracts/IFieldCollection.cs | 35 +++++++ .../Descriptors/InputObjectTypeDescriptor.cs | 81 +++++++++++++--- .../InputObjectTypeDescriptor~1.cs | 4 +- .../Types/Descriptors/ObjectTypeDescriptor.cs | 53 ++++------ .../Descriptors/ObjectTypeDescriptorBase~1.cs | 9 +- .../Core/src/Types/Types/FieldCollection.cs | 2 + .../Types/Helpers/FieldDescriptorUtilities.cs | 8 +- .../src/Types/Types/Helpers/TypeMemHelper.cs | 85 ++++++++++++++++ .../Serialization/InputObjectCompiler.cs | 42 ++++---- .../InputObjectConstructorResolver.cs | 97 ++++++++++++------- 11 files changed, 300 insertions(+), 121 deletions(-) create mode 100644 src/HotChocolate/Core/src/Types/Types/Helpers/TypeMemHelper.cs diff --git a/src/HotChocolate/Core/src/Types/SchemaBuilder.Setup.cs b/src/HotChocolate/Core/src/Types/SchemaBuilder.Setup.cs index cc7e19078e3..f0c1f0196e8 100644 --- a/src/HotChocolate/Core/src/Types/SchemaBuilder.Setup.cs +++ b/src/HotChocolate/Core/src/Types/SchemaBuilder.Setup.cs @@ -9,6 +9,7 @@ using HotChocolate.Types; using HotChocolate.Types.Descriptors; using HotChocolate.Types.Factories; +using HotChocolate.Types.Helpers; using HotChocolate.Types.Interceptors; using HotChocolate.Utilities; using HotChocolate.Utilities.Introspection; @@ -71,6 +72,10 @@ public static Schema Create(SchemaBuilder builder) context.SchemaInterceptor.OnError(context, ex); throw; } + finally + { + TypeMemHelper.Clear(); + } } public static DescriptorContext CreateContext( diff --git a/src/HotChocolate/Core/src/Types/Types/Contracts/IFieldCollection.cs b/src/HotChocolate/Core/src/Types/Types/Contracts/IFieldCollection.cs index 52a34955f58..a25c89efbd2 100644 --- a/src/HotChocolate/Core/src/Types/Types/Contracts/IFieldCollection.cs +++ b/src/HotChocolate/Core/src/Types/Types/Contracts/IFieldCollection.cs @@ -5,28 +5,63 @@ namespace HotChocolate.Types; +/// +/// Represents the field collection of a type. +/// +/// +/// The field type. +/// public interface IFieldCollection : IReadOnlyCollection where T : class, IField { + /// + /// Gets a field by its name. + /// T this[string fieldName] { get; } + /// + /// Gets a field by its index. + /// T this[int index] { get; } + /// + /// Checks if a field with the specified + /// exists in this collection. + /// + /// + /// The name of a field. + /// + /// + /// Returns true if a field with the specified exists; + /// otherwise, false. + /// bool ContainsField(string fieldName); } +/// +/// This helper class provides extensions to the interface +/// to allow for more efficiency when using the interface. +/// public static class FieldCollectionExtensions { + /// + /// Tries to get a field by its name from the field collection. + /// + /// + /// The type of the field. + /// public static bool TryGetField( this IFieldCollection collection, string fieldName, [NotNullWhen(true)] out T? field) where T : class, IField { + // if we use the default implementation we will use the TryGetField method on there. if (collection is FieldCollection fc) { return fc.TryGetField(fieldName, out field); } + // in any other case we simulate the behavior which is not as efficient. if (collection.ContainsField(fieldName)) { field = collection[fieldName]; diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/InputObjectTypeDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/InputObjectTypeDescriptor.cs index ace88ce4248..640ab7452f1 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/InputObjectTypeDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/InputObjectTypeDescriptor.cs @@ -1,3 +1,5 @@ +#nullable enable + using System; using System.Collections.Generic; using System.Linq; @@ -13,6 +15,8 @@ public class InputObjectTypeDescriptor : DescriptorBase , IInputObjectTypeDescriptor { + private readonly List _fields = new(); + protected InputObjectTypeDescriptor(IDescriptorContext context, Type runtimeType) : base(context) { @@ -43,14 +47,14 @@ protected InputObjectTypeDescriptor(IDescriptorContext context) foreach (var field in definition.Fields) { - Fields.Add(InputFieldDescriptor.From(Context, field)); + _fields.Add(InputFieldDescriptor.From(Context, field)); } } protected internal override InputObjectTypeDefinition Definition { get; protected set; } = new(); - protected List Fields { get; } = new(); + protected ICollection Fields => _fields; protected override void OnCreateDefinition( InputObjectTypeDefinition definition) @@ -64,28 +68,77 @@ protected InputObjectTypeDescriptor(IDescriptorContext context) Definition.AttributesAreApplied = true; } - var fields = new Dictionary(); - var handledProperties = new HashSet(); + var fields = TypeMemHelper.RentInputFieldDefinitionMap(); + var handledMembers = TypeMemHelper.RentMemberSet(); - FieldDescriptorUtilities.AddExplicitFields( - Fields.Select(t => t.CreateDefinition()), - f => f.Property, - fields, - handledProperties); + foreach (var fieldDescriptor in _fields) + { + var fieldDefinition = fieldDescriptor.CreateDefinition(); - OnCompleteFields(fields, handledProperties); + if (!fieldDefinition.Ignore && !string.IsNullOrEmpty(fieldDefinition.Name)) + { + fields[fieldDefinition.Name] = fieldDefinition; + } + if (fieldDefinition.Property is { } prop) + { + handledMembers.Add(prop); + } + } + + OnCompleteFields(fields, handledMembers); + + Definition.Fields.Clear(); Definition.Fields.AddRange(fields.Values); + TypeMemHelper.Return(fields); + TypeMemHelper.Return(handledMembers); + base.OnCreateDefinition(definition); } - protected virtual void OnCompleteFields( + protected void InferFieldsFromFieldBindingType( IDictionary fields, - ISet handledProperties) + ISet handledMembers) { + if (Definition.Fields.IsImplicitBinding()) + { + var inspector = Context.TypeInspector; + var naming = Context.Naming; + var type = Definition.RuntimeType; + var members = inspector.GetMembers(type); + + foreach (var member in members) + { + if (member.MemberType is MemberTypes.Property) + { + var name = naming.GetMemberName(member, MemberKind.InputObjectField); + + if (handledMembers.Add(member) && + !fields.ContainsKey(name)) + { + var descriptor = InputFieldDescriptor.New( + Context, + (PropertyInfo)member); + + _fields.Add(descriptor); + handledMembers.Add(member); + + // the create definition call will trigger the OnCompleteField call + // on the field description and trigger the initialization of the + // fields arguments. + fields[name] = descriptor.CreateDefinition(); + } + } + } + } } + protected virtual void OnCompleteFields( + IDictionary fields, + ISet handledProperties) + { } + public IInputObjectTypeDescriptor SyntaxNode( InputObjectTypeDefinitionNode inputObjectTypeDefinition) { @@ -107,7 +160,7 @@ public IInputObjectTypeDescriptor Description(string value) public IInputFieldDescriptor Field(string name) { - var fieldDescriptor = Fields.FirstOrDefault(t => t.Definition.Name.EqualsOrdinal(name)); + var fieldDescriptor = _fields.Find(t => t.Definition.Name.EqualsOrdinal(name)); if (fieldDescriptor is not null) { @@ -115,7 +168,7 @@ public IInputFieldDescriptor Field(string name) } fieldDescriptor = new InputFieldDescriptor(Context, name); - Fields.Add(fieldDescriptor); + _fields.Add(fieldDescriptor); return fieldDescriptor; } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/InputObjectTypeDescriptor~1.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/InputObjectTypeDescriptor~1.cs index b2dcd073de7..c407f95a414 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/InputObjectTypeDescriptor~1.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/InputObjectTypeDescriptor~1.cs @@ -33,14 +33,14 @@ protected internal InputObjectTypeDescriptor(IDescriptorContext context) protected override void OnCompleteFields( IDictionary fields, - ISet handledProperties) + ISet handledProperties) { if (Definition.Fields.IsImplicitBinding()) { FieldDescriptorUtilities.AddImplicitFields( this, p => InputFieldDescriptor - .New(Context, p) + .New(Context, (PropertyInfo)p) .CreateDefinition(), fields, handledProperties); diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectTypeDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectTypeDescriptor.cs index 28f6d61f37f..2778aaeddd8 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectTypeDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectTypeDescriptor.cs @@ -1,10 +1,7 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; using System.Reflection; -using System.Runtime.InteropServices; -using System.Threading; using HotChocolate.Language; using HotChocolate.Types.Descriptors.Definitions; using HotChocolate.Types.Helpers; @@ -18,11 +15,9 @@ namespace HotChocolate.Types.Descriptors; public class ObjectTypeDescriptor : DescriptorBase - , IObjectTypeDescriptor + , IObjectTypeDescriptor { private readonly List _fields = new(); - private static Dictionary? _definitionMap = null; - private static HashSet? _memberSet = null; protected ObjectTypeDescriptor(IDescriptorContext context, Type clrType) : base(context) @@ -52,7 +47,7 @@ protected ObjectTypeDescriptor(IDescriptorContext context) foreach (var field in definition.Fields) { - Fields.Add(ObjectFieldDescriptor.From(Context, field)); + _fields.Add(ObjectFieldDescriptor.From(Context, field)); } } @@ -84,10 +79,8 @@ protected ObjectTypeDescriptor(IDescriptorContext context) } } - var fields = Interlocked.Exchange(ref _definitionMap, null) ?? - new Dictionary(); - var handledMembers = - Interlocked.Exchange(ref _memberSet, null) ?? new HashSet(); + var fields = TypeMemHelper.RentObjectFieldDefinitionMap(); + var handledMembers = TypeMemHelper.RentMemberSet(); foreach (var fieldDescriptor in _fields) { @@ -116,29 +109,21 @@ protected ObjectTypeDescriptor(IDescriptorContext context) Definition.Fields.Clear(); Definition.Fields.AddRange(fields.Values); - fields.Clear(); - handledMembers.Clear(); - - Interlocked.CompareExchange(ref _definitionMap, fields, null); - Interlocked.CompareExchange(ref _memberSet, handledMembers, null); + TypeMemHelper.Return(fields); + TypeMemHelper.Return(handledMembers); base.OnCreateDefinition(definition); } internal void InferFieldsFromFieldBindingType() { - var fields = Interlocked.Exchange(ref _definitionMap, null) ?? - new Dictionary(); - var handledMembers = - Interlocked.Exchange(ref _memberSet, null) ?? new HashSet(); + var fields = TypeMemHelper.RentObjectFieldDefinitionMap(); + var handledMembers = TypeMemHelper.RentMemberSet(); InferFieldsFromFieldBindingType(fields, handledMembers); - fields.Clear(); - handledMembers.Clear(); - - Interlocked.CompareExchange(ref _definitionMap, fields, null); - Interlocked.CompareExchange(ref _memberSet, handledMembers, null); + TypeMemHelper.Return(fields); + TypeMemHelper.Return(handledMembers); } protected void InferFieldsFromFieldBindingType( @@ -176,7 +161,7 @@ internal void InferFieldsFromFieldBindingType() descriptor.Ignore(); } - Fields.Add(descriptor); + _fields.Add(descriptor); handledMembers.Add(member); // the create definition call will trigger the OnCompleteField call @@ -305,7 +290,7 @@ public IObjectTypeDescriptor IsOfType(IsOfType? isOfType) public IObjectFieldDescriptor Field(string name) { - var fieldDescriptor = Fields.FirstOrDefault(t => t.Definition.Name.EqualsOrdinal(name)); + var fieldDescriptor = _fields.Find(t => t.Definition.Name.EqualsOrdinal(name)); if (fieldDescriptor is not null) { @@ -313,7 +298,7 @@ public IObjectFieldDescriptor Field(string name) } fieldDescriptor = ObjectFieldDescriptor.New(Context, name); - Fields.Add(fieldDescriptor); + _fields.Add(fieldDescriptor); return fieldDescriptor; } @@ -331,8 +316,7 @@ public IObjectFieldDescriptor Field(string name) if (propertyOrMethod is PropertyInfo || propertyOrMethod is MethodInfo) { - var fieldDescriptor = Fields.FirstOrDefault( - t => t.Definition.Member == propertyOrMethod); + var fieldDescriptor = _fields.Find(t => t.Definition.Member == propertyOrMethod); if (fieldDescriptor is not null) { @@ -344,7 +328,7 @@ public IObjectFieldDescriptor Field(string name) propertyOrMethod, Definition.RuntimeType, propertyOrMethod.ReflectedType ?? Definition.RuntimeType); - Fields.Add(fieldDescriptor); + _fields.Add(fieldDescriptor); return fieldDescriptor; } @@ -365,8 +349,7 @@ public IObjectFieldDescriptor Field(string name) if (member is PropertyInfo or MethodInfo) { - var fieldDescriptor = Fields.FirstOrDefault( - t => t.Definition.Member == member); + var fieldDescriptor = _fields.Find(t => t.Definition.Member == member); if (fieldDescriptor is not null) { @@ -378,7 +361,7 @@ public IObjectFieldDescriptor Field(string name) member, Definition.RuntimeType, typeof(TResolver)); - Fields.Add(fieldDescriptor); + _fields.Add(fieldDescriptor); return fieldDescriptor; } @@ -389,7 +372,7 @@ public IObjectFieldDescriptor Field(string name) propertyOrMethod, Definition.RuntimeType, typeof(TResolver)); - Fields.Add(fieldDescriptor); + _fields.Add(fieldDescriptor); return fieldDescriptor; } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectTypeDescriptorBase~1.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectTypeDescriptorBase~1.cs index b856124b7be..23a6e3b7677 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectTypeDescriptorBase~1.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectTypeDescriptorBase~1.cs @@ -144,15 +144,11 @@ public new IObjectTypeDescriptor IsOfType(IsOfType isOfType) public IObjectFieldDescriptor Field( Expression> propertyOrMethod) - { - return base.Field(propertyOrMethod); - } + => base.Field(propertyOrMethod); public IObjectFieldDescriptor Field( Expression> propertyOrMethod) - { - return base.Field(propertyOrMethod); - } + => base.Field(propertyOrMethod); public new IObjectTypeDescriptor Directive( TDirective directiveInstance) @@ -177,4 +173,3 @@ public new IObjectTypeDescriptor Directive() return this; } } - diff --git a/src/HotChocolate/Core/src/Types/Types/FieldCollection.cs b/src/HotChocolate/Core/src/Types/Types/FieldCollection.cs index 1c18afaaaba..4fdc3562433 100644 --- a/src/HotChocolate/Core/src/Types/Types/FieldCollection.cs +++ b/src/HotChocolate/Core/src/Types/Types/FieldCollection.cs @@ -56,6 +56,8 @@ public bool TryGetField(string fieldName, [NotNullWhen(true)] out T? field) return false; } + internal ReadOnlySpan AsSpan() => _fields; + public IEnumerator GetEnumerator() => _fields.Length == 0 ? EmptyFieldEnumerator.Instance diff --git a/src/HotChocolate/Core/src/Types/Types/Helpers/FieldDescriptorUtilities.cs b/src/HotChocolate/Core/src/Types/Types/Helpers/FieldDescriptorUtilities.cs index 5c1eae5cb48..c30e1b71fc4 100644 --- a/src/HotChocolate/Core/src/Types/Types/Helpers/FieldDescriptorUtilities.cs +++ b/src/HotChocolate/Core/src/Types/Types/Helpers/FieldDescriptorUtilities.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Reflection; -using System.Threading; using HotChocolate.Internal; using HotChocolate.Types.Descriptors; using HotChocolate.Types.Descriptors.Definitions; @@ -12,8 +11,6 @@ namespace HotChocolate.Types.Helpers; public static class FieldDescriptorUtilities { - private static HashSet? _names = new(StringComparer.Ordinal); - public static void AddExplicitFields( IEnumerable fieldDefinitions, Func resolveMember, @@ -111,7 +108,7 @@ public static class FieldDescriptorUtilities if (member is MethodInfo method) { - var processedNames = Interlocked.Exchange(ref _names, null) ?? new(); + var processedNames = TypeMemHelper.RentNameSet(); try { @@ -142,8 +139,7 @@ public static class FieldDescriptorUtilities } finally { - processedNames.Clear(); - Interlocked.CompareExchange(ref _names, processedNames, null); + TypeMemHelper.Return(processedNames); } } } diff --git a/src/HotChocolate/Core/src/Types/Types/Helpers/TypeMemHelper.cs b/src/HotChocolate/Core/src/Types/Types/Helpers/TypeMemHelper.cs new file mode 100644 index 00000000000..f791868e00b --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Types/Helpers/TypeMemHelper.cs @@ -0,0 +1,85 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading; +using HotChocolate.Types.Descriptors.Definitions; + +namespace HotChocolate.Types.Helpers; + +/// +/// This internal helper is used centralize rented maps and list during type initialization. +/// This ensures that we can release these helper objects when the schema is created. +/// +internal static class TypeMemHelper +{ + private static Dictionary? _objectFieldDefinitionMap; + private static Dictionary? _inputFieldDefinitionMap; + private static Dictionary? _inputFieldMap; + private static HashSet? _memberSet; + private static HashSet? _nameSet; + + public static Dictionary RentObjectFieldDefinitionMap() + => Interlocked.Exchange(ref _objectFieldDefinitionMap, null) ?? + new Dictionary(StringComparer.Ordinal); + + public static void Return(Dictionary map) + { + map.Clear(); + Interlocked.CompareExchange(ref _objectFieldDefinitionMap, map, null); + } + + public static Dictionary RentInputFieldDefinitionMap() + => Interlocked.Exchange(ref _inputFieldDefinitionMap, null) ?? + new Dictionary(StringComparer.Ordinal); + + public static void Return(Dictionary map) + { + map.Clear(); + Interlocked.CompareExchange(ref _inputFieldDefinitionMap, map, null); + } + + public static Dictionary RentInputFieldMap() + => Interlocked.Exchange(ref _inputFieldMap, null) ?? + new Dictionary(StringComparer.Ordinal); + + public static void Return(Dictionary map) + { + map.Clear(); + Interlocked.CompareExchange(ref _inputFieldMap, map, null); + } + + public static HashSet RentMemberSet() + => Interlocked.Exchange(ref _memberSet, null) ?? + new HashSet(); + + public static void Return(HashSet set) + { + set.Clear(); + Interlocked.CompareExchange(ref _memberSet, set, null); + } + + public static HashSet RentNameSet() + => Interlocked.Exchange(ref _nameSet, null) ?? + new HashSet(StringComparer.Ordinal); + + public static void Return(HashSet set) + { + set.Clear(); + Interlocked.CompareExchange(ref _nameSet, set, null); + } + + // We allow the helper to clear all pooled objects so that after + // building the schema we can release the memory. + // There is a risk of extra allocation here if we build + // multiple schemas at the same time. + public static void Clear() + { + Interlocked.Exchange(ref _objectFieldDefinitionMap, null); + Interlocked.Exchange(ref _inputFieldDefinitionMap, null); + Interlocked.Exchange(ref _inputFieldMap, null); + Interlocked.Exchange(ref _memberSet, null); + Interlocked.Exchange(ref _nameSet, null); + } +} diff --git a/src/HotChocolate/Core/src/Types/Utilities/Serialization/InputObjectCompiler.cs b/src/HotChocolate/Core/src/Types/Utilities/Serialization/InputObjectCompiler.cs index c77b1c288d3..67f413ce3db 100644 --- a/src/HotChocolate/Core/src/Types/Utilities/Serialization/InputObjectCompiler.cs +++ b/src/HotChocolate/Core/src/Types/Utilities/Serialization/InputObjectCompiler.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Linq.Expressions; using System.Reflection; using HotChocolate.Types; +using HotChocolate.Types.Helpers; using static HotChocolate.Utilities.Serialization.InputObjectConstructorResolver; #nullable enable @@ -22,8 +22,10 @@ internal static class InputObjectCompiler InputObjectType inputType, ConstructorInfo? constructor = null) { - var fields = CreateFieldMap(inputType); - constructor ??= GetCompatibleConstructor(inputType.RuntimeType, fields); + var fields = TypeMemHelper.RentInputFieldMap(); + BuildFieldMap(inputType, fields); + + constructor ??= GetCompatibleConstructor(inputType.RuntimeType, inputType, fields); var instance = constructor is null ? Expression.New(inputType.RuntimeType) @@ -48,8 +50,6 @@ internal static class InputObjectCompiler public static Action CompileGetFieldValues(InputObjectType inputType) { - var fields = CreateFieldMap(inputType); - Expression instance = _obj; if (inputType.RuntimeType != typeof(object)) @@ -58,7 +58,14 @@ internal static class InputObjectCompiler } var expressions = new List(); - CompileGetProperties(instance, fields.Values, _fieldValues, expressions); + + foreach (var field in inputType.Fields.AsSpan()) + { + var getter = field.Property!.GetGetMethod(true)!; + Expression fieldValue = Expression.Call(instance, getter); + expressions.Add(SetFieldValue(field, _fieldValues, fieldValue)); + } + Expression body = Expression.Block(expressions); return Expression.Lambda>(body, _obj, _fieldValues).Compile(); @@ -135,20 +142,6 @@ internal static class InputObjectCompiler } } - private static void CompileGetProperties( - Expression instance, - IEnumerable fields, - Expression fieldValues, - List currentBlock) - { - foreach (var field in fields) - { - var getter = field.Property!.GetGetMethod(true)!; - Expression fieldValue = Expression.Call(instance, getter); - currentBlock.Add(SetFieldValue(field, fieldValues, fieldValue)); - } - } - private static Expression GetFieldValue(InputField field, Expression fieldValues) => Expression.ArrayIndex(fieldValues, Expression.Constant(field.Index)); @@ -163,8 +156,13 @@ private static Expression GetFieldValue(InputField field, Expression fieldValues return Expression.Assign(element, casted); } - private static Dictionary CreateFieldMap(InputObjectType type) - => type.Fields.ToDictionary(t => t.Property!.Name, StringComparer.Ordinal); + private static void BuildFieldMap(InputObjectType type, Dictionary fields) + { + foreach (var field in type.Fields.AsSpan()) + { + fields.Add(field.Property!.Name, field); + } + } private static Expression CreateOptional(Expression fieldValue, Type runtimeType) { diff --git a/src/HotChocolate/Core/src/Types/Utilities/Serialization/InputObjectConstructorResolver.cs b/src/HotChocolate/Core/src/Types/Utilities/Serialization/InputObjectConstructorResolver.cs index 6075483b9bb..3024e135266 100644 --- a/src/HotChocolate/Core/src/Types/Utilities/Serialization/InputObjectConstructorResolver.cs +++ b/src/HotChocolate/Core/src/Types/Utilities/Serialization/InputObjectConstructorResolver.cs @@ -4,6 +4,8 @@ using System.Linq; using System.Reflection; using HotChocolate.Types; +using HotChocolate.Types.Helpers; +using static System.Reflection.BindingFlags; #nullable enable @@ -13,70 +15,93 @@ internal static class InputObjectConstructorResolver { public static ConstructorInfo? GetCompatibleConstructor( Type type, + InputObjectType inputObjectType, IReadOnlyDictionary fields) { - var constructors = type.GetConstructors( - BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); - var defaultConstructor = constructors.FirstOrDefault( - t => t.GetParameters().Length == 0); + var constructors = type.GetConstructors(NonPublic | Public | Instance); - if (fields.Values.All(t => t.Property!.CanWrite)) + if (AllPropertiesCanWrite(inputObjectType)) { - if (defaultConstructor is not null) + if (constructors.Length == 0) { - return defaultConstructor; + return null; } - if (constructors.Length == 0) + var defaultCtor = Array.Find(constructors, t => t.GetParameters().Length == 0); + + if (defaultCtor is not null) { - return null; + return defaultCtor; } } - var required = new HashSet(); - CollectReadOnlyProperties(fields, required); + var required = TypeMemHelper.RentNameSet(); + CollectReadOnlyProperties(inputObjectType, required); + ConstructorInfo? compatibleCtor = null; - if (constructors.Length > 0) + if (constructors.Length == 1) + { + var constructor = constructors[0]; + if (IsCompatibleConstructor(constructor, fields, required)) + { + compatibleCtor = constructor; + } + } + else if (constructors.Length != 0) { foreach (var constructor in constructors.OrderByDescending(t => t.GetParameters().Length)) { if (IsCompatibleConstructor(constructor, fields, required)) { - return constructor; + compatibleCtor = constructor; } } } + TypeMemHelper.Return(required); + + if (compatibleCtor is not null) + { + return compatibleCtor; + } + throw new InvalidOperationException( $"No compatible constructor found for input type type `{type.FullName}`.\r\n" + "Either you have to provide a public constructor with settable properties or " + - "a public constructor that allows to pass in values for read-only properties." + + "a public constructor that allows to pass in values for read-only properties. " + $"There was no way to set the following properties: {string.Join(", ", required)}."); } - private static bool IsCompatibleConstructor( - ConstructorInfo constructor, - IReadOnlyDictionary fields, - ISet required) + private static bool AllPropertiesCanWrite(InputObjectType type) { - return IsCompatibleConstructor( - constructor.GetParameters(), - fields, - required); + foreach (var field in type.Fields.AsSpan()) + { + if (!(field.Property?.CanWrite ?? false)) + { + return false; + } + } + + return true; } private static bool IsCompatibleConstructor( - ParameterInfo[] parameters, + ConstructorInfo constructor, IReadOnlyDictionary fields, ISet required) { - foreach (var parameter in parameters) + var count = required.Count; + + foreach (var parameter in constructor.GetParameters()) { if (fields.TryGetParameter(parameter, out var field) && parameter.ParameterType == field.Property!.PropertyType) { - required.Remove(field.Name); + if (required.Contains(field.Name)) + { + count--; + } } else { @@ -84,20 +109,18 @@ internal static class InputObjectConstructorResolver } } - return required.Count == 0; + return count == 0; } private static void CollectReadOnlyProperties( - IReadOnlyDictionary fields, + InputObjectType type, ISet required) { - required.Clear(); - - foreach (var item in fields) + foreach (var item in type.Fields.AsSpan()) { - if (!item.Value.Property!.CanWrite) + if (!(item.Property?.CanWrite ?? false)) { - required.Add(item.Value.Name); + required.Add(item.Name); } } } @@ -110,12 +133,16 @@ internal static class InputObjectConstructorResolver var name = parameter.Name!; var alternativeName = GetAlternativeParameterName(parameter.Name!); - return (fields.TryGetValue(alternativeName, out field) || - fields.TryGetValue(name, out field)); + return fields.TryGetValue(alternativeName, out field) || + fields.TryGetValue(name, out field); } private static string GetAlternativeParameterName(string name) => name.Length > 1 - ? name.Substring(0, 1).ToUpperInvariant() + name.Substring(1) +#if NET6_0_OR_GREATER + ? string.Concat(name.Substring(0, 1).ToUpperInvariant(), name.AsSpan(1)) +#else + ? string.Concat(name.Substring(0, 1).ToUpperInvariant(), name.Substring(1)) +#endif : name.ToUpperInvariant(); }