diff --git a/src/HotChocolate/Core/src/Execution/HotChocolate.Execution.csproj b/src/HotChocolate/Core/src/Execution/HotChocolate.Execution.csproj index d1823310945..e2a9e78a5aa 100644 --- a/src/HotChocolate/Core/src/Execution/HotChocolate.Execution.csproj +++ b/src/HotChocolate/Core/src/Execution/HotChocolate.Execution.csproj @@ -159,6 +159,12 @@ ResultBuilder.cs + + ResolverTask.cs + + + ResolverTask.cs + diff --git a/src/HotChocolate/Core/src/Execution/Processing/Selection.cs b/src/HotChocolate/Core/src/Execution/Processing/Selection.cs index c1764a41a5c..2618d12f4e5 100644 --- a/src/HotChocolate/Core/src/Execution/Processing/Selection.cs +++ b/src/HotChocolate/Core/src/Execution/Processing/Selection.cs @@ -55,6 +55,11 @@ public class Selection : ISelection { _flags |= Flags.List; } + + if (Field.HasStreamResult) + { + _flags |= Flags.StreamResult; + } } protected Selection(Selection selection) @@ -123,10 +128,17 @@ protected Selection(Selection selection) /// public IArgumentMap Arguments { get; } - public bool IsStream(long includeFlags) + /// + public bool HasStreamResult => (_flags & Flags.StreamResult) == Flags.StreamResult; + + /// + public bool HasStreamDirective(long includeFlags) => (_flags & Flags.Stream) == Flags.Stream && (_streamIfCondition is 0 || (includeFlags & _streamIfCondition) != _streamIfCondition); + /// + /// Specifies if the current selection is immutable. + /// public bool IsReadOnly => (_flags & Flags.Sealed) == Flags.Sealed; /// @@ -324,6 +336,7 @@ private enum Flags Internal = 1, Sealed = 2, List = 4, - Stream = 8 + Stream = 8, + StreamResult = 16 } } diff --git a/src/HotChocolate/Core/src/Execution/Processing/Tasks/ResolverTask.Execute.cs b/src/HotChocolate/Core/src/Execution/Processing/Tasks/ResolverTask.Execute.cs index ba0b67db1c9..586ddba4cd4 100644 --- a/src/HotChocolate/Core/src/Execution/Processing/Tasks/ResolverTask.Execute.cs +++ b/src/HotChocolate/Core/src/Execution/Processing/Tasks/ResolverTask.Execute.cs @@ -130,12 +130,18 @@ private async ValueTask ExecuteResolverPipelineAsync(CancellationToken cancellat return; } - if (_selection.IsStream(_operationContext.IncludeFlags)) + if (_selection.HasStreamDirective(_operationContext.IncludeFlags)) { _resolverContext.Result = await CreateStreamResultAsync(result).ConfigureAwait(false); return; } + if (_selection.HasStreamResult) + { + _resolverContext.Result = await CreateListFromStreamAsync(result).ConfigureAwait(false); + return; + } + switch (_resolverContext.Result) { case IExecutable executable: diff --git a/src/HotChocolate/Core/src/Execution/Processing/Tasks/ResolverTask.cs b/src/HotChocolate/Core/src/Execution/Processing/Tasks/ResolverTask.cs index 561956524de..cbe6e5a605b 100644 --- a/src/HotChocolate/Core/src/Execution/Processing/Tasks/ResolverTask.cs +++ b/src/HotChocolate/Core/src/Execution/Processing/Tasks/ResolverTask.cs @@ -3,7 +3,6 @@ using System.Threading; using System.Threading.Tasks; using HotChocolate.Execution.Instrumentation; -using HotChocolate.Types; using Microsoft.Extensions.ObjectPool; namespace HotChocolate.Execution.Processing.Tasks; diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/IOptionalSelection.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/IOptionalSelection.cs index 27be66070ba..121cc684f5c 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/IOptionalSelection.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/IOptionalSelection.cs @@ -17,5 +17,17 @@ public interface IOptionalSelection /// bool IsConditional { get; } + /// + /// Defines if this selection will be included into the request execution. + /// + /// + /// The execution include flags. + /// + /// + /// Allow internal selections to be included. + /// + /// + /// True, if this selection shall be included into the request execution. + /// bool IsIncluded(long includeFlags, bool allowInternals = false); } diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/ISelection.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/ISelection.cs index ae4fa3db6a0..290baa2fe48 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/ISelection.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/ISelection.cs @@ -1,6 +1,6 @@ #nullable enable -using System.Diagnostics.SymbolStore; +using System.Collections.Generic; using HotChocolate.Language; using HotChocolate.Resolvers; using HotChocolate.Types; @@ -38,7 +38,7 @@ public interface ISelection : IOptionalSelection TypeKind TypeKind { get; } /// - /// Specifies if the return type fo this selection is a list. + /// Specifies if the return type of this selection is a list type. /// bool IsList { get; } @@ -82,5 +82,21 @@ public interface ISelection : IOptionalSelection /// IArgumentMap Arguments { get; } - bool IsStream(long includeFlags); + /// + /// Defines that the resolver pipeline returns an + /// as its result. + /// + bool HasStreamResult { get; } + + /// + /// Defines if this selection is annotated with the stream directive. + /// + /// + /// The execution include flags that determine if the stream directive is applied for the + /// current execution run. + /// + /// + /// Returns if this selection is annotated with the stream directive. + /// + bool HasStreamDirective(long includeFlags); } diff --git a/src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs b/src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs index c268b1f766c..b3caf752d78 100644 --- a/src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs +++ b/src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs @@ -1418,7 +1418,7 @@ internal class TypeResources { return ResourceManager.GetString("ConnectionType_TotalCount_Description", resourceCulture); } } - + internal static string CollectionSegmentType_PageInfo_Description { get { return ResourceManager.GetString("CollectionSegmentType_PageInfo_Description", resourceCulture); @@ -1526,5 +1526,11 @@ internal class TypeResources { return ResourceManager.GetString("InterfaceTypeDescriptor_MustBePropertyOrMethod", resourceCulture); } } + + internal static string ThrowHelper_FieldBase_Sealed { + get { + return ResourceManager.GetString("ThrowHelper_FieldBase_Sealed", resourceCulture); + } + } } } diff --git a/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx b/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx index 3ade3deb5d3..ba25655d28a 100644 --- a/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx +++ b/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx @@ -873,4 +873,7 @@ Type: `{0}` A field of an interface can only be inferred from a property or a method. + + The field is already sealed and cannot be mutated. + diff --git a/src/HotChocolate/Core/src/Types/Types/Attributes/StreamResultAttribute.cs b/src/HotChocolate/Core/src/Types/Types/Attributes/StreamResultAttribute.cs new file mode 100644 index 00000000000..611719132c9 --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Types/Attributes/StreamResultAttribute.cs @@ -0,0 +1,22 @@ +#nullable enable +using System; +using System.Reflection; +using HotChocolate.Types.Descriptors; + +namespace HotChocolate.Types; + +/// +/// Marks a resolver as returning a stream result +/// which will allow the execution engine to compile a result handler for the resolver. +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] +public sealed class StreamResultAttribute : ObjectFieldDescriptorAttribute +{ + public override void OnConfigure( + IDescriptorContext context, + IObjectFieldDescriptor descriptor, + MemberInfo member) + { + descriptor.StreamResult(); + } +} diff --git a/src/HotChocolate/Core/src/Types/Types/Contracts/IObjectField.cs b/src/HotChocolate/Core/src/Types/Types/Contracts/IObjectField.cs index 0ed94150fe2..b0d80e8a5e2 100644 --- a/src/HotChocolate/Core/src/Types/Types/Contracts/IObjectField.cs +++ b/src/HotChocolate/Core/src/Types/Types/Contracts/IObjectField.cs @@ -21,6 +21,12 @@ public interface IObjectField : IOutputField /// bool IsParallelExecutable { get; } + /// + /// Defines that the resolver pipeline returns an + /// as its result. + /// + bool HasStreamResult { get; } + /// /// Gets the field resolver middleware. /// diff --git a/src/HotChocolate/Core/src/Types/Types/Contracts/IOutputField.cs b/src/HotChocolate/Core/src/Types/Types/Contracts/IOutputField.cs index d84689f31e5..c9f390b9587 100644 --- a/src/HotChocolate/Core/src/Types/Types/Contracts/IOutputField.cs +++ b/src/HotChocolate/Core/src/Types/Types/Contracts/IOutputField.cs @@ -1,5 +1,7 @@ #nullable enable +using System.Collections.Generic; + namespace HotChocolate.Types; /// diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/IObjectFieldDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/IObjectFieldDescriptor.cs index 0e3158cf24b..919d45faf60 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/IObjectFieldDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/IObjectFieldDescriptor.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq.Expressions; using System.Reflection; using HotChocolate.Language; @@ -91,6 +92,12 @@ IObjectFieldDescriptor Type(TOutputType outputType) /// IObjectFieldDescriptor Type(Type type); + /// + /// Defines weather the resolver pipeline will return + /// as its result. + /// + IObjectFieldDescriptor StreamResult(bool hasStreamResult = true); + /// /// Defines a field argument. /// diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/FieldDefinitionBase.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/FieldDefinitionBase.cs index fc03bb01d70..d9b8c69491f 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/FieldDefinitionBase.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/FieldDefinitionBase.cs @@ -14,16 +14,38 @@ public abstract class FieldDefinitionBase , IHasIgnore { private List? _directives; + private string? _deprecationReason; + + /// + /// Gets the internal field flags from this field. + /// + internal FieldFlags Flags { get; set; } = FieldFlags.None; /// /// Describes why this syntax node is deprecated. /// - public string? DeprecationReason { get; set; } + public string? DeprecationReason + { + get => _deprecationReason; + set + { + if (string.IsNullOrEmpty(value)) + { + Flags &= ~FieldFlags.Deprecated; + } + else + { + Flags |= FieldFlags.Deprecated; + } + + _deprecationReason = value; + } + } /// /// If true, the field is deprecated /// - public bool IsDeprecated => !string.IsNullOrEmpty(DeprecationReason); + public bool IsDeprecated => (Flags & FieldFlags.Deprecated) == FieldFlags.Deprecated; /// /// Gets the field type. @@ -34,18 +56,32 @@ public abstract class FieldDefinitionBase /// Defines if this field is ignored and will /// not be included into the schema. /// - public bool Ignore { get; set; } + public bool Ignore + { + get => (Flags & FieldFlags.Ignored) == FieldFlags.Ignored; + set + { + if (value) + { + Flags |= FieldFlags.Ignored; + } + else + { + Flags &= ~FieldFlags.Ignored; + } + } + } /// /// Gets the list of directives that are annotated to this field. /// - public IList Directives => - _directives ??= new List(); + public IList Directives + => _directives ??= new List(); /// /// Specifies if this field has any directives. /// - public bool HasDirectives => _directives is { Count: > 0 }; + public bool HasDirectives => _directives?.Count > 0; /// /// Gets the list of directives that are annotated to this field. diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/ObjectFieldDefinition.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/ObjectFieldDefinition.cs index 39ff48eb930..53558b2d4b5 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/ObjectFieldDefinition.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/ObjectFieldDefinition.cs @@ -25,7 +25,10 @@ public class ObjectFieldDefinition : OutputFieldDefinitionBase /// /// Initializes a new instance of . /// - public ObjectFieldDefinition() { } + public ObjectFieldDefinition() + { + IsParallelExecutable = true; + } /// /// Initializes a new instance of . @@ -42,6 +45,7 @@ public class ObjectFieldDefinition : OutputFieldDefinitionBase Type = type; Resolver = resolver; PureResolver = pureResolver; + IsParallelExecutable = true; } /// @@ -142,12 +146,60 @@ public IList CustomSettings /// /// Defines if this field configuration represents an introspection field. /// - public bool IsIntrospectionField { get; internal set; } + public bool IsIntrospectionField + { + get => (Flags & FieldFlags.Introspection) == FieldFlags.Introspection; + internal set + { + if (value) + { + Flags |= FieldFlags.Introspection; + } + else + { + Flags &= ~FieldFlags.Introspection; + } + } + } /// /// Defines if this field can be executed in parallel with other fields. /// - public bool IsParallelExecutable { get; set; } = true; + public bool IsParallelExecutable + { + get => (Flags & FieldFlags.ParallelExecutable) == FieldFlags.ParallelExecutable; + set + { + if (value) + { + Flags |= FieldFlags.ParallelExecutable; + } + else + { + Flags &= ~FieldFlags.ParallelExecutable; + } + } + } + + /// + /// Defines that the resolver pipeline returns an + /// as its result. + /// + public bool HasStreamResult + { + get => (Flags & FieldFlags.Stream) == FieldFlags.Stream; + set + { + if (value) + { + Flags |= FieldFlags.Stream; + } + else + { + Flags &= ~FieldFlags.Stream; + } + } + } /// /// A list of middleware components which will be used to form the field pipeline. @@ -229,6 +281,7 @@ internal void CopyTo(ObjectFieldDefinition target) target.SubscribeResolver = SubscribeResolver; target.IsIntrospectionField = IsIntrospectionField; target.IsParallelExecutable = IsParallelExecutable; + target.HasStreamResult = HasStreamResult; } internal void MergeInto(ObjectFieldDefinition target) @@ -260,6 +313,11 @@ internal void MergeInto(ObjectFieldDefinition target) target.IsParallelExecutable = false; } + if (!HasStreamResult) + { + target.HasStreamResult = false; + } + if (ResolverType is not null) { target.ResolverType = ResolverType; @@ -387,3 +445,15 @@ internal void MergeInto(ObjectFieldDefinition target) } } } + +[Flags] +internal enum FieldFlags +{ + None = 0, + Introspection = 2, + Deprecated = 4, + Ignored = 8, + ParallelExecutable = 16, + Stream = 32, + Sealed = 64, +} diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/OutputFieldDefinitionBase.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/OutputFieldDefinitionBase.cs index 14caffc744a..80128172f6e 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/OutputFieldDefinitionBase.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/OutputFieldDefinitionBase.cs @@ -14,13 +14,13 @@ public class OutputFieldDefinitionBase { private List? _arguments; - public IList Arguments => - _arguments ??= new List(); + public IList Arguments + => _arguments ??= new List(); /// /// Specifies if this field has any arguments. /// - public bool HasArguments => _arguments is { Count: > 0 }; + public bool HasArguments => _arguments?.Count > 0; public IReadOnlyList GetArguments() { @@ -36,7 +36,7 @@ protected void CopyTo(OutputFieldDefinitionBase target) { base.CopyTo(target); - if (_arguments is { Count: > 0 }) + if (_arguments?.Count > 0) { target._arguments = new List(); diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectFieldDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectFieldDescriptor.cs index 73d4a2f163c..972340b34d9 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectFieldDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectFieldDescriptor.cs @@ -1,5 +1,5 @@ using System; -using System.IO; +using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -145,6 +145,13 @@ protected override void OnCreateDefinition(ObjectFieldDefinition definition) base.OnCreateDefinition(definition); CompleteArguments(definition); + + if (!definition.HasStreamResult && + definition.ResultType?.IsGenericType is true && + definition.ResultType.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>)) + { + definition.HasStreamResult = true; + } } private void CompleteArguments(ObjectFieldDefinition definition) @@ -234,6 +241,13 @@ public new IObjectFieldDescriptor Type(Type type) return this; } + /// + public IObjectFieldDescriptor StreamResult(bool hasStreamResult = true) + { + Definition.HasStreamResult = hasStreamResult; + return this; + } + /// public new IObjectFieldDescriptor Argument( string argumentName, diff --git a/src/HotChocolate/Core/src/Types/Types/FieldBase.cs b/src/HotChocolate/Core/src/Types/Types/FieldBase.cs index b631c6cd325..a810937992b 100644 --- a/src/HotChocolate/Core/src/Types/Types/FieldBase.cs +++ b/src/HotChocolate/Core/src/Types/Types/FieldBase.cs @@ -5,6 +5,7 @@ using HotChocolate.Types.Descriptors.Definitions; using HotChocolate.Types.Helpers; using HotChocolate.Utilities; +using ThrowHelper = HotChocolate.Utilities.ThrowHelper; #nullable enable @@ -16,6 +17,7 @@ public abstract class FieldBase where TDefinition : FieldDefinitionBase, IHasSyntaxNode { private TDefinition? _definition; + private FieldFlags _flags; protected FieldBase(TDefinition definition, int index) { @@ -25,6 +27,7 @@ protected FieldBase(TDefinition definition, int index) SyntaxNode = definition.SyntaxNode; Name = definition.Name.EnsureGraphQLName(); Description = definition.Description; + Flags = definition.Flags; DeclaringType = default!; ContextData = default!; Directives = default!; @@ -54,6 +57,16 @@ protected FieldBase(TDefinition definition, int index) /// public abstract Type RuntimeType { get; } + internal FieldFlags Flags + { + get => _flags; + set + { + AssertMutable(); + _flags = value; + } + } + /// public IReadOnlyDictionary ContextData { get; private set; } @@ -61,10 +74,13 @@ protected FieldBase(TDefinition definition, int index) ITypeCompletionContext context, ITypeSystemMember declaringMember) { + AssertMutable(); + OnCompleteField(context, declaringMember, _definition!); ContextData = _definition!.GetContextData(); _definition = null; + _flags |= FieldFlags.Sealed; } protected virtual void OnCompleteField( @@ -78,10 +94,19 @@ protected FieldBase(TDefinition definition, int index) : new FieldCoordinate(context.Type.Name, definition.Name); Directives = DirectiveCollection.CreateAndComplete( context, this, definition.GetDirectives()); + Flags = definition.Flags; } void IFieldCompletion.CompleteField( ITypeCompletionContext context, ITypeSystemMember declaringMember) => CompleteField(context, declaringMember); + + private void AssertMutable() + { + if ((_flags & FieldFlags.Sealed) == FieldFlags.Sealed) + { + throw ThrowHelper.FieldBase_Sealed(); + } + } } diff --git a/src/HotChocolate/Core/src/Types/Types/ObjectField.cs b/src/HotChocolate/Core/src/Types/Types/ObjectField.cs index b415d66d19b..701179cd864 100644 --- a/src/HotChocolate/Core/src/Types/Types/ObjectField.cs +++ b/src/HotChocolate/Core/src/Types/Types/ObjectField.cs @@ -33,8 +33,6 @@ internal ObjectField(ObjectFieldDefinition definition, int index) Resolver = definition.Resolver!; ResolverExpression = definition.Expression; SubscribeResolver = definition.SubscribeResolver; - IsIntrospectionField = definition.IsIntrospectionField; - IsParallelExecutable = definition.IsParallelExecutable; } /// @@ -47,7 +45,31 @@ internal ObjectField(ObjectFieldDefinition definition, int index) /// /// Defines if this field can be executed in parallel with other fields. /// - public bool IsParallelExecutable { get; private set; } + public bool IsParallelExecutable + { + get + { + return (Flags & FieldFlags.ParallelExecutable) == FieldFlags.ParallelExecutable; + } + private set + { + if (value) + { + Flags |= FieldFlags.ParallelExecutable; + } + else + { + Flags &= ~FieldFlags.ParallelExecutable; + } + } + } + + /// + /// Defines that the resolver pipeline returns an + /// as its result. + /// + public bool HasStreamResult + => (Flags & FieldFlags.Stream) == FieldFlags.Stream; /// /// Gets the field resolver middleware. @@ -104,11 +126,6 @@ internal ObjectField(ObjectFieldDefinition definition, int index) /// public Expression? ResolverExpression { get; } - /// - /// Defines if this field as a introspection field. - /// - public override bool IsIntrospectionField { get; } - protected override void OnCompleteField( ITypeCompletionContext context, ITypeSystemMember declaringMember, diff --git a/src/HotChocolate/Core/src/Types/Types/OutputFieldBase.cs b/src/HotChocolate/Core/src/Types/Types/OutputFieldBase.cs index 5d648e81207..4a4754946f5 100644 --- a/src/HotChocolate/Core/src/Types/Types/OutputFieldBase.cs +++ b/src/HotChocolate/Core/src/Types/Types/OutputFieldBase.cs @@ -17,7 +17,6 @@ public class OutputFieldBase internal OutputFieldBase(TDefinition definition, int index) : base(definition, index) { - IsDeprecated = !string.IsNullOrEmpty(definition.DeprecationReason); DeprecationReason = definition.DeprecationReason; } @@ -40,10 +39,12 @@ internal OutputFieldBase(TDefinition definition, int index) : base(definition, i /// /// Defines if this field as a introspection field. /// - public virtual bool IsIntrospectionField => false; + public bool IsIntrospectionField + => (Flags & FieldFlags.Introspection) == FieldFlags.Introspection; /// - public bool IsDeprecated { get; } + public bool IsDeprecated + => (Flags & FieldFlags.Deprecated) == FieldFlags.Deprecated; /// public string? DeprecationReason { get; } diff --git a/src/HotChocolate/Core/src/Types/Utilities/ThrowHelper.cs b/src/HotChocolate/Core/src/Types/Utilities/ThrowHelper.cs index f6c485c3e21..ee6998f3753 100644 --- a/src/HotChocolate/Core/src/Types/Utilities/ThrowHelper.cs +++ b/src/HotChocolate/Core/src/Types/Utilities/ThrowHelper.cs @@ -524,4 +524,7 @@ public static InvalidOperationException RewriteNullability_InvalidNullabilityStr "A type with the name `{0}` was not found.", coordinate.Name), coordinate); + + public static InvalidOperationException FieldBase_Sealed() + => new(ThrowHelper_FieldBase_Sealed); } diff --git a/src/HotChocolate/Core/test/Execution.Tests/StreamTests.cs b/src/HotChocolate/Core/test/Execution.Tests/StreamTests.cs index edda55b681a..0977ddcefc7 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/StreamTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/StreamTests.cs @@ -6,6 +6,7 @@ using GreenDonut; using HotChocolate.StarWars; using HotChocolate.Tests; +using HotChocolate.Types; using Microsoft.Extensions.DependencyInjection; using Snapshooter.Xunit; using Xunit; @@ -165,6 +166,23 @@ public async Task List_With_AsyncEnumerable() await Assert.IsType(result).MatchSnapshotAsync(); } + [Fact] + public async Task List_With_AsyncEnumerable_Wrapped_Into_An_Object() + { + var result = + await new ServiceCollection() + .AddGraphQL() + .AddQueryType() + .ExecuteRequestAsync( + @"{ + persons2 { + name + } + }"); + + await Assert.IsType(result).MatchSnapshotAsync(); + } + [Fact] public async Task Stream_With_AsyncEnumerable() { @@ -284,6 +302,21 @@ public async IAsyncEnumerable GetPersonsAsync() await Task.Delay(1); yield return new Person { Name = "Bar" }; } + + [StreamResult] + public PersonStream GetPersons2() => new PersonStream(); + } + + public class PersonStream : IAsyncEnumerable + { + public async IAsyncEnumerator GetAsyncEnumerator( + CancellationToken cancellationToken = default) + { + await Task.Delay(1); + yield return new Person { Name = "Foo" }; + await Task.Delay(1); + yield return new Person { Name = "Bar" }; + } } public class QueryWithDataLoader diff --git a/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/StreamTests.List_With_AsyncEnumerable_Wrapped_Into_An_Object.snap b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/StreamTests.List_With_AsyncEnumerable_Wrapped_Into_An_Object.snap new file mode 100644 index 00000000000..35f325e5720 --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/StreamTests.List_With_AsyncEnumerable_Wrapped_Into_An_Object.snap @@ -0,0 +1,12 @@ +{ + "data": { + "persons2": [ + { + "name": "Foo" + }, + { + "name": "Bar" + } + ] + } +}