diff --git a/src/HotChocolate/Core/src/Execution/Processing/EmptySelectionCollection.cs b/src/HotChocolate/Core/src/Execution/Processing/EmptySelectionCollection.cs new file mode 100644 index 00000000000..9dd57f4d745 --- /dev/null +++ b/src/HotChocolate/Core/src/Execution/Processing/EmptySelectionCollection.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using HotChocolate.Resolvers; + +namespace HotChocolate.Execution.Processing; + +internal sealed class EmptySelectionCollection : ISelectionCollection +{ + private static readonly ISelection[] _empty = Array.Empty(); + + public static EmptySelectionCollection Instance { get; } = new(); + + public int Count => 0; + + public ISelection this[int index] => throw new IndexOutOfRangeException(); + + public ISelectionCollection Select(string fieldName) + => Instance; + + public bool IsSelected(string fieldName) + => false; + + public bool IsSelected(string fieldName1, string fieldName2) + => false; + + public bool IsSelected(string fieldName1, string fieldName2, string fieldName3) + => false; + + public bool IsSelected(ISet fieldNames) + => false; + + public IEnumerator GetEnumerator() + => _empty.AsEnumerable().GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); +} \ No newline at end of file diff --git a/src/HotChocolate/Core/src/Execution/Processing/MiddlewareContext.Global.cs b/src/HotChocolate/Core/src/Execution/Processing/MiddlewareContext.Global.cs index 45a31eb7693..375c1103607 100644 --- a/src/HotChocolate/Core/src/Execution/Processing/MiddlewareContext.Global.cs +++ b/src/HotChocolate/Core/src/Execution/Processing/MiddlewareContext.Global.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using HotChocolate.Execution.Properties; @@ -46,49 +44,7 @@ public IServiceProvider Services public CancellationToken RequestAborted { get; private set; } public bool HasCleanupTasks => _cleanupTasks.Count > 0; - - public IReadOnlyList GetSelections( - IObjectType typeContext, - ISelection? selection = null, - bool allowInternals = false) - { - if (typeContext is null) - { - throw new ArgumentNullException(nameof(typeContext)); - } - - selection ??= _selection; - - if (selection.SelectionSet is null) - { - return Array.Empty(); - } - - var selectionSet = _operationContext.CollectFields(selection, typeContext); - - if (selectionSet.IsConditional) - { - var operationIncludeFlags = _operationContext.IncludeFlags; - var selectionCount = selectionSet.Selections.Count; - ref var selectionRef = ref ((SelectionSet)selectionSet).GetSelectionsReference(); - var finalFields = new List(); - - for (var i = 0; i < selectionCount; i++) - { - var childSelection = Unsafe.Add(ref selectionRef, i); - - if (childSelection.IsIncluded(operationIncludeFlags, allowInternals)) - { - finalFields.Add(childSelection); - } - } - - return finalFields; - } - - return selectionSet.Selections; - } - + public void ReportError(string errorMessage) { if (string.IsNullOrEmpty(errorMessage)) diff --git a/src/HotChocolate/Core/src/Execution/Processing/MiddlewareContext.Selection.cs b/src/HotChocolate/Core/src/Execution/Processing/MiddlewareContext.Selection.cs index 2126e682c23..9a3ac40a61c 100644 --- a/src/HotChocolate/Core/src/Execution/Processing/MiddlewareContext.Selection.cs +++ b/src/HotChocolate/Core/src/Execution/Processing/MiddlewareContext.Selection.cs @@ -1,4 +1,7 @@ +using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using HotChocolate.Resolvers; using HotChocolate.Types; @@ -39,4 +42,54 @@ public bool TryCreatePureContext( context = null; return false; } -} + + public IReadOnlyList GetSelections( + IObjectType typeContext, + ISelection? selection = null, + bool allowInternals = false) + { + if (typeContext is null) + { + throw new ArgumentNullException(nameof(typeContext)); + } + + selection ??= _selection; + + if (selection.SelectionSet is null) + { + return Array.Empty(); + } + + var selectionSet = _operationContext.CollectFields(selection, typeContext); + + if (selectionSet.IsConditional) + { + var operationIncludeFlags = _operationContext.IncludeFlags; + var selectionCount = selectionSet.Selections.Count; + ref var selectionRef = ref ((SelectionSet)selectionSet).GetSelectionsReference(); + var finalFields = new List(); + + for (var i = 0; i < selectionCount; i++) + { + var childSelection = Unsafe.Add(ref selectionRef, i); + + if (childSelection.IsIncluded(operationIncludeFlags, allowInternals)) + { + finalFields.Add(childSelection); + } + } + + return finalFields; + } + + return selectionSet.Selections; + } + + public ISelectionCollection Select(string fieldName) + => new SelectionCollection( + Schema, + Operation, + [Selection,], + _operationContext.IncludeFlags) + .Select(fieldName); +} \ No newline at end of file diff --git a/src/HotChocolate/Core/src/Execution/Processing/SelectionCollection.cs b/src/HotChocolate/Core/src/Execution/Processing/SelectionCollection.cs new file mode 100644 index 00000000000..98831960dfe --- /dev/null +++ b/src/HotChocolate/Core/src/Execution/Processing/SelectionCollection.cs @@ -0,0 +1,473 @@ +// ReSharper disable RedundantSuppressNullableWarningExpression + +using System; +using System.Buffers; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using HotChocolate.Resolvers; +using HotChocolate.Types; +using HotChocolate.Utilities; + +namespace HotChocolate.Execution.Processing; + +internal sealed class SelectionCollection( + ISchema schema, + IOperation operation, + ISelection[] selections, + long includeFlags) + : ISelectionCollection +{ + private readonly ISchema _schema = schema ?? throw new ArgumentNullException(nameof(schema)); + private readonly IOperation _operation = operation ?? throw new ArgumentNullException(nameof(operation)); + private readonly ISelection[] _selections = selections ?? throw new ArgumentNullException(nameof(selections)); + + public int Count => _selections.Length; + + public ISelection this[int index] => _selections[index]; + + public ISelectionCollection Select(string fieldName) + { + if (!CollectSelections(fieldName, out var buffer, out var size)) + { + return new SelectionCollection(_schema, _operation, Array.Empty(), includeFlags); + } + + var selections = new ISelection[size]; + buffer.AsSpan().Slice(0, size).CopyTo(selections); + ArrayPool.Shared.Return(buffer); + return new SelectionCollection(_schema, _operation, selections, includeFlags); + } + + public bool IsSelected(string fieldName) + { + if (fieldName is null) + { + throw new ArgumentNullException(nameof(fieldName)); + } + + ref var start = ref MemoryMarshal.GetReference(_selections.AsSpan()); + ref var end = ref Unsafe.Add(ref start, _selections.Length); + + while (Unsafe.IsAddressLessThan(ref start, ref end)) + { + if (!start.Type.IsCompositeType()) + { + return false; + } + + var namedType = start.Type.NamedType(); + + if (namedType.IsAbstractType()) + { + foreach (var possibleType in _schema.GetPossibleTypes(namedType)) + { + if (IsChildSelected( + _operation, + includeFlags, + possibleType, + start, + fieldName)) + { + return true; + } + } + } + else + { + if (IsChildSelected( + _operation, + includeFlags, + (ObjectType)namedType, + start, + fieldName)) + { + return true; + } + } + + start = ref Unsafe.Add(ref start, 1)!; + } + + return false; + + static bool IsChildSelected( + IOperation operation, + long includeFlags, + ObjectType objectType, + ISelection parent, + string fieldName) + { + var selectionSet = operation.GetSelectionSet(parent, objectType); + var operationIncludeFlags = includeFlags; + var selectionCount = selectionSet.Selections.Count; + ref var start = ref ((SelectionSet)selectionSet).GetSelectionsReference(); + ref var end = ref Unsafe.Add(ref start, selectionCount); + + while (Unsafe.IsAddressLessThan(ref start, ref end)) + { + if (start.IsIncluded(operationIncludeFlags) && + fieldName.EqualsOrdinal(start.Field.Name)) + { + return true; + } + + start = ref Unsafe.Add(ref start, 1)!; + } + + return false; + } + } + + public bool IsSelected(string fieldName1, string fieldName2) + { + if (fieldName1 is null) + { + throw new ArgumentNullException(nameof(fieldName1)); + } + + if (fieldName2 is null) + { + throw new ArgumentNullException(nameof(fieldName2)); + } + + ref var start = ref MemoryMarshal.GetReference(_selections.AsSpan()); + ref var end = ref Unsafe.Add(ref start, _selections.Length); + + while (Unsafe.IsAddressLessThan(ref start, ref end)) + { + if (!start.Type.IsCompositeType()) + { + return false; + } + + var namedType = start.Type.NamedType(); + + if (namedType.IsAbstractType()) + { + foreach (var possibleType in _schema.GetPossibleTypes(namedType)) + { + if (IsChildSelected( + _operation, + includeFlags, + possibleType, + start, + fieldName1, + fieldName2)) + { + return true; + } + } + } + else + { + if (IsChildSelected( + _operation, + includeFlags, + (ObjectType)namedType, + start, + fieldName1, + fieldName2)) + { + return true; + } + } + + start = ref Unsafe.Add(ref start, 1)!; + } + + return false; + + static bool IsChildSelected( + IOperation operation, + long includeFlags, + ObjectType objectType, + ISelection parent, + string fieldName1, + string fieldName2) + { + var selectionSet = operation.GetSelectionSet(parent, objectType); + var operationIncludeFlags = includeFlags; + var selectionCount = selectionSet.Selections.Count; + ref var start = ref ((SelectionSet)selectionSet).GetSelectionsReference(); + ref var end = ref Unsafe.Add(ref start, selectionCount); + + while (Unsafe.IsAddressLessThan(ref start, ref end)) + { + if (start.IsIncluded(operationIncludeFlags) && + (fieldName1.EqualsOrdinal(start.Field.Name) || + fieldName2.EqualsOrdinal(start.Field.Name))) + { + return true; + } + + start = ref Unsafe.Add(ref start, 1)!; + } + + return false; + } + } + + public bool IsSelected(string fieldName1, string fieldName2, string fieldName3) + { + if (fieldName1 is null) + { + throw new ArgumentNullException(nameof(fieldName1)); + } + + if (fieldName2 is null) + { + throw new ArgumentNullException(nameof(fieldName2)); + } + + if (fieldName3 is null) + { + throw new ArgumentNullException(nameof(fieldName3)); + } + + ref var start = ref MemoryMarshal.GetReference(_selections.AsSpan()); + ref var end = ref Unsafe.Add(ref start, _selections.Length); + + while (Unsafe.IsAddressLessThan(ref start, ref end)) + { + if (!start.Type.IsCompositeType()) + { + return false; + } + + var namedType = start.Type.NamedType(); + + if (namedType.IsAbstractType()) + { + foreach (var possibleType in _schema.GetPossibleTypes(namedType)) + { + if (IsChildSelected( + _operation, + includeFlags, + possibleType, + start, + fieldName1, + fieldName2, + fieldName3)) + { + return true; + } + } + } + else + { + if (IsChildSelected( + _operation, + includeFlags, + (ObjectType)namedType, + start, + fieldName1, + fieldName2, + fieldName3)) + { + return true; + } + } + + start = ref Unsafe.Add(ref start, 1)!; + } + + return false; + + static bool IsChildSelected( + IOperation operation, + long includeFlags, + ObjectType objectType, + ISelection parent, + string fieldName1, + string fieldName2, + string fieldName3) + { + var selectionSet = operation.GetSelectionSet(parent, objectType); + var operationIncludeFlags = includeFlags; + var selectionCount = selectionSet.Selections.Count; + ref var start = ref ((SelectionSet)selectionSet).GetSelectionsReference(); + ref var end = ref Unsafe.Add(ref start, selectionCount); + + while (Unsafe.IsAddressLessThan(ref start, ref end)) + { + if (start.IsIncluded(operationIncludeFlags) && + (fieldName1.EqualsOrdinal(start.Field.Name) || + fieldName2.EqualsOrdinal(start.Field.Name) || + fieldName3.EqualsOrdinal(start.Field.Name))) + { + return true; + } + + start = ref Unsafe.Add(ref start, 1)!; + } + + return false; + } + } + + public bool IsSelected(ISet fieldNames) + { + if (fieldNames is null) + { + throw new ArgumentNullException(nameof(fieldNames)); + } + + ref var start = ref MemoryMarshal.GetReference(_selections.AsSpan()); + ref var end = ref Unsafe.Add(ref start, _selections.Length); + + while (Unsafe.IsAddressLessThan(ref start, ref end)) + { + if (!start.Type.IsCompositeType()) + { + return false; + } + + var namedType = start.Type.NamedType(); + + if (namedType.IsAbstractType()) + { + foreach (var possibleType in _schema.GetPossibleTypes(namedType)) + { + if (IsChildSelected(_operation, includeFlags, possibleType, start, fieldNames)) + { + return true; + } + } + } + else + { + if (IsChildSelected(_operation, includeFlags, (ObjectType)namedType, start, fieldNames)) + { + return true; + } + } + + start = ref Unsafe.Add(ref start, 1)!; + } + + return false; + + static bool IsChildSelected( + IOperation operation, + long includeFlags, + ObjectType objectType, + ISelection parent, + ISet fieldNames) + { + var selectionSet = operation.GetSelectionSet(parent, objectType); + var operationIncludeFlags = includeFlags; + var selectionCount = selectionSet.Selections.Count; + ref var start = ref ((SelectionSet)selectionSet).GetSelectionsReference(); + ref var end = ref Unsafe.Add(ref start, selectionCount); + + while (Unsafe.IsAddressLessThan(ref start, ref end)) + { + if (start.IsIncluded(operationIncludeFlags) && + fieldNames.Contains(start.Field.Name)) + { + return true; + } + + start = ref Unsafe.Add(ref start, 1)!; + } + + return false; + } + } + + private bool CollectSelections(string fieldName, out ISelection[] buffer, out int size) + { + buffer = ArrayPool.Shared.Rent(4); + size = 0; + + ref var start = ref MemoryMarshal.GetReference(_selections.AsSpan()); + ref var end = ref Unsafe.Add(ref start, _selections.Length); + + while (Unsafe.IsAddressLessThan(ref start, ref end)) + { + if (!start.Type.IsCompositeType()) + { + continue; + } + + var namedType = start.Type.NamedType(); + + if (namedType.IsAbstractType()) + { + foreach (var possibleType in _schema.GetPossibleTypes(namedType)) + { + var selectionSet = _operation.GetSelectionSet(start, possibleType); + Collect(ref buffer, selectionSet, size, out var written); + size += written; + } + } + else + { + var selectionSet = _operation.GetSelectionSet(start, (ObjectType)namedType); + Collect(ref buffer, selectionSet, size, out var written); + size += written; + } + + start = ref Unsafe.Add(ref start, 1)!; + } + + if (size == 0) + { + ArrayPool.Shared.Return(buffer); + buffer = Array.Empty(); + } + + return size > 0; + + void Collect(ref ISelection[] buffer, ISelectionSet selectionSet, int index, out int written) + { + written = 0; + + var operationIncludeFlags = includeFlags; + var selectionCount = selectionSet.Selections.Count; + ref var selectionRef = ref ((SelectionSet)selectionSet).GetSelectionsReference(); + + EnsureCapacity(ref buffer, index, selectionCount); + + for (var i = 0; i < selectionCount; i++) + { + var childSelection = Unsafe.Add(ref selectionRef, i); + + if (childSelection.IsIncluded(operationIncludeFlags) && + childSelection.Field.Name.EqualsOrdinal(fieldName)) + { + buffer[index++] = childSelection; + written++; + } + } + } + } + + private static void EnsureCapacity(ref ISelection[] buffer, int index, int requiredSpace) + { + var capacity = buffer.Length - index; + + if (capacity >= requiredSpace) + { + return; + } + + while (capacity < requiredSpace) + { + capacity *= 2; + } + + var newBuffer = ArrayPool.Shared.Rent(capacity); + buffer.AsSpan().Slice(0, index).CopyTo(newBuffer); + ArrayPool.Shared.Return(buffer); + buffer = newBuffer; + } + + public IEnumerator GetEnumerator() + => ((IEnumerable)_selections).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); +} \ No newline at end of file diff --git a/src/HotChocolate/Core/src/Types/Extensions/ResolverContextExtensions.cs b/src/HotChocolate/Core/src/Types/Extensions/ResolverContextExtensions.cs index fcee980f0bf..a58b2f8212a 100644 --- a/src/HotChocolate/Core/src/Types/Extensions/ResolverContextExtensions.cs +++ b/src/HotChocolate/Core/src/Types/Extensions/ResolverContextExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Security.Claims; +using HotChocolate.Execution.Processing; using HotChocolate.Resolvers; using HotChocolate.Types; using static HotChocolate.Utilities.ThrowHelper; diff --git a/src/HotChocolate/Core/src/Types/Resolvers/DefaultResolverCompiler.cs b/src/HotChocolate/Core/src/Types/Resolvers/DefaultResolverCompiler.cs index 62f8261ae4b..f82d6c47da8 100644 --- a/src/HotChocolate/Core/src/Types/Resolvers/DefaultResolverCompiler.cs +++ b/src/HotChocolate/Core/src/Types/Resolvers/DefaultResolverCompiler.cs @@ -67,6 +67,7 @@ public DefaultResolverCompiler( new GlobalStateParameterExpressionBuilder(), new ScopedStateParameterExpressionBuilder(), new LocalStateParameterExpressionBuilder(), + new IsSelectedParameterExpressionBuilder(), new EventMessageParameterExpressionBuilder(), new ScopedServiceParameterExpressionBuilder(), new LegacyScopedServiceParameterExpressionBuilder(), diff --git a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/IsSelectedParameterExpressionBuilder.cs b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/IsSelectedParameterExpressionBuilder.cs new file mode 100644 index 00000000000..21d610b0bae --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/IsSelectedParameterExpressionBuilder.cs @@ -0,0 +1,97 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; +using HotChocolate.Internal; +using HotChocolate.Types; +using HotChocolate.Types.Descriptors; + +namespace HotChocolate.Resolvers.Expressions.Parameters; + +internal sealed class IsSelectedParameterExpressionBuilder : IParameterExpressionBuilder, IParameterFieldConfiguration +{ + public ArgumentKind Kind => ArgumentKind.LocalState; + + public bool IsPure => false; + + public bool IsDefaultHandler => false; + + public bool CanHandle(ParameterInfo parameter) + => parameter.IsDefined(typeof(IsSelectedAttribute)); + + public Expression Build(ParameterExpressionBuilderContext context) + { + var parameter = context.Parameter; + var key = $"isSelected.{parameter.Name}"; + Expression> expr = ctx => ctx.GetLocalState(key); + return Expression.Invoke(expr, context.ResolverContext); + } + + public void ApplyConfiguration(ParameterInfo parameter, ObjectFieldDescriptor descriptor) + { + var attribute = parameter.GetCustomAttribute()!; + + switch (attribute.FieldNames.Length) + { + case 1: + { + var fieldName = attribute.FieldNames[0]; + + descriptor.Use( + next => async ctx => + { + var isSelected = ctx.IsSelected(fieldName); + ctx.SetLocalState($"{nameof(isSelected)}.{parameter.Name}", isSelected); + await next(ctx); + }); + break; + } + + case 2: + { + var fieldName1 = attribute.FieldNames[0]; + var fieldName2 = attribute.FieldNames[1]; + + descriptor.Use( + next => async ctx => + { + var isSelected = ctx.IsSelected(fieldName1, fieldName2); + ctx.SetLocalState($"{nameof(isSelected)}.{parameter.Name}", isSelected); + await next(ctx); + }); + break; + } + + case 3: + { + var fieldName1 = attribute.FieldNames[0]; + var fieldName2 = attribute.FieldNames[1]; + var fieldName3 = attribute.FieldNames[2]; + + descriptor.Use( + next => async ctx => + { + var isSelected = ctx.IsSelected(fieldName1, fieldName2, fieldName3); + ctx.SetLocalState($"{nameof(isSelected)}.{parameter.Name}", isSelected); + await next(ctx); + }); + break; + } + + case > 3: + { + var fieldNames = new HashSet(attribute.FieldNames); + + descriptor.Use( + next => async ctx => + { + var isSelected = ctx.IsSelected(fieldNames); + ctx.SetLocalState($"{nameof(isSelected)}.{parameter.Name}", isSelected); + await next(ctx); + }); + break; + } + } + } +} \ No newline at end of file diff --git a/src/HotChocolate/Core/src/Types/Resolvers/IResolverContext.cs b/src/HotChocolate/Core/src/Types/Resolvers/IResolverContext.cs index 182c4f77bc3..185382be3da 100644 --- a/src/HotChocolate/Core/src/Types/Resolvers/IResolverContext.cs +++ b/src/HotChocolate/Core/src/Types/Resolvers/IResolverContext.cs @@ -128,6 +128,20 @@ IReadOnlyList GetSelections( ISelection? selection = null, bool allowInternals = false); + /// + /// Selects all child fields that match the given field name and + /// returns a containing + /// these selections. + /// + /// + /// The field name to select. + /// + /// + /// Returns a containing + /// the selections that match the given field name. + /// + ISelectionCollection Select(string fieldName); + /// /// Get the query root instance. /// diff --git a/src/HotChocolate/Core/src/Types/Resolvers/ISelectionCollection.cs b/src/HotChocolate/Core/src/Types/Resolvers/ISelectionCollection.cs new file mode 100644 index 00000000000..08cd058857f --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Resolvers/ISelectionCollection.cs @@ -0,0 +1,78 @@ +#nullable enable +using System.Collections.Generic; +using HotChocolate.Execution.Processing; + +namespace HotChocolate.Resolvers; + +/// +/// Represents a collection of selections. +/// +public interface ISelectionCollection : IReadOnlyList +{ + /// + /// Selects all child fields that match the given field name and + /// returns a containing + /// these selections. + /// + /// + /// The field name to select. + /// + /// + /// Returns a containing + /// the selections that match the given field name. + /// + ISelectionCollection Select(string fieldName); + + /// + /// Specifies if a child field with the given field name is selected. + /// + /// + /// The field name to check. + /// + /// + /// true if a child field with the given field name is selected; otherwise, false. + /// + bool IsSelected(string fieldName); + + /// + /// Specifies if a child field with one of the given field names is selected. + /// + /// + /// The first field name to check. + /// + /// + /// The second field name to check. + /// + /// + /// true if a child field with one of the given field names is selected; otherwise, false. + /// + bool IsSelected(string fieldName1, string fieldName2); + + /// + /// Specifies if a child field with one of the given field names is selected. + /// + /// + /// The first field name to check. + /// + /// + /// The second field name to check. + /// + /// + /// The third field name to check. + /// + /// + /// true if a child field with one of the given field names is selected; otherwise, false. + /// + bool IsSelected(string fieldName1, string fieldName2, string fieldName3); + + /// + /// Specifies if a child field with one of the given field names is selected. + /// + /// + /// The field names to check. + /// + /// + /// true if a child field with one of the given field names is selected; otherwise, false. + /// + bool IsSelected(ISet fieldNames); +} \ No newline at end of file diff --git a/src/HotChocolate/Core/src/Types/Types/Attributes/UseIsSelectedAttribute.cs b/src/HotChocolate/Core/src/Types/Types/Attributes/IsSelectedAttribute.cs similarity index 54% rename from src/HotChocolate/Core/src/Types/Types/Attributes/UseIsSelectedAttribute.cs rename to src/HotChocolate/Core/src/Types/Types/Attributes/IsSelectedAttribute.cs index e6dc4357573..2f0019bdd71 100644 --- a/src/HotChocolate/Core/src/Types/Types/Attributes/UseIsSelectedAttribute.cs +++ b/src/HotChocolate/Core/src/Types/Types/Attributes/IsSelectedAttribute.cs @@ -1,6 +1,5 @@ -using System.Collections.Generic; -using System.Reflection; -using HotChocolate.Types.Descriptors; +using System; +using HotChocolate.Resolvers.Expressions.Parameters; namespace HotChocolate.Types; @@ -17,8 +16,14 @@ namespace HotChocolate.Types; /// ]]> /// /// -public class UseIsSelectedAttribute : ObjectFieldDescriptorAttribute +[AttributeUsage( + AttributeTargets.Parameter, + Inherited = true, + AllowMultiple = true)] +public class IsSelectedAttribute : Attribute { + private static readonly IsSelectedParameterExpressionBuilder _builder = new(); + /// /// Adds a middleware that checks if the specified fields are selected. /// This middleware adds a local state called `isSelected`. @@ -35,7 +40,7 @@ public class UseIsSelectedAttribute : ObjectFieldDescriptorAttribute /// /// The field name that we check for. /// - public UseIsSelectedAttribute(string fieldName) + public IsSelectedAttribute(string fieldName) { FieldNames = [fieldName,]; } @@ -59,7 +64,7 @@ public UseIsSelectedAttribute(string fieldName) /// /// The second field name we check for. /// - public UseIsSelectedAttribute(string fieldName1, string fieldName2) + public IsSelectedAttribute(string fieldName1, string fieldName2) { FieldNames = [fieldName1, fieldName2,]; } @@ -86,7 +91,7 @@ public UseIsSelectedAttribute(string fieldName1, string fieldName2) /// /// The third field name we check for. /// - public UseIsSelectedAttribute(string fieldName1, string fieldName2, string fieldName3) + public IsSelectedAttribute(string fieldName1, string fieldName2, string fieldName3) { FieldNames = [fieldName1, fieldName2, fieldName3,]; } @@ -107,7 +112,7 @@ public UseIsSelectedAttribute(string fieldName1, string fieldName2, string field /// /// The field names we check for. /// - public UseIsSelectedAttribute(params string[] fieldNames) + public IsSelectedAttribute(params string[] fieldNames) { FieldNames = fieldNames; } @@ -116,72 +121,4 @@ public UseIsSelectedAttribute(params string[] fieldNames) /// Gets the field names we check for. /// public string[] FieldNames { get; } - - protected override void OnConfigure( - IDescriptorContext context, - IObjectFieldDescriptor descriptor, - MemberInfo member) - { - switch (FieldNames.Length) - { - case 1: - { - var fieldName = FieldNames[0]; - - descriptor.Use( - next => async ctx => - { - var isSelected = ctx.IsSelected(fieldName); - ctx.SetLocalState(nameof(isSelected), isSelected); - await next(ctx); - }); - break; - } - - case 2: - { - var fieldName1 = FieldNames[0]; - var fieldName2 = FieldNames[1]; - - descriptor.Use( - next => async ctx => - { - var isSelected = ctx.IsSelected(fieldName1, fieldName2); - ctx.SetLocalState(nameof(isSelected), isSelected); - await next(ctx); - }); - break; - } - - case 3: - { - var fieldName1 = FieldNames[0]; - var fieldName2 = FieldNames[1]; - var fieldName3 = FieldNames[2]; - - descriptor.Use( - next => async ctx => - { - var isSelected = ctx.IsSelected(fieldName1, fieldName2, fieldName3); - ctx.SetLocalState(nameof(isSelected), isSelected); - await next(ctx); - }); - break; - } - - case > 3: - { - var fieldNames = new HashSet(FieldNames); - - descriptor.Use( - next => async ctx => - { - var isSelected = ctx.IsSelected(fieldNames); - ctx.SetLocalState(nameof(isSelected), isSelected); - await next(ctx); - }); - break; - } - } - } } \ No newline at end of file diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/ArgumentDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/ArgumentDescriptor.cs index ef0059483a7..e7f98ea3908 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/ArgumentDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/ArgumentDescriptor.cs @@ -80,6 +80,8 @@ protected internal ArgumentDescriptor( /// protected override void OnCreateDefinition(ArgumentDefinition definition) { + Context.Descriptors.Push(this); + if (Definition is { AttributesAreApplied: false, Parameter: not null, }) { Context.TypeInspector.ApplyAttributes( @@ -90,6 +92,8 @@ protected override void OnCreateDefinition(ArgumentDefinition definition) } base.OnCreateDefinition(definition); + + Context.Descriptors.Pop(); } /// @@ -227,4 +231,4 @@ public static ArgumentDescriptor From( IDescriptorContext context, ArgumentDefinition argumentDefinition) => new(context, argumentDefinition); -} +} \ No newline at end of file diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/DescriptorContext.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/DescriptorContext.cs index a0aaf74f97c..319a2fbd5ea 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/DescriptorContext.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/DescriptorContext.cs @@ -128,6 +128,9 @@ public ITypeInspector TypeInspector /// public InputFormatter InputFormatter { get; } + /// + public IList Descriptors { get; } = new List(); + /// public IDictionary ContextData { get; } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/IDescriptorContext.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/IDescriptorContext.cs index 80c27a4891a..c08ccc7d4d4 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/IDescriptorContext.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/IDescriptorContext.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using HotChocolate.Configuration; using HotChocolate.Internal; @@ -62,6 +63,11 @@ public interface IDescriptorContext : IHasContextData, IDisposable /// Gets the input formatter. /// InputFormatter InputFormatter { get; } + + /// + /// Gets the descriptor currently in path. + /// + IList Descriptors { get; } /// /// Gets the registered type discovery handlers. @@ -88,3 +94,4 @@ bool TryGetSchemaDirective( T GetConventionOrDefault(Func defaultConvention, string? scope = null) where T : class, IConvention; } + diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/DirectiveArgumentDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/DirectiveArgumentDescriptor.cs index d3c4cfa2498..bf31f929d0a 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/DirectiveArgumentDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/DirectiveArgumentDescriptor.cs @@ -65,13 +65,17 @@ protected internal DirectiveArgumentDescriptor( /// protected override void OnCreateDefinition(DirectiveArgumentDefinition definition) { - if (!Definition.AttributesAreApplied && Definition.Property is not null) + Context.Descriptors.Push(this); + + if (Definition is { AttributesAreApplied: false, Property: not null, }) { Context.TypeInspector.ApplyAttributes(Context, this, Definition.Property); Definition.AttributesAreApplied = true; } base.OnCreateDefinition(definition); + + Context.Descriptors.Pop(); } /// diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/DirectiveTypeDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/DirectiveTypeDescriptor.cs index 381fb737005..15d993654ca 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/DirectiveTypeDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/DirectiveTypeDescriptor.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Reflection; using HotChocolate.Configuration; +using HotChocolate.Language; using HotChocolate.Resolvers; using HotChocolate.Types.Descriptors.Definitions; using HotChocolate.Types.Helpers; @@ -55,6 +56,8 @@ protected internal DirectiveTypeDescriptor( protected override void OnCreateDefinition( DirectiveTypeDefinition definition) { + Context.Descriptors.Push(this); + if (!Definition.AttributesAreApplied && Definition.RuntimeType != typeof(object)) { Context.TypeInspector.ApplyAttributes( @@ -78,6 +81,8 @@ protected override void OnCreateDefinition( definition.Arguments.AddRange(arguments.Values); base.OnCreateDefinition(definition); + + Context.Descriptors.Pop(); } protected virtual void OnCompleteArguments( diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/EnumTypeDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/EnumTypeDescriptor.cs index bf7c54a15c6..7b94854ffcf 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/EnumTypeDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/EnumTypeDescriptor.cs @@ -41,6 +41,8 @@ protected EnumTypeDescriptor(IDescriptorContext context, EnumTypeDefinition defi protected override void OnCreateDefinition( EnumTypeDefinition definition) { + Context.Descriptors.Push(this); + if (!Definition.AttributesAreApplied && Definition.RuntimeType != typeof(object)) { Context.TypeInspector.ApplyAttributes( @@ -61,6 +63,8 @@ protected override void OnCreateDefinition( } base.OnCreateDefinition(definition); + + Context.Descriptors.Pop(); } protected void AddImplicitValues( diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/EnumValueDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/EnumValueDescriptor.cs index 51a9d20f9a4..a4cb3c9b83c 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/EnumValueDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/EnumValueDescriptor.cs @@ -38,7 +38,9 @@ protected EnumValueDescriptor(IDescriptorContext context, EnumValueDefinition de protected override void OnCreateDefinition(EnumValueDefinition definition) { - if (!Definition.AttributesAreApplied && Definition.Member is not null) + Context.Descriptors.Push(this); + + if (Definition is { AttributesAreApplied: false, Member: not null, }) { Context.TypeInspector.ApplyAttributes( Context, @@ -53,6 +55,8 @@ protected override void OnCreateDefinition(EnumValueDefinition definition) } base.OnCreateDefinition(definition); + + Context.Descriptors.Pop(); } public IEnumValueDescriptor Name(string value) diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/InputFieldDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/InputFieldDescriptor.cs index 2ad66db5555..abaf8e33a5f 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/InputFieldDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/InputFieldDescriptor.cs @@ -62,7 +62,9 @@ protected internal InputFieldDescriptor( /// protected override void OnCreateDefinition(InputFieldDefinition definition) { - if (!Definition.AttributesAreApplied && Definition.Property is not null) + Context.Descriptors.Push(this); + + if (Definition is { AttributesAreApplied: false, Property: not null, }) { Context.TypeInspector.ApplyAttributes( Context, @@ -72,6 +74,8 @@ protected override void OnCreateDefinition(InputFieldDefinition definition) } base.OnCreateDefinition(definition); + + Context.Descriptors.Pop(); } /// diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/InputObjectTypeDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/InputObjectTypeDescriptor.cs index 2b7229f1b71..89dd65716df 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/InputObjectTypeDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/InputObjectTypeDescriptor.cs @@ -58,6 +58,8 @@ protected InputObjectTypeDescriptor( protected override void OnCreateDefinition( InputObjectTypeDefinition definition) { + Context.Descriptors.Push(this); + if (!Definition.AttributesAreApplied && Definition.RuntimeType != typeof(object)) { Context.TypeInspector.ApplyAttributes( @@ -94,6 +96,8 @@ protected override void OnCreateDefinition( TypeMemHelper.Return(handledMembers); base.OnCreateDefinition(definition); + + Context.Descriptors.Pop(); } protected void InferFieldsFromFieldBindingType( diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/InterfaceFieldDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/InterfaceFieldDescriptor.cs index c45b6fc6f80..8d621dfde84 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/InterfaceFieldDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/InterfaceFieldDescriptor.cs @@ -58,7 +58,9 @@ protected internal InterfaceFieldDescriptor( protected override void OnCreateDefinition(InterfaceFieldDefinition definition) { - if (!Definition.AttributesAreApplied && Definition.Member is not null) + Context.Descriptors.Push(this); + + if (Definition is { AttributesAreApplied: false, Member: not null, }) { Context.TypeInspector.ApplyAttributes(Context, this, Definition.Member); Definition.AttributesAreApplied = true; @@ -67,6 +69,8 @@ protected override void OnCreateDefinition(InterfaceFieldDefinition definition) base.OnCreateDefinition(definition); CompleteArguments(definition); + + Context.Descriptors.Pop(); } private void CompleteArguments(InterfaceFieldDefinition definition) diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/InterfaceTypeDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/InterfaceTypeDescriptor.cs index dd133b8516e..95e9d0d6460 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/InterfaceTypeDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/InterfaceTypeDescriptor.cs @@ -58,6 +58,8 @@ protected InterfaceTypeDescriptor( protected override void OnCreateDefinition( InterfaceTypeDefinition definition) { + Context.Descriptors.Push(this); + if (!Definition.AttributesAreApplied && Definition.RuntimeType != typeof(object)) { Context.TypeInspector.ApplyAttributes( @@ -82,6 +84,8 @@ protected override void OnCreateDefinition( Definition.Fields.AddRange(fields.Values); base.OnCreateDefinition(definition); + + Context.Descriptors.Pop(); } protected virtual void OnCompleteFields( diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectFieldDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectFieldDescriptor.cs index 94773cd0dc4..1bcd6bc7aaa 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectFieldDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectFieldDescriptor.cs @@ -136,6 +136,8 @@ protected ObjectFieldDescriptor( /// protected override void OnCreateDefinition(ObjectFieldDefinition definition) { + Context.Descriptors.Push(this); + var member = definition.ResolverMember ?? definition.Member; if (!Definition.AttributesAreApplied && member is not null) @@ -154,6 +156,8 @@ protected override void OnCreateDefinition(ObjectFieldDefinition definition) { definition.HasStreamResult = true; } + + Context.Descriptors.Pop(); } private void CompleteArguments(ObjectFieldDefinition definition) diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectTypeDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectTypeDescriptor.cs index 7497547ec69..f4710620a45 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectTypeDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectTypeDescriptor.cs @@ -58,7 +58,9 @@ protected ObjectTypeDescriptor( protected override void OnCreateDefinition( ObjectTypeDefinition definition) { - if (!Definition.AttributesAreApplied && Definition.FieldBindingType is not null) + Context.Descriptors.Push(this); + + if (Definition is { AttributesAreApplied: false, FieldBindingType: not null, }) { Context.TypeInspector.ApplyAttributes( Context, @@ -113,6 +115,8 @@ protected override void OnCreateDefinition( TypeMemHelper.Return(handledMembers); base.OnCreateDefinition(definition); + + Context.Descriptors.Pop(); } internal void InferFieldsFromFieldBindingType() diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/UnionTypeDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/UnionTypeDescriptor.cs index 9521409a5ad..4c186f17018 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/UnionTypeDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/UnionTypeDescriptor.cs @@ -40,6 +40,8 @@ protected UnionTypeDescriptor(IDescriptorContext context) protected override void OnCreateDefinition(UnionTypeDefinition definition) { + Context.Descriptors.Push(this); + if (!Definition.AttributesAreApplied && Definition.RuntimeType != typeof(object)) { Context.TypeInspector.ApplyAttributes(Context, this, Definition.RuntimeType); @@ -47,6 +49,8 @@ protected override void OnCreateDefinition(UnionTypeDefinition definition) } base.OnCreateDefinition(definition); + + Context.Descriptors.Pop(); } public IUnionTypeDescriptor Name(string value) diff --git a/src/HotChocolate/Core/test/Types.CursorPagination.Tests/QueryableCursorPagingProviderTests.cs b/src/HotChocolate/Core/test/Types.CursorPagination.Tests/QueryableCursorPagingProviderTests.cs index 93dd5ac687b..1674918d4e5 100644 --- a/src/HotChocolate/Core/test/Types.CursorPagination.Tests/QueryableCursorPagingProviderTests.cs +++ b/src/HotChocolate/Core/test/Types.CursorPagination.Tests/QueryableCursorPagingProviderTests.cs @@ -492,6 +492,11 @@ public IReadOnlyList GetSelections( throw new NotImplementedException(); } + public ISelectionCollection Select(string fieldName) + { + throw new NotImplementedException(); + } + public T GetQueryRoot() { throw new NotImplementedException(); diff --git a/src/HotChocolate/Core/test/Types.Tests/Resolvers/IsSelectedTests.cs b/src/HotChocolate/Core/test/Types.Tests/Resolvers/IsSelectedTests.cs index f474dc6eaee..682dc4f0415 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Resolvers/IsSelectedTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Resolvers/IsSelectedTests.cs @@ -284,7 +284,7 @@ public async Task IsSelected_Attribute_4_Not_Selected() result.MatchMarkdownSnapshot(); } - + [Fact] public async Task IsSelected_Context_1_Selected() { @@ -561,31 +561,118 @@ public async Task IsSelected_Context_4_Not_Selected() result.MatchMarkdownSnapshot(); } + [Fact] + public async Task Select_Category_Level_1() + { + var result = + await new ServiceCollection() + .AddGraphQL() + .AddQueryType( + c => + { + c.Name("Query"); + c.Field("user") + .Resolve( + ctx => + { + ((IMiddlewareContext)ctx).OperationResult.SetExtension( + "isSelected", + ctx.Select("category").IsSelected("next")); + return Query.DummyUser; + }); + }) + .ExecuteRequestAsync( + """ + query { + user { + name + category { + next { + name + } + } + } + } + """); + + result.MatchMarkdownSnapshot(); + } + + [Fact] + public async Task Select_Category_Level_2() + { + var result = + await new ServiceCollection() + .AddGraphQL() + .AddQueryType( + c => + { + c.Name("Query"); + c.Field("user") + .Resolve( + ctx => + { + ((IMiddlewareContext)ctx).OperationResult.SetExtension( + "isSelected", + ctx.Select("category").Select("next").IsSelected("name")); + return Query.DummyUser; + }); + }) + .ExecuteRequestAsync( + """ + query { + user { + name + category { + next { + next { + name + } + } + } + } + } + """); + + result.MatchMarkdownSnapshot(); + } + public class Query { - [UseIsSelected("email")] - public User GetUser_Attribute_1([LocalState] bool isSelected, IResolverContext context) + public static User DummyUser { get; } = + new() + { + Name = "a", + Email = "b", + Password = "c", + PhoneNumber = "d", + Address = "e", + City = "f", + }; + + public User GetUser_Attribute_1([IsSelected("email")] bool isSelected, IResolverContext context) { ((IMiddlewareContext)context).OperationResult.SetExtension("isSelected", isSelected); return new User { Name = "a", Email = "b", Password = "c", PhoneNumber = "d", Address = "e", City = "f", }; } - [UseIsSelected("email", "password")] - public User GetUser_Attribute_2([LocalState] bool isSelected, IResolverContext context) + public User GetUser_Attribute_2([IsSelected("email", "password")] bool isSelected, IResolverContext context) { ((IMiddlewareContext)context).OperationResult.SetExtension("isSelected", isSelected); return new User { Name = "a", Email = "b", Password = "c", PhoneNumber = "d", Address = "e", City = "f", }; } - [UseIsSelected("email", "password", "phoneNumber")] - public User GetUser_Attribute_3([LocalState] bool isSelected, IResolverContext context) + public User GetUser_Attribute_3( + [IsSelected("email", "password", "phoneNumber")] bool isSelected, + IResolverContext context) { ((IMiddlewareContext)context).OperationResult.SetExtension("isSelected", isSelected); return new User { Name = "a", Email = "b", Password = "c", PhoneNumber = "d", Address = "e", City = "f", }; } - [UseIsSelected("email", "password", "phoneNumber", "address")] - public User GetUser_Attribute_4([LocalState] bool isSelected, IResolverContext context) + public User GetUser_Attribute_4( + [IsSelected("email", "password", "phoneNumber", "address")] bool isSelected, + IResolverContext context) { ((IMiddlewareContext)context).OperationResult.SetExtension("isSelected", isSelected); return new User { Name = "a", Email = "b", Password = "c", PhoneNumber = "d", Address = "e", City = "f", }; @@ -596,7 +683,7 @@ public User GetUser_Context_1(IResolverContext context) ((IMiddlewareContext)context).OperationResult.SetExtension("isSelected", context.IsSelected("email")); return new User { Name = "a", Email = "b", Password = "c", PhoneNumber = "d", Address = "e", City = "f", }; } - + public User GetUser_Context_2(IResolverContext context) { ((IMiddlewareContext)context).OperationResult.SetExtension( @@ -604,7 +691,7 @@ public User GetUser_Context_2(IResolverContext context) context.IsSelected("email", "password")); return new User { Name = "a", Email = "b", Password = "c", PhoneNumber = "d", Address = "e", City = "f", }; } - + public User GetUser_Context_3(IResolverContext context) { ((IMiddlewareContext)context).OperationResult.SetExtension( @@ -612,7 +699,7 @@ public User GetUser_Context_3(IResolverContext context) context.IsSelected("email", "password", "phoneNumber")); return new User { Name = "a", Email = "b", Password = "c", PhoneNumber = "d", Address = "e", City = "f", }; } - + public User GetUser_Context_4(IResolverContext context) { ((IMiddlewareContext)context).OperationResult.SetExtension( @@ -635,5 +722,14 @@ public class User public string Address { get; set; } public string City { get; set; } + + public Category Category { get; set; } + } + + public class Category + { + public string Name { get; set; } + + public Category Next { get; set; } } } \ No newline at end of file diff --git a/src/HotChocolate/Core/test/Types.Tests/Resolvers/__snapshots__/IsSelectedTests.Select_Category_Level_1.md b/src/HotChocolate/Core/test/Types.Tests/Resolvers/__snapshots__/IsSelectedTests.Select_Category_Level_1.md new file mode 100644 index 00000000000..7a8ecf69815 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Tests/Resolvers/__snapshots__/IsSelectedTests.Select_Category_Level_1.md @@ -0,0 +1,15 @@ +# Select_Category_Level_1 + +```json +{ + "data": { + "user": { + "name": "a", + "category": null + } + }, + "extensions": { + "isSelected": true + } +} +``` diff --git a/src/HotChocolate/Core/test/Types.Tests/Resolvers/__snapshots__/IsSelectedTests.Select_Category_Level_2.md b/src/HotChocolate/Core/test/Types.Tests/Resolvers/__snapshots__/IsSelectedTests.Select_Category_Level_2.md new file mode 100644 index 00000000000..ced411e85d9 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Tests/Resolvers/__snapshots__/IsSelectedTests.Select_Category_Level_2.md @@ -0,0 +1,15 @@ +# Select_Category_Level_2 + +```json +{ + "data": { + "user": { + "name": "a", + "category": null + } + }, + "extensions": { + "isSelected": false + } +} +``` diff --git a/src/HotChocolate/Data/src/Data/Filters/FilterFieldDescriptor.cs b/src/HotChocolate/Data/src/Data/Filters/FilterFieldDescriptor.cs index e3f18eecfbe..107f8232723 100644 --- a/src/HotChocolate/Data/src/Data/Filters/FilterFieldDescriptor.cs +++ b/src/HotChocolate/Data/src/Data/Filters/FilterFieldDescriptor.cs @@ -75,13 +75,17 @@ protected internal FilterFieldDescriptor( protected override void OnCreateDefinition( FilterFieldDefinition definition) { - if (!Definition.AttributesAreApplied && Definition.Member is not null) + Context.Descriptors.Push(this); + + if (Definition is { AttributesAreApplied: false, Member: not null, }) { Context.TypeInspector.ApplyAttributes(Context, this, Definition.Member); Definition.AttributesAreApplied = true; } base.OnCreateDefinition(definition); + + Context.Descriptors.Pop(); } public IFilterFieldDescriptor Name(string value) diff --git a/src/HotChocolate/Data/src/Data/Filters/FilterInputTypeDescriptor.cs b/src/HotChocolate/Data/src/Data/Filters/FilterInputTypeDescriptor.cs index 522be04457f..aadbb8cb961 100644 --- a/src/HotChocolate/Data/src/Data/Filters/FilterInputTypeDescriptor.cs +++ b/src/HotChocolate/Data/src/Data/Filters/FilterInputTypeDescriptor.cs @@ -65,7 +65,9 @@ protected FilterInputTypeDescriptor( protected override void OnCreateDefinition(FilterInputTypeDefinition definition) { - if (!Definition.AttributesAreApplied && Definition.EntityType is not null) + Context.Descriptors.Push(this); + + if (Definition is { AttributesAreApplied: false, EntityType: not null, }) { Context.TypeInspector.ApplyAttributes(Context, this, Definition.EntityType); Definition.AttributesAreApplied = true; @@ -84,6 +86,8 @@ protected override void OnCreateDefinition(FilterInputTypeDefinition definition) OnCompleteFields(fields, handledProperties); Definition.Fields.AddRange(fields.Values); + + Context.Descriptors.Pop(); } protected virtual void OnCompleteFields( diff --git a/src/HotChocolate/Data/src/Data/Filters/FilterOperationFieldDescriptor.cs b/src/HotChocolate/Data/src/Data/Filters/FilterOperationFieldDescriptor.cs index 641ecea39eb..91cdf6bf5cd 100644 --- a/src/HotChocolate/Data/src/Data/Filters/FilterOperationFieldDescriptor.cs +++ b/src/HotChocolate/Data/src/Data/Filters/FilterOperationFieldDescriptor.cs @@ -28,13 +28,17 @@ protected FilterOperationFieldDescriptor( protected override void OnCreateDefinition( FilterOperationFieldDefinition definition) { - if (!Definition.AttributesAreApplied && Definition.Property is not null) + Context.Descriptors.Push(this); + + if (Definition is { AttributesAreApplied: false, Property: not null, }) { Context.TypeInspector.ApplyAttributes(Context, this, Definition.Property); Definition.AttributesAreApplied = true; } base.OnCreateDefinition(definition); + + Context.Descriptors.Pop(); } public IFilterOperationFieldDescriptor Name(string value) diff --git a/src/HotChocolate/Data/src/Data/Projections/Extensions/ProjectionObjectFieldDescriptorExtensions.cs b/src/HotChocolate/Data/src/Data/Projections/Extensions/ProjectionObjectFieldDescriptorExtensions.cs index bebdfd2cf69..a254cf21dc3 100644 --- a/src/HotChocolate/Data/src/Data/Projections/Extensions/ProjectionObjectFieldDescriptorExtensions.cs +++ b/src/HotChocolate/Data/src/Data/Projections/Extensions/ProjectionObjectFieldDescriptorExtensions.cs @@ -349,6 +349,9 @@ public IReadOnlyList GetSelections( bool allowInternals = false) => _context.GetSelections(typeContext, selection, allowInternals); + public ISelectionCollection Select(string fieldName) + => _context.Select(fieldName); + public T GetQueryRoot() => _context.GetQueryRoot(); public IMiddlewareContext Clone() => _context.Clone(); diff --git a/src/HotChocolate/Data/src/Data/Sorting/SortEnumTypeDescriptor.cs b/src/HotChocolate/Data/src/Data/Sorting/SortEnumTypeDescriptor.cs index c096cfb4a47..bf960d0a481 100644 --- a/src/HotChocolate/Data/src/Data/Sorting/SortEnumTypeDescriptor.cs +++ b/src/HotChocolate/Data/src/Data/Sorting/SortEnumTypeDescriptor.cs @@ -42,6 +42,8 @@ protected SortEnumTypeDescriptor( protected override void OnCreateDefinition( SortEnumTypeDefinition definition) { + Context.Descriptors.Push(this); + if (!Definition.AttributesAreApplied && Definition.RuntimeType != typeof(object)) { Context.TypeInspector.ApplyAttributes( @@ -63,6 +65,8 @@ protected override void OnCreateDefinition( } base.OnCreateDefinition(definition); + + Context.Descriptors.Pop(); } public ISortEnumTypeDescriptor Name(string value) diff --git a/src/HotChocolate/Data/src/Data/Sorting/SortFieldDescriptor.cs b/src/HotChocolate/Data/src/Data/Sorting/SortFieldDescriptor.cs index 9afa9e1c061..3b6e6c6bf2b 100644 --- a/src/HotChocolate/Data/src/Data/Sorting/SortFieldDescriptor.cs +++ b/src/HotChocolate/Data/src/Data/Sorting/SortFieldDescriptor.cs @@ -75,13 +75,17 @@ protected internal SortFieldDescriptor( protected override void OnCreateDefinition( SortFieldDefinition definition) { - if (!Definition.AttributesAreApplied && Definition.Member is not null) + Context.Descriptors.Push(this); + + if (Definition is { AttributesAreApplied: false, Member: not null, }) { Context.TypeInspector.ApplyAttributes(Context, this, Definition.Member); Definition.AttributesAreApplied = true; } base.OnCreateDefinition(definition); + + Context.Descriptors.Pop(); } public ISortFieldDescriptor Name(string value) diff --git a/src/HotChocolate/Data/src/Data/Sorting/SortInputTypeDescriptor.cs b/src/HotChocolate/Data/src/Data/Sorting/SortInputTypeDescriptor.cs index e760e4b96aa..cda2d03f1f6 100644 --- a/src/HotChocolate/Data/src/Data/Sorting/SortInputTypeDescriptor.cs +++ b/src/HotChocolate/Data/src/Data/Sorting/SortInputTypeDescriptor.cs @@ -63,7 +63,9 @@ protected SortInputTypeDescriptor( protected override void OnCreateDefinition( SortInputTypeDefinition definition) { - if (!Definition.AttributesAreApplied && Definition.EntityType is not null) + Context.Descriptors.Push(this); + + if (Definition is { AttributesAreApplied: false, EntityType: not null, }) { Context.TypeInspector.ApplyAttributes(Context, this, Definition.EntityType); Definition.AttributesAreApplied = true; @@ -81,6 +83,8 @@ protected override void OnCreateDefinition( OnCompleteFields(fields, handledProperties); Definition.Fields.AddRange(fields.Values); + + Context.Descriptors.Pop(); } protected virtual void OnCompleteFields(