diff --git a/src/HotChocolate/Core/src/Abstractions/ExtensionData.cs b/src/HotChocolate/Core/src/Abstractions/ExtensionData.cs index 39ebb85aaed..e92cb4e258e 100644 --- a/src/HotChocolate/Core/src/Abstractions/ExtensionData.cs +++ b/src/HotChocolate/Core/src/Abstractions/ExtensionData.cs @@ -8,7 +8,7 @@ namespace HotChocolate; -public class ExtensionData +public sealed class ExtensionData : IDictionary , IReadOnlyDictionary { @@ -231,6 +231,4 @@ IEnumerator IEnumerable.GetEnumerator() return GetEnumerator(); } } - - public static readonly ExtensionData Empty = new(); } diff --git a/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs b/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs index b5819e47746..7f858d4cae3 100644 --- a/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs +++ b/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs @@ -197,4 +197,14 @@ public static class WellKnownContextData /// The key to get the IdValue object from the context data. /// public const string IdValue = "HotChocolate.Relay.Node.Id.Value"; + + /// + /// The key to get check if a field is the node field. + /// + public const string IsNodeField = "HotChocolate.Relay.Node.IsNodeField"; + + /// + /// The key to get check if a field is the nodes field. + /// + public const string IsNodesField = "HotChocolate.Relay.Node.IsNodeField"; } diff --git a/src/HotChocolate/Core/src/Execution/Processing/SelectionSetOptimizerContext.cs b/src/HotChocolate/Core/src/Execution/Processing/SelectionSetOptimizerContext.cs index 5b038b8ac98..f2d2f864a8a 100644 --- a/src/HotChocolate/Core/src/Execution/Processing/SelectionSetOptimizerContext.cs +++ b/src/HotChocolate/Core/src/Execution/Processing/SelectionSetOptimizerContext.cs @@ -5,6 +5,7 @@ using HotChocolate.Types; using HotChocolate.Utilities; using static HotChocolate.Execution.Properties.Resources; +using NameUtils = HotChocolate.Utilities.NameUtils; namespace HotChocolate.Execution.Processing; @@ -126,7 +127,7 @@ public void AddSelection(string responseName, Selection newSelection) /// public void ReplaceSelection(string responseName, Selection newSelection) { - if (!NameUtils.IsValidGraphQLName(responseName)) + if (!responseName.IsValidGraphQLName()) { throw new ArgumentException( string.Format(SelectionSetOptimizerContext_InvalidFieldName, responseName)); diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/DefinitionBase.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/DefinitionBase.cs index 6f559d01832..de20a23a40f 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/DefinitionBase.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/DefinitionBase.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using HotChocolate.Utilities; #nullable enable @@ -106,7 +107,7 @@ public IReadOnlyList GetDependencies() { if (_contextData is null) { - return ExtensionData.Empty; + return ImmutableDictionary.Empty; } return _contextData; diff --git a/src/HotChocolate/Core/src/Types/Types/Relay/NodeFieldTypeInterceptor.cs b/src/HotChocolate/Core/src/Types/Types/Relay/NodeFieldTypeInterceptor.cs index 15d541268e9..a71ae5caced 100644 --- a/src/HotChocolate/Core/src/Types/Types/Relay/NodeFieldTypeInterceptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Relay/NodeFieldTypeInterceptor.cs @@ -75,6 +75,11 @@ internal sealed class NodeFieldTypeInterceptor : TypeInterceptor } }; + // In the projection interceptor we want to change the context data that is on this field + // after the field is completed. We need at least 1 element on the context data to avoid + // it to be replaced with ExtensionData.Empty + field.ContextData[WellKnownContextData.IsNodeField] = true; + fields.Insert(index, field); } @@ -107,6 +112,11 @@ internal sealed class NodeFieldTypeInterceptor : TypeInterceptor } }; + // In the projection interceptor we want to change the context data that is on this field + // after the field is completed. We need at least 1 element on the context data to avoid + // it to be replaced with ExtensionData.Empty + field.ContextData[WellKnownContextData.IsNodesField] = true; + fields.Insert(index, field); } } diff --git a/src/HotChocolate/Core/src/Types/Types/TypeSystemObjectBase~1.cs b/src/HotChocolate/Core/src/Types/Types/TypeSystemObjectBase~1.cs index bf49d0ee093..98ddea9ae4a 100644 --- a/src/HotChocolate/Core/src/Types/Types/TypeSystemObjectBase~1.cs +++ b/src/HotChocolate/Core/src/Types/Types/TypeSystemObjectBase~1.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; using HotChocolate.Configuration; using HotChocolate.Properties; @@ -13,7 +14,7 @@ public abstract class TypeSystemObjectBase : TypeSystemObjectBase where TDefinition : DefinitionBase { private TDefinition? _definition; - private ExtensionData? _contextData; + private IReadOnlyDictionary? _contextData; public override IReadOnlyDictionary ContextData { @@ -129,10 +130,10 @@ internal sealed override void CompleteType(ITypeCompletionContext context) _contextData = definition.ContextData; _definition = null; - OnAfterCompleteType(context, definition, _contextData); + OnAfterCompleteType(context, definition, definition.ContextData); ExecuteConfigurations(context, definition, ApplyConfigurationOn.AfterCompletion); - OnValidateType(context, definition, _contextData); + OnValidateType(context, definition, definition.ContextData); MarkCompleted(); } @@ -143,7 +144,7 @@ internal sealed override void FinalizeType(ITypeCompletionContext context) // collected by the GC. if (_contextData!.Count == 0) { - _contextData = ExtensionData.Empty; + _contextData = ImmutableDictionary.Empty; } MarkFinalized(); diff --git a/src/HotChocolate/Data/src/Data/DataResources.Designer.cs b/src/HotChocolate/Data/src/Data/DataResources.Designer.cs index 152714f977f..a08e3f09d54 100644 --- a/src/HotChocolate/Data/src/Data/DataResources.Designer.cs +++ b/src/HotChocolate/Data/src/Data/DataResources.Designer.cs @@ -441,6 +441,12 @@ internal class DataResources { } } + internal static string ProjectionConvention_NodeFieldWasInInvalidState { + get { + return ResourceManager.GetString("ProjectionConvention_NodeFieldWasInInvalidState", resourceCulture); + } + } + internal static string ProjectionVisitor_NodeFieldWasNotFound { get { return ResourceManager.GetString("ProjectionVisitor_NodeFieldWasNotFound", resourceCulture); @@ -494,13 +500,13 @@ internal class DataResources { return ResourceManager.GetString("QueryableFiltering_ExpressionParameterInvalid", resourceCulture); } } - + internal static string QueryableFilterProvider_ExpressionParameterInvalid { get { return ResourceManager.GetString("QueryableFilterProvider_ExpressionParameterInvalid", resourceCulture); } } - + internal static string QueryableFiltering_NoMemberDeclared { get { return ResourceManager.GetString("QueryableFiltering_NoMemberDeclared", resourceCulture); diff --git a/src/HotChocolate/Data/src/Data/DataResources.resx b/src/HotChocolate/Data/src/Data/DataResources.resx index f6be4b1f5cc..a292578d44a 100644 --- a/src/HotChocolate/Data/src/Data/DataResources.resx +++ b/src/HotChocolate/Data/src/Data/DataResources.resx @@ -2,9 +2,9 @@ + xmlns="" + xmlns:xsd="http://www.w3.org/2001/XMLSchema" + xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> @@ -16,11 +16,13 @@ 1.3 - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 @@ -221,6 +223,9 @@ Projection Visitor is in invalid state. Projection failed! + + The Query.node field is in a invalid state. The context data was empty, but it is expected to contain at least one element + Type {0} does not contain a valid node field. Only `items` and `nodes` are supported diff --git a/src/HotChocolate/Data/src/Data/Extensions/HotChocolateDataRequestBuilderExtensions.cs b/src/HotChocolate/Data/src/Data/Extensions/HotChocolateDataRequestBuilderExtensions.cs index e4498ac9128..480d5c70dcd 100644 --- a/src/HotChocolate/Data/src/Data/Extensions/HotChocolateDataRequestBuilderExtensions.cs +++ b/src/HotChocolate/Data/src/Data/Extensions/HotChocolateDataRequestBuilderExtensions.cs @@ -5,6 +5,7 @@ using HotChocolate.Data.Projections; using HotChocolate.Data.Sorting; using HotChocolate.Execution.Configuration; +using HotChocolate.Execution.Processing; using HotChocolate.Internal; namespace Microsoft.Extensions.DependencyInjection; @@ -199,11 +200,11 @@ public static class HotChocolateDataRequestBuilderExtensions public static IRequestExecutorBuilder AddProjections( this IRequestExecutorBuilder builder, Action configure, - string? name = null) => - builder.ConfigureSchema(s => s + string? name = null) + => builder.ConfigureSchema(s => s .TryAddTypeInterceptor() .TryAddConvention( - sp => new ProjectionConvention(configure), + _ => new ProjectionConvention(configure), name)); /// @@ -224,8 +225,8 @@ public static class HotChocolateDataRequestBuilderExtensions public static IRequestExecutorBuilder AddProjections( this IRequestExecutorBuilder builder, string? name = null) - where TConvention : class, IProjectionConvention => - builder.ConfigureSchema(s => s + where TConvention : class, IProjectionConvention + => builder.ConfigureSchema(s => s .TryAddTypeInterceptor() .TryAddConvention(name)); } diff --git a/src/HotChocolate/Data/src/Data/Projections/Expressions/QueryableProjectionProvider.cs b/src/HotChocolate/Data/src/Data/Projections/Expressions/QueryableProjectionProvider.cs index afcbae5dd34..77522fffbd5 100644 --- a/src/HotChocolate/Data/src/Data/Projections/Expressions/QueryableProjectionProvider.cs +++ b/src/HotChocolate/Data/src/Data/Projections/Expressions/QueryableProjectionProvider.cs @@ -1,13 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; using System.Threading.Tasks; -using HotChocolate.Execution.Processing; -using HotChocolate.Language; using HotChocolate.Resolvers; -using HotChocolate.Types; -using static HotChocolate.WellKnownContextData; namespace HotChocolate.Data.Projections.Expressions; @@ -57,52 +52,27 @@ private static ApplyProjection CreateApplicator() // if projections are already applied we can skip var skipProjection = - context.LocalContextData.TryGetValue(SkipProjectionKey, out var skip) && - skip is true; + context.LocalContextData.TryGetValue(SkipProjectionKey, out var skip) && + skip is true; // ensure sorting is only applied once context.LocalContextData = - context.LocalContextData.SetItem(SkipProjectionKey, true); + context.LocalContextData.SetItem(SkipProjectionKey, true); if (skipProjection) { return input; } - // in case we are being called from the node/nodes field we need to enrich - // the projections context with the type that shall be resolved. - Type? selectionRuntimeType = null; - ISelection? selection = null; - - if (context.LocalContextData.TryGetValue(InternalType, out var value) && - value is ObjectType objectType && - objectType.RuntimeType != typeof(object)) - { - selectionRuntimeType = objectType.RuntimeType; - var fieldProxy = new NodeFieldProxy(context.Selection.Field, objectType); - selection = CreateProxySelection(context.Selection, fieldProxy); - } - var visitorContext = new QueryableProjectionContext( context, context.ObjectType, - selectionRuntimeType ?? context.Selection.Type.UnwrapRuntimeType()); - var visitor = new QueryableProjectionVisitor(); + context.Selection.Type.UnwrapRuntimeType()); - // if we do not have a node selection proxy than this is a standard field and we - // just traverse - if (selection is null) - { - visitor.Visit(visitorContext); - } + var visitor = new QueryableProjectionVisitor(); - // but if we have a node selection proxy we will push that into the visitor to use - // it instead of the selection on the context. - else - { - visitor.Visit(visitorContext, selection); - } + visitor.Visit(visitorContext); var projection = visitorContext.Project(); @@ -114,80 +84,4 @@ private static ApplyProjection CreateApplicator() _ => input }; }; - - private static Selection CreateProxySelection(ISelection selection, NodeFieldProxy field) - { - var includeConditionsSource = ((Selection)selection).IncludeConditions; - var includeConditions = new long[includeConditionsSource.Length]; - includeConditionsSource.CopyTo(includeConditions); - - var proxy = new Selection(selection.Id, selection.DeclaringType, field, field.Type, selection.SyntaxNode, selection.ResponseName, selection.Arguments, includeConditions, selection.IsInternal, selection.Strategy != SelectionExecutionStrategy.Serial, selection.ResolverPipeline, selection.PureResolver); - proxy.SetSelectionSetId(((Selection)selection).SelectionSetId); - proxy.Seal(selection.DeclaringSelectionSet); - return proxy; - } - - private sealed class NodeFieldProxy : IObjectField - { - private readonly IObjectField _nodeField; - private readonly ObjectType _type; - private readonly Type _runtimeType; - - public NodeFieldProxy(IObjectField nodeField, ObjectType type) - { - _nodeField = nodeField; - _type = type; - _runtimeType = type.RuntimeType; - } - - public IObjectType DeclaringType => _nodeField.DeclaringType; - - public bool IsParallelExecutable => _nodeField.IsParallelExecutable; - - public bool HasStreamResult => _nodeField.HasStreamResult; - - public FieldDelegate Middleware => _nodeField.Middleware; - - public FieldResolverDelegate? Resolver => _nodeField.Resolver; - - public PureFieldDelegate? PureResolver => _nodeField.PureResolver; - - public SubscribeResolverDelegate? SubscribeResolver => _nodeField.SubscribeResolver; - - public IReadOnlyList ExecutableDirectives => _nodeField.ExecutableDirectives; - - public MemberInfo? Member => _nodeField.Member; - - public MemberInfo? ResolverMember => _nodeField.ResolverMember; - - public bool IsIntrospectionField => _nodeField.IsIntrospectionField; - - public bool IsDeprecated => _nodeField.IsDeprecated; - - public string? DeprecationReason => _nodeField.DeprecationReason; - - public int Index => _nodeField.Index; - - public string? Description => _nodeField.Description; - - public IDirectiveCollection Directives => _nodeField.Directives; - - public ISyntaxNode? SyntaxNode => _nodeField.SyntaxNode; - - public IReadOnlyDictionary ContextData => _nodeField.ContextData; - - public IOutputType Type => _type; - - public IFieldCollection Arguments => _nodeField.Arguments; - - public string Name => _nodeField.Name; - - public FieldCoordinate Coordinate => _nodeField.Coordinate; - - public Type RuntimeType => _runtimeType; - - IComplexOutputType IOutputField.DeclaringType => _nodeField.DeclaringType; - - ITypeSystemObject IField.DeclaringType => ((IField)_nodeField).DeclaringType; - } } diff --git a/src/HotChocolate/Data/src/Data/Projections/Extensions/ProjectionObjectFieldDescriptorExtensions.cs b/src/HotChocolate/Data/src/Data/Projections/Extensions/ProjectionObjectFieldDescriptorExtensions.cs new file mode 100644 index 00000000000..45baa7b8c9f --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Extensions/ProjectionObjectFieldDescriptorExtensions.cs @@ -0,0 +1,413 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Reflection; +using System.Security.AccessControl; +using System.Threading; +using System.Threading.Tasks; +using HotChocolate.Configuration; +using HotChocolate.Data; +using HotChocolate.Data.Projections; +using HotChocolate.Execution; +using HotChocolate.Execution.Processing; +using HotChocolate.Language; +using HotChocolate.Resolvers; +using HotChocolate.Types.Descriptors.Definitions; +using static HotChocolate.Data.Projections.ProjectionProvider; +using static HotChocolate.Execution.Processing.OperationCompilerOptimizerHelper; +using static HotChocolate.WellKnownContextData; + +namespace HotChocolate.Types; + +public static class ProjectionObjectFieldDescriptorExtensions +{ + private static readonly MethodInfo _factoryTemplate = + typeof(ProjectionObjectFieldDescriptorExtensions) + .GetMethod(nameof(CreateMiddleware), BindingFlags.Static | BindingFlags.NonPublic)!; + + /// + /// + /// Configure if this field should be projected by or if it + /// should be skipped + /// + /// + /// if is false, this field will never be projected even if + /// it is in the selection set + /// if is true, this field will always be projected even it + /// it is not in the selection set + /// + /// + /// The descriptor + /// + /// If false the field will never be projected, if true it will always be projected + /// + /// The descriptor passed in by + public static IObjectFieldDescriptor IsProjected( + this IObjectFieldDescriptor descriptor, + bool isProjected = true) + { + descriptor + .Extend() + .OnBeforeCreate( + x => x.ContextData[ProjectionConvention.IsProjectedKey] = isProjected); + + return descriptor; + } + + /// + /// Projects the selection set of the request onto the field. Registers a middleware that + /// uses the registered to apply the projections + /// + /// The descriptor + /// + /// Specify which is used, based on the value passed in + /// + /// + /// The descriptor passed in by + /// + /// In case the descriptor is null + /// + public static IObjectFieldDescriptor UseProjection( + this IObjectFieldDescriptor descriptor, + string? scope = null) + { + if (descriptor is null) + { + throw new ArgumentNullException(nameof(descriptor)); + } + + return UseProjection(descriptor, null, scope); + } + + /// + /// Projects the selection set of the request onto the field. Registers a middleware that + /// uses the registered to apply the projections + /// + /// The descriptor + /// + /// Specify which is used, based on the value passed in + /// + /// + /// + /// The of the resolved field + /// + /// The descriptor passed in by + /// + /// In case the descriptor is null + /// + public static IObjectFieldDescriptor UseProjection( + this IObjectFieldDescriptor descriptor, + string? scope = null) + { + if (descriptor is null) + { + throw new ArgumentNullException(nameof(descriptor)); + } + + return UseProjection(descriptor, typeof(T), scope); + } + + /// + /// Projects the selection set of the request onto the field. Registers a middleware that + /// uses the registered to apply the projections + /// + /// The descriptor + /// + /// The of the resolved field + /// + /// + /// Specify which is used, based on the value passed in + /// + /// + /// The descriptor passed in by + /// + /// In case the descriptor is null + /// + public static IObjectFieldDescriptor UseProjection( + this IObjectFieldDescriptor descriptor, + Type? type, + string? scope = null) + { + if (descriptor is null) + { + throw new ArgumentNullException(nameof(descriptor)); + } + + FieldMiddlewareDefinition placeholder = + new(_ => _ => default, key: WellKnownMiddleware.Projection); + + descriptor.Extend().Definition.MiddlewareDefinitions.Add(placeholder); + + descriptor + .Extend() + .OnBeforeCreate( + (context, definition) => + { + var selectionType = type; + + if (selectionType is null) + { + if (definition.ResultType is null || + !context.TypeInspector.TryCreateTypeInfo( + definition.ResultType, + out var typeInfo)) + { + throw new ArgumentException( + DataResources.UseProjection_CannotHandleType_, + nameof(descriptor)); + } + + selectionType = typeInfo.NamedType; + } + + definition.Configurations.Add( + new CompleteConfiguration( + (c, d) => + CompileMiddleware( + selectionType, + d, + placeholder, + c, + scope), + definition, + ApplyConfigurationOn.BeforeCompletion)); + }); + + return descriptor; + } + + private static void CompileMiddleware( + Type type, + ObjectFieldDefinition definition, + FieldMiddlewareDefinition placeholder, + ITypeCompletionContext context, + string? scope) + { + var convention = context.DescriptorContext.GetProjectionConvention(scope); + RegisterOptimizer(definition.ContextData, convention.CreateOptimizer()); + + definition.ContextData[ProjectionContextIdentifier] = true; + + var factory = _factoryTemplate.MakeGenericMethod(type); + var middleware = (FieldMiddleware)factory.Invoke(null, new object[] { convention })!; + var index = definition.MiddlewareDefinitions.IndexOf(placeholder); + definition.MiddlewareDefinitions[index] = + new(middleware, key: WellKnownMiddleware.Projection); + } + + private static FieldMiddleware CreateMiddleware(IProjectionConvention convention) + { + FieldMiddleware executor = convention.CreateExecutor(); + return next => context => + { + // in case we are being called from the node/nodes field we need to enrich + // the projections context with the type that shall be resolved. + if (context.LocalContextData.TryGetValue(InternalType, out var value) && + value is ObjectType objectType && + objectType.RuntimeType != typeof(object)) + { + var fieldProxy = new NodeFieldProxy(context.Selection.Field, objectType); + var selection = CreateProxySelection(context.Selection, fieldProxy); + context = new MiddlewareContextProxy(context, selection, objectType); + } + + return executor.Invoke(next).Invoke(context); + }; + } + + private sealed class MiddlewareContextProxy : IMiddlewareContext + { + private readonly IMiddlewareContext _context; + + public MiddlewareContextProxy( + IMiddlewareContext context, + ISelection selection, + IObjectType objectType) + { + _context = context; + Selection = selection; + ObjectType = objectType; + } + + public IDictionary ContextData => _context.ContextData; + + public ISchema Schema => _context.Schema; + + public IObjectType ObjectType { get; } + + public IOperation Operation => _context.Operation; + + public ISelection Selection { get; } + + public IVariableValueCollection Variables => _context.Variables; + + public Path Path => _context.Path; + + public string ResponseName => _context.ResponseName; + + public bool HasErrors => _context.HasErrors; + + public IServiceProvider Services + { + get => _context.Services; + set => _context.Services = value; + } + + public IImmutableDictionary ScopedContextData + { + get => _context.ScopedContextData; + set => _context.ScopedContextData = value; + } + + public IImmutableDictionary LocalContextData + { + get => _context.LocalContextData; + set => _context.LocalContextData = value; + } + + public IType? ValueType { get => _context.ValueType; set => _context.ValueType = value; } + + public object? Result { get => _context.Result; set => _context.Result = value; } + + public bool IsResultModified => _context.IsResultModified; + + public CancellationToken RequestAborted => _context.RequestAborted; + + public T Parent() => _context.Parent(); + + public T ArgumentValue(string name) => _context.ArgumentValue(name); + + public TValueNode ArgumentLiteral(string name) where TValueNode : IValueNode + => _context.ArgumentLiteral(name); + + public Optional ArgumentOptional(string name) => _context.ArgumentOptional(name); + + public ValueKind ArgumentKind(string name) => _context.ArgumentKind(name); + + public T Service() => _context.Service(); + + public T Resolver() => _context.Resolver(); + + public object Service(Type service) => _context.Service(service); + + public void ReportError(string errorMessage) => _context.ReportError(errorMessage); + + public void ReportError(IError error) => _context.ReportError(error); + + public void ReportError(Exception exception, Action? configure = null) + => _context.ReportError(exception, configure); + + public IReadOnlyList GetSelections( + IObjectType typeContext, + ISelection? selection = null, + bool allowInternals = false) + => _context.GetSelections(typeContext, selection, allowInternals); + + public T GetQueryRoot() => _context.GetQueryRoot(); + + public IMiddlewareContext Clone() => _context.Clone(); + + public ValueTask ResolveAsync() => _context.ResolveAsync(); + + public void RegisterForCleanup( + Func action, + CleanAfter cleanAfter = CleanAfter.Resolver) + => _context.RegisterForCleanup(action, cleanAfter); + + public IReadOnlyDictionary ReplaceArguments( + IReadOnlyDictionary newArgumentValues) + => _context.ReplaceArguments(newArgumentValues); + + public ArgumentValue ReplaceArgument(string argumentName, ArgumentValue newArgumentValue) + => _context.ReplaceArgument(argumentName, newArgumentValue); + + IResolverContext IResolverContext.Clone() => _context.Clone(); + } + + private static Selection CreateProxySelection(ISelection selection, NodeFieldProxy field) + { + var includeConditionsSource = ((Selection)selection).IncludeConditions; + var includeConditions = new long[includeConditionsSource.Length]; + includeConditionsSource.CopyTo(includeConditions); + + var proxy = new Selection(selection.Id, + selection.DeclaringType, + field, + field.Type, + selection.SyntaxNode, + selection.ResponseName, + selection.Arguments, + includeConditions, + selection.IsInternal, + selection.Strategy != SelectionExecutionStrategy.Serial, + selection.ResolverPipeline, + selection.PureResolver); + proxy.SetSelectionSetId(((Selection)selection).SelectionSetId); + proxy.Seal(selection.DeclaringSelectionSet); + return proxy; + } + + private sealed class NodeFieldProxy : IObjectField + { + private readonly IObjectField _nodeField; + private readonly ObjectType _type; + private readonly Type _runtimeType; + + public NodeFieldProxy(IObjectField nodeField, ObjectType type) + { + _nodeField = nodeField; + _type = type; + _runtimeType = type.RuntimeType; + } + + public IObjectType DeclaringType => _nodeField.DeclaringType; + + public bool IsParallelExecutable => _nodeField.IsParallelExecutable; + + public bool HasStreamResult => _nodeField.HasStreamResult; + + public FieldDelegate Middleware => _nodeField.Middleware; + + public FieldResolverDelegate? Resolver => _nodeField.Resolver; + + public PureFieldDelegate? PureResolver => _nodeField.PureResolver; + + public SubscribeResolverDelegate? SubscribeResolver => _nodeField.SubscribeResolver; + + public IReadOnlyList ExecutableDirectives => _nodeField.ExecutableDirectives; + + public MemberInfo? Member => _nodeField.Member; + + public MemberInfo? ResolverMember => _nodeField.ResolverMember; + + public bool IsIntrospectionField => _nodeField.IsIntrospectionField; + + public bool IsDeprecated => _nodeField.IsDeprecated; + + public string? DeprecationReason => _nodeField.DeprecationReason; + + public int Index => _nodeField.Index; + + public string? Description => _nodeField.Description; + + public IDirectiveCollection Directives => _nodeField.Directives; + + public ISyntaxNode? SyntaxNode => _nodeField.SyntaxNode; + + public IReadOnlyDictionary ContextData => _nodeField.ContextData; + + public IOutputType Type => _type; + + public IFieldCollection Arguments => _nodeField.Arguments; + + public string Name => _nodeField.Name; + + public FieldCoordinate Coordinate => _nodeField.Coordinate; + + public Type RuntimeType => _runtimeType; + + IComplexOutputType IOutputField.DeclaringType => _nodeField.DeclaringType; + + ITypeSystemObject IField.DeclaringType => ((IField)_nodeField).DeclaringType; + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Extensions/SelectionObjectFieldDescriptorExtensions.cs b/src/HotChocolate/Data/src/Data/Projections/Extensions/SelectionObjectFieldDescriptorExtensions.cs deleted file mode 100644 index e81f40185ea..00000000000 --- a/src/HotChocolate/Data/src/Data/Projections/Extensions/SelectionObjectFieldDescriptorExtensions.cs +++ /dev/null @@ -1,192 +0,0 @@ -using System; -using System.Reflection; -using HotChocolate.Configuration; -using HotChocolate.Data; -using HotChocolate.Data.Projections; -using HotChocolate.Resolvers; -using HotChocolate.Types.Descriptors.Definitions; -using static HotChocolate.Data.Projections.ProjectionProvider; -using static HotChocolate.Execution.Processing.OperationCompilerOptimizerHelper; - -namespace HotChocolate.Types; - -public static class ProjectionObjectFieldDescriptorExtensions -{ - private static readonly MethodInfo _factoryTemplate = - typeof(ProjectionObjectFieldDescriptorExtensions) - .GetMethod(nameof(CreateMiddleware), BindingFlags.Static | BindingFlags.NonPublic)!; - - /// - /// - /// Configure if this field should be projected by or if it - /// should be skipped - /// - /// - /// if is false, this field will never be projected even if - /// it is in the selection set - /// if is true, this field will always be projected even it - /// it is not in the selection set - /// - /// - /// The descriptor - /// - /// If false the field will never be projected, if true it will always be projected - /// - /// The descriptor passed in by - public static IObjectFieldDescriptor IsProjected( - this IObjectFieldDescriptor descriptor, - bool isProjected = true) - { - descriptor - .Extend() - .OnBeforeCreate( - x => x.ContextData[ProjectionConvention.IsProjectedKey] = isProjected); - - return descriptor; - } - - /// - /// Projects the selection set of the request onto the field. Registers a middleware that - /// uses the registered to apply the projections - /// - /// The descriptor - /// - /// Specify which is used, based on the value passed in - /// - /// - /// The descriptor passed in by - /// - /// In case the descriptor is null - /// - public static IObjectFieldDescriptor UseProjection( - this IObjectFieldDescriptor descriptor, - string? scope = null) - { - if (descriptor is null) - { - throw new ArgumentNullException(nameof(descriptor)); - } - - return UseProjection(descriptor, null, scope); - } - - /// - /// Projects the selection set of the request onto the field. Registers a middleware that - /// uses the registered to apply the projections - /// - /// The descriptor - /// - /// Specify which is used, based on the value passed in - /// - /// - /// - /// The of the resolved field - /// - /// The descriptor passed in by - /// - /// In case the descriptor is null - /// - public static IObjectFieldDescriptor UseProjection( - this IObjectFieldDescriptor descriptor, - string? scope = null) - { - if (descriptor is null) - { - throw new ArgumentNullException(nameof(descriptor)); - } - - return UseProjection(descriptor, typeof(T), scope); - } - - /// - /// Projects the selection set of the request onto the field. Registers a middleware that - /// uses the registered to apply the projections - /// - /// The descriptor - /// - /// The of the resolved field - /// - /// - /// Specify which is used, based on the value passed in - /// - /// - /// The descriptor passed in by - /// - /// In case the descriptor is null - /// - public static IObjectFieldDescriptor UseProjection( - this IObjectFieldDescriptor descriptor, - Type? type, - string? scope = null) - { - if (descriptor is null) - { - throw new ArgumentNullException(nameof(descriptor)); - } - - FieldMiddlewareDefinition placeholder = - new(_ => _ => default, key: WellKnownMiddleware.Projection); - - descriptor.Extend().Definition.MiddlewareDefinitions.Add(placeholder); - - descriptor - .Extend() - .OnBeforeCreate( - (context, definition) => - { - var selectionType = type; - - if (selectionType is null) - { - if (definition.ResultType is null || - !context.TypeInspector.TryCreateTypeInfo( - definition.ResultType, - out var typeInfo)) - { - throw new ArgumentException( - DataResources.UseProjection_CannotHandleType_, - nameof(descriptor)); - } - - selectionType = typeInfo.NamedType; - } - - definition.Configurations.Add( - new CompleteConfiguration( - (c, d) => - CompileMiddleware( - selectionType, - d, - placeholder, - c, - scope), - definition, - ApplyConfigurationOn.BeforeCompletion)); - }); - - return descriptor; - } - - private static void CompileMiddleware( - Type type, - ObjectFieldDefinition definition, - FieldMiddlewareDefinition placeholder, - ITypeCompletionContext context, - string? scope) - { - var convention = context.DescriptorContext.GetProjectionConvention(scope); - RegisterOptimizer(definition.ContextData, convention.CreateOptimizer()); - - definition.ContextData[ProjectionContextIdentifier] = true; - - var factory = _factoryTemplate.MakeGenericMethod(type); - var middleware = (FieldMiddleware)factory.Invoke(null, new object[] { convention })!; - var index = definition.MiddlewareDefinitions.IndexOf(placeholder); - definition.MiddlewareDefinitions[index] = - new(middleware, key: WellKnownMiddleware.Projection); - } - - private static FieldMiddleware CreateMiddleware( - IProjectionConvention convention) => - convention.CreateExecutor(); -} diff --git a/src/HotChocolate/Data/src/Data/Projections/Optimizers/NodeSelectionSetOptimizer.cs b/src/HotChocolate/Data/src/Data/Projections/Optimizers/NodeSelectionSetOptimizer.cs new file mode 100644 index 00000000000..e814ff5839b --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Optimizers/NodeSelectionSetOptimizer.cs @@ -0,0 +1,24 @@ +using HotChocolate.Execution.Processing; +using HotChocolate.Types.Relay; + +namespace HotChocolate.Data.Projections; + +internal sealed class NodeSelectionSetOptimizer : ISelectionSetOptimizer +{ + private readonly ISelectionSetOptimizer _optimizer; + + public NodeSelectionSetOptimizer(ISelectionSetOptimizer optimizer) + { + _optimizer = optimizer; + } + + public void OptimizeSelectionSet(SelectionSetOptimizerContext context) + { + if (context.Type.ContextData.TryGetValue(WellKnownContextData.NodeResolver, out var o) && + o is NodeResolverInfo { QueryField.ContextData: var fieldContextData } && + fieldContextData.ContainsKey(ProjectionProvider.ProjectionContextIdentifier)) + { + _optimizer.OptimizeSelectionSet(context); + } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/ProjectionOptimizer.cs b/src/HotChocolate/Data/src/Data/Projections/ProjectionOptimizer.cs index 11a3aa491cd..3baa5cd09f2 100644 --- a/src/HotChocolate/Data/src/Data/Projections/ProjectionOptimizer.cs +++ b/src/HotChocolate/Data/src/Data/Projections/ProjectionOptimizer.cs @@ -24,7 +24,9 @@ public void OptimizeSelectionSet(SelectionSetOptimizerContext context) { var rewrittenSelection = _provider.RewriteSelection(context, context.Selections[responseName]); + context.ReplaceSelection(responseName, rewrittenSelection); + processedSelections.Add(responseName); } } diff --git a/src/HotChocolate/Data/src/Data/Projections/ProjectionTypeInterceptor.cs b/src/HotChocolate/Data/src/Data/Projections/ProjectionTypeInterceptor.cs index 7d0916a4d93..81108f113c2 100644 --- a/src/HotChocolate/Data/src/Data/Projections/ProjectionTypeInterceptor.cs +++ b/src/HotChocolate/Data/src/Data/Projections/ProjectionTypeInterceptor.cs @@ -1,14 +1,47 @@ using System.Collections.Generic; using HotChocolate.Configuration; +using HotChocolate.Types; using HotChocolate.Types.Descriptors.Definitions; using static HotChocolate.Data.Projections.ProjectionConvention; +using static HotChocolate.Execution.Processing.OperationCompilerOptimizerHelper; namespace HotChocolate.Data.Projections; -public class ProjectionTypeInterceptor : TypeInterceptor +internal sealed class ProjectionTypeInterceptor : TypeInterceptor { public override bool CanHandle(ITypeSystemObjectContext context) => true; + public override void OnAfterCompleteType( + ITypeCompletionContext completionContext, + DefinitionBase? definition, + IDictionary contextData) + { + if ((completionContext.IsQueryType ?? false) && + completionContext.Type is ObjectType { Fields: var fields }) + { + foreach (var field in fields) + { + if (field.Name == "node") + { + var selectionOptimizer = completionContext.DescriptorContext + .GetProjectionConvention() + .CreateOptimizer(); + + if (field.ContextData is not ExtensionData extensionData) + { + throw ThrowHelper.ProjectionConvention_NodeFieldWasInInvalidState(); + } + + RegisterOptimizer( + extensionData, + new NodeSelectionSetOptimizer(selectionOptimizer)); + + break; + } + } + } + } + public override void OnAfterCompleteName( ITypeCompletionContext completionContext, DefinitionBase? definition, diff --git a/src/HotChocolate/Data/src/Data/ThrowHelper.cs b/src/HotChocolate/Data/src/Data/ThrowHelper.cs index facff875290..59ead3b6a03 100644 --- a/src/HotChocolate/Data/src/Data/ThrowHelper.cs +++ b/src/HotChocolate/Data/src/Data/ThrowHelper.cs @@ -390,6 +390,12 @@ internal static class ThrowHelper .SetMessage(DataResources.ProjectionConvention_CouldNotProject) .Build()); + public static SchemaException ProjectionConvention_NodeFieldWasInInvalidState() => + new SchemaException( + SchemaErrorBuilder.New() + .SetMessage(DataResources.ProjectionConvention_NodeFieldWasInInvalidState) + .Build()); + public static SchemaException Projection_ProjectionWasNotFound(IResolverContext context) => new SchemaException( SchemaErrorBuilder.New() diff --git a/src/HotChocolate/Data/test/Data.Projections.Tests/IntegrationTests.cs b/src/HotChocolate/Data/test/Data.Projections.Tests/IntegrationTests.cs index 647b001cccf..29ceb5a28d6 100644 --- a/src/HotChocolate/Data/test/Data.Projections.Tests/IntegrationTests.cs +++ b/src/HotChocolate/Data/test/Data.Projections.Tests/IntegrationTests.cs @@ -136,6 +136,8 @@ public async Task Node_Resolver_With_SingleOrDefault() .AddGraphQL() .AddQueryType() .AddObjectType(d => d.ImplementsNode().IdField(t => t.Bar)) + .AddObjectType(d => d.ImplementsNode().IdField(t => t.IdOfBar)) + .AddObjectType(d => d.ImplementsNode().IdField(t => t.Bar2)) .AddGlobalObjectIdentification() .AddProjections() .BuildRequestExecutorAsync(); @@ -144,16 +146,70 @@ public async Task Node_Resolver_With_SingleOrDefault() result.MatchSnapshot(); } + + [Fact] + public async Task Node_Resolver_With_SingleOrDefault_Fragments() + { + var executor = await new ServiceCollection() + .AddGraphQL() + .AddQueryType() + .AddObjectType(d => d.ImplementsNode().IdField(t => t.Bar)) + .AddObjectType(d => d.ImplementsNode().IdField(t => t.IdOfBar)) + .AddObjectType(d => d.ImplementsNode().IdField(t => t.Bar2)) + .AddGlobalObjectIdentification() + .AddProjections() + .BuildRequestExecutorAsync(); + + var result = await executor + .ExecuteAsync(""" + { + node(id: "Rm9vCmRB") { + id + __typename + ... on Baz { fieldOfBaz } + ... on Foo { fieldOfFoo } + } + } + """); + + result.MatchSnapshot(); + } + + [Fact] + public async Task Node_Resolver_Without_SingleOrDefault() + { + var executor = await new ServiceCollection() + .AddGraphQL() + .AddQueryType() + .AddObjectType(d => d.ImplementsNode().IdField(t => t.Bar)) + .AddObjectType(d => d.ImplementsNode().IdField(t => t.IdOfBar)) + .AddObjectType(d => d.ImplementsNode().IdField(t => t.Bar2)) + .AddGlobalObjectIdentification() + .AddProjections() + .BuildRequestExecutorAsync(); + + var result = await executor + .ExecuteAsync(""" + { + node(id: "QmFyCmRB") { + id + __typename + ... on Baz { fieldOfBaz } + ... on Foo { fieldOfFoo } + ... on Bar { fieldOfBar } + } + } + """); + + result.MatchSnapshot(); + } } public class Query { [UseProjection] - public IQueryable Foos => new Foo[] - { - new() { Bar = "A" }, - new() { Bar = "B" } - }.AsQueryable(); + public IQueryable Foos + => new Foo[] { new() { Bar = "A" }, new() { Bar = "B" } }.AsQueryable(); } [ExtendObjectType(typeof(Foo))] @@ -161,15 +217,9 @@ public class FooExtensions { public string Baz => "baz"; - public IEnumerable Qux => new[] - { - "baz" - }; + public IEnumerable Qux => new[] { "baz" }; - public IEnumerable NestedList => new[] - { - new Foo() { Bar = "C" } - }; + public IEnumerable NestedList => new[] { new Foo() { Bar = "C" } }; public Foo Nested => new() { Bar = "C" }; } @@ -177,23 +227,41 @@ public class FooExtensions public class Foo { public string? Bar { get; set; } + public string FieldOfFoo => "fieldOfFoo"; +} + +public class Baz +{ + public string? Bar2 { get; set; } + + public string FieldOfBaz => "fieldOfBaz"; +} + +public class Bar +{ + public string? IdOfBar { get; set; } + + public string FieldOfBar => "fieldOfBar"; } public class QueryWithNodeResolvers { [UseProjection] public IQueryable All() - => new Foo[] - { - new() { Bar = "A" }, - }.AsQueryable(); + => new Foo[] { new() { Bar = "A" }, }.AsQueryable(); [NodeResolver] [UseSingleOrDefault] [UseProjection] public IQueryable GetById(string id) - => new Foo[] - { - new() { Bar = "A" }, - }.AsQueryable(); + => new Foo[] { new() { Bar = "A" }, }.AsQueryable(); + + [NodeResolver] + [UseSingleOrDefault] + [UseProjection] + public IQueryable GetBazById(string id) + => new Baz[] { new() { Bar2 = "A" }, }.AsQueryable(); + + [NodeResolver] + public Bar GetBarById(string id) => new() { IdOfBar = "A" }; } diff --git a/src/HotChocolate/Data/test/Data.Projections.Tests/__snapshots__/IntegrationTests.Node_Resolver_With_SingleOrDefault.snap b/src/HotChocolate/Data/test/Data.Projections.Tests/__snapshots__/IntegrationTests.Node_Resolver_With_SingleOrDefault.snap index 5409d1bcbc9..a77c6664eee 100644 --- a/src/HotChocolate/Data/test/Data.Projections.Tests/__snapshots__/IntegrationTests.Node_Resolver_With_SingleOrDefault.snap +++ b/src/HotChocolate/Data/test/Data.Projections.Tests/__snapshots__/IntegrationTests.Node_Resolver_With_SingleOrDefault.snap @@ -1,9 +1,8 @@ { "data": { - "all": [ - { - "id": "Rm9vCmRB" - } - ] + "node": { + "id": "Rm9vCmRB", + "__typename": "Foo" + } } } \ No newline at end of file diff --git a/src/HotChocolate/Data/test/Data.Projections.Tests/__snapshots__/IntegrationTests.Node_Resolver_With_SingleOrDefault_Fragments.snap b/src/HotChocolate/Data/test/Data.Projections.Tests/__snapshots__/IntegrationTests.Node_Resolver_With_SingleOrDefault_Fragments.snap new file mode 100644 index 00000000000..213ac3057f3 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.Tests/__snapshots__/IntegrationTests.Node_Resolver_With_SingleOrDefault_Fragments.snap @@ -0,0 +1,9 @@ +{ + "data": { + "node": { + "id": "Rm9vCmRB", + "__typename": "Foo", + "fieldOfFoo": "fieldOfFoo" + } + } +} \ No newline at end of file diff --git a/src/HotChocolate/Data/test/Data.Projections.Tests/__snapshots__/IntegrationTests.Node_Resolver_Without_SingleOrDefault.snap b/src/HotChocolate/Data/test/Data.Projections.Tests/__snapshots__/IntegrationTests.Node_Resolver_Without_SingleOrDefault.snap new file mode 100644 index 00000000000..de2d120d0ad --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.Tests/__snapshots__/IntegrationTests.Node_Resolver_Without_SingleOrDefault.snap @@ -0,0 +1,9 @@ +{ + "data": { + "node": { + "id": "QmFyCmRB", + "__typename": "Bar", + "fieldOfBar": "fieldOfBar" + } + } +} \ No newline at end of file