From 187d3aad20393e01de987b30dc05d718ddd39b8f Mon Sep 17 00:00:00 2001 From: Pekka Heikura Date: Sat, 25 Mar 2023 13:31:29 +0200 Subject: [PATCH] Refactor ExecutableSchemaBuilder, add delegate subscribers --- dev/GraphQL.Dev.Reviews/Program.cs | 2 +- dev/graphql.dev.chat.data/ChatResolvers.cs | 10 +- dev/graphql.dev.chat.web/Program.cs | 2 +- samples/GraphQL.Samples.Http/Program.cs | 57 +-- ...rationExecutableSchemaBuilderExtensions.cs | 4 +- .../Executable/ExecutableSchemaBuilder.cs | 95 ++--- src/GraphQL/Executable/FieldsWithResolvers.cs | 15 + .../Executable/FieldsWithSubscribers.cs | 15 + .../Executable/ValueConvertersBuilder.cs | 30 ++ src/GraphQL/Validation/ExecutionRules.cs | 2 +- .../DelegateSubscriberFactory.cs | 153 ++++++++ .../ValueResolution/ResolversBuilder.cs | 7 +- .../ValueResolution/SubscriberBuilder.cs | 5 + .../GraphQLApplicationBuilder.cs | 20 +- src/graphql.server/SchemaOptions.cs | 9 +- .../TraceFacts.cs | 2 +- tests/graphql.tests/EmptyServiceProvider.cs | 13 + tests/graphql.tests/Executor.MutationFacts.cs | 9 +- tests/graphql.tests/Executor.QueryFacts.cs | 6 +- .../Executor.SubscriptionFacts.cs | 4 +- .../DelegateSubscriberFactoryFacts.cs | 371 ++++++++++++++++++ 21 files changed, 702 insertions(+), 129 deletions(-) create mode 100644 src/GraphQL/Executable/FieldsWithResolvers.cs create mode 100644 src/GraphQL/Executable/FieldsWithSubscribers.cs create mode 100644 src/GraphQL/Executable/ValueConvertersBuilder.cs create mode 100644 src/GraphQL/ValueResolution/DelegateSubscriberFactory.cs create mode 100644 tests/graphql.tests/EmptyServiceProvider.cs create mode 100644 tests/graphql.tests/ValueResolution/DelegateSubscriberFactoryFacts.cs diff --git a/dev/GraphQL.Dev.Reviews/Program.cs b/dev/GraphQL.Dev.Reviews/Program.cs index 4546a81db..ccb08a9af 100644 --- a/dev/GraphQL.Dev.Reviews/Program.cs +++ b/dev/GraphQL.Dev.Reviews/Program.cs @@ -14,7 +14,7 @@ // configure services builder.AddTankaGraphQL3() - .AddSchema("reviews", options => + .AddOptions("reviews", options => { options.AddReviews(); diff --git a/dev/graphql.dev.chat.data/ChatResolvers.cs b/dev/graphql.dev.chat.data/ChatResolvers.cs index 545628690..9c5656576 100644 --- a/dev/graphql.dev.chat.data/ChatResolvers.cs +++ b/dev/graphql.dev.chat.data/ChatResolvers.cs @@ -12,18 +12,18 @@ public static class ChatSchemaConfigurationExtensions { public static ExecutableSchemaBuilder AddChat(this ExecutableSchemaBuilder builder) { - builder.Object("Query", new Dictionary>() + builder.Add("Query", new () { { "messages: [Message!]!", b => b.Run(r => r.GetRequiredService().GetMessagesAsync(r)) } }); - builder.Object("Mutation", new Dictionary>() + builder.Add("Mutation", new () { { "addMessage(message: InputMessage!): Message!", b => b.Run(r => r.GetRequiredService().AddMessageAsync(r)) }, { "editMessage(id: String!, message: InputMessage!): Message", b => b.Run(r => r.GetRequiredService().EditMessageAsync(r)) } }); - builder.Object("Subscription", new Dictionary>() + builder.Add("Subscription", new () { { "messages: Message!", b => b.Run(r => r.GetRequiredService().ResolveMessageAsync(r)) } }, new() @@ -31,7 +31,7 @@ public static ExecutableSchemaBuilder AddChat(this ExecutableSchemaBuilder build { "messages: Message!", b => b.Run((r, ct) => r.GetRequiredService().StreamMessagesAsync(r, ct)) } }); - builder.Object("Message", new Dictionary>() + builder.Add("Message", new() { { "id: String!", context => context.ResolveAsPropertyOf(m => m.Id) }, { "from: From!", context => context.ResolveAsPropertyOf(m => m.From) }, @@ -39,7 +39,7 @@ public static ExecutableSchemaBuilder AddChat(this ExecutableSchemaBuilder build { "timestamp: String!", context => context.ResolveAsPropertyOf(m => m.Timestamp) } }); - builder.Object("From", new Dictionary>() + builder.Add("From", new () { { "userId: String!", context => context.ResolveAsPropertyOf(f => f.UserId) }, { "name: String!", context => context.ResolveAsPropertyOf(f => f.Name) } diff --git a/dev/graphql.dev.chat.web/Program.cs b/dev/graphql.dev.chat.web/Program.cs index ff0bd5eff..b53287748 100644 --- a/dev/graphql.dev.chat.web/Program.cs +++ b/dev/graphql.dev.chat.web/Program.cs @@ -13,7 +13,7 @@ }); // configure services builder.AddTankaGraphQL3() - .AddSchema("chat", options => { options.Configure(schema => schema.AddChat()); }) + .AddOptions("chat", options => { options.Configure(schema => schema.AddChat()); }) .AddHttp() .AddWebSockets(); diff --git a/samples/GraphQL.Samples.Http/Program.cs b/samples/GraphQL.Samples.Http/Program.cs index 34b578019..f1fd371bc 100644 --- a/samples/GraphQL.Samples.Http/Program.cs +++ b/samples/GraphQL.Samples.Http/Program.cs @@ -1,56 +1,41 @@ using System.Runtime.CompilerServices; -using System.Text.Json.Serialization; -using Microsoft.AspNetCore.Http.Json; +using Tanka.GraphQL.Executable; using Tanka.GraphQL.Fields; -using Tanka.GraphQL.Language.Nodes.TypeSystem; using Tanka.GraphQL.Server; -using Tanka.GraphQL.Server.WebSockets; using Tanka.GraphQL.ValueResolution; -var builder = WebApplication.CreateBuilder(args); - -builder.Services.Configure(json => -{ - json.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; -}); +WebApplicationBuilder? builder = WebApplication.CreateBuilder(args); // configure services builder.AddTankaGraphQL3() .AddSchema("schemaName", schema => { - schema.Configure(b => + schema.Add("Query", new FieldsWithResolvers { - b.Object("Query", new Dictionary() - { - { "system: System!", () => new {} } - }); + { "system: System!", () => new { } } + }); - b.Object("System", new Dictionary() - { - { "version: String!", () => "3.0" } - }); + schema.Add("System", new FieldsWithResolvers + { + { "version: String!", () => "3.0" } + }); - b.Object("Subscription", new Dictionary() + schema.Add("Subscription", new FieldsWithResolvers { { "counter: Int!", (int objectValue) => objectValue } - }, - new() + }, + new FieldsWithSubscribers { - { "counter(to: Int!): Int!", r => r.Run((c, ct) => { - c.ResolvedValue = Wrap(Count(c.GetArgument("to"), ct)); - return default; - })} + "counter(to: Int!): Int!", + (SubscriberContext c, CancellationToken ct) => Count(c.GetArgument("to"), ct) + } }); - - }); }) .AddHttp() - .AddWebSockets() - //.AddSignalR() - ; + .AddWebSockets(); -var app = builder.Build(); +WebApplication? app = builder.Build(); app.UseWebSockets(); @@ -66,15 +51,9 @@ app.Run(); -static async IAsyncEnumerable Wrap(IAsyncEnumerable source) -{ - await foreach (var o in source) - yield return o; -} - static async IAsyncEnumerable Count(int to, [EnumeratorCancellation] CancellationToken cancellationToken) { - int i = 0; + var i = 0; while (!cancellationToken.IsCancellationRequested) { yield return ++i; diff --git a/src/GraphQL.Extensions.ApolloFederation/FederationExecutableSchemaBuilderExtensions.cs b/src/GraphQL.Extensions.ApolloFederation/FederationExecutableSchemaBuilderExtensions.cs index c45a94564..af830370c 100644 --- a/src/GraphQL.Extensions.ApolloFederation/FederationExecutableSchemaBuilderExtensions.cs +++ b/src/GraphQL.Extensions.ApolloFederation/FederationExecutableSchemaBuilderExtensions.cs @@ -9,8 +9,8 @@ public static ExecutableSchemaBuilder AddSubgraph( SubgraphOptions options) { builder.Add(new SubgraphConfiguration(options)); - builder.Add("_Any", new AnyScalarConverter()); - builder.Add("_FieldSet", new FieldSetScalarConverter()); + builder.AddConverter("_Any", new AnyScalarConverter()); + builder.AddConverter("_FieldSet", new FieldSetScalarConverter()); return builder; } diff --git a/src/GraphQL/Executable/ExecutableSchemaBuilder.cs b/src/GraphQL/Executable/ExecutableSchemaBuilder.cs index a2a356277..739f59724 100644 --- a/src/GraphQL/Executable/ExecutableSchemaBuilder.cs +++ b/src/GraphQL/Executable/ExecutableSchemaBuilder.cs @@ -7,106 +7,83 @@ namespace Tanka.GraphQL.Executable; public class ExecutableSchemaBuilder { - public List Configurations { get; } = new(); + public SchemaBuilder Schema { get; } - public List Documents { get; } = new(); + public ResolversBuilder Resolvers { get; } - public Dictionary ValueConverters { get; } = new() - { - [Scalars.String.Name] = new StringConverter(), - [Scalars.Int.Name] = new IntConverter(), - [Scalars.Float.Name] = new DoubleConverter(), - [Scalars.Boolean.Name] = new BooleanConverter(), - [Scalars.ID.Name] = new IdConverter() - }; + public ValueConvertersBuilder ValueConverters { get; } - public Dictionary DirectiveVisitorFactories { get; } = new(); + public Dictionary DirectiveVisitorFactories { get; } - public ExecutableSchemaBuilder Add( - IExecutableSchemaConfiguration configuration) + public ExecutableSchemaBuilder() { - Configurations.Add(configuration); + Schema = new SchemaBuilder(); + Resolvers = new ResolversBuilder(); + ValueConverters = new ValueConvertersBuilder() + .AddDefaults(); - return this; + DirectiveVisitorFactories = new Dictionary(); } - public ExecutableSchemaBuilder Add( - TypeSystemDocument document) + public ExecutableSchemaBuilder Add(TypeSystemDocument document) { - Documents.Add(document); - + Schema.Add(document); return this; } - public ExecutableSchemaBuilder Object( - string type, - Dictionary> resolverFields, - Dictionary>? subscriberFields = null) + public ExecutableSchemaBuilder Add(IExecutableSchemaConfiguration configuration) { - Configurations.Add(new ObjectResolversConfiguration(type, resolverFields)); - - if (subscriberFields is not null) - Configurations.Add(new ObjectSubscribersConfiguration(type, subscriberFields)); - + configuration.Configure(Schema, Resolvers); return this; } - public ExecutableSchemaBuilder Object( - string type, - Dictionary resolverFields, - Dictionary>? subscriberFields = null) + public ExecutableSchemaBuilder Add(IResolverMap resolverMap) { - Configurations.Add(new ObjectDelegateResolversConfiguration(type, resolverFields)); - - if (subscriberFields is not null) - //todo: subscriber fields should be delegates as well - Configurations.Add(new ObjectSubscribersConfiguration(type, subscriberFields)); - + Add(new ResolversConfiguration(resolverMap)); return this; } - public ExecutableSchemaBuilder Add(string type, IValueConverter converter) + public ExecutableSchemaBuilder Add(TypeDefinition[] types) { - ValueConverters[type] = converter; + Schema.Add(types); return this; } - public ExecutableSchemaBuilder Add(string type, CreateDirectiveVisitor visitor) + public ExecutableSchemaBuilder Add( + string typeName, + FieldsWithResolvers fields, + FieldsWithSubscribers? subscribers = null) { - DirectiveVisitorFactories[type] = visitor; + Add(new ObjectResolversConfiguration(typeName, fields)); + + if (subscribers != null) + Add(new ObjectSubscribersConfiguration(typeName, subscribers)); + return this; } - public ExecutableSchemaBuilder Add(IResolverMap resolversMap) + public ExecutableSchemaBuilder AddConverter( + string typeName, + IValueConverter valueConverter) { - Add(new ResolversConfiguration(resolversMap)); + ValueConverters.Add(typeName, valueConverter); return this; } - + public async Task Build(Action? configureBuildOptions = null) { - var schemaBuilder = new SchemaBuilder(); - var resolversBuilder = new ResolversBuilder(); - - foreach (TypeSystemDocument typeSystemDocument in Documents) - schemaBuilder.Add(typeSystemDocument); - - foreach (IExecutableSchemaConfiguration configuration in Configurations) - await configuration.Configure(schemaBuilder, resolversBuilder); - var buildOptions = new SchemaBuildOptions { - Resolvers = resolversBuilder.BuildResolvers(), - Subscribers = resolversBuilder.BuildSubscribers(), - ValueConverters = ValueConverters, + Resolvers = Resolvers.BuildResolvers(), + Subscribers = Resolvers.BuildSubscribers(), + ValueConverters = ValueConverters.Build(), DirectiveVisitorFactories = DirectiveVisitorFactories.ToDictionary(kv => kv.Key, kv => kv.Value), BuildTypesFromOrphanedExtensions = true }; - configureBuildOptions?.Invoke(buildOptions); - ISchema schema = await schemaBuilder.Build(buildOptions); + ISchema schema = await Schema.Build(buildOptions); return schema; } diff --git a/src/GraphQL/Executable/FieldsWithResolvers.cs b/src/GraphQL/Executable/FieldsWithResolvers.cs new file mode 100644 index 000000000..39f44f5e7 --- /dev/null +++ b/src/GraphQL/Executable/FieldsWithResolvers.cs @@ -0,0 +1,15 @@ +using Tanka.GraphQL.Language.Nodes.TypeSystem; +using Tanka.GraphQL.ValueResolution; + +namespace Tanka.GraphQL.Executable; + +public class FieldsWithResolvers : Dictionary> +{ + public void Add(FieldDefinition fieldDefinition, Delegate resolver) + { + if (!TryAdd(fieldDefinition, b => b.Run(resolver))) + { + throw new InvalidOperationException($"{fieldDefinition} already has an resolver"); + } + } +} \ No newline at end of file diff --git a/src/GraphQL/Executable/FieldsWithSubscribers.cs b/src/GraphQL/Executable/FieldsWithSubscribers.cs new file mode 100644 index 000000000..bd2ee03fe --- /dev/null +++ b/src/GraphQL/Executable/FieldsWithSubscribers.cs @@ -0,0 +1,15 @@ +using Tanka.GraphQL.Language.Nodes.TypeSystem; +using Tanka.GraphQL.ValueResolution; + +namespace Tanka.GraphQL.Executable; + +public class FieldsWithSubscribers : Dictionary> +{ + public void Add(FieldDefinition fieldDefinition, Delegate subscriber) + { + if (!TryAdd(fieldDefinition, b => b.Run(subscriber))) + { + throw new InvalidOperationException($"{fieldDefinition} already has an subscriber"); + } + } +} \ No newline at end of file diff --git a/src/GraphQL/Executable/ValueConvertersBuilder.cs b/src/GraphQL/Executable/ValueConvertersBuilder.cs new file mode 100644 index 000000000..eb39f9796 --- /dev/null +++ b/src/GraphQL/Executable/ValueConvertersBuilder.cs @@ -0,0 +1,30 @@ +using Tanka.GraphQL.ValueSerialization; + +namespace Tanka.GraphQL.Executable; + +public class ValueConvertersBuilder +{ + public Dictionary ValueConverters { get; } = new(); + + public ValueConvertersBuilder AddDefaults() + { + ValueConverters.Add("Int", new IntConverter()); + ValueConverters.Add("Float", new DoubleConverter()); + ValueConverters.Add("String", new StringConverter()); + ValueConverters.Add("Boolean", new BooleanConverter()); + ValueConverters.Add("ID", new IdConverter()); + + return this; + } + + public ValueConvertersBuilder Add(string type, IValueConverter converter) + { + ValueConverters.Add(type, converter); + return this; + } + + public IReadOnlyDictionary Build() + { + return ValueConverters; + } +} \ No newline at end of file diff --git a/src/GraphQL/Validation/ExecutionRules.cs b/src/GraphQL/Validation/ExecutionRules.cs index ce33a28b9..13371a330 100644 --- a/src/GraphQL/Validation/ExecutionRules.cs +++ b/src/GraphQL/Validation/ExecutionRules.cs @@ -984,7 +984,7 @@ void IsValidScalar( try { var converter = context.Schema.GetValueConverter(type.Name) ?? throw new ValueCoercionException( - $"Value converter for '{Printer.Print(type)}' not found from schema.", + $"Value converter for '{type.Name}' not found from schema.", type, type); diff --git a/src/GraphQL/ValueResolution/DelegateSubscriberFactory.cs b/src/GraphQL/ValueResolution/DelegateSubscriberFactory.cs new file mode 100644 index 000000000..d69aa0475 --- /dev/null +++ b/src/GraphQL/ValueResolution/DelegateSubscriberFactory.cs @@ -0,0 +1,153 @@ +using System.Collections.Concurrent; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Microsoft.Extensions.DependencyInjection; + +namespace Tanka.GraphQL.ValueResolution; + +public static class DelegateSubscriberFactory +{ + private static readonly ParameterExpression ContextParam = Expression.Parameter(typeof(SubscriberContext), "context"); + private static readonly ParameterExpression CancellationTokenParam = Expression.Parameter(typeof(CancellationToken), "cancellationToken"); + + private static readonly IReadOnlyDictionary ContextParamProperties = typeof(SubscriberContext) + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .ToDictionary(p => p.Name.ToLowerInvariant(), p => (Expression)Expression.Property(ContextParam, p)); + + + private static readonly ConcurrentDictionary Cache = new(); + + public static Subscriber GetOrCreate(Delegate subscriberDelegate) + { + if (Cache.TryGetValue(subscriberDelegate, out var resolver)) + return resolver; + + return Create(subscriberDelegate); + } + + public static Subscriber Create(Delegate subscriberDelegate) + { +#if DEBUG + Trace.WriteLine( + $"Available context parameters:\n {string.Join(',', ContextParamProperties.Select(p => string.Concat($"{p.Key}: {p.Value.Type}")))}"); +#endif + + MethodInfo invokeMethod = subscriberDelegate.Method; + + Expression instanceExpression = null; + if (!invokeMethod.IsStatic) instanceExpression = Expression.Constant(subscriberDelegate.Target); + + IEnumerable argumentsExpressions = invokeMethod.GetParameters() + .Select(p => + { + if (p.ParameterType == typeof(SubscriberContext)) return ContextParam; + if (p.ParameterType == typeof(CancellationToken)) return CancellationTokenParam; + + if (p.Name is not null) + if (ContextParamProperties.TryGetValue(p.Name.ToLowerInvariant(), + out Expression? propertyExpression)) + { + if (p.ParameterType == propertyExpression.Type) + return propertyExpression; + + return Expression.Convert(propertyExpression, p.ParameterType); + } + + MemberExpression serviceProviderProperty = Expression.Property(ContextParam, "RequestServices"); + MethodInfo? getServiceMethodInfo = typeof(ServiceProviderServiceExtensions) + .GetMethods() + .FirstOrDefault(m => m.Name == nameof(ServiceProviderServiceExtensions.GetRequiredService) + && m.GetParameters().Length == 1 + && m.GetParameters()[0].ParameterType == typeof(IServiceProvider) + && m.IsGenericMethodDefinition); + + if (getServiceMethodInfo is null) + throw new InvalidOperationException("Could not find GetRequiredService method"); + + Type serviceType = p.ParameterType; + MethodInfo genericMethodInfo = getServiceMethodInfo.MakeGenericMethod(serviceType); + var getRequiredServiceCall = (Expression)Expression.Call( + null, + genericMethodInfo, + serviceProviderProperty); + + return Expression.Block( + getRequiredServiceCall + ); + }); + + MethodCallExpression invokeExpression = Expression.Call( + instanceExpression, + invokeMethod, + argumentsExpressions + ); + + Expression valueTaskExpression; + if (invokeMethod.ReturnType == typeof(ValueTask)) + { + valueTaskExpression = invokeExpression; + } + else if (invokeMethod.ReturnType == typeof(Task)) + { + valueTaskExpression = Expression.New( + typeof(ValueTask).GetConstructor(new[] { typeof(Task) })!, + invokeExpression + ); + } + else if (invokeMethod.ReturnType == typeof(void)) + { + valueTaskExpression = Expression.Block( + invokeExpression, + Expression.Constant(ValueTask.CompletedTask) + ); + } + else if (invokeMethod.ReturnType.IsGenericType && + invokeMethod.ReturnType.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>)) + { + var t = invokeMethod.ReturnType.GetGenericArguments()[0]; + valueTaskExpression = Expression.Call(ResolveAsyncEnumerableT.MakeGenericMethod(t), invokeExpression, CancellationTokenParam,ContextParam); + } + else + { + throw new InvalidAsynchronousStateException($"Subscriber delegate return value must be of type IAsyncEnumerable."); + } + + + var lambda = Expression.Lambda( + valueTaskExpression, + ContextParam, + CancellationTokenParam + ); + + var compiledLambda = lambda.Compile(); + Cache.TryAdd(subscriberDelegate, compiledLambda); + return compiledLambda; + } + + private static readonly MethodInfo ResolveAsyncEnumerableT = typeof(DelegateSubscriberFactory) + .GetMethod(nameof(ResolveAsyncEnumerable), BindingFlags.Static | BindingFlags.NonPublic)!; + + + private static ValueTask ResolveAsyncEnumerable(IAsyncEnumerable task, CancellationToken cancellationToken, SubscriberContext context) + { + + context.ResolvedValue = Wrap(task, cancellationToken); + return ValueTask.CompletedTask; + + [DebuggerStepThrough] + static async IAsyncEnumerable Wrap( + IAsyncEnumerable task, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var item in task.WithCancellation(cancellationToken)) + { + yield return item; + } + } + } + +} \ No newline at end of file diff --git a/src/GraphQL/ValueResolution/ResolversBuilder.cs b/src/GraphQL/ValueResolution/ResolversBuilder.cs index ae4a552ea..c71f7bd68 100644 --- a/src/GraphQL/ValueResolution/ResolversBuilder.cs +++ b/src/GraphQL/ValueResolution/ResolversBuilder.cs @@ -35,9 +35,12 @@ public void Add(string objectName, string fieldName, Action con configure(Resolver(objectName, fieldName)); } - public void Add(string objectName, string fieldName, Action configure) + public void Add(string objectName, string fieldName, + Action configureSubscriber, + Action configureResolver) { - configure(Subscriber(objectName, fieldName)); + configureSubscriber(Subscriber(objectName, fieldName)); + configureResolver(Resolver(objectName, fieldName)); } public ResolversBuilder Resolvers(string objectName, Dictionary> resolvers) diff --git a/src/GraphQL/ValueResolution/SubscriberBuilder.cs b/src/GraphQL/ValueResolution/SubscriberBuilder.cs index 2b078a4dd..f9142db55 100644 --- a/src/GraphQL/ValueResolution/SubscriberBuilder.cs +++ b/src/GraphQL/ValueResolution/SubscriberBuilder.cs @@ -15,6 +15,11 @@ public SubscriberBuilder Run(Subscriber subscriber) return Use(_ => subscriber); } + public SubscriberBuilder Run(Delegate subscriber) + { + return Use(_ => DelegateSubscriberFactory.GetOrCreate(subscriber)); + } + public Subscriber Build() { Subscriber subscriber = (_, _) => ValueTask.CompletedTask; diff --git a/src/graphql.server/GraphQLApplicationBuilder.cs b/src/graphql.server/GraphQLApplicationBuilder.cs index 355cba4e8..c3702dc55 100644 --- a/src/graphql.server/GraphQLApplicationBuilder.cs +++ b/src/graphql.server/GraphQLApplicationBuilder.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; +using Tanka.GraphQL.Executable; using Tanka.GraphQL.Validation; namespace Tanka.GraphQL.Server; @@ -27,7 +28,7 @@ public GraphQLApplicationBuilder AddHttp() return this; } - public GraphQLApplicationBuilder AddSchema( + public GraphQLApplicationBuilder AddOptions( string schemaName, Action configureOptions) { @@ -45,6 +46,23 @@ public GraphQLApplicationBuilder AddSchema( return this; } + public GraphQLApplicationBuilder AddSchema( + string schemaName, + Action configureExecutable) + { + OptionsBuilder schemaOptions = ApplicationServices + .AddOptions(schemaName); + + var optionsBuilder = new SchemaOptionsBuilder( + schemaOptions, + ApplicationServices); + + ApplicationOptionsBuilder.Configure(options => options.SchemaNames.Add(schemaName)); + optionsBuilder.Configure(configureExecutable); + + return this; + } + public GraphQLApplicationBuilder AddWebSockets() { ApplicationServices.TryAddEnumerable(ServiceDescriptor.Singleton()); diff --git a/src/graphql.server/SchemaOptions.cs b/src/graphql.server/SchemaOptions.cs index 28040fc13..22275eff3 100644 --- a/src/graphql.server/SchemaOptions.cs +++ b/src/graphql.server/SchemaOptions.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Tanka.GraphQL.Executable; -using Tanka.GraphQL.Language.Nodes.TypeSystem; -using Tanka.GraphQL.TypeSystem; +using Tanka.GraphQL.Executable; namespace Tanka.GraphQL.Server; @@ -11,5 +6,5 @@ public class SchemaOptions { public string SchemaName { get; set; } = string.Empty; - public ExecutableSchemaBuilder Builder = new(); + public ExecutableSchemaBuilder Builder { get; } = new(); } \ No newline at end of file diff --git a/tests/GraphQL.Extensions.Tracing.Tests/TraceFacts.cs b/tests/GraphQL.Extensions.Tracing.Tests/TraceFacts.cs index 761799497..0403c3248 100644 --- a/tests/GraphQL.Extensions.Tracing.Tests/TraceFacts.cs +++ b/tests/GraphQL.Extensions.Tracing.Tests/TraceFacts.cs @@ -12,7 +12,7 @@ public class TraceFacts public TraceFacts() { Schema = new ExecutableSchemaBuilder() - .Object("Query", new Dictionary>() + .Add("Query", new () { ["simple: String!"] = b => b.ResolveAs("string") }) diff --git a/tests/graphql.tests/EmptyServiceProvider.cs b/tests/graphql.tests/EmptyServiceProvider.cs new file mode 100644 index 000000000..d026a8f43 --- /dev/null +++ b/tests/graphql.tests/EmptyServiceProvider.cs @@ -0,0 +1,13 @@ +using System; + +namespace Tanka.GraphQL.Tests; + +public class EmptyServiceProvider : IServiceProvider +{ + public object? GetService(Type serviceType) + { + return null; + } + + public static IServiceProvider Instance { get; } = new EmptyServiceProvider(); +} \ No newline at end of file diff --git a/tests/graphql.tests/Executor.MutationFacts.cs b/tests/graphql.tests/Executor.MutationFacts.cs index e4c0d83aa..513868528 100644 --- a/tests/graphql.tests/Executor.MutationFacts.cs +++ b/tests/graphql.tests/Executor.MutationFacts.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; using Tanka.GraphQL.Executable; using Tanka.GraphQL.Language.Nodes; @@ -17,7 +16,7 @@ public async Task Simple_Scalar() { /* Given */ var schema = await new ExecutableSchemaBuilder() - .Object("Mutation", new Dictionary>() + .Add("Mutation", new () { { "version: String!", b => b.ResolveAs("1.0") } }) @@ -51,11 +50,11 @@ public async Task Object_with_ScalarField() { /* Given */ var schema = await new ExecutableSchemaBuilder() - .Object("System", new Dictionary>() + .Add("System", new () { { "version: String!", b => b.ResolveAs("1.0") } }) - .Object("Mutation", new Dictionary>() + .Add("Mutation", new () { { "system: System!", b => b.ResolveAs("System") } }) diff --git a/tests/graphql.tests/Executor.QueryFacts.cs b/tests/graphql.tests/Executor.QueryFacts.cs index d138b7cb5..12fc0e2be 100644 --- a/tests/graphql.tests/Executor.QueryFacts.cs +++ b/tests/graphql.tests/Executor.QueryFacts.cs @@ -17,7 +17,7 @@ public async Task Simple_Scalar() { /* Given */ var schema = await new ExecutableSchemaBuilder() - .Object("Query", new Dictionary>() + .Add("Query", new () { { "version: String!", b => b.ResolveAs("1.0") } }) @@ -51,11 +51,11 @@ public async Task Object_with_ScalarField() { /* Given */ var schema = await new ExecutableSchemaBuilder() - .Object("System", new Dictionary>() + .Add("System", new () { { "version: String!", b => b.ResolveAs("1.0") } }) - .Object("Query", new Dictionary>() + .Add("Query", new() { { "system: System!", b => b.ResolveAs("System") } }) diff --git a/tests/graphql.tests/Executor.SubscriptionFacts.cs b/tests/graphql.tests/Executor.SubscriptionFacts.cs index 603146caf..808320d6d 100644 --- a/tests/graphql.tests/Executor.SubscriptionFacts.cs +++ b/tests/graphql.tests/Executor.SubscriptionFacts.cs @@ -21,8 +21,8 @@ public async Task Simple_Scalar() { /* Given */ var schema = await new ExecutableSchemaBuilder() - .Object("Query", new Dictionary>()) - .Object("Subscription", new Dictionary>() + .Add("Query", new ()) + .Add("Subscription", new () { { "count: Int!", b => b.Run(ctx => diff --git a/tests/graphql.tests/ValueResolution/DelegateSubscriberFactoryFacts.cs b/tests/graphql.tests/ValueResolution/DelegateSubscriberFactoryFacts.cs new file mode 100644 index 000000000..f82f43b60 --- /dev/null +++ b/tests/graphql.tests/ValueResolution/DelegateSubscriberFactoryFacts.cs @@ -0,0 +1,371 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Tanka.GraphQL.Language.Nodes; +using Tanka.GraphQL.Language.Nodes.TypeSystem; +using Tanka.GraphQL.ValueResolution; +using Xunit; + +namespace Tanka.GraphQL.Tests.ValueResolution; + +public class DelegateSubscriberFactoryFacts +{ + + [Fact] + public async Task ReturnValue_is_Task() + { + /* Given */ + static async Task AsyncSubscriber(SubscriberContext context) + { + await Task.Delay(0); + context.ResolvedValue = AsyncEnumerable.Repeat((object)true, 1); + } + + Delegate subscriberDelegate = AsyncSubscriber; + + /* When */ + Subscriber subscriber = DelegateSubscriberFactory.Create(subscriberDelegate); + + /* Then */ + var context = new SubscriberContext + { + ObjectDefinition = null, + ObjectValue = null, + Field = null, + Selection = null, + Fields = null, + ArgumentValues = null, + Path = null, + QueryContext = null, + }; + + await subscriber(context, CancellationToken.None); + + Assert.NotNull(context.ResolvedValue); + Assert.True(await context.ResolvedValue.OfType().SingleAsync()); + } + + [Fact] + public async Task ReturnValue_is_Task_with_context_and_a_service_parameter() + { + /* Given */ + static async Task AsyncSubscriber(SubscriberContext context, IMyDependency dep1) + { + await Task.Delay(0); + context.ResolvedValue = AsyncEnumerable.Repeat((object)dep1, 1); + } + + Delegate subscriberDelegate = AsyncSubscriber; + + /* When */ + Subscriber subscriber = DelegateSubscriberFactory.Create(subscriberDelegate); + + /* Then */ + var context = new SubscriberContext + { + ObjectDefinition = null, + ObjectValue = null, + Field = null, + Selection = null, + Fields = null, + ArgumentValues = null, + Path = null, + QueryContext = new QueryContext() + { + RequestServices = new ServiceCollection() + .AddSingleton() + .BuildServiceProvider() + } + }; + + await subscriber(context, CancellationToken.None); + + Assert.NotNull(context.ResolvedValue); + Assert.IsAssignableFrom(await context.ResolvedValue.OfType().SingleAsync()); + } + + [Fact] + public async Task ReturnValue_is_Task_with_context_and_ObjectValue_parameter() + { + /* Given */ + static async Task AsyncSubscriber(SubscriberContext context, object? objectValue) + { + await Task.Delay(0); + context.ResolvedValue = AsyncEnumerable.Repeat((object?)objectValue, 1); + } + + Delegate subscriberDelegate = AsyncSubscriber; + + /* When */ + Subscriber subscriber = DelegateSubscriberFactory.Create(subscriberDelegate); + + /* Then */ + var context = new SubscriberContext + { + ObjectDefinition = null, + ObjectValue = "test", + Field = null, + Selection = null, + Fields = null, + ArgumentValues = null, + Path = null, + QueryContext = null + }; + + await subscriber(context, CancellationToken.None); + + Assert.NotNull(context.ResolvedValue); + Assert.Equal("test", await context.ResolvedValue.OfType().SingleAsync()); + } + + [Fact] + public async Task ReturnValue_is_Task_with_context_and_TypedObjectValue_parameter() + { + /* Given */ + static async Task AsyncSubscriber(SubscriberContext context, string? objectValue) + { + await Task.Delay(0); + context.ResolvedValue = AsyncEnumerable.Repeat((object?)objectValue, 1); + } + + Delegate subscriberDelegate = AsyncSubscriber; + + /* When */ + Subscriber subscriber = DelegateSubscriberFactory.Create(subscriberDelegate); + + /* Then */ + var context = new SubscriberContext + { + ObjectDefinition = null, + ObjectValue = "test", + Field = null, + Selection = null, + Fields = null, + ArgumentValues = null, + Path = null, + QueryContext = null + }; + + await subscriber(context, CancellationToken.None); + + Assert.NotNull(context.ResolvedValue); + Assert.Equal("test", await context.ResolvedValue.OfType().SingleAsync()); + } + + [Fact] + public async Task ReturnValue_is_Task_with_all_context_members() + { + /* Given */ + bool called = false; + + async Task AsyncSubscriber( + ObjectDefinition objectDefinition, + string objectValue, + FieldDefinition field, + FieldSelection selection, + IReadOnlyCollection fields, + IReadOnlyDictionary argumentValues, + NodePath path, + QueryContext queryContext) + { + await Task.Delay(0); + called = true; + } + + Delegate subscriberDelegate = AsyncSubscriber; + + /* When */ + Subscriber subscriber = DelegateSubscriberFactory.Create(subscriberDelegate); + + /* Then */ + var context = new SubscriberContext + { + ObjectDefinition = null, + ObjectValue = "test", + Field = null, + Selection = null, + Fields = null, + ArgumentValues = null, + Path = null, + QueryContext = null + }; + + await subscriber(context, CancellationToken.None); + + Assert.True(called); + } + + [Fact] + public async Task ReturnValue_is_Task_no_parameters() + { + /* Given */ + var called = false; + + Task AsyncSubscriber() + { + called = true; + return Task.CompletedTask; + } + + Delegate subscriberDelegate = AsyncSubscriber; + + /* When */ + Subscriber subscriber = DelegateSubscriberFactory.Create(subscriberDelegate); + + /* Then */ + var context = new SubscriberContext + { + ObjectDefinition = null, + ObjectValue = null, + Field = null, + Selection = null, + Fields = null, + ArgumentValues = null, + Path = null, + QueryContext = null + }; + + await subscriber(context, CancellationToken.None); + + Assert.True(called); + } + + [Fact] + public async Task ReturnValue_is_ValueTask() + { + /* Given */ + static async ValueTask AsyncSubscriber(SubscriberContext context) + { + await Task.Delay(0); + context.ResolvedValue = AsyncEnumerable.Repeat((object)true, 1); + } + + Delegate subscriberDelegate = AsyncSubscriber; + + /* When */ + Subscriber subscriber = DelegateSubscriberFactory.Create(subscriberDelegate); + + /* Then */ + var context = new SubscriberContext + { + ObjectDefinition = null, + ObjectValue = null, + Field = null, + Selection = null, + Fields = null, + ArgumentValues = null, + Path = null, + QueryContext = null + }; + + await subscriber(context, CancellationToken.None); + + Assert.NotNull(context.ResolvedValue); + Assert.True(await context.ResolvedValue.OfType().SingleAsync()); + } + + [Fact] + public async Task ReturnValue_is_ValueTask_no_parameters() + { + /* Given */ + var called = false; + + ValueTask AsyncSubscriber() + { + called = true; + return default; + } + + Delegate subscriberDelegate = AsyncSubscriber; + + /* When */ + Subscriber subscriber = DelegateSubscriberFactory.Create(subscriberDelegate); + + /* Then */ + var context = new SubscriberContext + { + ObjectDefinition = null, + ObjectValue = null, + Field = null, + Selection = null, + Fields = null, + ArgumentValues = null, + Path = null, + QueryContext = null + }; + + await subscriber(context, CancellationToken.None); + + Assert.True(called); + } + + [Fact] + public async Task ReturnValue_is_void() + { + /* Given */ + static void Subscriber(SubscriberContext context) + { + context.ResolvedValue = AsyncEnumerable.Repeat((object)true, 1); + } + + Delegate subscriberDelegate = Subscriber; + + /* When */ + Subscriber subscriber = DelegateSubscriberFactory.Create(subscriberDelegate); + + /* Then */ + var context = new SubscriberContext + { + ObjectDefinition = null, + ObjectValue = null, + Field = null, + Selection = null, + Fields = null, + ArgumentValues = null, + Path = null, + QueryContext = null + }; + + await subscriber(context, CancellationToken.None); + + Assert.NotNull(context.ResolvedValue); + Assert.True(await context.ResolvedValue.OfType().SingleAsync()); + } + + [Fact] + public async Task ReturnValue_is_T() + { + /* Given */ + static async IAsyncEnumerable Subscriber() + { + await Task.Delay(0); + yield return true; + } + + Delegate subscriberDelegate = Subscriber; + + /* When */ + Subscriber subscriber = DelegateSubscriberFactory.Create(subscriberDelegate); + + /* Then */ + var context = new SubscriberContext + { + ObjectDefinition = null, + ObjectValue = null, + Field = null, + Selection = null, + Fields = null, + ArgumentValues = null, + Path = null, + QueryContext = null + }; + + await subscriber(context, CancellationToken.None); + + Assert.NotNull(context.ResolvedValue); + Assert.True(await context.ResolvedValue.OfType().SingleAsync()); + } +} \ No newline at end of file