From ea3cada2ee28a0aaa7b9d2fcbfbdb71a5384ec2b Mon Sep 17 00:00:00 2001 From: Pekka Heikura Date: Wed, 29 Mar 2023 18:04:17 +0300 Subject: [PATCH 1/6] Argument binding for delegate resolvers and subscribers --- docs/0-getting-started/04-server.md | 2 +- ...executable-doc.md => 02-executable-doc.md} | 0 docs/2-language/02-imports.md | 120 ----------- docs/2-language/03-type-system-doc.md | 3 + docs/2-language/nav.md | 5 +- docs/2-language/syntax-tree/nav.md | 3 - .../syntax-tree/tanka-docs-section.yml | 6 - .../2-language/syntax-tree/type-system-doc.md | 3 - docs/2-language/{syntax-tree => }/tree.md | 0 docs/3-server/0-common.md | 63 ------ docs/3-server/0-index.md | 1 + docs/3-server/1-signalR/1-signalr-hub.md | 17 -- docs/3-server/1-signalR/2-apollo-link.md | 30 --- docs/3-server/1-signalR/nav.md | 2 - docs/3-server/2-graphql-ws/3-graphql-ws.md | 31 --- docs/3-server/2-graphql-ws/nav.md | 1 - docs/3-server/2-query-cost-limit.md | 19 -- docs/3-server/9-clients/1-dotnet-client.md | 1 - docs/3-server/nav.md | 3 +- docs/3-server/tanka-docs-section.yml | 4 +- samples/GraphQL.Samples.Http/Program.cs | 2 +- .../Features/IArgumentBinderFeature.cs | 2 + src/GraphQL/Fields/ArgumentBinderFeature.cs | 12 ++ .../ArgumentBinderQueryContextExtensions.cs | 12 ++ .../ArgumentsResolverContextExtensions.cs | 10 +- src/GraphQL/QueryContext.cs | 2 +- .../DelegateResolverFactory.cs | 199 ++++++++++++++---- .../DelegateSubscriberFactory.cs | 186 ++++++++++++---- tanka-graphql.sln | 6 +- .../DelegateResolverFactoryFacts.cs | 132 +++++++++++- .../DelegateSubscriberFactoryFacts.cs | 42 +++- 31 files changed, 523 insertions(+), 396 deletions(-) rename docs/2-language/{syntax-tree/executable-doc.md => 02-executable-doc.md} (100%) delete mode 100644 docs/2-language/02-imports.md create mode 100644 docs/2-language/03-type-system-doc.md delete mode 100644 docs/2-language/syntax-tree/nav.md delete mode 100644 docs/2-language/syntax-tree/tanka-docs-section.yml delete mode 100644 docs/2-language/syntax-tree/type-system-doc.md rename docs/2-language/{syntax-tree => }/tree.md (100%) delete mode 100644 docs/3-server/0-common.md create mode 100644 docs/3-server/0-index.md delete mode 100644 docs/3-server/1-signalR/1-signalr-hub.md delete mode 100644 docs/3-server/1-signalR/2-apollo-link.md delete mode 100644 docs/3-server/1-signalR/nav.md delete mode 100644 docs/3-server/2-graphql-ws/3-graphql-ws.md delete mode 100644 docs/3-server/2-graphql-ws/nav.md delete mode 100644 docs/3-server/2-query-cost-limit.md delete mode 100644 docs/3-server/9-clients/1-dotnet-client.md diff --git a/docs/0-getting-started/04-server.md b/docs/0-getting-started/04-server.md index 27f7a5b77..513b2b99e 100644 --- a/docs/0-getting-started/04-server.md +++ b/docs/0-getting-started/04-server.md @@ -6,7 +6,7 @@ required then you can implement your own server. > See also > -> - [Server](xref://server:0-common.md) +> - [Server](xref://server:0-index.md) ### Simple example with HTTP and websockets diff --git a/docs/2-language/syntax-tree/executable-doc.md b/docs/2-language/02-executable-doc.md similarity index 100% rename from docs/2-language/syntax-tree/executable-doc.md rename to docs/2-language/02-executable-doc.md diff --git a/docs/2-language/02-imports.md b/docs/2-language/02-imports.md deleted file mode 100644 index b3cc6ba66..000000000 --- a/docs/2-language/02-imports.md +++ /dev/null @@ -1,120 +0,0 @@ -## Imports - -Todo: fix syntax - -GraphQL specification does not yet provide a way of importing types -from SDL from other sources. There is some [discussion][] on -this topic but nothing concrete and "official" is yet released. - -> [graphql-import][] solves this with a JS style syntax - -Tanka GraphQL solves this by providing a similar syntax to -[graphql-import][] but not implementing it fully. - -### Syntax - -Syntax of the import requires providing a keyword, optional type filters -and the location from which to import. Location does not need to be a file -system. Imports need to be wrapped into block string and need to be at the -beginning of the document. - -Example: import all types from "/query" - -```graphql -""" -tanka_import from "/query" -""" -``` - -Example: import `Person` from `"/types/Person"` - -```graphql -""" -tanka_import Person from "/types/Person" -""" -``` - -Example: import `Person`, `Pet` from `"/types/Person"` - -```graphql -""" -tanka_import Person, Pet from "/types/Person" -""" -``` - -### Providers - -`Parser.ParseDocumentAsync` allows providing `ParserOptions` which allows -setting a list of import providers. Parser will query these providers when -it finds the `tanka_import`-keyword. Provider can tell the parser that it can -complete the import by returning true from `CanImport` function. If multiple -providers can import the import then the parser will pick the first one. - -Built in providers: (these are added to options by default) - -- `ExtensionsImportProvider`: provides Tanka GraphQL extension types, -- `FileSystemImportProvider`: import types from files, -- `EmbeddedResourceImportProvider`: import types from EmbeddedResources. - -Custom providers can be implemented by implementing `IImportProvider` interface -and adding the provider to the options. - -> Imports are only supported when using the `SchemaBuilder.SdlAsync` extension -> method or `Parser.ParseDocumentAsync` - -#### `ExtensionsImportProvider` - -This import provider allows importing Tanka GraphQL extensions. Currently these -extensions only include: - -- [cost-analysis][]: `@cost` directive. - -Syntax - -```graphql -""" -tanka_import from "tanka://" -""" -``` - -#### `FileSystemImportProvider` - -This import provider allows importing files. These files are parsed using the -same parser options as the main file and can also contain other imports. - -Syntax - -```graphql -""" -tanka_import from "path/to/file" -""" -``` - -If no file extension provided then ".graphql" will be appended to the path. - -Example -[{Tanka.GraphQL.Tests.Language.ImportProviders.FileSystemImportFacts.Parse_Sdl}] - -```csharp -//todo: update sample -``` - -#### `EmbeddedResourceImportProvider` - -This import provider allows importing files embedded into the assembly. These files are parsed using the -same parser options as the main file and can also contain other imports. - -Syntax - -```graphql -""" -tanka_import from "embedded:///" -""" -``` - -### Other Examples - -See [cost-analysis](xref://server:5-extensions/5-query-cost-analysis.md) for example import from the TGQL extensions. - -[discussion]: https://github.com/graphql/graphql-wg/blob/master/notes/2018-02-01.md#present-graphql-import -[graphql-import]: https://github.com/ardatan/graphql-import diff --git a/docs/2-language/03-type-system-doc.md b/docs/2-language/03-type-system-doc.md new file mode 100644 index 000000000..f997b80bc --- /dev/null +++ b/docs/2-language/03-type-system-doc.md @@ -0,0 +1,3 @@ +## Executable Document + +TODO: diff --git a/docs/2-language/nav.md b/docs/2-language/nav.md index ccbf5d818..c0b1f3d18 100644 --- a/docs/2-language/nav.md +++ b/docs/2-language/nav.md @@ -1,2 +1,3 @@ -- [Language](xref://01-parser.md) -- [Imports](xref://02-imports.md) +- [Parser](xref://01-parser.md) +- [Executable Document](xref://02-executable-doc.md) +- [Type System Document](xref://03-type-system-doc.md) diff --git a/docs/2-language/syntax-tree/nav.md b/docs/2-language/syntax-tree/nav.md deleted file mode 100644 index b20545b29..000000000 --- a/docs/2-language/syntax-tree/nav.md +++ /dev/null @@ -1,3 +0,0 @@ -- [Syntax Tree](xref://tree.md) - - [ExecutableDocument](xref://executable-doc.md) - - [TypeSystemDocument](xref://type-system-doc.md) diff --git a/docs/2-language/syntax-tree/tanka-docs-section.yml b/docs/2-language/syntax-tree/tanka-docs-section.yml deleted file mode 100644 index ab5402452..000000000 --- a/docs/2-language/syntax-tree/tanka-docs-section.yml +++ /dev/null @@ -1,6 +0,0 @@ -id: syntax -title: "Syntax Tree" -index_page: xref://syntax:tree.md -nav: - - xref://lang:nav.md - - xref://nav.md diff --git a/docs/2-language/syntax-tree/type-system-doc.md b/docs/2-language/syntax-tree/type-system-doc.md deleted file mode 100644 index 5f919989a..000000000 --- a/docs/2-language/syntax-tree/type-system-doc.md +++ /dev/null @@ -1,3 +0,0 @@ -## Type System Document - -TODO: diff --git a/docs/2-language/syntax-tree/tree.md b/docs/2-language/tree.md similarity index 100% rename from docs/2-language/syntax-tree/tree.md rename to docs/2-language/tree.md diff --git a/docs/3-server/0-common.md b/docs/3-server/0-common.md deleted file mode 100644 index d84d9fd2b..000000000 --- a/docs/3-server/0-common.md +++ /dev/null @@ -1,63 +0,0 @@ -## Common options - -Tanka provides SignalR hub and websockets server. Both of these use -same underlying services for query execution. - -### Add required common services - -Add services required for executing GraphQL queries, mutations -and subscriptions. Main service added is `IQueryStreamService` -which handles the plumping of execution. - -```csharp -todo: add sample -``` - -### Configure schema - -Configure `ISchema` for execution by providing a factory function (can be async) -used to get the schema for execution. - -Simple without dependencies - -```csharp -todo: add sample -``` - -Overloads are provided for providing a function with dependencies resolved from -services. - -```csharp -todo: add sample -``` - -### Configure rules - -Configure validation rules for execution. Note that by default all rules -specified in the specification are included. - -Add MaxCost validation rule - -```csharp -todo: add sample -``` - -Remove all rules - -```csharp -todo: add sample -``` - -With up to three dependencies resolved from service provider - -```csharp -todo: add sample -``` - -### Add extensions - -Add Apollo tracing extension - -```csharp -todo: add sample -``` diff --git a/docs/3-server/0-index.md b/docs/3-server/0-index.md new file mode 100644 index 000000000..06b7f3547 --- /dev/null +++ b/docs/3-server/0-index.md @@ -0,0 +1 @@ +## Server \ No newline at end of file diff --git a/docs/3-server/1-signalR/1-signalr-hub.md b/docs/3-server/1-signalR/1-signalr-hub.md deleted file mode 100644 index 2d8e702e0..000000000 --- a/docs/3-server/1-signalR/1-signalr-hub.md +++ /dev/null @@ -1,17 +0,0 @@ -## SignalR Hub - -Server is implemented as a SignalR Core Hub and it handles queries, mutations -and subscriptions. This projects provides an Apollo Link implementation to be -used with the provided hub. - -### GraphQL Server - -Configure SignalR server - -```csharp -//todo: add sample -``` - -```csharp -//todo: add sample -``` diff --git a/docs/3-server/1-signalR/2-apollo-link.md b/docs/3-server/1-signalR/2-apollo-link.md deleted file mode 100644 index 449306cae..000000000 --- a/docs/3-server/1-signalR/2-apollo-link.md +++ /dev/null @@ -1,30 +0,0 @@ -## Apollo link - -```js -import { ApolloClient } from 'apollo-client'; -import { InMemoryCache } from 'apollo-cache-inmemory'; -import { onError } from 'apollo-link-error'; -import { ApolloLink } from 'apollo-link'; -import { TankaLink, TankaClient } from '@tanka/graphql-server-link'; - -const serverClient = new TankaClient("/graphql"); -const serverLink = new TankaLink(serverClient); - -const client = new ApolloClient({ - connectToDevTools: true, - link: ApolloLink.from([ - onError(({ graphQLErrors, networkError }) => { - if (graphQLErrors) - graphQLErrors.map(({ message, locations, path }) => - console.log( - `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`, - ), - ); - if (networkError) console.log(`[Network error]: ${networkError}`); - }), - serverLink - ]), - cache: new InMemoryCache() -}); -export default client; -``` diff --git a/docs/3-server/1-signalR/nav.md b/docs/3-server/1-signalR/nav.md deleted file mode 100644 index ef36c6548..000000000 --- a/docs/3-server/1-signalR/nav.md +++ /dev/null @@ -1,2 +0,0 @@ -- [SignalR](xref://1-signalR/1-signalr-hub.md) - - [Apollo Link](xref://1-signalR/2-apollo-link.md) diff --git a/docs/3-server/2-graphql-ws/3-graphql-ws.md b/docs/3-server/2-graphql-ws/3-graphql-ws.md deleted file mode 100644 index b878721e4..000000000 --- a/docs/3-server/2-graphql-ws/3-graphql-ws.md +++ /dev/null @@ -1,31 +0,0 @@ -## GraphQL WS - -Besides the SignalR based server Tanka also provides a graphql-ws protocol -compatible websocket server. This server can be used with -[apollo-link-ws](https://www.apollographql.com/docs/link/links/ws). - -### Configure required services - -This will add the required services to execution pipeline. - -```csharp -//todo: add sample -``` - -### Add middleware to app pipeline - -```csharp -app.UseWebSockets(); -app.UseTankaGraphQLWebSockets("/api/graphql"); -``` - -### Configure protocol - -When `connection_init` message is received from client the protocol calls -`AcceptAsync` of the options to accept the connection. By default it accepts -the connection and sends `connection_ack` message back to the client. You can -configure this behavior with your own logic. - -```csharp -//todo: add sample -``` diff --git a/docs/3-server/2-graphql-ws/nav.md b/docs/3-server/2-graphql-ws/nav.md deleted file mode 100644 index 0b595d75a..000000000 --- a/docs/3-server/2-graphql-ws/nav.md +++ /dev/null @@ -1 +0,0 @@ -- [GraphQL-WS](xref://2-graphql-ws/3-graphql-ws.md) diff --git a/docs/3-server/2-query-cost-limit.md b/docs/3-server/2-query-cost-limit.md deleted file mode 100644 index dc94460ba..000000000 --- a/docs/3-server/2-query-cost-limit.md +++ /dev/null @@ -1,19 +0,0 @@ -## Query Cost Limit - -GraphQL has some unique characteristics which open services -for various types of attacks. Common attack is to overwhelm -the service with resource heavy queries. Common way to counter -this type of attack is to limit the query cost based on complexity. - -## Query Cost Analysis - -See the detailed explanation and schema configuration in -[Query Cost Analysis](xref://server:5-extensions/5-query-cost-analysis.md). - -## Usage with server - -Add cost limiting validation rule to options - -```csharp -//todo: add sample -``` diff --git a/docs/3-server/9-clients/1-dotnet-client.md b/docs/3-server/9-clients/1-dotnet-client.md deleted file mode 100644 index 904f75ddf..000000000 --- a/docs/3-server/9-clients/1-dotnet-client.md +++ /dev/null @@ -1 +0,0 @@ -Dotnet client is available at https://github.com/anttikajanus/tanka-graphql-net-client diff --git a/docs/3-server/nav.md b/docs/3-server/nav.md index dd724ebfa..2e535304c 100644 --- a/docs/3-server/nav.md +++ b/docs/3-server/nav.md @@ -1,2 +1 @@ -- [Common](xref://0-common.md) -- [Query Cost Limitting](xref://2-query-cost-limit.md) +- [Introduction](xref://0-index.md) \ No newline at end of file diff --git a/docs/3-server/tanka-docs-section.yml b/docs/3-server/tanka-docs-section.yml index 8db1ea9b7..f3ab183e9 100644 --- a/docs/3-server/tanka-docs-section.yml +++ b/docs/3-server/tanka-docs-section.yml @@ -1,8 +1,6 @@ id: server title: "Server" -index_page: xref://server:0-common.md +index_page: xref://server:0-index.md nav: - xref://nav.md - - xref://1-signalR/nav.md - - xref://2-graphql-ws/nav.md - xref://5-extensions/nav.md diff --git a/samples/GraphQL.Samples.Http/Program.cs b/samples/GraphQL.Samples.Http/Program.cs index 8ea6658b6..785b7431f 100644 --- a/samples/GraphQL.Samples.Http/Program.cs +++ b/samples/GraphQL.Samples.Http/Program.cs @@ -24,7 +24,7 @@ schema.Add("Subscription", new FieldsWithResolvers { { "counter: Int!", (int objectValue) => objectValue } - }, + }, new FieldsWithSubscribers { { diff --git a/src/GraphQL/Features/IArgumentBinderFeature.cs b/src/GraphQL/Features/IArgumentBinderFeature.cs index 195acb5eb..73972c67b 100644 --- a/src/GraphQL/Features/IArgumentBinderFeature.cs +++ b/src/GraphQL/Features/IArgumentBinderFeature.cs @@ -10,4 +10,6 @@ public interface IArgumentBinderFeature where T : new(); IEnumerable? BindInputObjectList(ResolverContextBase context, string name) where T : new(); + + bool HasArgument(ResolverContextBase context, string name); } \ No newline at end of file diff --git a/src/GraphQL/Fields/ArgumentBinderFeature.cs b/src/GraphQL/Fields/ArgumentBinderFeature.cs index bfe33ede0..d5ebd290a 100644 --- a/src/GraphQL/Fields/ArgumentBinderFeature.cs +++ b/src/GraphQL/Fields/ArgumentBinderFeature.cs @@ -6,6 +6,11 @@ namespace Tanka.GraphQL.Fields; public class ArgumentBinderFeature : IArgumentBinderFeature { + public bool HasArgument(ResolverContextBase context, string name) + { + return context.ArgumentValues.ContainsKey(name); + } + public T? BindInputObject(ResolverContextBase context, string name) where T : new() { @@ -17,7 +22,9 @@ public class ArgumentBinderFeature : IArgumentBinderFeature if (argument is not IReadOnlyDictionary inputObjectArgumentValue) throw new InvalidOperationException("Argument is not an input object"); + var target = new T(); + BindInputObject(inputObjectArgumentValue, target); return target; } @@ -67,6 +74,11 @@ public static void BindInputObject(IReadOnlyDictionary input } } + public T? BindValueArgument(ResolverContextBase context, string name) + { + return context.GetArgument(name); + } + private static string FormatPropertyName(string fieldName) { ArgumentException.ThrowIfNullOrEmpty(fieldName); diff --git a/src/GraphQL/Fields/ArgumentBinderQueryContextExtensions.cs b/src/GraphQL/Fields/ArgumentBinderQueryContextExtensions.cs index 99dfd03ae..9745e33eb 100644 --- a/src/GraphQL/Fields/ArgumentBinderQueryContextExtensions.cs +++ b/src/GraphQL/Fields/ArgumentBinderQueryContextExtensions.cs @@ -20,4 +20,16 @@ public static class ArgumentBinderQueryContextExtensions return queryContext.ArgumentBinder.BindInputObjectList(context, name); } + + + + public static bool HasArgument( + this QueryContext queryContext, + ResolverContextBase context, + string name) + { + ArgumentNullException.ThrowIfNull(queryContext.ArgumentBinder); + + return queryContext.ArgumentBinder.HasArgument(context, name); + } } \ No newline at end of file diff --git a/src/GraphQL/Fields/ArgumentsResolverContextExtensions.cs b/src/GraphQL/Fields/ArgumentsResolverContextExtensions.cs index f27ff6545..a1edde368 100644 --- a/src/GraphQL/Fields/ArgumentsResolverContextExtensions.cs +++ b/src/GraphQL/Fields/ArgumentsResolverContextExtensions.cs @@ -8,7 +8,7 @@ public static class ArgumentsResolverContextExtensions { if (!context.ArgumentValues.TryGetValue(name, out var arg)) throw new ArgumentOutOfRangeException(nameof(name), name, - $"Field '{context.Field.Name}' does not contain argument with name '{name}''"); + $"Field '{context.Field?.Name}' does not contain argument with name '{name}''"); return (T?)arg; } @@ -18,9 +18,17 @@ public static class ArgumentsResolverContextExtensions { return context.QueryContext.BindInputObject(context, name); } + public static IEnumerable? BindInputObjectList(this ResolverContextBase context, string name) where T : new() { return context.QueryContext.BindInputObjectList(context, name); } + + public static bool HasArgument( + this ResolverContextBase context, + string name) + { + return context.QueryContext.HasArgument(context, name); + } } \ No newline at end of file diff --git a/src/GraphQL/QueryContext.cs b/src/GraphQL/QueryContext.cs index 011e153c5..daf86566c 100644 --- a/src/GraphQL/QueryContext.cs +++ b/src/GraphQL/QueryContext.cs @@ -59,7 +59,7 @@ public QueryContext() : this(new FeatureCollection(12)) _features.Fetch(ref _features.Cache.ValueCompletion, _ => null); public IArgumentBinderFeature? ArgumentBinder => - _features.Fetch(ref _features.Cache.ArgumentBinder, _ => null); + _features.Fetch(ref _features.Cache.ArgumentBinder, _ => new ArgumentBinderFeature()); private IFieldExecutorFeature? FieldExecutorFeature => _features.Fetch(ref _features.Cache.FieldExecutor, _ => null); diff --git a/src/GraphQL/ValueResolution/DelegateResolverFactory.cs b/src/GraphQL/ValueResolution/DelegateResolverFactory.cs index 2f0078bde..3342faf7a 100644 --- a/src/GraphQL/ValueResolution/DelegateResolverFactory.cs +++ b/src/GraphQL/ValueResolution/DelegateResolverFactory.cs @@ -15,15 +15,44 @@ public static class DelegateResolverFactory .ToDictionary(p => p.Name.ToLowerInvariant(), p => (Expression)Expression.Property(ContextParam, p)); - private static readonly ConcurrentDictionary Cache = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary Cache = new(); - public static Resolver GetOrCreate(Delegate resolverDelegate) - { - if (Cache.TryGetValue(resolverDelegate, out var resolver)) - return resolver; - return Create(resolverDelegate); - } + private static readonly MethodInfo ThrowMethod = typeof(DelegateResolverFactory) + .GetMethod(nameof(Throw), BindingFlags.NonPublic | BindingFlags.Static)!; + + private static readonly MethodInfo GetArgumentMethod = typeof(ArgumentsResolverContextExtensions) + .GetMethod( + nameof(ArgumentsResolverContextExtensions.GetArgument), + new[] { typeof(ResolverContextBase), typeof(string) } + )!; + + private static readonly MethodInfo BindInputObjectMethod = typeof(ArgumentsResolverContextExtensions) + .GetMethod( + nameof(ArgumentsResolverContextExtensions.BindInputObject), + new[] { typeof(ResolverContextBase), typeof(string) } + )!; + + private static readonly MethodInfo BindInputObjectListMethod = typeof(ArgumentsResolverContextExtensions) + .GetMethod( + nameof(ArgumentsResolverContextExtensions.BindInputObjectList), + new[] { typeof(ResolverContextBase), typeof(string) } + )!; + + private static readonly MethodInfo HasArgumentMethod = typeof(ArgumentsResolverContextExtensions) + .GetMethod( + nameof(ArgumentsResolverContextExtensions.HasArgument), + new[] { typeof(ResolverContextBase), typeof(string) } + )!; + + 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)!; public static Resolver Create(Delegate resolverDelegate) { @@ -52,25 +81,12 @@ public static Resolver Create(Delegate resolverDelegate) 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( + Expression hasArgumentCall = HasArgumentCall(p); + Expression resolveArgumentCall = ResolveArgumentCall(p); + Expression getRequiredServiceCall = GetRequiredServiceCall(p); + return Expression.Condition( + hasArgumentCall, + resolveArgumentCall, getRequiredServiceCall ); }); @@ -103,19 +119,22 @@ public static Resolver Create(Delegate resolverDelegate) else if (invokeMethod.ReturnType.IsGenericType && invokeMethod.ReturnType.GetGenericTypeDefinition() == typeof(Task<>)) { - var t = invokeMethod.ReturnType.GetGenericArguments()[0]; - valueTaskExpression = Expression.Call(ResolveValueTaskMethod.MakeGenericMethod(t), invokeExpression, ContextParam); + Type 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); + Type 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); + Type t = invokeMethod.ReturnType; + valueTaskExpression = Expression.Call(ResolveValueObjectMethod.MakeGenericMethod(t), invokeExpression, + ContextParam); } @@ -124,19 +143,112 @@ public static Resolver Create(Delegate resolverDelegate) ContextParam ); - var compiledLambda = lambda.Compile(); + Resolver compiledLambda = lambda.Compile(); Cache.TryAdd(resolverDelegate, compiledLambda); return compiledLambda; } - private static readonly MethodInfo ResolveValueTaskMethod = typeof(DelegateResolverFactory) - .GetMethod(nameof(ResolveValueTask), BindingFlags.Static | BindingFlags.NonPublic)!; + public static Resolver GetOrCreate(Delegate resolverDelegate) + { + if (Cache.TryGetValue(resolverDelegate, out Resolver? resolver)) + return resolver; - private static readonly MethodInfo ResolveValueValueTaskMethod = typeof(DelegateResolverFactory) - .GetMethod(nameof(ResolveValueValueTask), BindingFlags.Static | BindingFlags.NonPublic)!; + return Create(resolverDelegate); + } - private static readonly MethodInfo ResolveValueObjectMethod = typeof(DelegateResolverFactory) - .GetMethod(nameof(ResolveValueObject), BindingFlags.Static | BindingFlags.NonPublic)!; + private static Expression GetRequiredServiceCall(ParameterInfo p) + { + 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 getRequiredServiceCall; + } + + + private static Expression HasArgumentCall(ParameterInfo p) + { + ArgumentException.ThrowIfNullOrEmpty(p.Name); + ConstantExpression name = Expression.Constant(p.Name, typeof(string)); + return Expression.Call( + null, + HasArgumentMethod, + ContextParam, + name); + } + + private static bool IsPrimitiveOrNullablePrimitive(Type type) + { + if (type.IsPrimitive) + return true; + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) + return type.GetGenericArguments()[0].IsPrimitive; + return false; + } + + private static Expression ResolveArgumentCall(ParameterInfo p) + { + ConstantExpression nameParam = Expression.Constant(p.Name, typeof(string)); + + Expression? callExpr; + if (IsPrimitiveOrNullablePrimitive(p.ParameterType)) + { + MethodInfo methodInfo = GetArgumentMethod.MakeGenericMethod(p.ParameterType); + callExpr = Expression.Call(methodInfo, ContextParam, nameParam); + } + else if (p.ParameterType == typeof(string)) + { + MethodInfo methodInfo = GetArgumentMethod.MakeGenericMethod(p.ParameterType); + callExpr = Expression.Call(methodInfo, ContextParam, nameParam); + } + else if (p.ParameterType.IsGenericType + && p.ParameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>) + && (p.ParameterType.GetGenericArguments()[0].IsClass || + (p.ParameterType.GetGenericArguments()[0].IsValueType && + p.ParameterType.GetGenericArguments()[0].GetConstructor(Type.EmptyTypes) != null))) + { + MethodInfo methodInfo = + BindInputObjectListMethod.MakeGenericMethod(p.ParameterType.GetGenericArguments()[0]); + callExpr = Expression.Call(methodInfo, ContextParam, nameParam); + } + else if (p.ParameterType.IsClass) + { + MethodInfo methodInfo = BindInputObjectMethod.MakeGenericMethod(p.ParameterType); + callExpr = Expression.Call(methodInfo, ContextParam, nameParam); + } + else + { + MethodInfo methodInfo = ThrowMethod.MakeGenericMethod(p.ParameterType); + callExpr = Expression.Call(methodInfo, + Expression.Constant($"Unsupported parameter type '{p.ParameterType}' " + + $"for parameter '{p.Name}'. Only primitive types, classes " + + "and structs with a default constructor are supported.")); + } + + + //callExpr = Expression.Convert(callExpr, p.ParameterType); + + return callExpr; + } + + private static ValueTask ResolveValueObject(T result, ResolverContext context) + { + context.ResolvedValue = result; + return ValueTask.CompletedTask; + } private static ValueTask ResolveValueTask(Task task, ResolverContext context) { @@ -148,7 +260,7 @@ static async ValueTask AwaitResolveValue(Task task, ResolverContext context) if (task.IsCompletedSuccessfully) { context.ResolvedValue = task.Result; - return default; + return default(ValueTask); } return AwaitResolveValue(task, context); @@ -164,15 +276,14 @@ static async ValueTask AwaitResolveValue(ValueTask task, ResolverContext cont if (task.IsCompletedSuccessfully) { context.ResolvedValue = task.Result; - return default; + return default(ValueTask); } return AwaitResolveValue(task, context); } - private static ValueTask ResolveValueObject(T result, ResolverContext context) + private static T? Throw(string message) { - context.ResolvedValue = result; - return ValueTask.CompletedTask; + throw new ArgumentException(message); } } \ No newline at end of file diff --git a/src/GraphQL/ValueResolution/DelegateSubscriberFactory.cs b/src/GraphQL/ValueResolution/DelegateSubscriberFactory.cs index d69aa0475..35d577095 100644 --- a/src/GraphQL/ValueResolution/DelegateSubscriberFactory.cs +++ b/src/GraphQL/ValueResolution/DelegateSubscriberFactory.cs @@ -4,15 +4,18 @@ 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 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) @@ -21,13 +24,36 @@ public static class DelegateSubscriberFactory private static readonly ConcurrentDictionary Cache = new(); - public static Subscriber GetOrCreate(Delegate subscriberDelegate) - { - if (Cache.TryGetValue(subscriberDelegate, out var resolver)) - return resolver; - return Create(subscriberDelegate); - } + private static readonly MethodInfo ThrowMethod = typeof(DelegateResolverFactory) + .GetMethod(nameof(Throw), BindingFlags.NonPublic | BindingFlags.Static)!; + + private static readonly MethodInfo GetArgumentMethod = typeof(ArgumentsResolverContextExtensions) + .GetMethod( + nameof(ArgumentsResolverContextExtensions.GetArgument), + new[] { typeof(ResolverContextBase), typeof(string) } + )!; + + private static readonly MethodInfo BindInputObjectMethod = typeof(ArgumentsResolverContextExtensions) + .GetMethod( + nameof(ArgumentsResolverContextExtensions.BindInputObject), + new[] { typeof(ResolverContextBase), typeof(string) } + )!; + + private static readonly MethodInfo BindInputObjectListMethod = typeof(ArgumentsResolverContextExtensions) + .GetMethod( + nameof(ArgumentsResolverContextExtensions.BindInputObjectList), + new[] { typeof(ResolverContextBase), typeof(string) } + )!; + + private static readonly MethodInfo HasArgumentMethod = typeof(ArgumentsResolverContextExtensions) + .GetMethod( + nameof(ArgumentsResolverContextExtensions.HasArgument), + new[] { typeof(ResolverContextBase), typeof(string) } + )!; + + private static readonly MethodInfo ResolveAsyncEnumerableT = typeof(DelegateSubscriberFactory) + .GetMethod(nameof(ResolveAsyncEnumerable), BindingFlags.Static | BindingFlags.NonPublic)!; public static Subscriber Create(Delegate subscriberDelegate) { @@ -57,25 +83,12 @@ public static Subscriber Create(Delegate subscriberDelegate) 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( + Expression hasArgumentCall = HasArgumentCall(p); + Expression resolveArgumentCall = ResolveArgumentCall(p); + Expression getRequiredServiceCall = GetRequiredServiceCall(p); + return Expression.Condition( + hasArgumentCall, + resolveArgumentCall, getRequiredServiceCall ); }); @@ -108,12 +121,14 @@ public static Subscriber Create(Delegate subscriberDelegate) 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); + Type 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."); + throw new InvalidAsynchronousStateException( + "Subscriber delegate return value must be of type IAsyncEnumerable."); } @@ -123,18 +138,110 @@ public static Subscriber Create(Delegate subscriberDelegate) CancellationTokenParam ); - var compiledLambda = lambda.Compile(); + Subscriber compiledLambda = lambda.Compile(); Cache.TryAdd(subscriberDelegate, compiledLambda); return compiledLambda; } - private static readonly MethodInfo ResolveAsyncEnumerableT = typeof(DelegateSubscriberFactory) - .GetMethod(nameof(ResolveAsyncEnumerable), BindingFlags.Static | BindingFlags.NonPublic)!; + public static Subscriber GetOrCreate(Delegate subscriberDelegate) + { + if (Cache.TryGetValue(subscriberDelegate, out Subscriber? resolver)) + return resolver; + + return Create(subscriberDelegate); + } + + private static Expression GetRequiredServiceCall(ParameterInfo p) + { + 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 getRequiredServiceCall; + } - private static ValueTask ResolveAsyncEnumerable(IAsyncEnumerable task, CancellationToken cancellationToken, SubscriberContext context) + private static Expression HasArgumentCall(ParameterInfo p) { + ArgumentException.ThrowIfNullOrEmpty(p.Name); + ConstantExpression name = Expression.Constant(p.Name, typeof(string)); + return Expression.Call( + null, + HasArgumentMethod, + ContextParam, + name); + } + + private static bool IsPrimitiveOrNullablePrimitive(Type type) + { + if (type.IsPrimitive) + return true; + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) + return type.GetGenericArguments()[0].IsPrimitive; + return false; + } + + private static Expression ResolveArgumentCall(ParameterInfo p) + { + ConstantExpression nameParam = Expression.Constant(p.Name, typeof(string)); + Expression? callExpr; + if (IsPrimitiveOrNullablePrimitive(p.ParameterType)) + { + MethodInfo methodInfo = GetArgumentMethod.MakeGenericMethod(p.ParameterType); + callExpr = Expression.Call(methodInfo, ContextParam, nameParam); + } + else if (p.ParameterType == typeof(string)) + { + MethodInfo methodInfo = GetArgumentMethod.MakeGenericMethod(p.ParameterType); + callExpr = Expression.Call(methodInfo, ContextParam, nameParam); + } + else if (p.ParameterType.IsGenericType + && p.ParameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>) + && (p.ParameterType.GetGenericArguments()[0].IsClass || + (p.ParameterType.GetGenericArguments()[0].IsValueType && + p.ParameterType.GetGenericArguments()[0].GetConstructor(Type.EmptyTypes) != null))) + { + MethodInfo methodInfo = + BindInputObjectListMethod.MakeGenericMethod(p.ParameterType.GetGenericArguments()[0]); + callExpr = Expression.Call(methodInfo, ContextParam, nameParam); + } + else if (p.ParameterType.IsClass) + { + MethodInfo methodInfo = BindInputObjectMethod.MakeGenericMethod(p.ParameterType); + callExpr = Expression.Call(methodInfo, ContextParam, nameParam); + } + else + { + MethodInfo methodInfo = ThrowMethod.MakeGenericMethod(p.ParameterType); + callExpr = Expression.Call(methodInfo, + Expression.Constant($"Unsupported parameter type '{p.ParameterType}' " + + $"for parameter '{p.Name}'. Only primitive types, classes " + + "and structs with a default constructor are supported.")); + } + + + //callExpr = Expression.Convert(callExpr, p.ParameterType); + + return callExpr; + } + + private static ValueTask ResolveAsyncEnumerable(IAsyncEnumerable task, CancellationToken cancellationToken, + SubscriberContext context) + { context.ResolvedValue = Wrap(task, cancellationToken); return ValueTask.CompletedTask; @@ -143,11 +250,12 @@ private static ValueTask ResolveAsyncEnumerable(IAsyncEnumerable task, Can IAsyncEnumerable task, [EnumeratorCancellation] CancellationToken cancellationToken) { - await foreach (var item in task.WithCancellation(cancellationToken)) - { - yield return item; - } + await foreach (T item in task.WithCancellation(cancellationToken)) yield return item; } } + private static T? Throw(string message) + { + throw new ArgumentException(message); + } } \ No newline at end of file diff --git a/tanka-graphql.sln b/tanka-graphql.sln index 2ff1ea284..330b9ddea 100644 --- a/tanka-graphql.sln +++ b/tanka-graphql.sln @@ -54,7 +54,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Samples.Http", "sam EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL", "src\GraphQL\GraphQL.csproj", "{7C6C3C24-E9B2-44F3-8703-8BD43BEA85B7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL.Extensions.Tracing.Tests", "tests\GraphQL.Extensions.Tracing.Tests\GraphQL.Extensions.Tracing.Tests.csproj", "{2C8512FA-4778-478F-B748-0B3BF2F4AA66}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Extensions.Tracing.Tests", "tests\GraphQL.Extensions.Tracing.Tests\GraphQL.Extensions.Tracing.Tests.csproj", "{2C8512FA-4778-478F-B748-0B3BF2F4AA66}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{B9BE1B74-A36A-4A10-9BCE-E5EFDF6D15A8}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -279,7 +281,7 @@ Global {2E65DD3E-7AA0-41D1-A5A8-F20C798FF62F} = {56733FE3-0AB2-491B-9514-AE59DCC94428} {44CA7F79-94E3-4DAD-BB73-39FA98D86902} = {56733FE3-0AB2-491B-9514-AE59DCC94428} {60709A73-4981-4973-8AF7-752B24E425B7} = {56733FE3-0AB2-491B-9514-AE59DCC94428} - {8B3C3296-8AE2-47F0-93B8-9CFF929EBAA4} = {56733FE3-0AB2-491B-9514-AE59DCC94428} + {8B3C3296-8AE2-47F0-93B8-9CFF929EBAA4} = {B9BE1B74-A36A-4A10-9BCE-E5EFDF6D15A8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6EC1BBAB-620C-44FB-A12E-E68F69D689B6} diff --git a/tests/graphql.tests/ValueResolution/DelegateResolverFactoryFacts.cs b/tests/graphql.tests/ValueResolution/DelegateResolverFactoryFacts.cs index 74ec7073c..9ed2f1097 100644 --- a/tests/graphql.tests/ValueResolution/DelegateResolverFactoryFacts.cs +++ b/tests/graphql.tests/ValueResolution/DelegateResolverFactoryFacts.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; +using Tanka.GraphQL.Features; using Tanka.GraphQL.Language.Nodes; using Tanka.GraphQL.Language.Nodes.TypeSystem; using Tanka.GraphQL.ValueResolution; @@ -69,13 +70,13 @@ static async Task AsyncResolver(ResolverContext context, IMyDependency dep1) Field = null, Selection = null, Fields = null, - ArgumentValues = null, + ArgumentValues = new Dictionary(), Path = null, QueryContext = new QueryContext() { RequestServices = new ServiceCollection() .AddSingleton() - .BuildServiceProvider() + .BuildServiceProvider(), } }; @@ -397,6 +398,126 @@ static async Task AsyncResolver() Assert.True((bool)context.ResolvedValue); } + [Theory] + [InlineData(123)] + [InlineData(null)] + public async Task ReturnValue_is_TaskT_with_argument_binding_to_NullableInt(int? value) + { + /* Given */ + static async Task AsyncResolver(int? arg1) + { + await Task.Delay(0); + return arg1; + } + + 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 = new Dictionary() + { + ["arg1"] = value + }, + Path = null, + QueryContext = new QueryContext() + }; + + await resolver(context); + + Assert.Equal(value, context.ResolvedValue); + } + + [Theory] + [InlineData("test test")] + [InlineData(null)] + public async Task ReturnValue_is_TaskT_with_argument_binding_to_string(string? value) + { + /* Given */ + static async Task AsyncResolver(string? arg1) + { + await Task.Delay(0); + return arg1; + } + + 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 = new Dictionary() + { + ["arg1"] = value + }, + Path = null, + QueryContext = new QueryContext() + }; + + await resolver(context); + + Assert.Equal(value, context.ResolvedValue); + } + + [Fact] + public async Task ReturnValue_is_TaskT_with_argument_binding_to_class() + { + /* Given */ + static async Task AsyncResolver(MyInputClass arg1) + { + await Task.Delay(0); + return arg1; + } + + 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 = new Dictionary() + { + ["arg1"] = new Dictionary() + { + [nameof(MyInputClass.DoubleField)] = 123.456, + [nameof(MyInputClass.NullableIntField1)] = null, + } + }, + Path = null, + QueryContext = new QueryContext() + }; + + await resolver(context); + + Assert.Equal(new MyInputClass() + { + DoubleField = 123.456, + NullableIntField1 = null + }, context.ResolvedValue); + } + [Fact] public async Task ReturnValue_is_ValueTaskT() { @@ -463,4 +584,11 @@ static bool Resolver() Assert.NotNull(context.ResolvedValue); Assert.True((bool)context.ResolvedValue); } +} + +public record class MyInputClass +{ + public int? NullableIntField1 { get; set; } + + public double DoubleField { get; set; } } \ No newline at end of file diff --git a/tests/graphql.tests/ValueResolution/DelegateSubscriberFactoryFacts.cs b/tests/graphql.tests/ValueResolution/DelegateSubscriberFactoryFacts.cs index f82f43b60..5f81dfde4 100644 --- a/tests/graphql.tests/ValueResolution/DelegateSubscriberFactoryFacts.cs +++ b/tests/graphql.tests/ValueResolution/DelegateSubscriberFactoryFacts.cs @@ -71,13 +71,13 @@ static async Task AsyncSubscriber(SubscriberContext context, IMyDependency dep1) Field = null, Selection = null, Fields = null, - ArgumentValues = null, + ArgumentValues = new Dictionary(), Path = null, QueryContext = new QueryContext() { RequestServices = new ServiceCollection() .AddSingleton() - .BuildServiceProvider() + .BuildServiceProvider(), } }; @@ -302,6 +302,44 @@ ValueTask AsyncSubscriber() Assert.True(called); } + [Fact] + public async Task ReturnValue_is_ValueTask_with_string_arg1() + { + /* Given */ + string? arg1test = null; + ValueTask AsyncSubscriber(string arg1) + { + arg1test = arg1; + 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 = new Dictionary() + { + ["arg1"] = "test" + }, + Path = null, + QueryContext = new QueryContext() + }; + + await subscriber(context, CancellationToken.None); + + Assert.NotNull(arg1test); + Assert.Equal("test", arg1test); + } + [Fact] public async Task ReturnValue_is_void() { From 82036e29ec3d031b91ea5392ce39349f3b002c52 Mon Sep 17 00:00:00 2001 From: Pekka Heikura Date: Wed, 29 Mar 2023 20:13:19 +0300 Subject: [PATCH 2/6] Clean delegate factories --- .../ValueResolution/DelegateFactoryBase.cs | 205 ++++++++++++++++++ .../DelegateResolverFactory.cs | 193 ++--------------- .../DelegateSubscriberFactory.cs | 200 ++--------------- .../ValueResolution/FieldResolversMap.cs | 4 +- .../ValueResolution/ResolverBuilder.cs | 2 +- .../ValueResolution/SubscriberBuilder.cs | 2 +- .../DelegateResolverFactoryFacts.cs | 32 +-- .../DelegateSubscriberFactoryFacts.cs | 26 +-- 8 files changed, 265 insertions(+), 399 deletions(-) create mode 100644 src/GraphQL/ValueResolution/DelegateFactoryBase.cs diff --git a/src/GraphQL/ValueResolution/DelegateFactoryBase.cs b/src/GraphQL/ValueResolution/DelegateFactoryBase.cs new file mode 100644 index 000000000..e0bcc60a7 --- /dev/null +++ b/src/GraphQL/ValueResolution/DelegateFactoryBase.cs @@ -0,0 +1,205 @@ +using System.Collections.Concurrent; +using System.Linq.Expressions; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; + +namespace Tanka.GraphQL.ValueResolution; + +public abstract class DelegateFactoryBase + where TContext : ResolverContextBase +{ + private readonly ConcurrentDictionary _cache = new(); + + protected readonly MethodInfo BindInputObjectListMethod = typeof(ArgumentsResolverContextExtensions) + .GetMethod( + nameof(ArgumentsResolverContextExtensions.BindInputObjectList), + new[] { typeof(ResolverContextBase), typeof(string) } + )!; + + protected readonly MethodInfo BindInputObjectMethod = typeof(ArgumentsResolverContextExtensions) + .GetMethod( + nameof(ArgumentsResolverContextExtensions.BindInputObject), + new[] { typeof(ResolverContextBase), typeof(string) } + )!; + + public readonly ParameterExpression CancellationTokenParam = + Expression.Parameter(typeof(CancellationToken), "cancellationToken"); + + public readonly ParameterExpression ContextParam = + Expression.Parameter(typeof(TContext), "context"); + + + protected readonly MethodInfo HasArgumentMethod = typeof(ArgumentsResolverContextExtensions) + .GetMethod( + nameof(ArgumentsResolverContextExtensions.HasArgument), + new[] { typeof(ResolverContextBase), typeof(string) } + )!; + + protected readonly MethodInfo ThrowMethod = typeof(DelegateFactoryBase) + .GetMethod(nameof(Throw), BindingFlags.NonPublic | BindingFlags.Static)!; + + protected MethodInfo GetArgumentMethod = typeof(ArgumentsResolverContextExtensions) + .GetMethod( + nameof(ArgumentsResolverContextExtensions.GetArgument), + new[] { typeof(ResolverContextBase), typeof(string) } + )!; + + public abstract TLambda Create(Delegate subscriberDelegate); + + public TLambda GetOrCreate(Delegate delegateFunc) + { + if (_cache.TryGetValue(delegateFunc, out TLambda? resolver)) + return resolver; + + resolver = Create(delegateFunc); + _cache.TryAdd(delegateFunc, resolver); + return resolver; + } + + protected IEnumerable GetArgumentExpressions(MethodInfo invokeMethod) + { + IReadOnlyDictionary contextProperties = GetContextParamProperties(); + return invokeMethod.GetParameters() + .Select(p => + { + if (p.ParameterType == typeof(TContext)) + return ContextParam; + + if (p.Name is not null) + if (contextProperties.TryGetValue(p.Name.ToLowerInvariant(), + out Expression? propertyExpression)) + { + if (p.ParameterType == propertyExpression.Type) + return propertyExpression; + + return Expression.Convert(propertyExpression, p.ParameterType); + } + + Expression hasArgumentCall = HasArgumentCall(p); + Expression resolveArgumentCall = ResolveArgumentCall(p); + Expression getRequiredServiceCall = GetRequiredServiceCall(p); + return Expression.Condition( + hasArgumentCall, + resolveArgumentCall, + getRequiredServiceCall + ); + }); + } + + + protected IReadOnlyDictionary GetContextParamProperties() + { + return typeof(TContext) + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .ToDictionary(p => p.Name.ToLowerInvariant(), p => (Expression)Expression.Property(ContextParam, p)); + } + + protected MethodCallExpression GetDelegateMethodCallExpression(MethodInfo method, object? target) + { + Expression? instanceExpression = null; + + if (!method.IsStatic) instanceExpression = Expression.Constant(target); + + IEnumerable argumentsExpressions = GetArgumentExpressions(method); + MethodCallExpression invokeExpression = Expression.Call( + instanceExpression, + method, + argumentsExpressions + ); + + return invokeExpression; + } + + private Expression GetRequiredServiceCall(ParameterInfo p) + { + 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 getRequiredServiceCall; + } + + + private Expression HasArgumentCall(ParameterInfo p) + { + ArgumentException.ThrowIfNullOrEmpty(p.Name); + ConstantExpression name = Expression.Constant(p.Name, typeof(string)); + return Expression.Call( + null, + HasArgumentMethod, + ContextParam, + name); + } + + private static bool IsPrimitiveOrNullablePrimitive(Type type) + { + if (type.IsPrimitive) + return true; + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) + return type.GetGenericArguments()[0].IsPrimitive; + return false; + } + + private Expression ResolveArgumentCall(ParameterInfo p) + { + ConstantExpression nameParam = Expression.Constant(p.Name, typeof(string)); + + Expression? callExpr; + if (IsPrimitiveOrNullablePrimitive(p.ParameterType)) + { + MethodInfo methodInfo = GetArgumentMethod.MakeGenericMethod(p.ParameterType); + callExpr = Expression.Call(methodInfo, ContextParam, nameParam); + } + else if (p.ParameterType == typeof(string)) + { + MethodInfo methodInfo = GetArgumentMethod.MakeGenericMethod(p.ParameterType); + callExpr = Expression.Call(methodInfo, ContextParam, nameParam); + } + else if (p.ParameterType.IsGenericType + && p.ParameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>) + && (p.ParameterType.GetGenericArguments()[0].IsClass || + (p.ParameterType.GetGenericArguments()[0].IsValueType && + p.ParameterType.GetGenericArguments()[0].GetConstructor(Type.EmptyTypes) != null))) + { + MethodInfo methodInfo = + BindInputObjectListMethod.MakeGenericMethod(p.ParameterType.GetGenericArguments()[0]); + callExpr = Expression.Call(methodInfo, ContextParam, nameParam); + } + else if (p.ParameterType.IsClass) + { + MethodInfo methodInfo = BindInputObjectMethod.MakeGenericMethod(p.ParameterType); + callExpr = Expression.Call(methodInfo, ContextParam, nameParam); + } + else + { + MethodInfo methodInfo = ThrowMethod.MakeGenericMethod(p.ParameterType); + callExpr = Expression.Call(methodInfo, + Expression.Constant($"Unsupported parameter type '{p.ParameterType}' " + + $"for parameter '{p.Name}'. Only primitive types, classes " + + "and structs with a default constructor are supported.")); + } + + + //callExpr = Expression.Convert(callExpr, p.ParameterType); + + return callExpr; + } + + private static T? Throw(string message) + { + throw new ArgumentException(message); + } +} \ No newline at end of file diff --git a/src/GraphQL/ValueResolution/DelegateResolverFactory.cs b/src/GraphQL/ValueResolution/DelegateResolverFactory.cs index 3342faf7a..9942e72d1 100644 --- a/src/GraphQL/ValueResolution/DelegateResolverFactory.cs +++ b/src/GraphQL/ValueResolution/DelegateResolverFactory.cs @@ -1,50 +1,10 @@ -using System.Collections.Concurrent; -using System.Diagnostics; -using System.Linq.Expressions; +using System.Linq.Expressions; using System.Reflection; -using Microsoft.Extensions.DependencyInjection; namespace Tanka.GraphQL.ValueResolution; -public static class DelegateResolverFactory +public class DelegateResolverFactory : DelegateFactoryBase { - 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(); - - - private static readonly MethodInfo ThrowMethod = typeof(DelegateResolverFactory) - .GetMethod(nameof(Throw), BindingFlags.NonPublic | BindingFlags.Static)!; - - private static readonly MethodInfo GetArgumentMethod = typeof(ArgumentsResolverContextExtensions) - .GetMethod( - nameof(ArgumentsResolverContextExtensions.GetArgument), - new[] { typeof(ResolverContextBase), typeof(string) } - )!; - - private static readonly MethodInfo BindInputObjectMethod = typeof(ArgumentsResolverContextExtensions) - .GetMethod( - nameof(ArgumentsResolverContextExtensions.BindInputObject), - new[] { typeof(ResolverContextBase), typeof(string) } - )!; - - private static readonly MethodInfo BindInputObjectListMethod = typeof(ArgumentsResolverContextExtensions) - .GetMethod( - nameof(ArgumentsResolverContextExtensions.BindInputObjectList), - new[] { typeof(ResolverContextBase), typeof(string) } - )!; - - private static readonly MethodInfo HasArgumentMethod = typeof(ArgumentsResolverContextExtensions) - .GetMethod( - nameof(ArgumentsResolverContextExtensions.HasArgument), - new[] { typeof(ResolverContextBase), typeof(string) } - )!; - private static readonly MethodInfo ResolveValueTaskMethod = typeof(DelegateResolverFactory) .GetMethod(nameof(ResolveValueTask), BindingFlags.Static | BindingFlags.NonPublic)!; @@ -53,48 +13,23 @@ public static class DelegateResolverFactory private static readonly MethodInfo ResolveValueObjectMethod = typeof(DelegateResolverFactory) .GetMethod(nameof(ResolveValueObject), BindingFlags.Static | BindingFlags.NonPublic)!; + + private static readonly Lazy InstanceFactory = new(() => new DelegateResolverFactory()); + + public static DelegateResolverFactory Instance => InstanceFactory.Value; - public static Resolver Create(Delegate resolverDelegate) + public static Resolver Get(Delegate resolverDelegate) { -#if DEBUG - Trace.WriteLine( - $"Available context parameters:\n {string.Join(',', ContextParamProperties.Select(p => string.Concat($"{p.Key}: {p.Value.Type}")))}"); -#endif + return Instance.GetOrCreate(resolverDelegate); + } + public override Resolver Create(Delegate resolverDelegate) + { 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); - } - - Expression hasArgumentCall = HasArgumentCall(p); - Expression resolveArgumentCall = ResolveArgumentCall(p); - Expression getRequiredServiceCall = GetRequiredServiceCall(p); - return Expression.Condition( - hasArgumentCall, - resolveArgumentCall, - getRequiredServiceCall - ); - }); - - MethodCallExpression invokeExpression = Expression.Call( - instanceExpression, + MethodCallExpression invokeExpression = GetDelegateMethodCallExpression( invokeMethod, - argumentsExpressions + resolverDelegate.Target ); Expression valueTaskExpression; @@ -144,106 +79,9 @@ public static Resolver Create(Delegate resolverDelegate) ); Resolver compiledLambda = lambda.Compile(); - Cache.TryAdd(resolverDelegate, compiledLambda); return compiledLambda; } - public static Resolver GetOrCreate(Delegate resolverDelegate) - { - if (Cache.TryGetValue(resolverDelegate, out Resolver? resolver)) - return resolver; - - return Create(resolverDelegate); - } - - private static Expression GetRequiredServiceCall(ParameterInfo p) - { - 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 getRequiredServiceCall; - } - - - private static Expression HasArgumentCall(ParameterInfo p) - { - ArgumentException.ThrowIfNullOrEmpty(p.Name); - ConstantExpression name = Expression.Constant(p.Name, typeof(string)); - return Expression.Call( - null, - HasArgumentMethod, - ContextParam, - name); - } - - private static bool IsPrimitiveOrNullablePrimitive(Type type) - { - if (type.IsPrimitive) - return true; - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) - return type.GetGenericArguments()[0].IsPrimitive; - return false; - } - - private static Expression ResolveArgumentCall(ParameterInfo p) - { - ConstantExpression nameParam = Expression.Constant(p.Name, typeof(string)); - - Expression? callExpr; - if (IsPrimitiveOrNullablePrimitive(p.ParameterType)) - { - MethodInfo methodInfo = GetArgumentMethod.MakeGenericMethod(p.ParameterType); - callExpr = Expression.Call(methodInfo, ContextParam, nameParam); - } - else if (p.ParameterType == typeof(string)) - { - MethodInfo methodInfo = GetArgumentMethod.MakeGenericMethod(p.ParameterType); - callExpr = Expression.Call(methodInfo, ContextParam, nameParam); - } - else if (p.ParameterType.IsGenericType - && p.ParameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>) - && (p.ParameterType.GetGenericArguments()[0].IsClass || - (p.ParameterType.GetGenericArguments()[0].IsValueType && - p.ParameterType.GetGenericArguments()[0].GetConstructor(Type.EmptyTypes) != null))) - { - MethodInfo methodInfo = - BindInputObjectListMethod.MakeGenericMethod(p.ParameterType.GetGenericArguments()[0]); - callExpr = Expression.Call(methodInfo, ContextParam, nameParam); - } - else if (p.ParameterType.IsClass) - { - MethodInfo methodInfo = BindInputObjectMethod.MakeGenericMethod(p.ParameterType); - callExpr = Expression.Call(methodInfo, ContextParam, nameParam); - } - else - { - MethodInfo methodInfo = ThrowMethod.MakeGenericMethod(p.ParameterType); - callExpr = Expression.Call(methodInfo, - Expression.Constant($"Unsupported parameter type '{p.ParameterType}' " + - $"for parameter '{p.Name}'. Only primitive types, classes " + - "and structs with a default constructor are supported.")); - } - - - //callExpr = Expression.Convert(callExpr, p.ParameterType); - - return callExpr; - } - private static ValueTask ResolveValueObject(T result, ResolverContext context) { context.ResolvedValue = result; @@ -281,9 +119,4 @@ static async ValueTask AwaitResolveValue(ValueTask task, ResolverContext cont return AwaitResolveValue(task, context); } - - private static T? Throw(string message) - { - throw new ArgumentException(message); - } } \ No newline at end of file diff --git a/src/GraphQL/ValueResolution/DelegateSubscriberFactory.cs b/src/GraphQL/ValueResolution/DelegateSubscriberFactory.cs index 35d577095..273463c39 100644 --- a/src/GraphQL/ValueResolution/DelegateSubscriberFactory.cs +++ b/src/GraphQL/ValueResolution/DelegateSubscriberFactory.cs @@ -1,102 +1,32 @@ -using System.Collections.Concurrent; -using System.ComponentModel; +using System.ComponentModel; using System.Diagnostics; using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; -using Microsoft.Extensions.DependencyInjection; namespace Tanka.GraphQL.ValueResolution; - -public static class DelegateSubscriberFactory +public class DelegateSubscriberFactory : DelegateFactoryBase { - 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(); - - - private static readonly MethodInfo ThrowMethod = typeof(DelegateResolverFactory) - .GetMethod(nameof(Throw), BindingFlags.NonPublic | BindingFlags.Static)!; - - private static readonly MethodInfo GetArgumentMethod = typeof(ArgumentsResolverContextExtensions) - .GetMethod( - nameof(ArgumentsResolverContextExtensions.GetArgument), - new[] { typeof(ResolverContextBase), typeof(string) } - )!; - - private static readonly MethodInfo BindInputObjectMethod = typeof(ArgumentsResolverContextExtensions) - .GetMethod( - nameof(ArgumentsResolverContextExtensions.BindInputObject), - new[] { typeof(ResolverContextBase), typeof(string) } - )!; - - private static readonly MethodInfo BindInputObjectListMethod = typeof(ArgumentsResolverContextExtensions) - .GetMethod( - nameof(ArgumentsResolverContextExtensions.BindInputObjectList), - new[] { typeof(ResolverContextBase), typeof(string) } - )!; - - private static readonly MethodInfo HasArgumentMethod = typeof(ArgumentsResolverContextExtensions) - .GetMethod( - nameof(ArgumentsResolverContextExtensions.HasArgument), - new[] { typeof(ResolverContextBase), typeof(string) } - )!; - private static readonly MethodInfo ResolveAsyncEnumerableT = typeof(DelegateSubscriberFactory) .GetMethod(nameof(ResolveAsyncEnumerable), BindingFlags.Static | BindingFlags.NonPublic)!; - 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; + private static readonly Lazy + InstanceFactory = new(() => new DelegateSubscriberFactory()); - if (p.Name is not null) - if (ContextParamProperties.TryGetValue(p.Name.ToLowerInvariant(), - out Expression? propertyExpression)) - { - if (p.ParameterType == propertyExpression.Type) - return propertyExpression; + public static DelegateSubscriberFactory Instance => InstanceFactory.Value; - return Expression.Convert(propertyExpression, p.ParameterType); - } - - Expression hasArgumentCall = HasArgumentCall(p); - Expression resolveArgumentCall = ResolveArgumentCall(p); - Expression getRequiredServiceCall = GetRequiredServiceCall(p); - return Expression.Condition( - hasArgumentCall, - resolveArgumentCall, - getRequiredServiceCall - ); - }); + public static Subscriber Get(Delegate subscriberDelegate) + { + return Instance.GetOrCreate(subscriberDelegate); + } - MethodCallExpression invokeExpression = Expression.Call( - instanceExpression, + public override Subscriber Create(Delegate subscriberDelegate) + { + MethodInfo invokeMethod = subscriberDelegate.Method; + MethodCallExpression invokeExpression = GetDelegateMethodCallExpression( invokeMethod, - argumentsExpressions + subscriberDelegate.Target ); Expression valueTaskExpression; @@ -139,106 +69,9 @@ public static Subscriber Create(Delegate subscriberDelegate) ); Subscriber compiledLambda = lambda.Compile(); - Cache.TryAdd(subscriberDelegate, compiledLambda); return compiledLambda; } - public static Subscriber GetOrCreate(Delegate subscriberDelegate) - { - if (Cache.TryGetValue(subscriberDelegate, out Subscriber? resolver)) - return resolver; - - return Create(subscriberDelegate); - } - - private static Expression GetRequiredServiceCall(ParameterInfo p) - { - 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 getRequiredServiceCall; - } - - - private static Expression HasArgumentCall(ParameterInfo p) - { - ArgumentException.ThrowIfNullOrEmpty(p.Name); - ConstantExpression name = Expression.Constant(p.Name, typeof(string)); - return Expression.Call( - null, - HasArgumentMethod, - ContextParam, - name); - } - - private static bool IsPrimitiveOrNullablePrimitive(Type type) - { - if (type.IsPrimitive) - return true; - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) - return type.GetGenericArguments()[0].IsPrimitive; - return false; - } - - private static Expression ResolveArgumentCall(ParameterInfo p) - { - ConstantExpression nameParam = Expression.Constant(p.Name, typeof(string)); - - Expression? callExpr; - if (IsPrimitiveOrNullablePrimitive(p.ParameterType)) - { - MethodInfo methodInfo = GetArgumentMethod.MakeGenericMethod(p.ParameterType); - callExpr = Expression.Call(methodInfo, ContextParam, nameParam); - } - else if (p.ParameterType == typeof(string)) - { - MethodInfo methodInfo = GetArgumentMethod.MakeGenericMethod(p.ParameterType); - callExpr = Expression.Call(methodInfo, ContextParam, nameParam); - } - else if (p.ParameterType.IsGenericType - && p.ParameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>) - && (p.ParameterType.GetGenericArguments()[0].IsClass || - (p.ParameterType.GetGenericArguments()[0].IsValueType && - p.ParameterType.GetGenericArguments()[0].GetConstructor(Type.EmptyTypes) != null))) - { - MethodInfo methodInfo = - BindInputObjectListMethod.MakeGenericMethod(p.ParameterType.GetGenericArguments()[0]); - callExpr = Expression.Call(methodInfo, ContextParam, nameParam); - } - else if (p.ParameterType.IsClass) - { - MethodInfo methodInfo = BindInputObjectMethod.MakeGenericMethod(p.ParameterType); - callExpr = Expression.Call(methodInfo, ContextParam, nameParam); - } - else - { - MethodInfo methodInfo = ThrowMethod.MakeGenericMethod(p.ParameterType); - callExpr = Expression.Call(methodInfo, - Expression.Constant($"Unsupported parameter type '{p.ParameterType}' " + - $"for parameter '{p.Name}'. Only primitive types, classes " + - "and structs with a default constructor are supported.")); - } - - - //callExpr = Expression.Convert(callExpr, p.ParameterType); - - return callExpr; - } - private static ValueTask ResolveAsyncEnumerable(IAsyncEnumerable task, CancellationToken cancellationToken, SubscriberContext context) { @@ -253,9 +86,4 @@ private static ValueTask ResolveAsyncEnumerable(IAsyncEnumerable task, Can await foreach (T item in task.WithCancellation(cancellationToken)) yield return item; } } - - private static T? Throw(string message) - { - throw new ArgumentException(message); - } } \ No newline at end of file diff --git a/src/GraphQL/ValueResolution/FieldResolversMap.cs b/src/GraphQL/ValueResolution/FieldResolversMap.cs index dd3515633..94fa2ffc0 100644 --- a/src/GraphQL/ValueResolution/FieldResolversMap.cs +++ b/src/GraphQL/ValueResolution/FieldResolversMap.cs @@ -45,7 +45,7 @@ public void Add(string key, Resolver resolver) public void Add(string key, Delegate resolver) { - _resolvers.Add(key, DelegateResolverFactory.GetOrCreate(resolver)); + _resolvers.Add(key, DelegateResolverFactory.Get(resolver)); } public void Add(string key, Subscriber subscriber) @@ -70,7 +70,7 @@ public void Add(string key, Subscriber subscriber, Delegate resolver) if (resolver == null) throw new ArgumentNullException(nameof(resolver)); _subscribers.Add(key, subscriber); - _resolvers.Add(key, DelegateResolverFactory.GetOrCreate(resolver)); + _resolvers.Add(key, DelegateResolverFactory.Get(resolver)); } public Resolver? GetResolver(string key) diff --git a/src/GraphQL/ValueResolution/ResolverBuilder.cs b/src/GraphQL/ValueResolution/ResolverBuilder.cs index 2adfab501..26955de0e 100644 --- a/src/GraphQL/ValueResolution/ResolverBuilder.cs +++ b/src/GraphQL/ValueResolution/ResolverBuilder.cs @@ -17,7 +17,7 @@ public ResolverBuilder Run(Resolver resolver) public ResolverBuilder Run(Delegate resolver) { - return Use(_ => DelegateResolverFactory.GetOrCreate(resolver)); + return Use(_ => DelegateResolverFactory.Get(resolver)); } public Resolver Build() diff --git a/src/GraphQL/ValueResolution/SubscriberBuilder.cs b/src/GraphQL/ValueResolution/SubscriberBuilder.cs index f9142db55..4c1be24a5 100644 --- a/src/GraphQL/ValueResolution/SubscriberBuilder.cs +++ b/src/GraphQL/ValueResolution/SubscriberBuilder.cs @@ -17,7 +17,7 @@ public SubscriberBuilder Run(Subscriber subscriber) public SubscriberBuilder Run(Delegate subscriber) { - return Use(_ => DelegateSubscriberFactory.GetOrCreate(subscriber)); + return Use(_ => DelegateSubscriberFactory.Get(subscriber)); } public Subscriber Build() diff --git a/tests/graphql.tests/ValueResolution/DelegateResolverFactoryFacts.cs b/tests/graphql.tests/ValueResolution/DelegateResolverFactoryFacts.cs index 9ed2f1097..de1378e2f 100644 --- a/tests/graphql.tests/ValueResolution/DelegateResolverFactoryFacts.cs +++ b/tests/graphql.tests/ValueResolution/DelegateResolverFactoryFacts.cs @@ -26,7 +26,7 @@ static async Task AsyncResolver(ResolverContext context) Delegate resolverDelegate = AsyncResolver; /* When */ - Resolver resolver = DelegateResolverFactory.Create(resolverDelegate); + Resolver resolver = DelegateResolverFactory.Get(resolverDelegate); /* Then */ var context = new ResolverContext @@ -60,7 +60,7 @@ static async Task AsyncResolver(ResolverContext context, IMyDependency dep1) Delegate resolverDelegate = AsyncResolver; /* When */ - Resolver resolver = DelegateResolverFactory.Create(resolverDelegate); + Resolver resolver = DelegateResolverFactory.Get(resolverDelegate); /* Then */ var context = new ResolverContext @@ -99,7 +99,7 @@ static async Task AsyncResolver(ResolverContext context, object? objectValue) Delegate resolverDelegate = AsyncResolver; /* When */ - Resolver resolver = DelegateResolverFactory.Create(resolverDelegate); + Resolver resolver = DelegateResolverFactory.Get(resolverDelegate); /* Then */ var context = new ResolverContext @@ -133,7 +133,7 @@ static async Task AsyncResolver(ResolverContext context, string? objectValue) Delegate resolverDelegate = AsyncResolver; /* When */ - Resolver resolver = DelegateResolverFactory.Create(resolverDelegate); + Resolver resolver = DelegateResolverFactory.Get(resolverDelegate); /* Then */ var context = new ResolverContext @@ -176,7 +176,7 @@ async Task AsyncResolver( Delegate resolverDelegate = AsyncResolver; /* When */ - Resolver resolver = DelegateResolverFactory.Create(resolverDelegate); + Resolver resolver = DelegateResolverFactory.Get(resolverDelegate); /* Then */ var context = new ResolverContext @@ -210,7 +210,7 @@ Task AsyncResolver() Delegate resolverDelegate = AsyncResolver; /* When */ - Resolver resolver = DelegateResolverFactory.Create(resolverDelegate); + Resolver resolver = DelegateResolverFactory.Get(resolverDelegate); /* Then */ var context = new ResolverContext @@ -243,7 +243,7 @@ static async ValueTask AsyncResolver(ResolverContext context) Delegate resolverDelegate = AsyncResolver; /* When */ - Resolver resolver = DelegateResolverFactory.Create(resolverDelegate); + Resolver resolver = DelegateResolverFactory.Get(resolverDelegate); /* Then */ var context = new ResolverContext @@ -278,7 +278,7 @@ ValueTask AsyncResolver() Delegate resolverDelegate = AsyncResolver; /* When */ - Resolver resolver = DelegateResolverFactory.Create(resolverDelegate); + Resolver resolver = DelegateResolverFactory.Get(resolverDelegate); /* Then */ var context = new ResolverContext @@ -310,7 +310,7 @@ static void Resolver(ResolverContext context) Delegate resolverDelegate = Resolver; /* When */ - Resolver resolver = DelegateResolverFactory.Create(resolverDelegate); + Resolver resolver = DelegateResolverFactory.Get(resolverDelegate); /* Then */ var context = new ResolverContext @@ -344,7 +344,7 @@ void Resolver() Delegate resolverDelegate = Resolver; /* When */ - Resolver resolver = DelegateResolverFactory.Create(resolverDelegate); + Resolver resolver = DelegateResolverFactory.Get(resolverDelegate); /* Then */ var context = new ResolverContext @@ -377,7 +377,7 @@ static async Task AsyncResolver() Delegate resolverDelegate = AsyncResolver; /* When */ - Resolver resolver = DelegateResolverFactory.Create(resolverDelegate); + Resolver resolver = DelegateResolverFactory.Get(resolverDelegate); /* Then */ var context = new ResolverContext @@ -413,7 +413,7 @@ public async Task ReturnValue_is_TaskT_with_argument_binding_to_NullableInt(int? Delegate resolverDelegate = AsyncResolver; /* When */ - Resolver resolver = DelegateResolverFactory.Create(resolverDelegate); + Resolver resolver = DelegateResolverFactory.Get(resolverDelegate); /* Then */ var context = new ResolverContext @@ -451,7 +451,7 @@ public async Task ReturnValue_is_TaskT_with_argument_binding_to_string(string? v Delegate resolverDelegate = AsyncResolver; /* When */ - Resolver resolver = DelegateResolverFactory.Create(resolverDelegate); + Resolver resolver = DelegateResolverFactory.Get(resolverDelegate); /* Then */ var context = new ResolverContext @@ -487,7 +487,7 @@ public async Task ReturnValue_is_TaskT_with_argument_binding_to_class() Delegate resolverDelegate = AsyncResolver; /* When */ - Resolver resolver = DelegateResolverFactory.Create(resolverDelegate); + Resolver resolver = DelegateResolverFactory.Get(resolverDelegate); /* Then */ var context = new ResolverContext @@ -531,7 +531,7 @@ static async ValueTask AsyncResolver() Delegate resolverDelegate = AsyncResolver; /* When */ - Resolver resolver = DelegateResolverFactory.Create(resolverDelegate); + Resolver resolver = DelegateResolverFactory.Get(resolverDelegate); /* Then */ var context = new ResolverContext @@ -564,7 +564,7 @@ static bool Resolver() Delegate resolverDelegate = Resolver; /* When */ - Resolver resolver = DelegateResolverFactory.Create(resolverDelegate); + Resolver resolver = DelegateResolverFactory.Get(resolverDelegate); /* Then */ var context = new ResolverContext diff --git a/tests/graphql.tests/ValueResolution/DelegateSubscriberFactoryFacts.cs b/tests/graphql.tests/ValueResolution/DelegateSubscriberFactoryFacts.cs index 5f81dfde4..a5fd44976 100644 --- a/tests/graphql.tests/ValueResolution/DelegateSubscriberFactoryFacts.cs +++ b/tests/graphql.tests/ValueResolution/DelegateSubscriberFactoryFacts.cs @@ -27,7 +27,7 @@ static async Task AsyncSubscriber(SubscriberContext context) Delegate subscriberDelegate = AsyncSubscriber; /* When */ - Subscriber subscriber = DelegateSubscriberFactory.Create(subscriberDelegate); + Subscriber subscriber = DelegateSubscriberFactory.Get(subscriberDelegate); /* Then */ var context = new SubscriberContext @@ -37,9 +37,9 @@ static async Task AsyncSubscriber(SubscriberContext context) Field = null, Selection = null, Fields = null, - ArgumentValues = null, + ArgumentValues = new Dictionary(), Path = null, - QueryContext = null, + QueryContext = new QueryContext(), }; await subscriber(context, CancellationToken.None); @@ -61,7 +61,7 @@ static async Task AsyncSubscriber(SubscriberContext context, IMyDependency dep1) Delegate subscriberDelegate = AsyncSubscriber; /* When */ - Subscriber subscriber = DelegateSubscriberFactory.Create(subscriberDelegate); + Subscriber subscriber = DelegateSubscriberFactory.Get(subscriberDelegate); /* Then */ var context = new SubscriberContext @@ -100,7 +100,7 @@ static async Task AsyncSubscriber(SubscriberContext context, object? objectValue Delegate subscriberDelegate = AsyncSubscriber; /* When */ - Subscriber subscriber = DelegateSubscriberFactory.Create(subscriberDelegate); + Subscriber subscriber = DelegateSubscriberFactory.Get(subscriberDelegate); /* Then */ var context = new SubscriberContext @@ -134,7 +134,7 @@ static async Task AsyncSubscriber(SubscriberContext context, string? objectValue Delegate subscriberDelegate = AsyncSubscriber; /* When */ - Subscriber subscriber = DelegateSubscriberFactory.Create(subscriberDelegate); + Subscriber subscriber = DelegateSubscriberFactory.Get(subscriberDelegate); /* Then */ var context = new SubscriberContext @@ -178,7 +178,7 @@ async Task AsyncSubscriber( Delegate subscriberDelegate = AsyncSubscriber; /* When */ - Subscriber subscriber = DelegateSubscriberFactory.Create(subscriberDelegate); + Subscriber subscriber = DelegateSubscriberFactory.Get(subscriberDelegate); /* Then */ var context = new SubscriberContext @@ -213,7 +213,7 @@ Task AsyncSubscriber() Delegate subscriberDelegate = AsyncSubscriber; /* When */ - Subscriber subscriber = DelegateSubscriberFactory.Create(subscriberDelegate); + Subscriber subscriber = DelegateSubscriberFactory.Get(subscriberDelegate); /* Then */ var context = new SubscriberContext @@ -246,7 +246,7 @@ static async ValueTask AsyncSubscriber(SubscriberContext context) Delegate subscriberDelegate = AsyncSubscriber; /* When */ - Subscriber subscriber = DelegateSubscriberFactory.Create(subscriberDelegate); + Subscriber subscriber = DelegateSubscriberFactory.Get(subscriberDelegate); /* Then */ var context = new SubscriberContext @@ -282,7 +282,7 @@ ValueTask AsyncSubscriber() Delegate subscriberDelegate = AsyncSubscriber; /* When */ - Subscriber subscriber = DelegateSubscriberFactory.Create(subscriberDelegate); + Subscriber subscriber = DelegateSubscriberFactory.Get(subscriberDelegate); /* Then */ var context = new SubscriberContext @@ -316,7 +316,7 @@ ValueTask AsyncSubscriber(string arg1) Delegate subscriberDelegate = AsyncSubscriber; /* When */ - Subscriber subscriber = DelegateSubscriberFactory.Create(subscriberDelegate); + Subscriber subscriber = DelegateSubscriberFactory.Get(subscriberDelegate); /* Then */ var context = new SubscriberContext @@ -352,7 +352,7 @@ static void Subscriber(SubscriberContext context) Delegate subscriberDelegate = Subscriber; /* When */ - Subscriber subscriber = DelegateSubscriberFactory.Create(subscriberDelegate); + Subscriber subscriber = DelegateSubscriberFactory.Get(subscriberDelegate); /* Then */ var context = new SubscriberContext @@ -386,7 +386,7 @@ static async IAsyncEnumerable Subscriber() Delegate subscriberDelegate = Subscriber; /* When */ - Subscriber subscriber = DelegateSubscriberFactory.Create(subscriberDelegate); + Subscriber subscriber = DelegateSubscriberFactory.Get(subscriberDelegate); /* Then */ var context = new SubscriberContext From 86ce51f2179192721a9c6daf10388d9358df0092 Mon Sep 17 00:00:00 2001 From: Pekka Heikura Date: Thu, 30 Mar 2023 19:40:48 +0300 Subject: [PATCH 3/6] Add delegates as middlewares, create sample wip for authorization --- dev/GraphQL.Dev.Reviews/Program.cs | 6 +- dev/graphql.dev.chat.web/Program.cs | 6 +- .../GraphQL.Samples.Authorization.csproj | 15 ++++ .../GraphQL.Samples.Authorization/Program.cs | 59 +++++++++++++ .../Properties/launchSettings.json | 14 +++ .../appsettings.Development.json | 8 ++ .../appsettings.json | 9 ++ .../GraphQL.Samples.Http.csproj | 2 - samples/GraphQL.Samples.Http/Program.cs | 68 ++++++++------- .../Properties/launchSettings.json | 8 -- .../ValueResolution/DelegateFactoryBase.cs | 53 ++++++------ .../DelegateResolverFactory.cs | 10 +-- .../DelegateResolverMiddlewareFactory.cs | 86 +++++++++++++++++++ .../DelegateSubscriberFactory.cs | 14 ++- .../DelegateSubscriberMiddlewareFactory.cs | 85 ++++++++++++++++++ .../ValueResolution/ResolverBuilder.cs | 7 ++ .../ValueResolution/ResolversBuilder.cs | 15 +++- .../ValueResolution/SubscriberBuilder.cs | 7 ++ src/graphql.server/GraphQLApplication.cs | 5 +- .../GraphQLApplicationBuilder.cs | 25 ++---- .../GraphQLApplicationOptions.cs | 4 +- src/graphql.server/SchemaOptions.cs | 2 - src/graphql.server/SchemaOptionsBuilder.cs | 53 ------------ .../WebApplicationBuilderExtensions.cs | 6 +- .../WebApplicationExtensions.cs | 4 +- tanka-graphql.sln | 17 ++++ .../DelegateResolverFactoryFacts.cs | 2 - .../DelegateResolverMiddlewareFactoryFacts.cs | 42 +++++++++ ...elegateSubscriberMiddlewareFactoryFacts.cs | 48 +++++++++++ 29 files changed, 507 insertions(+), 173 deletions(-) create mode 100644 samples/GraphQL.Samples.Authorization/GraphQL.Samples.Authorization.csproj create mode 100644 samples/GraphQL.Samples.Authorization/Program.cs create mode 100644 samples/GraphQL.Samples.Authorization/Properties/launchSettings.json create mode 100644 samples/GraphQL.Samples.Authorization/appsettings.Development.json create mode 100644 samples/GraphQL.Samples.Authorization/appsettings.json create mode 100644 src/GraphQL/ValueResolution/DelegateResolverMiddlewareFactory.cs create mode 100644 src/GraphQL/ValueResolution/DelegateSubscriberMiddlewareFactory.cs delete mode 100644 src/graphql.server/SchemaOptionsBuilder.cs create mode 100644 tests/graphql.tests/ValueResolution/DelegateResolverMiddlewareFactoryFacts.cs create mode 100644 tests/graphql.tests/ValueResolution/DelegateSubscriberMiddlewareFactoryFacts.cs diff --git a/dev/GraphQL.Dev.Reviews/Program.cs b/dev/GraphQL.Dev.Reviews/Program.cs index ccb08a9af..e532cf5be 100644 --- a/dev/GraphQL.Dev.Reviews/Program.cs +++ b/dev/GraphQL.Dev.Reviews/Program.cs @@ -13,8 +13,8 @@ builder.Services.AddSingleton(); // configure services -builder.AddTankaGraphQL3() - .AddOptions("reviews", options => +builder.AddTankaGraphQL() + .AddSchemaOptions("reviews", options => { options.AddReviews(); @@ -35,6 +35,6 @@ app.UseWebSockets(); // this uses the default pipeline -app.MapTankaGraphQL3("/graphql", "reviews"); +app.MapTankaGraphQL("/graphql", "reviews"); app.Run(); \ No newline at end of file diff --git a/dev/graphql.dev.chat.web/Program.cs b/dev/graphql.dev.chat.web/Program.cs index b53287748..48f496511 100644 --- a/dev/graphql.dev.chat.web/Program.cs +++ b/dev/graphql.dev.chat.web/Program.cs @@ -12,8 +12,8 @@ json.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; }); // configure services -builder.AddTankaGraphQL3() - .AddOptions("chat", options => { options.Configure(schema => schema.AddChat()); }) +builder.AddTankaGraphQL() + .AddSchemaOptions("chat", options => { options.Configure(schema => schema.AddChat()); }) .AddHttp() .AddWebSockets(); @@ -25,6 +25,6 @@ app.UseWebSockets(); // this uses the default pipeline -app.MapTankaGraphQL3("/graphql", "chat"); +app.MapTankaGraphQL("/graphql", "chat"); app.Run(); \ No newline at end of file diff --git a/samples/GraphQL.Samples.Authorization/GraphQL.Samples.Authorization.csproj b/samples/GraphQL.Samples.Authorization/GraphQL.Samples.Authorization.csproj new file mode 100644 index 000000000..af026bfed --- /dev/null +++ b/samples/GraphQL.Samples.Authorization/GraphQL.Samples.Authorization.csproj @@ -0,0 +1,15 @@ + + + + net7.0 + enable + enable + + + + + + + + + diff --git a/samples/GraphQL.Samples.Authorization/Program.cs b/samples/GraphQL.Samples.Authorization/Program.cs new file mode 100644 index 000000000..45ba92edb --- /dev/null +++ b/samples/GraphQL.Samples.Authorization/Program.cs @@ -0,0 +1,59 @@ +using System.Runtime.CompilerServices; +using Tanka.GraphQL.Executable; +using Tanka.GraphQL.Server; + +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); + +// add required services +builder.AddTankaGraphQL() + // add http transport + .AddHttp() + // add websocket transport for subscriptions + .AddWebSockets() + // add named schema + .AddSchema("System", schema => + { + // add Query root + schema.Add( + "Query", + new FieldsWithResolvers + { + // Simple string field with hard coded resolved value + { "hello: String!", () => "Hello World"} + }); + + // add Subscription root + schema.Add( + "Subscription", + new FieldsWithResolvers + { + // this will resolve the actual resolved value from the produced values + { "hello: String!", (string objectValue) => objectValue } + }, + new FieldsWithSubscribers + { + // this is our subscription producer + { "hello: String!", (CancellationToken unsubscribe) => + { + return Hello(unsubscribe); + + static async IAsyncEnumerable Hello([EnumeratorCancellation]CancellationToken unsubscribe) + { + yield return "Hello"; + await Task.Delay(TimeSpan.FromSeconds(5), unsubscribe); + yield return "World"; + } + }} + }); + }); + +WebApplication? app = builder.Build(); + +// this is required by the websocket transport +app.UseWebSockets(); + +// this uses the default pipeline +// you can access GraphiQL at "/graphql/ui" +app.MapTankaGraphQL("/graphql", "System"); + +app.Run(); \ No newline at end of file diff --git a/samples/GraphQL.Samples.Authorization/Properties/launchSettings.json b/samples/GraphQL.Samples.Authorization/Properties/launchSettings.json new file mode 100644 index 000000000..1661ccbfa --- /dev/null +++ b/samples/GraphQL.Samples.Authorization/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "profiles": { + "GraphQL.Samples.Http": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "https://localhost:7239/graphql/ui", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:7239", + "dotnetRunMessages": true + } + } +} \ No newline at end of file diff --git a/samples/GraphQL.Samples.Authorization/appsettings.Development.json b/samples/GraphQL.Samples.Authorization/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/samples/GraphQL.Samples.Authorization/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/GraphQL.Samples.Authorization/appsettings.json b/samples/GraphQL.Samples.Authorization/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/samples/GraphQL.Samples.Authorization/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/samples/GraphQL.Samples.Http/GraphQL.Samples.Http.csproj b/samples/GraphQL.Samples.Http/GraphQL.Samples.Http.csproj index c920d00c3..af026bfed 100644 --- a/samples/GraphQL.Samples.Http/GraphQL.Samples.Http.csproj +++ b/samples/GraphQL.Samples.Http/GraphQL.Samples.Http.csproj @@ -4,8 +4,6 @@ net7.0 enable enable - Tanka.$(MSBuildProjectName) - Tanka.$(MSBuildProjectName.Replace(" ", "_")) diff --git a/samples/GraphQL.Samples.Http/Program.cs b/samples/GraphQL.Samples.Http/Program.cs index 785b7431f..f8a84a743 100644 --- a/samples/GraphQL.Samples.Http/Program.cs +++ b/samples/GraphQL.Samples.Http/Program.cs @@ -1,40 +1,50 @@ using System.Runtime.CompilerServices; using Tanka.GraphQL.Executable; -using Tanka.GraphQL.Fields; using Tanka.GraphQL.Server; -using Tanka.GraphQL.ValueResolution; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); -// configure services -builder.AddTankaGraphQL3() +// add required services +builder.AddTankaGraphQL() + // add http transport + .AddHttp() + // add websocket transport for subscriptions + .AddWebSockets() // add named schema .AddSchema("System", schema => { - schema.Add("Query", new FieldsWithResolvers - { - { "system: System!", () => new { } } - }); + // add Query root + schema.Add( + "Query", + new FieldsWithResolvers + { + // We will just return new object as resolved value + { "system: System!", () => new SystemDefinition() } + }); - schema.Add("System", new FieldsWithResolvers - { - { "version: String!", () => "3.0" } - }); + // Add system type with version field of type String! + schema.Add( + "System", + new FieldsWithResolvers + { + // version is resolved from the objectValue (the parent value of type SystemDefinition) + { "version: String!", (SystemDefinition objectValue) => objectValue.Version } + }); - schema.Add("Subscription", new FieldsWithResolvers + // add Subscription root + schema.Add( + "Subscription", + new FieldsWithResolvers { + // this will resolve the actual resolved value from the produced values { "counter: Int!", (int objectValue) => objectValue } - }, + }, new FieldsWithSubscribers { - { - "counter(to: Int!): Int!", (SubscriberContext c, CancellationToken ct) - => Count(c.GetArgument("to"), ct) - } + // this is our subscription producer + { "counter(to: Int!): Int!", Count } }); - }) - .AddHttp() - .AddWebSockets(); + }); WebApplication? app = builder.Build(); @@ -42,18 +52,12 @@ app.UseWebSockets(); // this uses the default pipeline -app.MapTankaGraphQL3("/graphql", "System"); - -// this allows customization of the pipeline -app.MapTankaGraphQL3("/graphql-custom", gql => -{ - gql.SetProperty("TraceEnabled", app.Environment.IsDevelopment()); - gql.UseDefaults("System"); -}); +// you can access GraphiQL at "/graphql/ui" +app.MapTankaGraphQL("/graphql", "System"); app.Run(); -// simple subscriber generating numbers from 0 to the given number +// simple subscription generating numbers from 0 to the given number static async IAsyncEnumerable Count(int to, [EnumeratorCancellation] CancellationToken cancellationToken) { var i = 0; @@ -66,4 +70,6 @@ static async IAsyncEnumerable Count(int to, [EnumeratorCancellation] Cancel await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); } -} \ No newline at end of file +} + +public record SystemDefinition(string Version = "3.0"); \ No newline at end of file diff --git a/samples/GraphQL.Samples.Http/Properties/launchSettings.json b/samples/GraphQL.Samples.Http/Properties/launchSettings.json index ac2a5c3ab..536e0d04f 100644 --- a/samples/GraphQL.Samples.Http/Properties/launchSettings.json +++ b/samples/GraphQL.Samples.Http/Properties/launchSettings.json @@ -1,12 +1,4 @@ { - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:52285", - "sslPort": 44378 - } - }, "profiles": { "GraphQL.Samples.Http": { "commandName": "Project", diff --git a/src/GraphQL/ValueResolution/DelegateFactoryBase.cs b/src/GraphQL/ValueResolution/DelegateFactoryBase.cs index e0bcc60a7..64c92af90 100644 --- a/src/GraphQL/ValueResolution/DelegateFactoryBase.cs +++ b/src/GraphQL/ValueResolution/DelegateFactoryBase.cs @@ -22,9 +22,6 @@ public abstract class DelegateFactoryBase new[] { typeof(ResolverContextBase), typeof(string) } )!; - public readonly ParameterExpression CancellationTokenParam = - Expression.Parameter(typeof(CancellationToken), "cancellationToken"); - public readonly ParameterExpression ContextParam = Expression.Parameter(typeof(TContext), "context"); @@ -56,34 +53,36 @@ public TLambda GetOrCreate(Delegate delegateFunc) return resolver; } - protected IEnumerable GetArgumentExpressions(MethodInfo invokeMethod) + protected virtual IEnumerable GetArgumentExpressions(MethodInfo invokeMethod) { IReadOnlyDictionary contextProperties = GetContextParamProperties(); return invokeMethod.GetParameters() - .Select(p => + .Select(p => GetArgumentExpression(p, contextProperties)); + } + + protected virtual Expression GetArgumentExpression(ParameterInfo p, IReadOnlyDictionary contextProperties) + { + if (p.ParameterType == typeof(TContext)) + return ContextParam; + + if (p.Name is not null) + if (contextProperties.TryGetValue(p.Name.ToLowerInvariant(), + out Expression? propertyExpression)) { - if (p.ParameterType == typeof(TContext)) - return ContextParam; - - if (p.Name is not null) - if (contextProperties.TryGetValue(p.Name.ToLowerInvariant(), - out Expression? propertyExpression)) - { - if (p.ParameterType == propertyExpression.Type) - return propertyExpression; - - return Expression.Convert(propertyExpression, p.ParameterType); - } - - Expression hasArgumentCall = HasArgumentCall(p); - Expression resolveArgumentCall = ResolveArgumentCall(p); - Expression getRequiredServiceCall = GetRequiredServiceCall(p); - return Expression.Condition( - hasArgumentCall, - resolveArgumentCall, - getRequiredServiceCall - ); - }); + if (p.ParameterType == propertyExpression.Type) + return propertyExpression; + + return Expression.Convert(propertyExpression, p.ParameterType); + } + + Expression hasArgumentCall = HasArgumentCall(p); + Expression resolveArgumentCall = ResolveArgumentCall(p); + Expression getRequiredServiceCall = GetRequiredServiceCall(p); + return Expression.Condition( + hasArgumentCall, + resolveArgumentCall, + getRequiredServiceCall + ); } diff --git a/src/GraphQL/ValueResolution/DelegateResolverFactory.cs b/src/GraphQL/ValueResolution/DelegateResolverFactory.cs index 9942e72d1..2da56e89a 100644 --- a/src/GraphQL/ValueResolution/DelegateResolverFactory.cs +++ b/src/GraphQL/ValueResolution/DelegateResolverFactory.cs @@ -5,19 +5,19 @@ namespace Tanka.GraphQL.ValueResolution; public class DelegateResolverFactory : DelegateFactoryBase { - private static readonly MethodInfo ResolveValueTaskMethod = typeof(DelegateResolverFactory) + internal static readonly MethodInfo ResolveValueTaskMethod = typeof(DelegateResolverFactory) .GetMethod(nameof(ResolveValueTask), BindingFlags.Static | BindingFlags.NonPublic)!; - private static readonly MethodInfo ResolveValueValueTaskMethod = typeof(DelegateResolverFactory) + internal static readonly MethodInfo ResolveValueValueTaskMethod = typeof(DelegateResolverFactory) .GetMethod(nameof(ResolveValueValueTask), BindingFlags.Static | BindingFlags.NonPublic)!; - private static readonly MethodInfo ResolveValueObjectMethod = typeof(DelegateResolverFactory) + internal static readonly MethodInfo ResolveValueObjectMethod = typeof(DelegateResolverFactory) .GetMethod(nameof(ResolveValueObject), BindingFlags.Static | BindingFlags.NonPublic)!; private static readonly Lazy InstanceFactory = new(() => new DelegateResolverFactory()); - + public static DelegateResolverFactory Instance => InstanceFactory.Value; - + public static Resolver Get(Delegate resolverDelegate) { return Instance.GetOrCreate(resolverDelegate); diff --git a/src/GraphQL/ValueResolution/DelegateResolverMiddlewareFactory.cs b/src/GraphQL/ValueResolution/DelegateResolverMiddlewareFactory.cs new file mode 100644 index 000000000..f6d4a6c9c --- /dev/null +++ b/src/GraphQL/ValueResolution/DelegateResolverMiddlewareFactory.cs @@ -0,0 +1,86 @@ +using System.Linq.Expressions; +using System.Reflection; + +namespace Tanka.GraphQL.ValueResolution; + +public class DelegateResolverMiddlewareFactory : DelegateFactoryBase> +{ + private static readonly Lazy InstanceFactory = new(() => new DelegateResolverMiddlewareFactory()); + + public static DelegateResolverMiddlewareFactory Instance => InstanceFactory.Value; + + private static readonly ParameterExpression NextParam = Expression.Parameter(typeof(Resolver), "next"); + + public static Func Get(Delegate middlewareDelegate) + { + return Instance.GetOrCreate(middlewareDelegate); + } + + protected override Expression GetArgumentExpression(ParameterInfo p, IReadOnlyDictionary contextProperties) + { + if (p.ParameterType == typeof(Resolver)) + return NextParam; + + return base.GetArgumentExpression(p, contextProperties); + } + + public override Func Create(Delegate resolverDelegate) + { + MethodInfo invokeMethod = resolverDelegate.Method; + + MethodCallExpression invokeExpression = GetDelegateMethodCallExpression( + invokeMethod, + resolverDelegate.Target + ); + + 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<>)) + { + Type t = invokeMethod.ReturnType.GetGenericArguments()[0]; + valueTaskExpression = + Expression.Call(DelegateResolverFactory.ResolveValueTaskMethod.MakeGenericMethod(t), invokeExpression, ContextParam); + } + else if (invokeMethod.ReturnType.IsGenericType && + invokeMethod.ReturnType.GetGenericTypeDefinition() == typeof(ValueTask<>)) + { + Type t = invokeMethod.ReturnType.GetGenericArguments()[0]; + valueTaskExpression = Expression.Call(DelegateResolverFactory.ResolveValueValueTaskMethod.MakeGenericMethod(t), invokeExpression, + ContextParam); + } + else + { + Type t = invokeMethod.ReturnType; + valueTaskExpression = Expression.Call(DelegateResolverFactory.ResolveValueObjectMethod.MakeGenericMethod(t), invokeExpression, + ContextParam); + } + + + var lambda = Expression.Lambda>( + valueTaskExpression, + ContextParam, + NextParam + ); + + var compiledLambda = lambda.Compile(); + return compiledLambda; + } +} \ No newline at end of file diff --git a/src/GraphQL/ValueResolution/DelegateSubscriberFactory.cs b/src/GraphQL/ValueResolution/DelegateSubscriberFactory.cs index 273463c39..a6546d5d4 100644 --- a/src/GraphQL/ValueResolution/DelegateSubscriberFactory.cs +++ b/src/GraphQL/ValueResolution/DelegateSubscriberFactory.cs @@ -8,12 +8,15 @@ namespace Tanka.GraphQL.ValueResolution; public class DelegateSubscriberFactory : DelegateFactoryBase { - private static readonly MethodInfo ResolveAsyncEnumerableT = typeof(DelegateSubscriberFactory) + internal static readonly MethodInfo ResolveAsyncEnumerableT = typeof(DelegateSubscriberFactory) .GetMethod(nameof(ResolveAsyncEnumerable), BindingFlags.Static | BindingFlags.NonPublic)!; private static readonly Lazy InstanceFactory = new(() => new DelegateSubscriberFactory()); + public readonly ParameterExpression CancellationTokenParam = + Expression.Parameter(typeof(CancellationToken), "unsubscribe"); + public static DelegateSubscriberFactory Instance => InstanceFactory.Value; public static Subscriber Get(Delegate subscriberDelegate) @@ -21,6 +24,15 @@ public static Subscriber Get(Delegate subscriberDelegate) return Instance.GetOrCreate(subscriberDelegate); } + protected override Expression GetArgumentExpression(ParameterInfo p, IReadOnlyDictionary contextProperties) + { + if (p.ParameterType == typeof(CancellationToken)) + return CancellationTokenParam; + + return base.GetArgumentExpression(p, contextProperties); + + } + public override Subscriber Create(Delegate subscriberDelegate) { MethodInfo invokeMethod = subscriberDelegate.Method; diff --git a/src/GraphQL/ValueResolution/DelegateSubscriberMiddlewareFactory.cs b/src/GraphQL/ValueResolution/DelegateSubscriberMiddlewareFactory.cs new file mode 100644 index 000000000..e7d98294d --- /dev/null +++ b/src/GraphQL/ValueResolution/DelegateSubscriberMiddlewareFactory.cs @@ -0,0 +1,85 @@ +using System.ComponentModel; +using System.Linq.Expressions; +using System.Reflection; + +namespace Tanka.GraphQL.ValueResolution; + +public class DelegateSubscriberMiddlewareFactory : DelegateFactoryBase> +{ + private static readonly Lazy InstanceFactory = new(() => new DelegateSubscriberMiddlewareFactory()); + + public static DelegateSubscriberMiddlewareFactory Instance => InstanceFactory.Value; + + private static readonly ParameterExpression NextParam = Expression.Parameter(typeof(Subscriber), "next"); + + public readonly ParameterExpression CancellationTokenParam = + Expression.Parameter(typeof(CancellationToken), "unsubscribe"); + + public static Func Get(Delegate middlewareDelegate) + { + return Instance.GetOrCreate(middlewareDelegate); + } + + protected override Expression GetArgumentExpression(ParameterInfo p, IReadOnlyDictionary contextProperties) + { + if (p.ParameterType == typeof(Subscriber)) + return NextParam; + + if (p.ParameterType == typeof(CancellationToken)) + return CancellationTokenParam; + + return base.GetArgumentExpression(p, contextProperties); + } + + public override Func Create(Delegate subscriberDelegate) + { + MethodInfo invokeMethod = subscriberDelegate.Method; + MethodCallExpression invokeExpression = GetDelegateMethodCallExpression( + invokeMethod, + subscriberDelegate.Target + ); + + 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<>)) + { + Type t = invokeMethod.ReturnType.GetGenericArguments()[0]; + valueTaskExpression = Expression.Call(DelegateSubscriberFactory.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, + NextParam, + CancellationTokenParam + ); + + var compiledLambda = lambda.Compile(); + return compiledLambda; + } +} \ No newline at end of file diff --git a/src/GraphQL/ValueResolution/ResolverBuilder.cs b/src/GraphQL/ValueResolution/ResolverBuilder.cs index 26955de0e..0abccf700 100644 --- a/src/GraphQL/ValueResolution/ResolverBuilder.cs +++ b/src/GraphQL/ValueResolution/ResolverBuilder.cs @@ -10,6 +10,13 @@ public ResolverBuilder Use(Func middleware) return this; } + public ResolverBuilder Use(Delegate middleware) + { + var middlewareFunc = DelegateResolverMiddlewareFactory.Get(middleware); + _components.Add(next => context => middlewareFunc(context, next)); + return this; + } + public ResolverBuilder Run(Resolver resolver) { return Use(_ => resolver); diff --git a/src/GraphQL/ValueResolution/ResolversBuilder.cs b/src/GraphQL/ValueResolution/ResolversBuilder.cs index cddbd03c3..899ba17f4 100644 --- a/src/GraphQL/ValueResolution/ResolversBuilder.cs +++ b/src/GraphQL/ValueResolution/ResolversBuilder.cs @@ -1,5 +1,4 @@ using System.Collections; -using Tanka.GraphQL.Executable; using Tanka.GraphQL.Internal; namespace Tanka.GraphQL.ValueResolution; @@ -88,13 +87,21 @@ public SubscriberBuilder Subscriber(string objectName, string fieldName) return builder; } + public ResolversBuilder Subscriber( + string objectName, + string fieldName, + Action configureResolver) + { + configureResolver(Subscriber(objectName, fieldName)); + return this; + } + public SubscriberBuilder Subscriber( string objectName, string fieldName, - Action configureResolver) + Delegate subscriber) { - configureResolver(Resolver(objectName, fieldName)); - return Subscriber(objectName, fieldName); + return Subscriber(objectName, fieldName).Run(subscriber); } public bool HasResolver(string objectName, string fieldName) diff --git a/src/GraphQL/ValueResolution/SubscriberBuilder.cs b/src/GraphQL/ValueResolution/SubscriberBuilder.cs index 4c1be24a5..ede871566 100644 --- a/src/GraphQL/ValueResolution/SubscriberBuilder.cs +++ b/src/GraphQL/ValueResolution/SubscriberBuilder.cs @@ -10,6 +10,13 @@ public SubscriberBuilder Use(Func middleware) return this; } + public SubscriberBuilder Use(Delegate middleware) + { + var middlewareFunc = DelegateSubscriberMiddlewareFactory.Get(middleware); + _components.Add(next => (context, unsubscribe) => middlewareFunc(context, next, unsubscribe)); + return this; + } + public SubscriberBuilder Run(Subscriber subscriber) { return Use(_ => subscriber); diff --git a/src/graphql.server/GraphQLApplication.cs b/src/graphql.server/GraphQLApplication.cs index b3f00e718..29809b39e 100644 --- a/src/graphql.server/GraphQLApplication.cs +++ b/src/graphql.server/GraphQLApplication.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Routing; diff --git a/src/graphql.server/GraphQLApplicationBuilder.cs b/src/graphql.server/GraphQLApplicationBuilder.cs index c3702dc55..f243e93d3 100644 --- a/src/graphql.server/GraphQLApplicationBuilder.cs +++ b/src/graphql.server/GraphQLApplicationBuilder.cs @@ -3,7 +3,6 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using Tanka.GraphQL.Executable; -using Tanka.GraphQL.Validation; namespace Tanka.GraphQL.Server; @@ -28,18 +27,14 @@ public GraphQLApplicationBuilder AddHttp() return this; } - public GraphQLApplicationBuilder AddOptions( + public GraphQLApplicationBuilder AddSchemaOptions( string schemaName, - Action configureOptions) + Action> configureOptions) { OptionsBuilder schemaOptions = ApplicationServices .AddOptions(schemaName); - - var optionsBuilder = new SchemaOptionsBuilder( - schemaOptions, - ApplicationServices); - - configureOptions(optionsBuilder); + + configureOptions(schemaOptions); ApplicationOptionsBuilder.Configure(options => options.SchemaNames.Add(schemaName)); @@ -50,17 +45,7 @@ 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; + return AddSchemaOptions(schemaName, builder => builder.Configure(opt => configureExecutable(opt.Builder))); } public GraphQLApplicationBuilder AddWebSockets() diff --git a/src/graphql.server/GraphQLApplicationOptions.cs b/src/graphql.server/GraphQLApplicationOptions.cs index 1e4d444d5..31513f149 100644 --- a/src/graphql.server/GraphQLApplicationOptions.cs +++ b/src/graphql.server/GraphQLApplicationOptions.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace Tanka.GraphQL.Server; +namespace Tanka.GraphQL.Server; public class GraphQLApplicationOptions { diff --git a/src/graphql.server/SchemaOptions.cs b/src/graphql.server/SchemaOptions.cs index 22275eff3..81dc3b71e 100644 --- a/src/graphql.server/SchemaOptions.cs +++ b/src/graphql.server/SchemaOptions.cs @@ -4,7 +4,5 @@ namespace Tanka.GraphQL.Server; public class SchemaOptions { - public string SchemaName { get; set; } = string.Empty; - public ExecutableSchemaBuilder Builder { get; } = new(); } \ No newline at end of file diff --git a/src/graphql.server/SchemaOptionsBuilder.cs b/src/graphql.server/SchemaOptionsBuilder.cs deleted file mode 100644 index a7bc4dd6b..000000000 --- a/src/graphql.server/SchemaOptionsBuilder.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Tanka.GraphQL.Executable; -using Tanka.GraphQL.Language.Nodes.TypeSystem; - -namespace Tanka.GraphQL.Server; - -public class SchemaOptionsBuilder -{ - public SchemaOptionsBuilder( - OptionsBuilder builder, - IServiceCollection services) - { - Services = services; - Builder = builder; - Builder.Configure(options => options.SchemaName = builder.Name); - } - - public IServiceCollection Services { get; } - - public OptionsBuilder Builder { get; } - - public SchemaOptionsBuilder Configure(Action configureAction) - { - Builder.Configure(options => configureAction(options.Builder)); - return this; - } - - public SchemaOptionsBuilder Configure(Action configureAction) where T : class - { - Builder.Configure((options, dep1) => configureAction(options.Builder, dep1)); - return this; - } - - public SchemaOptionsBuilder PostConfigure(Action configureAction) where T : class - { - Builder.PostConfigure((options, dep1) => configureAction(options.Builder, dep1)); - return this; - } - - public SchemaOptionsBuilder AddConfiguration(IExecutableSchemaConfiguration configuration) - { - Builder.Configure(options => options.Builder.Add(configuration)); - return this; - } - - public SchemaOptionsBuilder AddTypeSystem(TypeSystemDocument document) - { - Builder.Configure(options => options.Builder.Add(document)); - return this; - } -} \ No newline at end of file diff --git a/src/graphql.server/WebApplicationBuilderExtensions.cs b/src/graphql.server/WebApplicationBuilderExtensions.cs index 25bff225f..9a323cbfe 100644 --- a/src/graphql.server/WebApplicationBuilderExtensions.cs +++ b/src/graphql.server/WebApplicationBuilderExtensions.cs @@ -5,12 +5,12 @@ namespace Tanka.GraphQL.Server; public static class WebApplicationBuilderExtensions { - public static GraphQLApplicationBuilder AddTankaGraphQL3(this WebApplicationBuilder builder) + public static GraphQLApplicationBuilder AddTankaGraphQL(this WebApplicationBuilder builder) { - return builder.Services.AddTankaGraphQL3(); + return builder.Services.AddTankaGraphQL(); } - public static GraphQLApplicationBuilder AddTankaGraphQL3(this IServiceCollection services) + public static GraphQLApplicationBuilder AddTankaGraphQL(this IServiceCollection services) { return new GraphQLApplicationBuilder(services); } diff --git a/src/graphql.server/WebApplicationExtensions.cs b/src/graphql.server/WebApplicationExtensions.cs index eacd897d0..5660d72b3 100644 --- a/src/graphql.server/WebApplicationExtensions.cs +++ b/src/graphql.server/WebApplicationExtensions.cs @@ -6,7 +6,7 @@ namespace Tanka.GraphQL.Server; public static class WebApplicationExtensions { - public static IEndpointConventionBuilder MapTankaGraphQL3( + public static IEndpointConventionBuilder MapTankaGraphQL( this IEndpointRouteBuilder webApp, string pattern, string schemaName) @@ -16,7 +16,7 @@ public static IEndpointConventionBuilder MapTankaGraphQL3( return tankaApp.MapDefault(pattern, schemaName, webApp); } - public static IEndpointConventionBuilder MapTankaGraphQL3( + public static IEndpointConventionBuilder MapTankaGraphQL( this IEndpointRouteBuilder webApp, string pattern, Action configurePipeline) diff --git a/tanka-graphql.sln b/tanka-graphql.sln index 330b9ddea..70f01f3f9 100644 --- a/tanka-graphql.sln +++ b/tanka-graphql.sln @@ -58,6 +58,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Extensions.Tracing. EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{B9BE1B74-A36A-4A10-9BCE-E5EFDF6D15A8}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "templates", "templates", "{416523FC-2F5B-4081-BA0C-FCC37E635A29}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Samples.Authorization", "samples\GraphQL.Samples.Authorization\GraphQL.Samples.Authorization.csproj", "{79ECE521-99C5-4BC9-BE15-716EB4939FD1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -272,6 +276,18 @@ Global {2C8512FA-4778-478F-B748-0B3BF2F4AA66}.Release|x64.Build.0 = Release|Any CPU {2C8512FA-4778-478F-B748-0B3BF2F4AA66}.Release|x86.ActiveCfg = Release|Any CPU {2C8512FA-4778-478F-B748-0B3BF2F4AA66}.Release|x86.Build.0 = Release|Any CPU + {79ECE521-99C5-4BC9-BE15-716EB4939FD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {79ECE521-99C5-4BC9-BE15-716EB4939FD1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {79ECE521-99C5-4BC9-BE15-716EB4939FD1}.Debug|x64.ActiveCfg = Debug|Any CPU + {79ECE521-99C5-4BC9-BE15-716EB4939FD1}.Debug|x64.Build.0 = Debug|Any CPU + {79ECE521-99C5-4BC9-BE15-716EB4939FD1}.Debug|x86.ActiveCfg = Debug|Any CPU + {79ECE521-99C5-4BC9-BE15-716EB4939FD1}.Debug|x86.Build.0 = Debug|Any CPU + {79ECE521-99C5-4BC9-BE15-716EB4939FD1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {79ECE521-99C5-4BC9-BE15-716EB4939FD1}.Release|Any CPU.Build.0 = Release|Any CPU + {79ECE521-99C5-4BC9-BE15-716EB4939FD1}.Release|x64.ActiveCfg = Release|Any CPU + {79ECE521-99C5-4BC9-BE15-716EB4939FD1}.Release|x64.Build.0 = Release|Any CPU + {79ECE521-99C5-4BC9-BE15-716EB4939FD1}.Release|x86.ActiveCfg = Release|Any CPU + {79ECE521-99C5-4BC9-BE15-716EB4939FD1}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -282,6 +298,7 @@ Global {44CA7F79-94E3-4DAD-BB73-39FA98D86902} = {56733FE3-0AB2-491B-9514-AE59DCC94428} {60709A73-4981-4973-8AF7-752B24E425B7} = {56733FE3-0AB2-491B-9514-AE59DCC94428} {8B3C3296-8AE2-47F0-93B8-9CFF929EBAA4} = {B9BE1B74-A36A-4A10-9BCE-E5EFDF6D15A8} + {79ECE521-99C5-4BC9-BE15-716EB4939FD1} = {B9BE1B74-A36A-4A10-9BCE-E5EFDF6D15A8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6EC1BBAB-620C-44FB-A12E-E68F69D689B6} diff --git a/tests/graphql.tests/ValueResolution/DelegateResolverFactoryFacts.cs b/tests/graphql.tests/ValueResolution/DelegateResolverFactoryFacts.cs index de1378e2f..890743c86 100644 --- a/tests/graphql.tests/ValueResolution/DelegateResolverFactoryFacts.cs +++ b/tests/graphql.tests/ValueResolution/DelegateResolverFactoryFacts.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; -using Tanka.GraphQL.Features; using Tanka.GraphQL.Language.Nodes; using Tanka.GraphQL.Language.Nodes.TypeSystem; using Tanka.GraphQL.ValueResolution; @@ -12,7 +11,6 @@ namespace Tanka.GraphQL.Tests.ValueResolution; public class DelegateResolverFactoryFacts { - [Fact] public async Task ReturnValue_is_Task() { diff --git a/tests/graphql.tests/ValueResolution/DelegateResolverMiddlewareFactoryFacts.cs b/tests/graphql.tests/ValueResolution/DelegateResolverMiddlewareFactoryFacts.cs new file mode 100644 index 000000000..dad730d6c --- /dev/null +++ b/tests/graphql.tests/ValueResolution/DelegateResolverMiddlewareFactoryFacts.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading.Tasks; +using Tanka.GraphQL.ValueResolution; +using Xunit; + +namespace Tanka.GraphQL.Tests.ValueResolution; + +public class DelegateResolverMiddlewareFactoryFacts +{ + [Fact] + public async Task Middleware_Next() + { + /* Given */ + static async Task Middleware(ResolverContext context, Resolver next) + { + await next(context); + } + + Delegate middlewareDelegate = Middleware; + + /* When */ + var middleware = DelegateResolverMiddlewareFactory.Get(middlewareDelegate); + + /* Then */ + var context = new ResolverContext + { + ObjectDefinition = null, + ObjectValue = null, + Field = null, + Selection = null, + Fields = null, + ArgumentValues = null, + Path = null, + QueryContext = null + }; + + await middleware(context, r => r.ResolveAs(true)); + + Assert.NotNull(context.ResolvedValue); + Assert.True((bool)context.ResolvedValue); + } +} \ No newline at end of file diff --git a/tests/graphql.tests/ValueResolution/DelegateSubscriberMiddlewareFactoryFacts.cs b/tests/graphql.tests/ValueResolution/DelegateSubscriberMiddlewareFactoryFacts.cs new file mode 100644 index 000000000..492264bf3 --- /dev/null +++ b/tests/graphql.tests/ValueResolution/DelegateSubscriberMiddlewareFactoryFacts.cs @@ -0,0 +1,48 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Tanka.GraphQL.ValueResolution; +using Xunit; + +namespace Tanka.GraphQL.Tests.ValueResolution; + +public class DelegateSubscriberMiddlewareFactoryFacts +{ + [Fact] + public async Task Middleware_Next() + { + /* Given */ + static async Task Middleware(SubscriberContext context, Subscriber next, CancellationToken unsubscribe) + { + await next(context, unsubscribe); + } + + Delegate middlewareDelegate = Middleware; + + /* When */ + var middleware = DelegateSubscriberMiddlewareFactory.Get(middlewareDelegate); + + /* Then */ + var context = new SubscriberContext() + { + ObjectDefinition = null, + ObjectValue = null, + Field = null, + Selection = null, + Fields = null, + ArgumentValues = null, + Path = null, + QueryContext = null + }; + + await middleware(context, (r, _) => + { + r.ResolvedValue = AsyncEnumerable.Repeat(true, 1); + return default; + }, CancellationToken.None); + + Assert.NotNull(context.ResolvedValue); + Assert.True((bool?)await context.ResolvedValue.SingleAsync(CancellationToken.None)); + } +} \ No newline at end of file From dcae5fcb5073298deff2d39e601012e8fc2b09e4 Mon Sep 17 00:00:00 2001 From: Pekka Heikura Date: Fri, 31 Mar 2023 09:29:40 +0300 Subject: [PATCH 4/6] Add authorization sample --- .../GraphQL.Samples.Authorization/Program.cs | 49 +++++++++++++++---- .../Properties/launchSettings.json | 2 +- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/samples/GraphQL.Samples.Authorization/Program.cs b/samples/GraphQL.Samples.Authorization/Program.cs index 45ba92edb..f3ffde26b 100644 --- a/samples/GraphQL.Samples.Authorization/Program.cs +++ b/samples/GraphQL.Samples.Authorization/Program.cs @@ -1,4 +1,7 @@ using System.Runtime.CompilerServices; +using System.Security.Claims; +using System.Security.Principal; +using Microsoft.AspNetCore.Authentication; using Tanka.GraphQL.Executable; using Tanka.GraphQL.Server; @@ -19,7 +22,7 @@ new FieldsWithResolvers { // Simple string field with hard coded resolved value - { "hello: String!", () => "Hello World"} + { "hello: String!", () => "Hello World" } }); // add Subscription root @@ -33,27 +36,53 @@ new FieldsWithSubscribers { // this is our subscription producer - { "hello: String!", (CancellationToken unsubscribe) => { - return Hello(unsubscribe); - - static async IAsyncEnumerable Hello([EnumeratorCancellation]CancellationToken unsubscribe) + "hello: String!", (CancellationToken unsubscribe) => { - yield return "Hello"; - await Task.Delay(TimeSpan.FromSeconds(5), unsubscribe); - yield return "World"; + return Hello(unsubscribe); + + static async IAsyncEnumerable Hello( + [EnumeratorCancellation] CancellationToken unsubscribe) + { + yield return "Hello"; + await Task.Delay(TimeSpan.FromSeconds(1), unsubscribe); + yield return "World"; + } } - }} + } }); }); +// add cookie authentication +builder.Services.AddAuthentication().AddCookie(options => +{ + options.LoginPath = "/login"; + options.LogoutPath = "/logout"; + options.AccessDeniedPath = "/login"; +}); + +builder.Services.AddAuthorization(); + WebApplication? app = builder.Build(); +app.UseAuthentication(); +app.UseAuthorization(); // this is required by the websocket transport app.UseWebSockets(); // this uses the default pipeline // you can access GraphiQL at "/graphql/ui" -app.MapTankaGraphQL("/graphql", "System"); +app.MapTankaGraphQL("/graphql", "System") + // we require a user name User + .RequireAuthorization(policy => policy.RequireUserName("User")); + +// map login (required by the cookie authentication) +app.MapGet("/login", async (HttpContext http, string returnUrl) => +{ + // login as hardcoded user + await http.SignInAsync(new ClaimsPrincipal(new GenericIdentity("User"))); + return Results.Redirect(returnUrl); +}); + app.Run(); \ No newline at end of file diff --git a/samples/GraphQL.Samples.Authorization/Properties/launchSettings.json b/samples/GraphQL.Samples.Authorization/Properties/launchSettings.json index 1661ccbfa..20439c4f7 100644 --- a/samples/GraphQL.Samples.Authorization/Properties/launchSettings.json +++ b/samples/GraphQL.Samples.Authorization/Properties/launchSettings.json @@ -1,6 +1,6 @@ { "profiles": { - "GraphQL.Samples.Http": { + "GraphQL.Samples.Authorization": { "commandName": "Project", "launchBrowser": true, "launchUrl": "https://localhost:7239/graphql/ui", From 3d270f613529661dc1e9308da9b2000eba7a85f0 Mon Sep 17 00:00:00 2001 From: Pekka Heikura Date: Sun, 2 Apr 2023 20:17:14 +0300 Subject: [PATCH 5/6] Add server docs wip --- docs/0-getting-started/04-server.md | 2 +- docs/2-language/02-executable-doc.md | 10 +++++++++- docs/2-language/03-type-system-doc.md | 9 ++++++++- docs/2-language/tree.md | 3 --- docs/3-server/0-index.md | 1 - docs/3-server/00-index.md | 14 ++++++++++++++ docs/3-server/05-features/00-list.md | 4 ++++ docs/3-server/05-features/01-http.md | 11 +++++++++++ docs/3-server/05-features/02-authorization.md | 9 +++++++++ docs/3-server/05-features/nav.md | 3 +++ .../5-query-cost-analysis.md | 0 .../{5-extensions => 10-extensions}/nav.md | 0 docs/3-server/nav.md | 2 +- docs/3-server/tanka-docs-section.yml | 4 ++-- 14 files changed, 62 insertions(+), 10 deletions(-) delete mode 100644 docs/2-language/tree.md delete mode 100644 docs/3-server/0-index.md create mode 100644 docs/3-server/00-index.md create mode 100644 docs/3-server/05-features/00-list.md create mode 100644 docs/3-server/05-features/01-http.md create mode 100644 docs/3-server/05-features/02-authorization.md create mode 100644 docs/3-server/05-features/nav.md rename docs/3-server/{5-extensions => 10-extensions}/5-query-cost-analysis.md (100%) rename docs/3-server/{5-extensions => 10-extensions}/nav.md (100%) diff --git a/docs/0-getting-started/04-server.md b/docs/0-getting-started/04-server.md index 513b2b99e..f4f31f685 100644 --- a/docs/0-getting-started/04-server.md +++ b/docs/0-getting-started/04-server.md @@ -6,7 +6,7 @@ required then you can implement your own server. > See also > -> - [Server](xref://server:0-index.md) +> - [Server](xref://server:00-index.md) ### Simple example with HTTP and websockets diff --git a/docs/2-language/02-executable-doc.md b/docs/2-language/02-executable-doc.md index f997b80bc..ab1d3a84b 100644 --- a/docs/2-language/02-executable-doc.md +++ b/docs/2-language/02-executable-doc.md @@ -1,3 +1,11 @@ ## Executable Document -TODO: +Tanka GraphQL splits the GraphQL document into two parts: executable document and type system document. +Executable document is the part of the document that is executed by the GraphQL server. Type system document +is the part of the document that is used to define the GraphQL schema. + +Here's the definition of executable document: + +```csharp +#include::xref://src:graphql.language/Nodes/ExecutableDocument.cs +``` \ No newline at end of file diff --git a/docs/2-language/03-type-system-doc.md b/docs/2-language/03-type-system-doc.md index f997b80bc..e8123aa53 100644 --- a/docs/2-language/03-type-system-doc.md +++ b/docs/2-language/03-type-system-doc.md @@ -1,3 +1,10 @@ ## Executable Document -TODO: +Type System Document is used to define the GraphQL schema. It is used by the +GraphQL server to validate the query and to resolve the query. + +Here's the definition of type system document: + +```csharp +#include::xref://src:graphql.language/Nodes/TypeSystem/TypeSystemDocument.cs +``` \ No newline at end of file diff --git a/docs/2-language/tree.md b/docs/2-language/tree.md deleted file mode 100644 index a674c2e6a..000000000 --- a/docs/2-language/tree.md +++ /dev/null @@ -1,3 +0,0 @@ -## Syntax Tree - -TODO: diff --git a/docs/3-server/0-index.md b/docs/3-server/0-index.md deleted file mode 100644 index 06b7f3547..000000000 --- a/docs/3-server/0-index.md +++ /dev/null @@ -1 +0,0 @@ -## Server \ No newline at end of file diff --git a/docs/3-server/00-index.md b/docs/3-server/00-index.md new file mode 100644 index 000000000..69e36192f --- /dev/null +++ b/docs/3-server/00-index.md @@ -0,0 +1,14 @@ +## Tanka GraphQL Server + +Server provides a way to host GraphQL schema and execute queries against it. It runs on top +of ASP.NET Core and has transports for HTTP and WebSockets. + +### Installation + +Server is available as a NuGet package: + +```csharp +dotnet add package tanka.graphql.server +``` + +See features on the side for more information. \ No newline at end of file diff --git a/docs/3-server/05-features/00-list.md b/docs/3-server/05-features/00-list.md new file mode 100644 index 000000000..7a9f04074 --- /dev/null +++ b/docs/3-server/05-features/00-list.md @@ -0,0 +1,4 @@ +## Features + +Features are show by a sample projects. Samples shows how to use the feature with minimal code. +Full source code of each sample is available at [GitHub](https://github.com/pekkah/tanka-graphql). \ No newline at end of file diff --git a/docs/3-server/05-features/01-http.md b/docs/3-server/05-features/01-http.md new file mode 100644 index 000000000..7b144d363 --- /dev/null +++ b/docs/3-server/05-features/01-http.md @@ -0,0 +1,11 @@ +## Http and WebSockets + +This sample provides a basic HTTP and WebSockets server with GraphQL schema and query execution. +It supports both GET and POST requests and uses WebSockets for subscriptions. + +WebSockets transport uses the newer [graphql-ws](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md) protocol + + +```csharp +#include::xref://samples:GraphQL.Samples.Http/Program.cs +``` \ No newline at end of file diff --git a/docs/3-server/05-features/02-authorization.md b/docs/3-server/05-features/02-authorization.md new file mode 100644 index 000000000..a110503d1 --- /dev/null +++ b/docs/3-server/05-features/02-authorization.md @@ -0,0 +1,9 @@ +## Authorization + +This sample supports both websockets and HTTP requests. It uses a simple cookie authentication +to authenticate the user and then applies a authorization policy to the GraphQL endpoint. + + +```csharp +#include::xref://samples:GraphQL.Samples.Authorization/Program.cs +``` \ No newline at end of file diff --git a/docs/3-server/05-features/nav.md b/docs/3-server/05-features/nav.md new file mode 100644 index 000000000..c9719527e --- /dev/null +++ b/docs/3-server/05-features/nav.md @@ -0,0 +1,3 @@ +- [Features](xref://05-features/00-list.md) + - [Http and WebSockets](xref://05-features/01-http.md) + - [Authorization](xref://05-features/02-authorization.md) \ No newline at end of file diff --git a/docs/3-server/5-extensions/5-query-cost-analysis.md b/docs/3-server/10-extensions/5-query-cost-analysis.md similarity index 100% rename from docs/3-server/5-extensions/5-query-cost-analysis.md rename to docs/3-server/10-extensions/5-query-cost-analysis.md diff --git a/docs/3-server/5-extensions/nav.md b/docs/3-server/10-extensions/nav.md similarity index 100% rename from docs/3-server/5-extensions/nav.md rename to docs/3-server/10-extensions/nav.md diff --git a/docs/3-server/nav.md b/docs/3-server/nav.md index 2e535304c..ef38c8931 100644 --- a/docs/3-server/nav.md +++ b/docs/3-server/nav.md @@ -1 +1 @@ -- [Introduction](xref://0-index.md) \ No newline at end of file +- [Introduction](xref://00-index.md) \ No newline at end of file diff --git a/docs/3-server/tanka-docs-section.yml b/docs/3-server/tanka-docs-section.yml index f3ab183e9..bb7620717 100644 --- a/docs/3-server/tanka-docs-section.yml +++ b/docs/3-server/tanka-docs-section.yml @@ -1,6 +1,6 @@ id: server title: "Server" -index_page: xref://server:0-index.md +index_page: xref://server:00-index.md nav: - xref://nav.md - - xref://5-extensions/nav.md + - xref://05-features/nav.md From 04c47f3efa35508aca99339b60ecf1dc538d89a8 Mon Sep 17 00:00:00 2001 From: Pekka Heikura Date: Sun, 2 Apr 2023 20:23:32 +0300 Subject: [PATCH 6/6] Fix some code --- dev/GraphQL.Dev.Reviews/Program.cs | 6 ++++-- .../SchemaOptionsBuilderExtensions.cs | 15 +++++++++------ dev/graphql.dev.chat.web/Program.cs | 2 +- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/dev/GraphQL.Dev.Reviews/Program.cs b/dev/GraphQL.Dev.Reviews/Program.cs index e532cf5be..c6c6cb1a5 100644 --- a/dev/GraphQL.Dev.Reviews/Program.cs +++ b/dev/GraphQL.Dev.Reviews/Program.cs @@ -1,8 +1,8 @@ using GraphQL.Dev.Reviews; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; +using Tanka.GraphQL.Dev.Reviews; using Tanka.GraphQL.Extensions.ApolloFederation; -using Tanka.GraphQL.Language; using Tanka.GraphQL.Server; @@ -19,8 +19,10 @@ options.AddReviews(); // add federation as last step - options.Configure((schema, referenceResolvers) => + options.Configure((options, referenceResolvers) => { + var schema = options.Builder; + // federation should be added as last step so // that all entity types are correctly detected schema.AddSubgraph(new(referenceResolvers)); diff --git a/dev/GraphQL.Dev.Reviews/SchemaOptionsBuilderExtensions.cs b/dev/GraphQL.Dev.Reviews/SchemaOptionsBuilderExtensions.cs index 1883007f5..317ecfc69 100644 --- a/dev/GraphQL.Dev.Reviews/SchemaOptionsBuilderExtensions.cs +++ b/dev/GraphQL.Dev.Reviews/SchemaOptionsBuilderExtensions.cs @@ -1,14 +1,17 @@ +using GraphQL.Dev.Reviews; +using Microsoft.Extensions.Options; using Tanka.GraphQL.Server; -namespace GraphQL.Dev.Reviews; +namespace Tanka.GraphQL.Dev.Reviews; public static class SchemaOptionsBuilderExtensions { - public static SchemaOptionsBuilder AddReviews(this SchemaOptionsBuilder options) + public static OptionsBuilder AddReviews(this OptionsBuilder optionsBuilder) { - options.Configure((schema, resolvers) => + optionsBuilder.Configure((options, resolvers) => { - schema.Add(""" + var builder = options.Builder; + builder.Add(""" type Review @key(fields: "id") { id: ID! body: String @@ -28,9 +31,9 @@ type Product @key(fields: "upc") @extends { } """); - schema.Add(resolvers); + builder.Add(resolvers); }); - return options; + return optionsBuilder; } } \ No newline at end of file diff --git a/dev/graphql.dev.chat.web/Program.cs b/dev/graphql.dev.chat.web/Program.cs index 48f496511..1c17f79d4 100644 --- a/dev/graphql.dev.chat.web/Program.cs +++ b/dev/graphql.dev.chat.web/Program.cs @@ -13,7 +13,7 @@ }); // configure services builder.AddTankaGraphQL() - .AddSchemaOptions("chat", options => { options.Configure(schema => schema.AddChat()); }) + .AddSchema("chat", schema => schema.AddChat()) .AddHttp() .AddWebSockets();