diff --git a/samples/GraphQL.Samples.Http/Program.cs b/samples/GraphQL.Samples.Http/Program.cs index b32b58225..13e27d0b1 100644 --- a/samples/GraphQL.Samples.Http/Program.cs +++ b/samples/GraphQL.Samples.Http/Program.cs @@ -1,4 +1,7 @@ using System.Runtime.CompilerServices; + +using Microsoft.AspNetCore.Mvc; + using Tanka.GraphQL.Executable; using Tanka.GraphQL.Server; @@ -59,7 +62,7 @@ app.Run(); // simple subscription generating numbers from 0 to the given number -static async IAsyncEnumerable Count(int to, [EnumeratorCancellation] CancellationToken cancellationToken) +static async IAsyncEnumerable Count(int to, [FromServices]ILogger logger, [EnumeratorCancellation] CancellationToken cancellationToken) { var i = 0; while (!cancellationToken.IsCancellationRequested) diff --git a/src/GraphQL.Server/WebSockets/ServerMethods.cs b/src/GraphQL.Server/WebSockets/ServerMethods.cs index 992157b14..51b6053b0 100644 --- a/src/GraphQL.Server/WebSockets/ServerMethods.cs +++ b/src/GraphQL.Server/WebSockets/ServerMethods.cs @@ -1,6 +1,8 @@ using System.Collections.Concurrent; using Microsoft.AspNetCore.Http; + +using Tanka.GraphQL.Fields; using Tanka.GraphQL.Server.WebSockets.WebSocketPipe; using Tanka.GraphQL.Validation; @@ -56,40 +58,32 @@ await Channel.Complete( private async Task Execute(Subscribe subscribe, CancellationTokenSource unsubscribeOrAborted) { var cancellationToken = unsubscribeOrAborted.Token; + var context = new GraphQLRequestContext + { + HttpContext = _httpContext, + RequestServices = _httpContext.RequestServices, + Request = new() + { + InitialValue = null, + Document = subscribe.Payload.Query, + OperationName = subscribe.Payload.OperationName, + Variables = subscribe.Payload.Variables + } + }; try { - var context = new GraphQLRequestContext - { - HttpContext = _httpContext, - RequestServices = _httpContext.RequestServices, - Request = new() - { - InitialValue = null, - Document = subscribe.Payload.Query, - OperationName = subscribe.Payload.OperationName, - Variables = subscribe.Payload.Variables - } - }; - await _requestDelegate(context); await using var enumerator = context.Response.GetAsyncEnumerator(cancellationToken); while (await enumerator.MoveNextAsync()) { - await Client.Next(new Next() - { - Id = subscribe.Id, - Payload = enumerator.Current - }, cancellationToken); + await Client.Next(new Next() { Id = subscribe.Id, Payload = enumerator.Current }, cancellationToken); } if (!cancellationToken.IsCancellationRequested) { - await Client.Complete(new Complete() - { - Id = subscribe.Id - }, cancellationToken); + await Client.Complete(new Complete() { Id = subscribe.Id }, cancellationToken); } } catch (OperationCanceledException) @@ -99,26 +93,36 @@ await Client.Complete(new Complete() catch (ValidationException x) { var validationResult = x.Result; - await Client.Error(new Error() - { - Id = subscribe.Id, - Payload = validationResult.Errors.Select(ve => ve.ToError()).ToArray() - }, cancellationToken); + await Client.Error( + new Error() + { + Id = subscribe.Id, + Payload = validationResult.Errors.Select(ve => ve.ToError()).ToArray() + }, cancellationToken); } catch (QueryException x) { - await Client.Error(new Error() - { - Id = subscribe.Id, - Payload = new[] + await Client.Error( + new Error() + { + Id = subscribe.Id, + Payload = new[] + { + context.Errors?.FormatError(x)! + } + }, cancellationToken); + } + catch (Exception x) + { + await Client.Error( + new Error() { - new ExecutionError() + Id = subscribe.Id, + Payload = new[] { - Path = x.Path.Segments.ToArray(), - Message = x.Message + context.Errors?.FormatError(x)! } - } - }, cancellationToken); + }, cancellationToken); } finally { diff --git a/src/GraphQL/ErrorCollectorFeature.cs b/src/GraphQL/ErrorCollectorFeature.cs index 46cc1d1e5..dd4af410d 100644 --- a/src/GraphQL/ErrorCollectorFeature.cs +++ b/src/GraphQL/ErrorCollectorFeature.cs @@ -18,6 +18,11 @@ public IEnumerable GetErrors() return _bag.Select(DefaultFormatError); } + public ExecutionError FormatError(Exception x) + { + return DefaultFormatError(x); + } + public ExecutionError DefaultFormatError(Exception exception) { var rootCause = exception.GetBaseException(); diff --git a/src/GraphQL/ExceptionExtensions.cs b/src/GraphQL/ExceptionExtensions.cs index a0894c413..0f3acf3f3 100644 --- a/src/GraphQL/ExceptionExtensions.cs +++ b/src/GraphQL/ExceptionExtensions.cs @@ -1,5 +1,4 @@ -using Tanka.GraphQL.Fields; -using Tanka.GraphQL.Language.Nodes; +using Tanka.GraphQL.Language.Nodes; using Tanka.GraphQL.Language.Nodes.TypeSystem; using Tanka.GraphQL.ValueResolution; diff --git a/src/GraphQL/ExecutionError.cs b/src/GraphQL/ExecutionError.cs index 08337ea5f..dcc408419 100644 --- a/src/GraphQL/ExecutionError.cs +++ b/src/GraphQL/ExecutionError.cs @@ -11,7 +11,7 @@ public class ExecutionError [JsonPropertyName("locations")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public List? Locations { get; set; } + public List? Locations { get; set; } [JsonPropertyName("message")] public string Message { get; set; } = string.Empty; @@ -23,4 +23,28 @@ public void Extend(string key, object value) Extensions[key] = value; } +} + +public class SerializedLocation +{ + public int Line { get; set; } + + public int Column { get; set; } + + public static implicit operator SerializedLocation(Location location) + { + return new() + { + Line = location.Line, + Column = location.Column + }; + } +} + +public static class ExecutionErrorExtensions +{ + public static List ToSerializedLocations(this IEnumerable locations) + { + return [.. locations]; + } } \ No newline at end of file diff --git a/src/GraphQL/Executor.ExecuteSubscription.cs b/src/GraphQL/Executor.ExecuteSubscription.cs index 597022fdd..e5321d9a7 100644 --- a/src/GraphQL/Executor.ExecuteSubscription.cs +++ b/src/GraphQL/Executor.ExecuteSubscription.cs @@ -1,4 +1,7 @@ -using System.Runtime.CompilerServices; +using System; +using System.Runtime.CompilerServices; + +using Microsoft.VisualBasic.FileIO; using Tanka.GraphQL.Features; using Tanka.GraphQL.Language.Nodes; @@ -15,11 +18,11 @@ public partial class Executor /// /// /// - public static Task ExecuteSubscription(QueryContext context) + public static async Task ExecuteSubscription(QueryContext context) { context.RequestCancelled.ThrowIfCancellationRequested(); - IAsyncEnumerable sourceStream = CreateSourceEventStream( + IAsyncEnumerable sourceStream = await CreateSourceEventStream( context, context.RequestCancelled); @@ -29,7 +32,6 @@ public static Task ExecuteSubscription(QueryContext context) context.RequestCancelled); context.Response = responseStream; - return Task.CompletedTask; } @@ -40,9 +42,9 @@ public static Task ExecuteSubscription(QueryContext context) /// /// /// - public static async IAsyncEnumerable CreateSourceEventStream( + public static async Task> CreateSourceEventStream( QueryContext context, - [EnumeratorCancellation] CancellationToken cancellationToken) + CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(context.Schema.Subscription); @@ -56,9 +58,9 @@ public static Task ExecuteSubscription(QueryContext context) ); List fields = groupedFieldSet.Values.First(); - Name fieldName = fields.First().Name; FieldSelection fieldSelection = fields.First(); - + Name fieldName = fieldSelection.Name; + IReadOnlyDictionary coercedArgumentValues = ArgumentCoercion.CoerceArgumentValues( context.Schema, subscriptionType, @@ -68,7 +70,7 @@ public static Task ExecuteSubscription(QueryContext context) FieldDefinition? field = context.Schema.GetField(subscriptionType.Name, fieldName); if (field is null) - yield break; + return AsyncEnumerableEx.Empty(); var path = new NodePath(); Subscriber? subscriber = context.Schema.GetSubscriber(subscriptionType.Name, fieldName); @@ -92,13 +94,60 @@ public static Task ExecuteSubscription(QueryContext context) QueryContext = context }; - await subscriber(resolverContext, cancellationToken); + try + { + await subscriber(resolverContext, cancellationToken); - if (resolverContext.ResolvedValue is null) - yield break; + if (resolverContext.ResolvedValue is null) + return AsyncEnumerableEx.Empty(); + } + catch (Exception exception) + { + if (exception is not FieldException) + throw new FieldException(exception.Message, exception) + { + ObjectDefinition = subscriptionType, + Field = field, + Selection = fieldSelection, + Path = path + }; + + throw; + } + + return Core(resolverContext, cancellationToken); + + static async IAsyncEnumerable Core(SubscriberContext resolverContext, [EnumeratorCancellation]CancellationToken cancellationToken) + { + await using var e = resolverContext.ResolvedValue!.GetAsyncEnumerator(cancellationToken); - await foreach (object? evnt in resolverContext.ResolvedValue.WithCancellation(cancellationToken)) - yield return evnt; + while (true) + { + try + { + if (!await e.MoveNextAsync()) + { + yield break; + } + + } + catch (Exception exception) + { + if (exception is not FieldException) + throw new FieldException(exception.Message, exception) + { + ObjectDefinition = resolverContext.ObjectDefinition, + Field = resolverContext.Field, + Selection = resolverContext.Selection, + Path = resolverContext.Path + }; + + throw; + } + + yield return e.Current; + } + } } /// @@ -109,25 +158,36 @@ public static Task ExecuteSubscription(QueryContext context) /// /// /// - public static async IAsyncEnumerable MapSourceToResponseEventStream( + public static IAsyncEnumerable MapSourceToResponseEventStream( QueryContext context, IAsyncEnumerable sourceStream, - [EnumeratorCancellation] CancellationToken cancellationToken) + CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(context.Schema.Subscription); ObjectDefinition? subscriptionType = context.Schema.Subscription; SelectionSet selectionSet = context.OperationDefinition.SelectionSet; - await foreach (object? sourceEvnt in sourceStream.WithCancellation(cancellationToken)) + return Core(context, sourceStream, subscriptionType, selectionSet, cancellationToken); + + static async IAsyncEnumerable Core( + QueryContext context, + IAsyncEnumerable sourceStream, + ObjectDefinition subscriptionType, + SelectionSet selectionSet, + [EnumeratorCancellation] CancellationToken cancellationToken) { - var path = new NodePath(); - yield return await ExecuteSourceEvent( - context, - selectionSet, - subscriptionType, - sourceEvnt, - path); + + await foreach (var sourceEvnt in sourceStream.WithCancellation(cancellationToken)) + { + var path = new NodePath(); + yield return await ExecuteSourceEvent( + context, + selectionSet, + subscriptionType, + sourceEvnt, + path); + } } } diff --git a/src/GraphQL/Features/IErrorCollectorFeature.cs b/src/GraphQL/Features/IErrorCollectorFeature.cs index df9965798..8d5403068 100644 --- a/src/GraphQL/Features/IErrorCollectorFeature.cs +++ b/src/GraphQL/Features/IErrorCollectorFeature.cs @@ -4,4 +4,5 @@ public interface IErrorCollectorFeature { void Add(Exception error); IEnumerable GetErrors(); + ExecutionError FormatError(Exception x); } \ No newline at end of file diff --git a/src/GraphQL/Fields/FieldExecutorFeature.cs b/src/GraphQL/Fields/FieldExecutorFeature.cs index 409ea6c26..2ea906598 100644 --- a/src/GraphQL/Fields/FieldExecutorFeature.cs +++ b/src/GraphQL/Fields/FieldExecutorFeature.cs @@ -23,10 +23,13 @@ public class FieldExecutorFeature : IFieldExecutorFeature // __typename hack if (fieldName == "__typename") return objectDefinition.Name.Value; + + FieldDefinition? field = schema.GetField(objectDefinition.Name, fieldName); + + if (field is null) + return null; - TypeBase? fieldType = schema - .GetField(objectDefinition.Name, fieldName)? - .Type; + TypeBase fieldType = field.Type; if (fieldType == null) throw new QueryException( @@ -34,13 +37,8 @@ public class FieldExecutorFeature : IFieldExecutorFeature { Path = path }; - - FieldDefinition? field = schema.GetField(objectDefinition.Name, fieldName); object? completedValue = null; - if (field is null) - return null; - IReadOnlyDictionary argumentValues = ArgumentCoercion.CoerceArgumentValues( schema, objectDefinition, diff --git a/src/GraphQL/Fields/FieldPipelineExecutorFeature.cs b/src/GraphQL/Fields/FieldPipelineExecutorFeature.cs index a2ae0db96..26fb3820d 100644 --- a/src/GraphQL/Fields/FieldPipelineExecutorFeature.cs +++ b/src/GraphQL/Fields/FieldPipelineExecutorFeature.cs @@ -8,15 +8,8 @@ namespace Tanka.GraphQL.Fields; public delegate ValueTask FieldDelegate(ResolverContext context); -public class FieldPipelineExecutorFeature : IFieldExecutorFeature +public class FieldPipelineExecutorFeature(FieldDelegate fieldDelegate) : IFieldExecutorFeature { - private readonly FieldDelegate _fieldDelegate; - - public FieldPipelineExecutorFeature(FieldDelegate fieldDelegate) - { - _fieldDelegate = fieldDelegate; - } - public async Task Execute( QueryContext context, ObjectDefinition objectDefinition, @@ -37,7 +30,7 @@ public FieldPipelineExecutorFeature(FieldDelegate fieldDelegate) QueryContext = context }; - await _fieldDelegate(resolverContext); + await fieldDelegate(resolverContext); return resolverContext.CompletedValue; } diff --git a/src/GraphQL/QueryContext.cs b/src/GraphQL/QueryContext.cs index ee04a1398..6b7e918f2 100644 --- a/src/GraphQL/QueryContext.cs +++ b/src/GraphQL/QueryContext.cs @@ -123,13 +123,14 @@ public IAsyncEnumerable Response /// /// Request services /// - //todo: turn into a feature public IServiceProvider RequestServices { get => RequestServicesFeature.RequestServices; set => RequestServicesFeature.RequestServices = value; } + public IErrorCollectorFeature? Errors => ErrorCollectorFeature; + /// /// Add error to the context /// diff --git a/src/GraphQL/Validation/ValidationError.cs b/src/GraphQL/Validation/ValidationError.cs index 75eadeca8..cede1ea6b 100644 --- a/src/GraphQL/Validation/ValidationError.cs +++ b/src/GraphQL/Validation/ValidationError.cs @@ -61,7 +61,7 @@ public ExecutionError ToError() return new() { Message = ToString(), - Locations = Nodes.Where(n => n.Location != null).Select(n => n.Location.Value).ToList(), + Locations = Nodes.Where(n => n.Location != null).Select(n => n.Location!.Value).ToSerializedLocations(), Extensions = new() { {