diff --git a/dev/GraphQL.Dev.Reviews/SchemaOptionsBuilderExtensions.cs b/dev/GraphQL.Dev.Reviews/SchemaOptionsBuilderExtensions.cs index f2d8daf01..1883007f5 100644 --- a/dev/GraphQL.Dev.Reviews/SchemaOptionsBuilderExtensions.cs +++ b/dev/GraphQL.Dev.Reviews/SchemaOptionsBuilderExtensions.cs @@ -8,7 +8,7 @@ public static SchemaOptionsBuilder AddReviews(this SchemaOptionsBuilder options) { options.Configure((schema, resolvers) => { - schema.AddTypeSystem(""" + schema.Add(""" type Review @key(fields: "id") { id: ID! body: String @@ -28,7 +28,7 @@ type Product @key(fields: "upc") @extends { } """); - schema.AddResolvers(resolvers); + schema.Add(resolvers); }); return options; diff --git a/dev/graphql.dev.chat.data/ChatResolvers.cs b/dev/graphql.dev.chat.data/ChatResolvers.cs index 3fd43b6b5..545628690 100644 --- a/dev/graphql.dev.chat.data/ChatResolvers.cs +++ b/dev/graphql.dev.chat.data/ChatResolvers.cs @@ -1,4 +1,7 @@ -using Tanka.GraphQL.Executable; +using System; +using System.Collections.Generic; +using Tanka.GraphQL.Executable; +using Tanka.GraphQL.Language.Nodes.TypeSystem; using Tanka.GraphQL.Samples.Chat.Data.Domain; using Tanka.GraphQL.Server; using Tanka.GraphQL.ValueResolution; @@ -9,18 +12,18 @@ public static class ChatSchemaConfigurationExtensions { public static ExecutableSchemaBuilder AddChat(this ExecutableSchemaBuilder builder) { - builder.ConfigureObject("Query", new() + builder.Object("Query", new Dictionary>() { { "messages: [Message!]!", b => b.Run(r => r.GetRequiredService().GetMessagesAsync(r)) } }); - builder.ConfigureObject("Mutation", new() + builder.Object("Mutation", new Dictionary>() { { "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.ConfigureObject("Subscription", new() + builder.Object("Subscription", new Dictionary>() { { "messages: Message!", b => b.Run(r => r.GetRequiredService().ResolveMessageAsync(r)) } }, new() @@ -28,7 +31,7 @@ public static ExecutableSchemaBuilder AddChat(this ExecutableSchemaBuilder build { "messages: Message!", b => b.Run((r, ct) => r.GetRequiredService().StreamMessagesAsync(r, ct)) } }); - builder.ConfigureObject("Message", new() + builder.Object("Message", new Dictionary>() { { "id: String!", context => context.ResolveAsPropertyOf(m => m.Id) }, { "from: From!", context => context.ResolveAsPropertyOf(m => m.From) }, @@ -36,13 +39,13 @@ public static ExecutableSchemaBuilder AddChat(this ExecutableSchemaBuilder build { "timestamp: String!", context => context.ResolveAsPropertyOf(m => m.Timestamp) } }); - builder.ConfigureObject("From", new() + builder.Object("From", new Dictionary>() { { "userId: String!", context => context.ResolveAsPropertyOf(f => f.UserId) }, { "name: String!", context => context.ResolveAsPropertyOf(f => f.Name) } }); - builder.AddTypeSystem(""" + builder.Add(""" input InputMessage { content: String! } diff --git a/samples/GraphQL.Samples.Http/Program.cs b/samples/GraphQL.Samples.Http/Program.cs index 932b289c4..34b578019 100644 --- a/samples/GraphQL.Samples.Http/Program.cs +++ b/samples/GraphQL.Samples.Http/Program.cs @@ -20,20 +20,21 @@ { schema.Configure(b => { - b.ConfigureObject("Query", new() + b.Object("Query", new Dictionary() { - { "system: System!", context => context.ResolveAs(new { }) } + { "system: System!", () => new {} } }); - b.ConfigureObject("System", new() + b.Object("System", new Dictionary() { - { "version: String!", context => context.ResolveAs("3.0") } + { "version: String!", () => "3.0" } }); - b.ConfigureObject("Subscription", new Dictionary>() + b.Object("Subscription", new Dictionary() { - { "counter: Int!", r => r.Run(c => c.ResolveAs(c.ObjectValue)) } - }, new() + { "counter: Int!", (int objectValue) => objectValue } + }, + new() { { "counter(to: Int!): Int!", r => r.Run((c, ct) => { diff --git a/src/GraphQL.Extensions.ApolloFederation/FederationExecutableSchemaBuilderExtensions.cs b/src/GraphQL.Extensions.ApolloFederation/FederationExecutableSchemaBuilderExtensions.cs index 0dd990b44..c45a94564 100644 --- a/src/GraphQL.Extensions.ApolloFederation/FederationExecutableSchemaBuilderExtensions.cs +++ b/src/GraphQL.Extensions.ApolloFederation/FederationExecutableSchemaBuilderExtensions.cs @@ -8,9 +8,9 @@ public static ExecutableSchemaBuilder AddSubgraph( this ExecutableSchemaBuilder builder, SubgraphOptions options) { - builder.AddConfiguration(new SubgraphConfiguration(options)); - builder.AddValueConverter("_Any", new AnyScalarConverter()); - builder.AddValueConverter("_FieldSet", new FieldSetScalarConverter()); + builder.Add(new SubgraphConfiguration(options)); + builder.Add("_Any", new AnyScalarConverter()); + builder.Add("_FieldSet", new FieldSetScalarConverter()); return builder; } diff --git a/src/GraphQL/DefaultOperationPipelineBuilderExtensions.cs b/src/GraphQL/DefaultOperationPipelineBuilderExtensions.cs index 73f021633..8606efac2 100644 --- a/src/GraphQL/DefaultOperationPipelineBuilderExtensions.cs +++ b/src/GraphQL/DefaultOperationPipelineBuilderExtensions.cs @@ -13,7 +13,6 @@ public static class DefaultOperationDelegateBuilderExtensions public static OperationDelegateBuilder AddDefaultFeatures( this OperationDelegateBuilder builder) { - var errorFeature = new ConcurrentBagErrorCollectorFeature(); var argumentBinderFeature = new ArgumentBinderFeature(); var defaultSelectionSetExecutorFeature = new DefaultSelectionSetExecutorFeature(); var fieldExecutorFeature = new FieldExecutorFeature(); @@ -21,7 +20,8 @@ public static OperationDelegateBuilder AddDefaultFeatures( return builder.Use(next => context => { - context.Features.Set(errorFeature); + // errors use state + context.Features.Set(new ConcurrentBagErrorCollectorFeature()); context.Features.Set(argumentBinderFeature); context.Features.Set(defaultSelectionSetExecutorFeature); context.Features.Set(fieldExecutorFeature); diff --git a/src/GraphQL/Executable/ExecutableSchemaBuilder.cs b/src/GraphQL/Executable/ExecutableSchemaBuilder.cs index eab5add9e..a2a356277 100644 --- a/src/GraphQL/Executable/ExecutableSchemaBuilder.cs +++ b/src/GraphQL/Executable/ExecutableSchemaBuilder.cs @@ -22,7 +22,7 @@ public class ExecutableSchemaBuilder public Dictionary DirectiveVisitorFactories { get; } = new(); - public ExecutableSchemaBuilder AddConfiguration( + public ExecutableSchemaBuilder Add( IExecutableSchemaConfiguration configuration) { Configurations.Add(configuration); @@ -30,7 +30,7 @@ public ExecutableSchemaBuilder AddConfiguration( return this; } - public ExecutableSchemaBuilder AddTypeSystem( + public ExecutableSchemaBuilder Add( TypeSystemDocument document) { Documents.Add(document); @@ -38,7 +38,7 @@ public ExecutableSchemaBuilder AddTypeSystem( return this; } - public ExecutableSchemaBuilder ConfigureObject( + public ExecutableSchemaBuilder Object( string type, Dictionary> resolverFields, Dictionary>? subscriberFields = null) @@ -51,27 +51,48 @@ public ExecutableSchemaBuilder ConfigureObject( return this; } - public ExecutableSchemaBuilder AddValueConverter(string type, IValueConverter converter) + public ExecutableSchemaBuilder Object( + string type, + Dictionary resolverFields, + Dictionary>? subscriberFields = null) + { + 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)); + + return this; + } + + public ExecutableSchemaBuilder Add(string type, IValueConverter converter) { ValueConverters[type] = converter; return this; } - public ExecutableSchemaBuilder AddDirectiveVisitor(string type, CreateDirectiveVisitor visitor) + public ExecutableSchemaBuilder Add(string type, CreateDirectiveVisitor visitor) { DirectiveVisitorFactories[type] = visitor; return this; } + public ExecutableSchemaBuilder Add(IResolverMap resolversMap) + { + Add(new ResolversConfiguration(resolversMap)); + return this; + } + public async Task Build(Action? configureBuildOptions = null) { var schemaBuilder = new SchemaBuilder(); var resolversBuilder = new ResolversBuilder(); - foreach (var typeSystemDocument in Documents) + foreach (TypeSystemDocument typeSystemDocument in Documents) schemaBuilder.Add(typeSystemDocument); - foreach (var configuration in Configurations) await configuration.Configure(schemaBuilder, resolversBuilder); + foreach (IExecutableSchemaConfiguration configuration in Configurations) + await configuration.Configure(schemaBuilder, resolversBuilder); var buildOptions = new SchemaBuildOptions { @@ -85,14 +106,8 @@ public async Task Build(Action? configureBuildOptio configureBuildOptions?.Invoke(buildOptions); - var schema = await schemaBuilder.Build(buildOptions); + ISchema schema = await schemaBuilder.Build(buildOptions); return schema; } - - public ExecutableSchemaBuilder AddResolvers(IResolverMap resolversMap) - { - AddConfiguration(new ResolversConfiguration(resolversMap)); - return this; - } } \ No newline at end of file diff --git a/src/GraphQL/Executable/ObjectDelegateResolversConfiguration.cs b/src/GraphQL/Executable/ObjectDelegateResolversConfiguration.cs new file mode 100644 index 000000000..cab6d97ac --- /dev/null +++ b/src/GraphQL/Executable/ObjectDelegateResolversConfiguration.cs @@ -0,0 +1,47 @@ +using Tanka.GraphQL.Language.Nodes.TypeSystem; +using Tanka.GraphQL.ValueResolution; + +namespace Tanka.GraphQL.Executable; + +public class ObjectDelegateResolversConfiguration : IExecutableSchemaConfiguration +{ + public ObjectDelegateResolversConfiguration( + string type, + IReadOnlyDictionary fields) + { + Type = type; + Fields = fields; + } + + public string Type { get; } + + public IReadOnlyDictionary Fields { get; } + + public Task Configure(SchemaBuilder schema, ResolversBuilder resolvers) + { + return Task.WhenAll(Configure(schema), Configure(resolvers)); + } + + private Task Configure(ResolversBuilder builder) + { + foreach (var (field, resolverDelegate) in Fields) + builder.Resolver(Type, field.Name.Value).Run(resolverDelegate); + + return Task.CompletedTask; + } + + private Task Configure(SchemaBuilder builder) + { + var fields = Fields.Select(kv => kv.Key).ToList(); + + // we add as type extension so we don't hit any conflicts with type names + // WARNING: if schema is built with BuildTypesFromOrphanedExtensions set to false + // the build will fail + builder.Add(new TypeExtension(new ObjectDefinition( + null, + Type, + fields: new(fields)))); + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/GraphQL/QueryContext.cs b/src/GraphQL/QueryContext.cs index 1429bdb66..011e153c5 100644 --- a/src/GraphQL/QueryContext.cs +++ b/src/GraphQL/QueryContext.cs @@ -95,6 +95,9 @@ public IAsyncEnumerable Response set => ResponseFeature.Response = value; } + //todo: turn into a feature + public IServiceProvider RequestServices { get; set; } + public void AddError(Exception x) { ArgumentNullException.ThrowIfNull(ErrorCollectorFeature); diff --git a/src/GraphQL/ValueResolution/DelegateResolverFactory.cs b/src/GraphQL/ValueResolution/DelegateResolverFactory.cs new file mode 100644 index 000000000..2f0078bde --- /dev/null +++ b/src/GraphQL/ValueResolution/DelegateResolverFactory.cs @@ -0,0 +1,178 @@ +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Linq.Expressions; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; + +namespace Tanka.GraphQL.ValueResolution; + +public static class DelegateResolverFactory +{ + private static readonly ParameterExpression ContextParam = Expression.Parameter(typeof(ResolverContext), "context"); + + private static readonly IReadOnlyDictionary ContextParamProperties = typeof(ResolverContext) + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .ToDictionary(p => p.Name.ToLowerInvariant(), p => (Expression)Expression.Property(ContextParam, p)); + + + private static readonly ConcurrentDictionary Cache = new ConcurrentDictionary(); + + public static Resolver GetOrCreate(Delegate resolverDelegate) + { + if (Cache.TryGetValue(resolverDelegate, out var resolver)) + return resolver; + + return Create(resolverDelegate); + } + + public static Resolver Create(Delegate resolverDelegate) + { +#if DEBUG + Trace.WriteLine( + $"Available context parameters:\n {string.Join(',', ContextParamProperties.Select(p => string.Concat($"{p.Key}: {p.Value.Type}")))}"); +#endif + + MethodInfo invokeMethod = resolverDelegate.Method; + + Expression instanceExpression = null; + if (!invokeMethod.IsStatic) instanceExpression = Expression.Constant(resolverDelegate.Target); + + IEnumerable argumentsExpressions = invokeMethod.GetParameters() + .Select(p => + { + if (p.ParameterType == typeof(ResolverContext)) return ContextParam; + + 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(Task<>)) + { + var t = invokeMethod.ReturnType.GetGenericArguments()[0]; + valueTaskExpression = Expression.Call(ResolveValueTaskMethod.MakeGenericMethod(t), invokeExpression, ContextParam); + } + else if (invokeMethod.ReturnType.IsGenericType && + invokeMethod.ReturnType.GetGenericTypeDefinition() == typeof(ValueTask<>)) + { + var t = invokeMethod.ReturnType.GetGenericArguments()[0]; + valueTaskExpression = Expression.Call(ResolveValueValueTaskMethod.MakeGenericMethod(t), invokeExpression, ContextParam); + } + else + { + var t = invokeMethod.ReturnType; + valueTaskExpression = Expression.Call(ResolveValueObjectMethod.MakeGenericMethod(t), invokeExpression, ContextParam); + } + + + var lambda = Expression.Lambda( + valueTaskExpression, + ContextParam + ); + + var compiledLambda = lambda.Compile(); + Cache.TryAdd(resolverDelegate, compiledLambda); + return compiledLambda; + } + + private static readonly MethodInfo ResolveValueTaskMethod = typeof(DelegateResolverFactory) + .GetMethod(nameof(ResolveValueTask), BindingFlags.Static | BindingFlags.NonPublic)!; + + private static readonly MethodInfo ResolveValueValueTaskMethod = typeof(DelegateResolverFactory) + .GetMethod(nameof(ResolveValueValueTask), BindingFlags.Static | BindingFlags.NonPublic)!; + + private static readonly MethodInfo ResolveValueObjectMethod = typeof(DelegateResolverFactory) + .GetMethod(nameof(ResolveValueObject), BindingFlags.Static | BindingFlags.NonPublic)!; + + private static ValueTask ResolveValueTask(Task task, ResolverContext context) + { + static async ValueTask AwaitResolveValue(Task task, ResolverContext context) + { + context.ResolvedValue = await task; + } + + if (task.IsCompletedSuccessfully) + { + context.ResolvedValue = task.Result; + return default; + } + + return AwaitResolveValue(task, context); + } + + private static ValueTask ResolveValueValueTask(ValueTask task, ResolverContext context) + { + static async ValueTask AwaitResolveValue(ValueTask task, ResolverContext context) + { + context.ResolvedValue = await task; + } + + if (task.IsCompletedSuccessfully) + { + context.ResolvedValue = task.Result; + return default; + } + + return AwaitResolveValue(task, context); + } + + private static ValueTask ResolveValueObject(T result, ResolverContext context) + { + context.ResolvedValue = result; + return ValueTask.CompletedTask; + } +} \ No newline at end of file diff --git a/src/GraphQL/ValueResolution/FieldResolversMap.cs b/src/GraphQL/ValueResolution/FieldResolversMap.cs index 8cd857f10..dd3515633 100644 --- a/src/GraphQL/ValueResolution/FieldResolversMap.cs +++ b/src/GraphQL/ValueResolution/FieldResolversMap.cs @@ -43,6 +43,11 @@ public void Add(string key, Resolver resolver) _resolvers.Add(key, resolver); } + public void Add(string key, Delegate resolver) + { + _resolvers.Add(key, DelegateResolverFactory.GetOrCreate(resolver)); + } + public void Add(string key, Subscriber subscriber) { _subscribers.Add(key, subscriber); @@ -58,6 +63,16 @@ public void Add(string key, Subscriber subscriber, Resolver resolver) _resolvers.Add(key, resolver); } + public void Add(string key, Subscriber subscriber, Delegate resolver) + { + if (key == null) throw new ArgumentNullException(nameof(key)); + if (subscriber == null) throw new ArgumentNullException(nameof(subscriber)); + if (resolver == null) throw new ArgumentNullException(nameof(resolver)); + + _subscribers.Add(key, subscriber); + _resolvers.Add(key, DelegateResolverFactory.GetOrCreate(resolver)); + } + public Resolver? GetResolver(string key) { if (!_resolvers.ContainsKey(key)) diff --git a/src/GraphQL/ValueResolution/ResolverBuilder.cs b/src/GraphQL/ValueResolution/ResolverBuilder.cs index 132904110..2adfab501 100644 --- a/src/GraphQL/ValueResolution/ResolverBuilder.cs +++ b/src/GraphQL/ValueResolution/ResolverBuilder.cs @@ -15,6 +15,11 @@ public ResolverBuilder Run(Resolver resolver) return Use(_ => resolver); } + public ResolverBuilder Run(Delegate resolver) + { + return Use(_ => DelegateResolverFactory.GetOrCreate(resolver)); + } + public Resolver Build() { Resolver resolver = context => ValueTask.CompletedTask; diff --git a/src/GraphQL/ValueResolution/ResolverContextBase.cs b/src/GraphQL/ValueResolution/ResolverContextBase.cs index 6d996edb1..ea64b7392 100644 --- a/src/GraphQL/ValueResolution/ResolverContextBase.cs +++ b/src/GraphQL/ValueResolution/ResolverContextBase.cs @@ -24,4 +24,6 @@ public class ResolverContextBase public ISchema Schema => QueryContext.Schema; public string FieldName => Selection.Name.Value; + + public IServiceProvider RequestServices => QueryContext.RequestServices; } \ No newline at end of file diff --git a/src/GraphQL/ValueResolution/ResolversMap.cs b/src/GraphQL/ValueResolution/ResolversMap.cs index e5902ab43..6e67fb20d 100644 --- a/src/GraphQL/ValueResolution/ResolversMap.cs +++ b/src/GraphQL/ValueResolution/ResolversMap.cs @@ -68,6 +68,16 @@ public void Add(string typeName, string fieldName, Resolver resolver, Subscriber fieldsResolvers.Add(fieldName, subscriber, resolver); } + public void Add(string typeName, string fieldName, Delegate resolver, Subscriber? subscriber = null) + { + if (!TryGetValue(typeName, out var fieldsResolvers)) fieldsResolvers = this[typeName] = new(); + + if (subscriber is null) + fieldsResolvers.Add(fieldName, resolver); + else + fieldsResolvers.Add(fieldName, subscriber, resolver); + } + public bool Add(string typeName, string fieldName, Subscriber subscriber) { if (!TryGetValue(typeName, out var fieldsResolvers)) fieldsResolvers = this[typeName] = new(); diff --git a/src/graphql.server/SchemaOptionsBuilder.cs b/src/graphql.server/SchemaOptionsBuilder.cs index f59315fef..a7bc4dd6b 100644 --- a/src/graphql.server/SchemaOptionsBuilder.cs +++ b/src/graphql.server/SchemaOptionsBuilder.cs @@ -41,13 +41,13 @@ public SchemaOptionsBuilder PostConfigure(Action public SchemaOptionsBuilder AddConfiguration(IExecutableSchemaConfiguration configuration) { - Builder.Configure(options => options.Builder.AddConfiguration(configuration)); + Builder.Configure(options => options.Builder.Add(configuration)); return this; } public SchemaOptionsBuilder AddTypeSystem(TypeSystemDocument document) { - Builder.Configure(options => options.Builder.AddTypeSystem(document)); + Builder.Configure(options => options.Builder.Add(document)); return this; } } \ No newline at end of file diff --git a/tests/GraphQL.Extensions.ApolloFederation.Tests/FederationSchemaBuilderFacts.cs b/tests/GraphQL.Extensions.ApolloFederation.Tests/FederationSchemaBuilderFacts.cs index 3c6361bd1..999f6472e 100644 --- a/tests/GraphQL.Extensions.ApolloFederation.Tests/FederationSchemaBuilderFacts.cs +++ b/tests/GraphQL.Extensions.ApolloFederation.Tests/FederationSchemaBuilderFacts.cs @@ -18,7 +18,7 @@ public async Task EntityUnion_does_not_contain_object_without_key_directive() { /* Given */ var builder = new ExecutableSchemaBuilder() - .AddTypeSystem(@" + .Add(@" type Person @key(fields: ""id"") { id: ID! } @@ -43,7 +43,7 @@ public async Task EntityUnion_has_possible_type_with_key_directive() { /* Given */ var builder = new ExecutableSchemaBuilder() - .AddTypeSystem(@" + .Add(@" type Person @key(fields: ""id"") { id: ID! }") @@ -64,7 +64,7 @@ public async Task Query_entities() { /* Given */ var builder = new ExecutableSchemaBuilder() - .AddTypeSystem(@" + .Add(@" type Person @key(fields: ""id"") { id: ID! name: String! @@ -77,7 +77,7 @@ type Address @key(fields: ""street"") { ["Person"] = (context, type, representation) => new( new ResolveReferenceResult(type, representation)) })) - .AddResolvers(new ResolversMap + .Add(new ResolversMap { ["Person"] = new() { @@ -133,7 +133,7 @@ public async Task Query_sdl() { /* Given */ var builder = new ExecutableSchemaBuilder() - .AddTypeSystem(@" + .Add(@" type Review @key(fields: ""id"") { id: ID! product: Product diff --git a/tests/GraphQL.Extensions.ApolloFederation.Tests/SchemaFactory.cs b/tests/GraphQL.Extensions.ApolloFederation.Tests/SchemaFactory.cs index a9727e808..a4688ec9c 100644 --- a/tests/GraphQL.Extensions.ApolloFederation.Tests/SchemaFactory.cs +++ b/tests/GraphQL.Extensions.ApolloFederation.Tests/SchemaFactory.cs @@ -37,13 +37,13 @@ type Query { "; var builder = new ExecutableSchemaBuilder(); - builder.AddTypeSystem(typeDefs); + builder.Add(typeDefs); builder.AddSubgraph(new(new DictionaryReferenceResolversMap { ["User"] = UserReference, ["Product"] = ProductReference })); - builder.AddResolvers(new ResolversMap + builder.Add(new ResolversMap { ["User"] = new() { diff --git a/tests/GraphQL.Extensions.Tracing.Tests/TraceFacts.cs b/tests/GraphQL.Extensions.Tracing.Tests/TraceFacts.cs index e1ec39d8b..761799497 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() - .ConfigureObject("Query", new Dictionary>() + .Object("Query", new Dictionary>() { ["simple: String!"] = b => b.ResolveAs("string") }) diff --git a/tests/graphql.tests/Executor.MutationFacts.cs b/tests/graphql.tests/Executor.MutationFacts.cs index 45734e251..e4c0d83aa 100644 --- a/tests/graphql.tests/Executor.MutationFacts.cs +++ b/tests/graphql.tests/Executor.MutationFacts.cs @@ -1,6 +1,9 @@ -using System.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; using Tanka.GraphQL.Executable; using Tanka.GraphQL.Language.Nodes; +using Tanka.GraphQL.Language.Nodes.TypeSystem; using Tanka.GraphQL.Request; using Tanka.GraphQL.ValueResolution; using Xunit; @@ -14,7 +17,7 @@ public async Task Simple_Scalar() { /* Given */ var schema = await new ExecutableSchemaBuilder() - .ConfigureObject("Mutation", new() + .Object("Mutation", new Dictionary>() { { "version: String!", b => b.ResolveAs("1.0") } }) @@ -48,11 +51,11 @@ public async Task Object_with_ScalarField() { /* Given */ var schema = await new ExecutableSchemaBuilder() - .ConfigureObject("System", new() + .Object("System", new Dictionary>() { { "version: String!", b => b.ResolveAs("1.0") } }) - .ConfigureObject("Mutation", new() + .Object("Mutation", new Dictionary>() { { "system: System!", b => b.ResolveAs("System") } }) diff --git a/tests/graphql.tests/Executor.QueryFacts.cs b/tests/graphql.tests/Executor.QueryFacts.cs index a6d09a0ea..d138b7cb5 100644 --- a/tests/graphql.tests/Executor.QueryFacts.cs +++ b/tests/graphql.tests/Executor.QueryFacts.cs @@ -1,6 +1,9 @@ -using System.Threading.Tasks; +using System.Collections.Generic; +using System; +using System.Threading.Tasks; using Tanka.GraphQL.Executable; using Tanka.GraphQL.Language.Nodes; +using Tanka.GraphQL.Language.Nodes.TypeSystem; using Tanka.GraphQL.Request; using Tanka.GraphQL.ValueResolution; using Xunit; @@ -14,7 +17,7 @@ public async Task Simple_Scalar() { /* Given */ var schema = await new ExecutableSchemaBuilder() - .ConfigureObject("Query", new() + .Object("Query", new Dictionary>() { { "version: String!", b => b.ResolveAs("1.0") } }) @@ -48,11 +51,11 @@ public async Task Object_with_ScalarField() { /* Given */ var schema = await new ExecutableSchemaBuilder() - .ConfigureObject("System", new() + .Object("System", new Dictionary>() { { "version: String!", b => b.ResolveAs("1.0") } }) - .ConfigureObject("Query", new() + .Object("Query", new Dictionary>() { { "system: System!", b => b.ResolveAs("System") } }) diff --git a/tests/graphql.tests/Executor.SubscriptionFacts.cs b/tests/graphql.tests/Executor.SubscriptionFacts.cs index f2f1b9cef..603146caf 100644 --- a/tests/graphql.tests/Executor.SubscriptionFacts.cs +++ b/tests/graphql.tests/Executor.SubscriptionFacts.cs @@ -5,7 +5,9 @@ using System.Threading.Tasks; using Tanka.GraphQL.Executable; using Tanka.GraphQL.Language.Nodes; +using Tanka.GraphQL.Language.Nodes.TypeSystem; using Tanka.GraphQL.Request; +using Tanka.GraphQL.ValueResolution; using Xunit; namespace Tanka.GraphQL.Tests; @@ -19,8 +21,8 @@ public async Task Simple_Scalar() { /* Given */ var schema = await new ExecutableSchemaBuilder() - .ConfigureObject("Query", new()) - .ConfigureObject("Subscription", new() + .Object("Query", new Dictionary>()) + .Object("Subscription", new Dictionary>() { { "count: Int!", b => b.Run(ctx => diff --git a/tests/graphql.tests/ValueResolution/DelegateResolverFactoryFacts.cs b/tests/graphql.tests/ValueResolution/DelegateResolverFactoryFacts.cs new file mode 100644 index 000000000..74ec7073c --- /dev/null +++ b/tests/graphql.tests/ValueResolution/DelegateResolverFactoryFacts.cs @@ -0,0 +1,466 @@ +using System; +using System.Collections.Generic; +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 DelegateResolverFactoryFacts +{ + + [Fact] + public async Task ReturnValue_is_Task() + { + /* Given */ + static async Task AsyncResolver(ResolverContext context) + { + await Task.Delay(0); + context.ResolvedValue = true; + } + + Delegate resolverDelegate = AsyncResolver; + + /* When */ + Resolver resolver = DelegateResolverFactory.Create(resolverDelegate); + + /* Then */ + var context = new ResolverContext + { + ObjectDefinition = null, + ObjectValue = null, + Field = null, + Selection = null, + Fields = null, + ArgumentValues = null, + Path = null, + QueryContext = null + }; + + await resolver(context); + + Assert.NotNull(context.ResolvedValue); + Assert.True((bool)context.ResolvedValue); + } + + [Fact] + public async Task ReturnValue_is_Task_with_context_and_a_service_parameter() + { + /* Given */ + static async Task AsyncResolver(ResolverContext context, IMyDependency dep1) + { + await Task.Delay(0); + context.ResolvedValue = dep1; + } + + Delegate resolverDelegate = AsyncResolver; + + /* When */ + Resolver resolver = DelegateResolverFactory.Create(resolverDelegate); + + /* Then */ + var context = new ResolverContext + { + ObjectDefinition = null, + ObjectValue = null, + Field = null, + Selection = null, + Fields = null, + ArgumentValues = null, + Path = null, + QueryContext = new QueryContext() + { + RequestServices = new ServiceCollection() + .AddSingleton() + .BuildServiceProvider() + } + }; + + await resolver(context); + + Assert.NotNull(context.ResolvedValue); + Assert.IsAssignableFrom(context.ResolvedValue); + } + + [Fact] + public async Task ReturnValue_is_Task_with_context_and_ObjectValue_parameter() + { + /* Given */ + static async Task AsyncResolver(ResolverContext context, object? objectValue) + { + await Task.Delay(0); + context.ResolvedValue = objectValue; + } + + Delegate resolverDelegate = AsyncResolver; + + /* When */ + Resolver resolver = DelegateResolverFactory.Create(resolverDelegate); + + /* Then */ + var context = new ResolverContext + { + ObjectDefinition = null, + ObjectValue = "test", + Field = null, + Selection = null, + Fields = null, + ArgumentValues = null, + Path = null, + QueryContext = null + }; + + await resolver(context); + + Assert.NotNull(context.ResolvedValue); + Assert.Equal("test", context.ResolvedValue); + } + + [Fact] + public async Task ReturnValue_is_Task_with_context_and_TypedObjectValue_parameter() + { + /* Given */ + static async Task AsyncResolver(ResolverContext context, string? objectValue) + { + await Task.Delay(0); + context.ResolvedValue = objectValue; + } + + Delegate resolverDelegate = AsyncResolver; + + /* When */ + Resolver resolver = DelegateResolverFactory.Create(resolverDelegate); + + /* Then */ + var context = new ResolverContext + { + ObjectDefinition = null, + ObjectValue = "test", + Field = null, + Selection = null, + Fields = null, + ArgumentValues = null, + Path = null, + QueryContext = null + }; + + await resolver(context); + + Assert.NotNull(context.ResolvedValue); + Assert.Equal("test", context.ResolvedValue); + } + + [Fact] + public async Task ReturnValue_is_Task_with_all_context_members() + { + /* Given */ + bool called = false; + async Task AsyncResolver( + ObjectDefinition objectDefinition, + string objectValue, + FieldDefinition field, + FieldSelection selection, + IReadOnlyCollection fields, + IReadOnlyDictionary argumentValues, + NodePath path, + QueryContext queryContext) + { + await Task.Delay(0); + called = true; + } + + Delegate resolverDelegate = AsyncResolver; + + /* When */ + Resolver resolver = DelegateResolverFactory.Create(resolverDelegate); + + /* Then */ + var context = new ResolverContext + { + ObjectDefinition = null, + ObjectValue = "test", + Field = null, + Selection = null, + Fields = null, + ArgumentValues = null, + Path = null, + QueryContext = null + }; + + await resolver(context); + + Assert.True(called); + } + + [Fact] + public async Task ReturnValue_is_Task_no_parameters() + { + /* Given */ + var called = false; + Task AsyncResolver() + { + called = true; + return Task.CompletedTask; + } + + Delegate resolverDelegate = AsyncResolver; + + /* When */ + Resolver resolver = DelegateResolverFactory.Create(resolverDelegate); + + /* Then */ + var context = new ResolverContext + { + ObjectDefinition = null, + ObjectValue = null, + Field = null, + Selection = null, + Fields = null, + ArgumentValues = null, + Path = null, + QueryContext = null + }; + + await resolver(context); + + Assert.True(called); + } + + [Fact] + public async Task ReturnValue_is_ValueTask() + { + /* Given */ + static async ValueTask AsyncResolver(ResolverContext context) + { + await Task.Delay(0); + context.ResolvedValue = true; + } + + Delegate resolverDelegate = AsyncResolver; + + /* When */ + Resolver resolver = DelegateResolverFactory.Create(resolverDelegate); + + /* Then */ + var context = new ResolverContext + { + ObjectDefinition = null, + ObjectValue = null, + Field = null, + Selection = null, + Fields = null, + ArgumentValues = null, + Path = null, + QueryContext = null + }; + + await resolver(context); + + Assert.NotNull(context.ResolvedValue); + Assert.True((bool)context.ResolvedValue); + } + + [Fact] + public async Task ReturnValue_is_ValueTask_no_parameters() + { + /* Given */ + var called = false; + ValueTask AsyncResolver() + { + called = true; + return default; + } + + Delegate resolverDelegate = AsyncResolver; + + /* When */ + Resolver resolver = DelegateResolverFactory.Create(resolverDelegate); + + /* Then */ + var context = new ResolverContext + { + ObjectDefinition = null, + ObjectValue = null, + Field = null, + Selection = null, + Fields = null, + ArgumentValues = null, + Path = null, + QueryContext = null + }; + + await resolver(context); + + Assert.True(called); + } + + [Fact] + public async Task ReturnValue_is_void() + { + /* Given */ + static void Resolver(ResolverContext context) + { + context.ResolvedValue = true; + } + + Delegate resolverDelegate = Resolver; + + /* When */ + Resolver resolver = DelegateResolverFactory.Create(resolverDelegate); + + /* Then */ + var context = new ResolverContext + { + ObjectDefinition = null, + ObjectValue = null, + Field = null, + Selection = null, + Fields = null, + ArgumentValues = null, + Path = null, + QueryContext = null + }; + + await resolver(context); + + Assert.NotNull(context.ResolvedValue); + Assert.True((bool)context.ResolvedValue); + } + + [Fact] + public async Task ReturnValue_is_void_no_parameters() + { + /* Given */ + var called = false; + void Resolver() + { + called = true; + } + + Delegate resolverDelegate = Resolver; + + /* When */ + Resolver resolver = DelegateResolverFactory.Create(resolverDelegate); + + /* Then */ + var context = new ResolverContext + { + ObjectDefinition = null, + ObjectValue = null, + Field = null, + Selection = null, + Fields = null, + ArgumentValues = null, + Path = null, + QueryContext = null + }; + + await resolver(context); + + Assert.True(called); + } + + [Fact] + public async Task ReturnValue_is_TaskT() + { + /* Given */ + static async Task AsyncResolver() + { + await Task.Delay(0); + return true; + } + + Delegate resolverDelegate = AsyncResolver; + + /* When */ + Resolver resolver = DelegateResolverFactory.Create(resolverDelegate); + + /* Then */ + var context = new ResolverContext + { + ObjectDefinition = null, + ObjectValue = null, + Field = null, + Selection = null, + Fields = null, + ArgumentValues = null, + Path = null, + QueryContext = null + }; + + await resolver(context); + + Assert.NotNull(context.ResolvedValue); + Assert.True((bool)context.ResolvedValue); + } + + [Fact] + public async Task ReturnValue_is_ValueTaskT() + { + /* Given */ + static async ValueTask AsyncResolver() + { + await Task.Delay(0); + return true; + } + + Delegate resolverDelegate = AsyncResolver; + + /* When */ + Resolver resolver = DelegateResolverFactory.Create(resolverDelegate); + + /* Then */ + var context = new ResolverContext + { + ObjectDefinition = null, + ObjectValue = null, + Field = null, + Selection = null, + Fields = null, + ArgumentValues = null, + Path = null, + QueryContext = null + }; + + await resolver(context); + + Assert.NotNull(context.ResolvedValue); + Assert.True((bool)context.ResolvedValue); + } + + [Fact] + public async Task ReturnValue_is_T() + { + /* Given */ + static bool Resolver() + { + return true; + } + + Delegate resolverDelegate = Resolver; + + /* When */ + Resolver resolver = DelegateResolverFactory.Create(resolverDelegate); + + /* Then */ + var context = new ResolverContext + { + ObjectDefinition = null, + ObjectValue = null, + Field = null, + Selection = null, + Fields = null, + ArgumentValues = null, + Path = null, + QueryContext = null + }; + + await resolver(context); + + Assert.NotNull(context.ResolvedValue); + Assert.True((bool)context.ResolvedValue); + } +} \ No newline at end of file diff --git a/tests/graphql.tests/ValueResolution/IMyDependency.cs b/tests/graphql.tests/ValueResolution/IMyDependency.cs new file mode 100644 index 000000000..8dadbb747 --- /dev/null +++ b/tests/graphql.tests/ValueResolution/IMyDependency.cs @@ -0,0 +1,9 @@ +namespace Tanka.GraphQL.Tests.ValueResolution; + +public interface IMyDependency +{ +} + +class MyDependency : IMyDependency +{ +} \ No newline at end of file diff --git a/tests/graphql.tests/graphql.tests.csproj b/tests/graphql.tests/graphql.tests.csproj index 97265143c..b2f70c179 100644 --- a/tests/graphql.tests/graphql.tests.csproj +++ b/tests/graphql.tests/graphql.tests.csproj @@ -5,12 +5,14 @@ false tanka.graphql.tests Tanka.GraphQL.Tests + enable - - - + + + + diff --git a/tutorials/graphql.tutorials.getting-started/GettingStarted.cs b/tutorials/graphql.tutorials.getting-started/GettingStarted.cs index 85874d727..e9082c923 100644 --- a/tutorials/graphql.tutorials.getting-started/GettingStarted.cs +++ b/tutorials/graphql.tutorials.getting-started/GettingStarted.cs @@ -60,7 +60,7 @@ type Query { Resolvers = new ResolversMap { { - "Query", "name", context => context.ResolveAs("Test") + "Query", "name", () => "Test" } } }); @@ -102,7 +102,7 @@ type Query { "Query", new FieldResolversMap { { - "name", context => context.ResolveAs("Test") + "name", () => "Test" } } } @@ -180,7 +180,7 @@ type Query { "Query", new FieldResolversMap { { - "url", context => context.ResolveAs(new Uri("https://localhost/")) + "url", () => new Uri("https://localhost/") } } }