diff --git a/global.json b/global.json
index 4d051c4aaca..5c393dc1ff6 100644
--- a/global.json
+++ b/global.json
@@ -1,6 +1,6 @@
{
"sdk": {
- "version": "7.0.100-preview.6.22352.1",
+ "version": "7.0.100-preview.7.22377.5",
"rollForward": "latestMinor"
}
}
\ No newline at end of file
diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/DefaultHttpResultSerializer.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/DefaultHttpResultSerializer.cs
index a0c3157b893..f9394335bb5 100644
--- a/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/DefaultHttpResultSerializer.cs
+++ b/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/DefaultHttpResultSerializer.cs
@@ -9,7 +9,7 @@ namespace HotChocolate.AspNetCore.Serialization;
public class DefaultHttpResultSerializer : IHttpResultSerializer
{
- private readonly JsonQueryResultFormatter _jsonFormatter;
+ private readonly IQueryResultFormatter _jsonFormatter;
private readonly string _deferContentType;
private readonly IResponseStreamFormatter _deferFormatter;
@@ -69,6 +69,41 @@ public class DefaultHttpResultSerializer : IHttpResultSerializer
}
}
+ ///
+ /// Creates a new instance of .
+ ///
+ protected DefaultHttpResultSerializer(
+ IQueryResultFormatter jsonFormatter,
+ HttpResultSerialization batchSerialization = HttpResultSerialization.MultiPartChunked,
+ HttpResultSerialization deferSerialization = HttpResultSerialization.MultiPartChunked)
+ {
+ _jsonFormatter = jsonFormatter;
+ var jsonArrayFormatter = new JsonArrayResponseStreamFormatter(_jsonFormatter);
+ var multiPartFormatter = new MultiPartResponseStreamFormatter(_jsonFormatter);
+
+ if (deferSerialization is HttpResultSerialization.JsonArray)
+ {
+ _deferContentType = ContentType.Json;
+ _deferFormatter = jsonArrayFormatter;
+ }
+ else
+ {
+ _deferContentType = ContentType.MultiPart;
+ _deferFormatter = multiPartFormatter;
+ }
+
+ if (batchSerialization is HttpResultSerialization.JsonArray)
+ {
+ _batchContentType = ContentType.Json;
+ _batchFormatter = jsonArrayFormatter;
+ }
+ else
+ {
+ _batchContentType = ContentType.MultiPart;
+ _batchFormatter = multiPartFormatter;
+ }
+ }
+
public virtual string GetContentType(IExecutionResult result)
{
if (result is null)
diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/Apollo/ApolloSubscriptionProtocolHandler.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/Apollo/ApolloSubscriptionProtocolHandler.cs
index 95a3a55ac47..0ea238c63d6 100644
--- a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/Apollo/ApolloSubscriptionProtocolHandler.cs
+++ b/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/Apollo/ApolloSubscriptionProtocolHandler.cs
@@ -225,7 +225,7 @@ public ApolloSubscriptionProtocolHandler(ISocketSessionInterceptor interceptor)
jsonWriter.WriteString(Id, operationSessionId);
jsonWriter.WriteString(MessageProperties.Type, Utf8Messages.Data);
jsonWriter.WritePropertyName(Payload);
- _formatter.Serialize(result, jsonWriter);
+ _formatter.Format(result, jsonWriter);
jsonWriter.WriteEndObject();
await jsonWriter.FlushAsync(cancellationToken);
await session.Connection.SendAsync(arrayWriter.Body, cancellationToken);
@@ -243,7 +243,7 @@ public ApolloSubscriptionProtocolHandler(ISocketSessionInterceptor interceptor)
jsonWriter.WriteString(Id, operationSessionId);
jsonWriter.WriteString(MessageProperties.Type, Utf8Messages.Error);
jsonWriter.WritePropertyName(Payload);
- _formatter.Serialize(errors[0], jsonWriter);
+ _formatter.Format(errors[0], jsonWriter);
jsonWriter.WriteEndObject();
await jsonWriter.FlushAsync(cancellationToken);
await session.Connection.SendAsync(arrayWriter.Body, cancellationToken);
diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/GraphQLOverWebSocketProtocolHandler.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/GraphQLOverWebSocketProtocolHandler.cs
index 66d6aebb9de..3a4c4309d05 100644
--- a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/GraphQLOverWebSocketProtocolHandler.cs
+++ b/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/GraphQLOverWebSocketProtocolHandler.cs
@@ -204,7 +204,7 @@ public GraphQLOverWebSocketProtocolHandler(ISocketSessionInterceptor interceptor
jsonWriter.WriteString(Id, operationSessionId);
jsonWriter.WriteString(MessageProperties.Type, Utf8Messages.Next);
jsonWriter.WritePropertyName(Payload);
- _formatter.Serialize(result, jsonWriter);
+ _formatter.Format(result, jsonWriter);
jsonWriter.WriteEndObject();
await jsonWriter.FlushAsync(cancellationToken);
await session.Connection.SendAsync(arrayWriter.Body, cancellationToken);
@@ -222,7 +222,7 @@ public GraphQLOverWebSocketProtocolHandler(ISocketSessionInterceptor interceptor
jsonWriter.WriteString(Id, operationSessionId);
jsonWriter.WriteString(MessageProperties.Type, Utf8Messages.Error);
jsonWriter.WritePropertyName(Payload);
- _formatter.Serialize(errors, jsonWriter);
+ _formatter.Format(errors, jsonWriter);
jsonWriter.WriteEndObject();
await jsonWriter.FlushAsync(cancellationToken);
await session.Connection.SendAsync(arrayWriter.Body, cancellationToken);
diff --git a/src/HotChocolate/AspNetCore/test/Directory.Build.props b/src/HotChocolate/AspNetCore/test/Directory.Build.props
index 92438c4b95d..f1f3b7fc9ce 100644
--- a/src/HotChocolate/AspNetCore/test/Directory.Build.props
+++ b/src/HotChocolate/AspNetCore/test/Directory.Build.props
@@ -21,7 +21,7 @@
-
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
diff --git a/src/HotChocolate/Core/src/Execution/Extensions/ExecutionResultExtensions.cs b/src/HotChocolate/Core/src/Execution/Extensions/ExecutionResultExtensions.cs
index d6ca876b3a1..06fc56ef400 100644
--- a/src/HotChocolate/Core/src/Execution/Extensions/ExecutionResultExtensions.cs
+++ b/src/HotChocolate/Core/src/Execution/Extensions/ExecutionResultExtensions.cs
@@ -49,8 +49,8 @@ public static class ExecutionResultExtensions
if (result is IQueryResult queryResult)
{
return withIndentations
- ? _formatterIndented.Serialize(queryResult)
- : _formatter.Serialize(queryResult);
+ ? _formatterIndented.Format(queryResult)
+ : _formatter.Format(queryResult);
}
throw new NotSupportedException(ExecutionResultExtensions_OnlyQueryResults);
diff --git a/src/HotChocolate/Core/src/Execution/Serialization/JsonArrayResponseStreamFormatter.cs b/src/HotChocolate/Core/src/Execution/Serialization/JsonArrayResponseStreamFormatter.cs
index b2a073263a4..c3562ebee59 100644
--- a/src/HotChocolate/Core/src/Execution/Serialization/JsonArrayResponseStreamFormatter.cs
+++ b/src/HotChocolate/Core/src/Execution/Serialization/JsonArrayResponseStreamFormatter.cs
@@ -12,7 +12,7 @@ public sealed class JsonArrayResponseStreamFormatter : IResponseStreamFormatter
private const byte _leftBracket = (byte)'[';
private const byte _rightBracket = (byte)']';
private const byte _comma = (byte)',';
- private readonly JsonQueryResultFormatter _formatter;
+ private readonly IQueryResultFormatter _formatter;
///
/// Creates a new instance of .
@@ -44,7 +44,7 @@ public sealed class JsonArrayResponseStreamFormatter : IResponseStreamFormatter
/// is null.
///
public JsonArrayResponseStreamFormatter(
- JsonQueryResultFormatter formatter)
+ IQueryResultFormatter formatter)
{
_formatter = formatter ??
throw new ArgumentNullException(nameof(formatter));
diff --git a/src/HotChocolate/Core/src/Execution/Serialization/JsonQueryResultFormatter.cs b/src/HotChocolate/Core/src/Execution/Serialization/JsonQueryResultFormatter.cs
index 90ff09a24a6..35f15acbc95 100644
--- a/src/HotChocolate/Core/src/Execution/Serialization/JsonQueryResultFormatter.cs
+++ b/src/HotChocolate/Core/src/Execution/Serialization/JsonQueryResultFormatter.cs
@@ -37,7 +37,7 @@ public JsonQueryResultFormatter(bool indented = false, JavaScriptEncoder? encode
_options = new JsonWriterOptions { Indented = indented, Encoder = encoder };
}
- public unsafe string Serialize(IQueryResult result)
+ public unsafe string Format(IQueryResult result)
{
if (result is null)
{
@@ -54,7 +54,7 @@ public unsafe string Serialize(IQueryResult result)
}
}
- public void Serialize(IQueryResult result, Utf8JsonWriter writer)
+ public void Format(IQueryResult result, Utf8JsonWriter writer)
{
if (result is null)
{
@@ -69,7 +69,7 @@ public void Serialize(IQueryResult result, Utf8JsonWriter writer)
WriteResult(writer, result);
}
- public void Serialize(IError error, Utf8JsonWriter writer)
+ public void Format(IError error, Utf8JsonWriter writer)
{
if (error is null)
{
@@ -84,7 +84,7 @@ public void Serialize(IError error, Utf8JsonWriter writer)
WriteError(writer, error);
}
- public void Serialize(IReadOnlyList errors, Utf8JsonWriter writer)
+ public void Format(IReadOnlyList errors, Utf8JsonWriter writer)
{
if (errors is null)
{
@@ -359,7 +359,7 @@ private static void WritePathValue(Utf8JsonWriter writer, Path path)
ref var searchSpace = ref objectResult.GetReference();
- for(var i = 0; i < objectResult.Capacity; i++)
+ for (var i = 0; i < objectResult.Capacity; i++)
{
var field = Unsafe.Add(ref searchSpace, i);
if (field.IsInitialized)
@@ -403,6 +403,76 @@ private static void WritePathValue(Utf8JsonWriter writer, Path path)
writer.WriteEndArray();
}
+#if NET5_0_OR_GREATER
+ private void WriteJsonElement(
+ Utf8JsonWriter writer,
+ JsonElement element)
+ {
+ switch (element.ValueKind)
+ {
+ case JsonValueKind.Object:
+ WriteJsonObject(writer, element);
+ break;
+
+ case JsonValueKind.Array:
+ WriteJsonArray(writer, element);
+ break;
+
+ case JsonValueKind.String:
+ writer.WriteStringValue(element.GetString());
+ break;
+
+ case JsonValueKind.Number:
+ writer.WriteRawValue(element.GetRawText());
+ break;
+
+ case JsonValueKind.True:
+ writer.WriteBooleanValue(true);
+ break;
+
+ case JsonValueKind.False:
+ writer.WriteBooleanValue(false);
+ break;
+
+ case JsonValueKind.Null:
+ writer.WriteNullValue();
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ private void WriteJsonObject(
+ Utf8JsonWriter writer,
+ JsonElement element)
+ {
+ writer.WriteStartObject();
+
+ foreach (var item in element.EnumerateObject())
+ {
+ writer.WritePropertyName(item.Name);
+ WriteJsonElement(writer, item.Value);
+ }
+
+ writer.WriteEndObject();
+ }
+
+ private void WriteJsonArray(
+ Utf8JsonWriter writer,
+ JsonElement element)
+ {
+ writer.WriteStartArray();
+
+ foreach (var item in element.EnumerateArray())
+ {
+ WriteJsonElement(writer, item);
+ }
+
+ writer.WriteEndArray();
+ }
+
+#endif
private void WriteFieldValue(
Utf8JsonWriter writer,
object? value)
@@ -423,6 +493,11 @@ private static void WritePathValue(Utf8JsonWriter writer, Path path)
WriteListResult(writer, resultMapList);
break;
+#if NET5_0_OR_GREATER
+ case JsonElement element:
+ WriteJsonElement(writer, element);
+ break;
+#endif
case Dictionary dict:
WriteDictionary(writer, dict);
break;
diff --git a/src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj b/src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj
index 53080602b2b..d8643115285 100644
--- a/src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj
+++ b/src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj
@@ -38,7 +38,7 @@
-
+
diff --git a/src/HotChocolate/Data/src/EntityFramework/HotChocolate.Data.EntityFramework.csproj b/src/HotChocolate/Data/src/EntityFramework/HotChocolate.Data.EntityFramework.csproj
index 6054899ad5f..d94e9c735cf 100644
--- a/src/HotChocolate/Data/src/EntityFramework/HotChocolate.Data.EntityFramework.csproj
+++ b/src/HotChocolate/Data/src/EntityFramework/HotChocolate.Data.EntityFramework.csproj
@@ -17,7 +17,7 @@
-
+
diff --git a/src/HotChocolate/Data/test/Data.AutoMapper.Tests/HotChocolate.Data.AutoMapper.Tests.csproj b/src/HotChocolate/Data/test/Data.AutoMapper.Tests/HotChocolate.Data.AutoMapper.Tests.csproj
index da562dc4853..e7298086e1b 100644
--- a/src/HotChocolate/Data/test/Data.AutoMapper.Tests/HotChocolate.Data.AutoMapper.Tests.csproj
+++ b/src/HotChocolate/Data/test/Data.AutoMapper.Tests/HotChocolate.Data.AutoMapper.Tests.csproj
@@ -22,10 +22,10 @@
-
-
-
-
+
+
+
+
diff --git a/src/HotChocolate/Data/test/Data.EntityFramework.Tests/HotChocolate.Data.EntityFramework.Tests.csproj b/src/HotChocolate/Data/test/Data.EntityFramework.Tests/HotChocolate.Data.EntityFramework.Tests.csproj
index ce2c36a17c9..d0d5fcf20e5 100644
--- a/src/HotChocolate/Data/test/Data.EntityFramework.Tests/HotChocolate.Data.EntityFramework.Tests.csproj
+++ b/src/HotChocolate/Data/test/Data.EntityFramework.Tests/HotChocolate.Data.EntityFramework.Tests.csproj
@@ -15,8 +15,8 @@
-
-
+
+
diff --git a/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/HotChocolate.Data.Filters.SqlServer.Tests.csproj b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/HotChocolate.Data.Filters.SqlServer.Tests.csproj
index ba112b9c049..3444f7bbba1 100644
--- a/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/HotChocolate.Data.Filters.SqlServer.Tests.csproj
+++ b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/HotChocolate.Data.Filters.SqlServer.Tests.csproj
@@ -16,9 +16,9 @@
-
-
-
+
+
+
diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/HotChocolate.Data.Projections.SqlServer.Tests.csproj b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/HotChocolate.Data.Projections.SqlServer.Tests.csproj
index aacd0c98719..d08666fb6b7 100644
--- a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/HotChocolate.Data.Projections.SqlServer.Tests.csproj
+++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/HotChocolate.Data.Projections.SqlServer.Tests.csproj
@@ -17,10 +17,10 @@
-
-
-
-
+
+
+
+
diff --git a/src/HotChocolate/Data/test/Data.Sorting.SqlLite.Tests/HotChocolate.Data.Sorting.SqlLite.Tests.csproj b/src/HotChocolate/Data/test/Data.Sorting.SqlLite.Tests/HotChocolate.Data.Sorting.SqlLite.Tests.csproj
index bad1c7f13ab..f1b29a6e14f 100644
--- a/src/HotChocolate/Data/test/Data.Sorting.SqlLite.Tests/HotChocolate.Data.Sorting.SqlLite.Tests.csproj
+++ b/src/HotChocolate/Data/test/Data.Sorting.SqlLite.Tests/HotChocolate.Data.Sorting.SqlLite.Tests.csproj
@@ -16,9 +16,9 @@
-
-
-
+
+
+
diff --git a/src/HotChocolate/Fusion/src/Core/Clients/GraphQLHttpClient.cs b/src/HotChocolate/Fusion/src/Core/Clients/GraphQLHttpClient.cs
index 25f8a373632..762b6a64a6a 100644
--- a/src/HotChocolate/Fusion/src/Core/Clients/GraphQLHttpClient.cs
+++ b/src/HotChocolate/Fusion/src/Core/Clients/GraphQLHttpClient.cs
@@ -7,6 +7,9 @@
namespace HotChocolate.Fusion.Clients;
+// note: should the GraphQL client handle the capabilities?
+// meaning the execution engine should just use batching and
+// all and the client decides to batch if batching is available?
public sealed class GraphQLHttpClient : IGraphQLClient
{
private readonly IHttpClientFactory _httpClientFactory;
@@ -18,6 +21,7 @@ public GraphQLHttpClient(string schemaName, IHttpClientFactory httpClientFactory
SchemaName = schemaName;
}
+ // TODO: naming? SubGraphName?
public string SchemaName { get; }
public async Task ExecuteAsync(GraphQLRequest request, CancellationToken cancellationToken)
diff --git a/src/HotChocolate/Fusion/src/Core/DependencyInjection/RequestExecutorBuilderExtensions.cs b/src/HotChocolate/Fusion/src/Core/DependencyInjection/RequestExecutorBuilderExtensions.cs
index 8358d3edcbb..c76fcd90ac8 100644
--- a/src/HotChocolate/Fusion/src/Core/DependencyInjection/RequestExecutorBuilderExtensions.cs
+++ b/src/HotChocolate/Fusion/src/Core/DependencyInjection/RequestExecutorBuilderExtensions.cs
@@ -4,6 +4,7 @@
using HotChocolate.Fusion.Metadata;
using HotChocolate.Fusion.Pipeline;
using HotChocolate.Fusion.Planning;
+using HotChocolate.Language;
using Microsoft.Extensions.DependencyInjection.Extensions;
// ReSharper disable once CheckNamespace
@@ -11,15 +12,53 @@ namespace Microsoft.Extensions.DependencyInjection;
public static class RequestExecutorBuilderExtensions
{
- public static IRequestExecutorBuilder AddGraphQLGateway(
- this IRequestExecutorBuilder builder,
- string serviceConfig,
- string sdl)
+ public static IRequestExecutorBuilder AddFusionGatewayServer(
+ this IServiceCollection services,
+ string serviceConfiguration)
{
- var configuration = ServiceConfiguration.Load(serviceConfig);
+ if (services is null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
- return builder
- .AddDocumentFromString(sdl)
+ if (string.IsNullOrEmpty(serviceConfiguration))
+ {
+ throw new ArgumentNullException(nameof(serviceConfiguration));
+ }
+
+ var serviceConfDoc = Utf8GraphQLParser.Parse(serviceConfiguration);
+ return AddFusionGatewayServer(services, serviceConfDoc);
+ }
+
+ public static IRequestExecutorBuilder AddFusionGatewayServer(
+ this IServiceCollection services,
+ DocumentNode serviceConfiguration)
+ {
+ if (services is null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+
+ if (serviceConfiguration is null)
+ {
+ throw new ArgumentNullException(nameof(serviceConfiguration));
+ }
+
+ var configuration = ServiceConfiguration.Load(serviceConfiguration);
+ var context = ConfigurationDirectiveNamesContext.From(serviceConfiguration);
+ var rewriter = new ServiceConfigurationToSchemaRewriter();
+ var schemaDoc = (DocumentNode?)rewriter.Rewrite(serviceConfiguration, context);
+
+ if (schemaDoc is null)
+ {
+ // todo : exception.
+ throw new InvalidOperationException(
+ "A valid service configuration must always produce a schema document.");
+ }
+
+ return services
+ .AddGraphQLServer()
+ .AddDocument(schemaDoc)
.UseField(next => next)
.UseDefaultGatewayPipeline()
.ConfigureSchemaServices(
@@ -42,7 +81,7 @@ public static class RequestExecutorBuilderExtensions
});
}
- public static IRequestExecutorBuilder UseDefaultGatewayPipeline(
+ private static IRequestExecutorBuilder UseDefaultGatewayPipeline(
this IRequestExecutorBuilder builder)
{
return builder
diff --git a/src/HotChocolate/Fusion/src/Core/HotChocolate.Fusion.csproj b/src/HotChocolate/Fusion/src/Core/HotChocolate.Fusion.csproj
index 0db03610dd6..e0250468a41 100644
--- a/src/HotChocolate/Fusion/src/Core/HotChocolate.Fusion.csproj
+++ b/src/HotChocolate/Fusion/src/Core/HotChocolate.Fusion.csproj
@@ -8,12 +8,19 @@
+
-
-
+
+
+
+
+
+
+
+
diff --git a/src/HotChocolate/Fusion/src/Core/Metadata/ConfigurationDirectiveNames.cs b/src/HotChocolate/Fusion/src/Core/Metadata/ConfigurationDirectiveNames.cs
new file mode 100644
index 00000000000..19d0be36614
--- /dev/null
+++ b/src/HotChocolate/Fusion/src/Core/Metadata/ConfigurationDirectiveNames.cs
@@ -0,0 +1,20 @@
+using static HotChocolate.Fusion.Metadata.ConfigurationDirectiveNames;
+
+namespace HotChocolate.Fusion.Metadata;
+
+internal static class ConfigurationDirectiveNames
+{
+ public const string VariableDirective = "variable";
+ public const string FetchDirective = "fetch";
+ public const string BindDirective = "bind";
+ public const string HttpDirective = "httpClient";
+ public const string FusionDirective = "fusion";
+ public const string NameArg = "name";
+ public const string SelectArg = "select";
+ public const string TypeArg = "type";
+ public const string FromArg = "from";
+ public const string ToArg = "to";
+ public const string AsArg = "as";
+ public const string ArgumentArg = "argument";
+ public const string BaseAddressArg = "baseAddress";
+}
diff --git a/src/HotChocolate/Fusion/src/Core/Metadata/ConfigurationDirectiveNamesContext.cs b/src/HotChocolate/Fusion/src/Core/Metadata/ConfigurationDirectiveNamesContext.cs
new file mode 100644
index 00000000000..e7f137d89db
--- /dev/null
+++ b/src/HotChocolate/Fusion/src/Core/Metadata/ConfigurationDirectiveNamesContext.cs
@@ -0,0 +1,151 @@
+using System.Diagnostics.CodeAnalysis;
+using HotChocolate.Language;
+using HotChocolate.Language.Visitors;
+using HotChocolate.Utilities;
+
+namespace HotChocolate.Fusion.Metadata;
+
+internal class ConfigurationDirectiveNamesContext : ISyntaxVisitorContext
+{
+ private ConfigurationDirectiveNamesContext(
+ string variableDirective,
+ string fetchDirective,
+ string bindDirective,
+ string httpDirective,
+ string fusionDirective)
+ {
+ VariableDirective = variableDirective;
+ FetchDirective = fetchDirective;
+ BindDirective = bindDirective;
+ HttpDirective = httpDirective;
+ FusionDirective = fusionDirective;
+ }
+
+ public string VariableDirective { get; }
+ public string FetchDirective { get; }
+ public string BindDirective { get; }
+ public string HttpDirective { get; }
+ public string FusionDirective { get; }
+
+ public bool IsConfigurationDirective(string name)
+ => VariableDirective.EqualsOrdinal(name) ||
+ FetchDirective.EqualsOrdinal(name) ||
+ BindDirective.EqualsOrdinal(name) ||
+ HttpDirective.EqualsOrdinal(name) ||
+ FusionDirective.EqualsOrdinal(name);
+
+ public static ConfigurationDirectiveNamesContext Create(
+ string? prefix = null,
+ bool prefixSelf = false)
+ {
+ if (prefix is not null)
+ {
+ return new ConfigurationDirectiveNamesContext(
+ $"{prefix}_{ConfigurationDirectiveNames.VariableDirective}",
+ $"{prefix}_{ConfigurationDirectiveNames.FetchDirective}",
+ $"{prefix}_{ConfigurationDirectiveNames.BindDirective}",
+ $"{prefix}_{ConfigurationDirectiveNames.HttpDirective}",
+ prefixSelf
+ ? $"{prefix}_{ConfigurationDirectiveNames.FusionDirective}"
+ : ConfigurationDirectiveNames.FusionDirective);
+ }
+
+ return new ConfigurationDirectiveNamesContext(
+ ConfigurationDirectiveNames.VariableDirective,
+ ConfigurationDirectiveNames.FetchDirective,
+ ConfigurationDirectiveNames.BindDirective,
+ ConfigurationDirectiveNames.HttpDirective,
+ ConfigurationDirectiveNames.FusionDirective);
+ }
+
+ public static ConfigurationDirectiveNamesContext From(DocumentNode document)
+ {
+ if (document is null)
+ {
+ throw new ArgumentNullException(nameof(document));
+ }
+
+ var schemaDef = document.Definitions.OfType().FirstOrDefault();
+
+ if (schemaDef is null)
+ {
+ // todo : exception
+ throw new ArgumentException(
+ "The provided document must at least contain a schema definition.",
+ nameof(document));
+ }
+
+ if (TryGetPrefix(schemaDef.Directives, out var prefixSelf, out var prefix))
+ {
+ return new ConfigurationDirectiveNamesContext(
+ $"{prefix}_{ConfigurationDirectiveNames.VariableDirective}",
+ $"{prefix}_{ConfigurationDirectiveNames.FetchDirective}",
+ $"{prefix}_{ConfigurationDirectiveNames.BindDirective}",
+ $"{prefix}_{ConfigurationDirectiveNames.HttpDirective}",
+ prefixSelf
+ ? $"{prefix}_{ConfigurationDirectiveNames.FusionDirective}"
+ : ConfigurationDirectiveNames.FusionDirective);
+ }
+
+ return new ConfigurationDirectiveNamesContext(
+ ConfigurationDirectiveNames.VariableDirective,
+ ConfigurationDirectiveNames.FetchDirective,
+ ConfigurationDirectiveNames.BindDirective,
+ ConfigurationDirectiveNames.HttpDirective,
+ ConfigurationDirectiveNames.FusionDirective);
+ }
+
+ private static bool TryGetPrefix(
+ IReadOnlyList schemaDirectives,
+ out bool prefixSelf,
+ [NotNullWhen(true)] out string? prefix)
+ {
+ const string prefixedFusionDir = "_" + ConfigurationDirectiveNames.FusionDirective;
+
+ foreach (var directive in schemaDirectives)
+ {
+ if (directive.Name.Value.EndsWith(prefixedFusionDir, StringComparison.Ordinal))
+ {
+ var prefixSelfArg =
+ directive.Arguments.FirstOrDefault(
+ t => t.Name.Value.EqualsOrdinal("prefixSelf"));
+
+ if (prefixSelfArg?.Value is BooleanValueNode { Value: true })
+ {
+ var prefixArg =
+ directive.Arguments.FirstOrDefault(
+ t => t.Name.Value.EqualsOrdinal("prefix"));
+
+ if (prefixArg?.Value is StringValueNode prefixVal &&
+ directive.Name.Value.EqualsOrdinal($"{prefixVal.Value}{prefixedFusionDir}"))
+ {
+ prefixSelf = true;
+ prefix = prefixVal.Value;
+ return true;
+ }
+ }
+ }
+ }
+
+ foreach (var directive in schemaDirectives)
+ {
+ if (directive.Name.Value.EqualsOrdinal(ConfigurationDirectiveNames.FusionDirective))
+ {
+ var prefixArg =
+ directive.Arguments.FirstOrDefault(
+ t => t.Name.Value.EqualsOrdinal("prefix"));
+
+ if (prefixArg?.Value is StringValueNode prefixVal)
+ {
+ prefixSelf = false;
+ prefix = prefixVal.Value;
+ return true;
+ }
+ }
+ }
+
+ prefixSelf = false;
+ prefix = null;
+ return false;
+ }
+}
diff --git a/src/HotChocolate/Fusion/src/Core/Metadata/ServiceConfiguration.cs b/src/HotChocolate/Fusion/src/Core/Metadata/ServiceConfiguration.cs
index 8dfe0ae3664..37ee01a344d 100644
--- a/src/HotChocolate/Fusion/src/Core/Metadata/ServiceConfiguration.cs
+++ b/src/HotChocolate/Fusion/src/Core/Metadata/ServiceConfiguration.cs
@@ -1,8 +1,4 @@
using HotChocolate.Language;
-using HotChocolate.Language.Visitors;
-using HotChocolate.Utilities;
-using static HotChocolate.Fusion.Metadata.FusionDirectiveNames;
-using static HotChocolate.Language.Utf8GraphQLParser.Syntax;
namespace HotChocolate.Fusion.Metadata;
@@ -18,6 +14,7 @@ public ServiceConfiguration(IEnumerable bindings, IEnumerable typ
_types = types.ToDictionary(t => t.Name, StringComparer.Ordinal);
}
+ // todo: Should be named SchemaNames or maybe SubGraphNames?
public IReadOnlyList Bindings => _bindings;
public T GetType(string typeName) where T : IType
@@ -66,410 +63,10 @@ public string GetTypeName(TypeInfo typeInfo)
}
public static ServiceConfiguration Load(string sourceText)
- => new SchemaReader().Read(sourceText);
-}
-
-internal sealed class SchemaReader
-{
- private readonly HashSet _assert = new();
-
- public ServiceConfiguration Read(string sourceText)
- => ReadServiceDefinition(Utf8GraphQLParser.Parse(sourceText));
-
- private ServiceConfiguration ReadServiceDefinition(DocumentNode documentNode)
- {
- var types = new List();
- IReadOnlyList? httpClientConfigs = null;
-
- foreach (var definition in documentNode.Definitions)
- {
- switch (definition)
- {
- case ObjectTypeDefinitionNode node:
- types.Add(ReadObjectType(node));
- break;
-
- case SchemaDefinitionNode node:
- httpClientConfigs = ReadHttpClientConfigs(node.Directives);
- break;
- }
- }
-
- if (httpClientConfigs is not { Count: > 0 })
- {
- // TODO : EXCEPTION
- throw new Exception("No clients configured");
- }
-
- if (types.Count == 0)
- {
- // TODO : EXCEPTION
- throw new Exception("No types");
- }
-
-
- return new ServiceConfiguration(
- httpClientConfigs.Select(t => t.SchemaName),
- types);
- }
-
- private ObjectType ReadObjectType(ObjectTypeDefinitionNode typeDef)
- {
- var variables = ReadFieldVariableDefinitions(typeDef.Directives);
- var resolvers = ReadFetchDefinitions(typeDef.Directives);
- var fields = ReadObjectFields(typeDef.Fields);
- return new ObjectType(typeDef.Name.Value, variables, resolvers, fields);
- }
-
- private ObjectFieldCollection ReadObjectFields(
- IReadOnlyList fieldDefinitionNodes)
- {
- var collection = new List();
-
- foreach (var fieldDef in fieldDefinitionNodes)
- {
- var resolvers = ReadFetchDefinitions(fieldDef.Directives);
- var bindings = ReadMemberBindings(fieldDef.Directives, fieldDef, resolvers);
- var variables = ReadArgumentVariableDefinitions(fieldDef.Directives, fieldDef);
- var field = new ObjectField(fieldDef.Name.Value, bindings, variables, resolvers);
- collection.Add(field);
- }
-
- return new ObjectFieldCollection(collection);
- }
-
- private IReadOnlyList ReadHttpClientConfigs(
- IReadOnlyList directiveNodes)
- {
- var configs = new List();
-
- foreach (var directiveNode in directiveNodes)
- {
- if (directiveNode.Name.Value.EqualsOrdinal(HttpDirective))
- {
- configs.Add(ReadHttpClientConfig(directiveNode));
- }
- }
-
- return configs;
- }
-
- private HttpClientConfig ReadHttpClientConfig(
- DirectiveNode directiveNode)
- {
- AssertName(directiveNode, HttpDirective);
- AssertArguments(directiveNode, NameArg, BaseAddressArg);
-
- string name = default!; ;
- string baseAddress = default!;
-
- foreach (var argument in directiveNode.Arguments)
- {
- switch (argument.Name.Value)
- {
- case NameArg:
- name = Expect(argument.Value).Value;
- break;
-
- case BaseAddressArg:
- baseAddress = Expect(argument.Value).Value;
- break;
- }
- }
-
- return new HttpClientConfig(name, new Uri(baseAddress));
- }
-
- private VariableDefinitionCollection ReadFieldVariableDefinitions(
- IReadOnlyList directiveNodes)
- {
- var definitions = new List();
+ => new ServiceConfigurationReader().Read(sourceText);
- foreach (var directiveNode in directiveNodes)
- {
- if (directiveNode.Name.Value.EqualsOrdinal(VariableDirective))
- {
- definitions.Add(ReadFieldVariableDefinition(directiveNode));
- }
- }
-
- return new VariableDefinitionCollection(definitions);
- }
-
- private FieldVariableDefinition ReadFieldVariableDefinition(DirectiveNode directiveNode)
- {
- AssertName(directiveNode, VariableDirective);
- AssertArguments(directiveNode, NameArg, SelectArg, TypeArg, FromArg);
-
- string name = default!;
- FieldNode select = default!;
- ITypeNode type = default!;
- string schemaName = default!;
-
- foreach (var argument in directiveNode.Arguments)
- {
- switch (argument.Name.Value)
- {
- case NameArg:
- name = Expect(argument.Value).Value;
- break;
-
- case SelectArg:
- select = ParseField(Expect(argument.Value).Value);
- break;
-
- case TypeArg:
- type = ParseTypeReference(Expect(argument.Value).Value);
- break;
-
- case FromArg:
- schemaName = Expect(argument.Value).Value;
- break;
- }
- }
-
- return new FieldVariableDefinition(name, schemaName, type, select);
- }
-
- private FetchDefinitionCollection ReadFetchDefinitions(
- IReadOnlyList directiveNodes)
- {
- var definitions = new List();
-
- foreach (var directiveNode in directiveNodes)
- {
- if (directiveNode.Name.Value.EqualsOrdinal(FetchDirective))
- {
- definitions.Add(ReadFetchDefinition(directiveNode));
- }
- }
-
- return new FetchDefinitionCollection(definitions);
- }
-
- private FetchDefinition ReadFetchDefinition(DirectiveNode directiveNode)
- {
- AssertName(directiveNode, FetchDirective);
- AssertArguments(directiveNode, SelectArg, FromArg);
-
- ISelectionNode select = default!;
- string schemaName = default!;
-
- foreach (var argument in directiveNode.Arguments)
- {
- switch (argument.Name.Value)
- {
- case SelectArg:
- select = ParseField(Expect(argument.Value).Value);
- break;
-
- case FromArg:
- schemaName = Expect(argument.Value).Value;
- break;
- }
- }
-
- FragmentSpreadNode? placeholder = null;
- _assert.Clear();
-
- SyntaxVisitor
- .Create(
- enter: node =>
- {
- if (node is FragmentSpreadNode p)
- {
- placeholder = p;
- return SyntaxVisitor.Break;
- }
-
- if (node is VariableNode v)
- {
- _assert.Add(v.Name.Value);
- }
-
- return SyntaxVisitor.Continue;
- },
- options: new() { VisitArguments = true })
- .Visit(select);
-
- return new FetchDefinition(
- schemaName,
- select,
- placeholder,
- _assert.Count == 0
- ? Array.Empty()
- : _assert.ToArray());
- }
-
- private MemberBindingCollection ReadMemberBindings(
- IReadOnlyList directiveNodes,
- FieldDefinitionNode annotatedField,
- FetchDefinitionCollection resolvers)
- {
- var definitions = new List();
-
- foreach (var directiveNode in directiveNodes)
- {
- if (directiveNode.Name.Value.EqualsOrdinal(BindDirective))
- {
- definitions.Add(ReadMemberBinding(directiveNode, annotatedField));
- }
- }
-
- if (resolvers.Count > 0)
- {
- _assert.Clear();
-
- foreach (var binding in definitions)
- {
- _assert.Add(binding.SchemaName);
- }
-
- foreach (var resolver in resolvers)
- {
- if (_assert.Add(resolver.SchemaName))
- {
- definitions.Add(
- new MemberBinding(resolver.SchemaName, annotatedField.Name.Value));
- }
- }
- }
-
- return new MemberBindingCollection(definitions);
- }
-
- private MemberBinding ReadMemberBinding(
- DirectiveNode directiveNode,
- FieldDefinitionNode annotatedField)
- {
- AssertName(directiveNode, BindDirective);
- AssertArguments(directiveNode, ToArg, AsArg);
-
- string? name = null;
- string schemaName = default!;
-
- foreach (var argument in directiveNode.Arguments)
- {
- switch (argument.Name.Value)
- {
- case AsArg:
- name = Expect(argument.Value).Value;
- break;
-
- case ToArg:
- schemaName = Expect(argument.Value).Value;
- break;
- }
- }
-
- return new MemberBinding(schemaName, name ?? annotatedField.Name.Value);
- }
-
- private ArgumentVariableDefinitionCollection ReadArgumentVariableDefinitions(
- IReadOnlyList directiveNodes,
- FieldDefinitionNode annotatedField)
- {
- var definitions = new List();
-
- foreach (var directiveNode in directiveNodes)
- {
- if (directiveNode.Name.Value.EqualsOrdinal(VariableDirective))
- {
- definitions.Add(ReadArgumentVariableDefinition(directiveNode, annotatedField));
- }
- }
-
- return new ArgumentVariableDefinitionCollection(definitions);
- }
-
- private ArgumentVariableDefinition ReadArgumentVariableDefinition(
- DirectiveNode directiveNode,
- FieldDefinitionNode annotatedField)
- {
- AssertName(directiveNode, VariableDirective);
- AssertArguments(directiveNode, NameArg, ArgumentArg);
-
- string name = default!;
- string argumentName = default!;
-
- foreach (var argument in directiveNode.Arguments)
- {
- switch (argument.Name.Value)
- {
- case NameArg:
- name = Expect(argument.Value).Value;
- break;
-
- case ArgumentArg:
- argumentName = Expect(argument.Value).Value;
- break;
- }
- }
-
- var arg = annotatedField.Arguments.Single(t => t.Name.Value.EqualsOrdinal(argumentName));
-
- return new ArgumentVariableDefinition(name, arg.Type, argumentName);
- }
-
- private static T Expect(IValueNode valueNode) where T : IValueNode
- {
- if (valueNode is not T casted)
- {
- // TODO : EXCEPTION
- throw new InvalidOperationException("Invalid value");
- }
-
- return casted;
- }
-
- private void AssertName(DirectiveNode directive, string expectedName)
- {
- if (!directive.Name.Value.EqualsOrdinal(expectedName))
- {
- // TODO : EXCEPTION
- throw new InvalidOperationException("INVALID DIRECTIVE NAME");
- }
- }
-
- private void AssertArguments(DirectiveNode directive, params string[] expectedArguments)
- {
- if (directive.Arguments.Count < 0)
- {
- // TODO : EXCEPTION
- throw new InvalidOperationException("INVALID ARGS");
- }
-
- _assert.Clear();
-
- foreach (var argument in directive.Arguments)
- {
- _assert.Add(argument.Name.Value);
- }
-
- _assert.ExceptWith(expectedArguments);
-
- if (_assert.Count > 0)
- {
- // TODO : EXCEPTION
- throw new InvalidOperationException("INVALID ARGS");
- }
- }
-}
-
-internal static class FusionDirectiveNames
-{
- public const string VariableDirective = "variable";
- public const string FetchDirective = "fetch";
- public const string BindDirective = "bind";
- public const string HttpDirective = "httpClient";
- public const string NameArg = "name";
- public const string SelectArg = "select";
- public const string TypeArg = "type";
- public const string FromArg = "from";
- public const string ToArg = "to";
- public const string AsArg = "as";
- public const string ArgumentArg = "argument";
- public const string BaseAddressArg = "baseAddress";
+ public static ServiceConfiguration Load(DocumentNode document)
+ => new ServiceConfigurationReader().Read(document);
}
public readonly struct TypeInfo
diff --git a/src/HotChocolate/Fusion/src/Core/Metadata/ServiceConfigurationReader.cs b/src/HotChocolate/Fusion/src/Core/Metadata/ServiceConfigurationReader.cs
new file mode 100644
index 00000000000..76f5ee2b55d
--- /dev/null
+++ b/src/HotChocolate/Fusion/src/Core/Metadata/ServiceConfigurationReader.cs
@@ -0,0 +1,418 @@
+using HotChocolate.Language;
+using HotChocolate.Language.Visitors;
+using HotChocolate.Utilities;
+using static HotChocolate.Fusion.Metadata.ConfigurationDirectiveNames;
+using static HotChocolate.Language.Utf8GraphQLParser.Syntax;
+
+namespace HotChocolate.Fusion.Metadata;
+
+internal sealed class ServiceConfigurationReader
+{
+ private readonly HashSet _assert = new();
+
+ public ServiceConfiguration Read(string sourceText)
+ => ReadServiceDefinition(Utf8GraphQLParser.Parse(sourceText));
+
+ public ServiceConfiguration Read(DocumentNode document)
+ => ReadServiceDefinition(document);
+
+ private ServiceConfiguration ReadServiceDefinition(DocumentNode documentNode)
+ {
+ var context = ConfigurationDirectiveNamesContext.From(documentNode);
+
+ var types = new List();
+ IReadOnlyList? httpClientConfigs = null;
+
+ foreach (var definition in documentNode.Definitions)
+ {
+ switch (definition)
+ {
+ case ObjectTypeDefinitionNode node:
+ types.Add(ReadObjectType(context, node));
+ break;
+
+ case SchemaDefinitionNode node:
+ httpClientConfigs = ReadHttpClientConfigs(context, node.Directives);
+ break;
+ }
+ }
+
+ if (httpClientConfigs is not { Count: > 0 })
+ {
+ // TODO : EXCEPTION
+ throw new Exception("No clients configured");
+ }
+
+ if (types.Count == 0)
+ {
+ // TODO : EXCEPTION
+ throw new Exception("No types");
+ }
+
+
+ return new ServiceConfiguration(
+ httpClientConfigs.Select(t => t.SchemaName),
+ types);
+ }
+
+ private ObjectType ReadObjectType(
+ ConfigurationDirectiveNamesContext context,
+ ObjectTypeDefinitionNode typeDef)
+ {
+ var variables = ReadFieldVariableDefinitions(context, typeDef.Directives);
+ var resolvers = ReadFetchDefinitions(context, typeDef.Directives);
+ var fields = ReadObjectFields(context, typeDef.Fields);
+ return new ObjectType(typeDef.Name.Value, variables, resolvers, fields);
+ }
+
+ private ObjectFieldCollection ReadObjectFields(
+ ConfigurationDirectiveNamesContext context,
+ IReadOnlyList fieldDefinitionNodes)
+ {
+ var collection = new List();
+
+ foreach (var fieldDef in fieldDefinitionNodes)
+ {
+ var resolvers = ReadFetchDefinitions(context, fieldDef.Directives);
+ var bindings = ReadMemberBindings(context, fieldDef.Directives, fieldDef, resolvers);
+ var variables = ReadArgumentVariableDefinitions(context, fieldDef.Directives, fieldDef);
+ var field = new ObjectField(fieldDef.Name.Value, bindings, variables, resolvers);
+ collection.Add(field);
+ }
+
+ return new ObjectFieldCollection(collection);
+ }
+
+ private IReadOnlyList ReadHttpClientConfigs(
+ ConfigurationDirectiveNamesContext context,
+ IReadOnlyList directiveNodes)
+ {
+ var configs = new List();
+
+ foreach (var directiveNode in directiveNodes)
+ {
+ if (directiveNode.Name.Value.EqualsOrdinal(context.HttpDirective))
+ {
+ configs.Add(ReadHttpClientConfig(context, directiveNode));
+ }
+ }
+
+ return configs;
+ }
+
+ private HttpClientConfig ReadHttpClientConfig(
+ ConfigurationDirectiveNamesContext context,
+ DirectiveNode directiveNode)
+ {
+ AssertName(directiveNode, context.HttpDirective);
+ AssertArguments(directiveNode, NameArg, BaseAddressArg);
+
+ string name = default!;
+ string baseAddress = default!;
+
+ foreach (var argument in directiveNode.Arguments)
+ {
+ switch (argument.Name.Value)
+ {
+ case NameArg:
+ name = Expect(argument.Value).Value;
+ break;
+
+ case BaseAddressArg:
+ baseAddress = Expect(argument.Value).Value;
+ break;
+ }
+ }
+
+ return new HttpClientConfig(name, new Uri(baseAddress));
+ }
+
+ private VariableDefinitionCollection ReadFieldVariableDefinitions(
+ ConfigurationDirectiveNamesContext context,
+ IReadOnlyList directiveNodes)
+ {
+ var definitions = new List();
+
+ foreach (var directiveNode in directiveNodes)
+ {
+ if (directiveNode.Name.Value.EqualsOrdinal(context.VariableDirective))
+ {
+ definitions.Add(ReadFieldVariableDefinition(context, directiveNode));
+ }
+ }
+
+ return new VariableDefinitionCollection(definitions);
+ }
+
+ private FieldVariableDefinition ReadFieldVariableDefinition(
+ ConfigurationDirectiveNamesContext context,
+ DirectiveNode directiveNode)
+ {
+ AssertName(directiveNode, context.VariableDirective);
+ AssertArguments(directiveNode, NameArg, SelectArg, TypeArg, FromArg);
+
+ string name = default!;
+ FieldNode select = default!;
+ ITypeNode type = default!;
+ string schemaName = default!;
+
+ foreach (var argument in directiveNode.Arguments)
+ {
+ switch (argument.Name.Value)
+ {
+ case NameArg:
+ name = Expect(argument.Value).Value;
+ break;
+
+ case SelectArg:
+ select = ParseField(Expect(argument.Value).Value);
+ break;
+
+ case TypeArg:
+ type = ParseTypeReference(Expect(argument.Value).Value);
+ break;
+
+ case FromArg:
+ schemaName = Expect(argument.Value).Value;
+ break;
+ }
+ }
+
+ return new FieldVariableDefinition(name, schemaName, type, select);
+ }
+
+ private FetchDefinitionCollection ReadFetchDefinitions(
+ ConfigurationDirectiveNamesContext context,
+ IReadOnlyList directiveNodes)
+ {
+ var definitions = new List();
+
+ foreach (var directiveNode in directiveNodes)
+ {
+ if (directiveNode.Name.Value.EqualsOrdinal(context.FetchDirective))
+ {
+ definitions.Add(ReadFetchDefinition(context, directiveNode));
+ }
+ }
+
+ return new FetchDefinitionCollection(definitions);
+ }
+
+ private FetchDefinition ReadFetchDefinition(
+ ConfigurationDirectiveNamesContext context,
+ DirectiveNode directiveNode)
+ {
+ AssertName(directiveNode, context.FetchDirective);
+ AssertArguments(directiveNode, SelectArg, FromArg);
+
+ ISelectionNode select = default!;
+ string schemaName = default!;
+
+ foreach (var argument in directiveNode.Arguments)
+ {
+ switch (argument.Name.Value)
+ {
+ case SelectArg:
+ select = ParseField(Expect(argument.Value).Value);
+ break;
+
+ case FromArg:
+ schemaName = Expect(argument.Value).Value;
+ break;
+ }
+ }
+
+ FragmentSpreadNode? placeholder = null;
+ _assert.Clear();
+
+ SyntaxVisitor
+ .Create(
+ enter: node =>
+ {
+ if (node is FragmentSpreadNode p)
+ {
+ placeholder = p;
+ return SyntaxVisitor.Break;
+ }
+
+ if (node is VariableNode v)
+ {
+ _assert.Add(v.Name.Value);
+ }
+
+ return SyntaxVisitor.Continue;
+ },
+ options: new() { VisitArguments = true })
+ .Visit(select);
+
+ return new FetchDefinition(
+ schemaName,
+ select,
+ placeholder,
+ _assert.Count == 0
+ ? Array.Empty()
+ : _assert.ToArray());
+ }
+
+ private MemberBindingCollection ReadMemberBindings(
+ ConfigurationDirectiveNamesContext context,
+ IReadOnlyList directiveNodes,
+ FieldDefinitionNode annotatedField,
+ FetchDefinitionCollection resolvers)
+ {
+ var definitions = new List();
+
+ foreach (var directiveNode in directiveNodes)
+ {
+ if (directiveNode.Name.Value.EqualsOrdinal(context.BindDirective))
+ {
+ definitions.Add(ReadMemberBinding(context, directiveNode, annotatedField));
+ }
+ }
+
+ if (resolvers.Count > 0)
+ {
+ _assert.Clear();
+
+ foreach (var binding in definitions)
+ {
+ _assert.Add(binding.SchemaName);
+ }
+
+ foreach (var resolver in resolvers)
+ {
+ if (_assert.Add(resolver.SchemaName))
+ {
+ definitions.Add(
+ new MemberBinding(resolver.SchemaName, annotatedField.Name.Value));
+ }
+ }
+ }
+
+ return new MemberBindingCollection(definitions);
+ }
+
+ private MemberBinding ReadMemberBinding(
+ ConfigurationDirectiveNamesContext context,
+ DirectiveNode directiveNode,
+ FieldDefinitionNode annotatedField)
+ {
+ AssertName(directiveNode, context.BindDirective);
+ AssertArguments(directiveNode, ToArg, AsArg);
+
+ string? name = null;
+ string schemaName = default!;
+
+ foreach (var argument in directiveNode.Arguments)
+ {
+ switch (argument.Name.Value)
+ {
+ case AsArg:
+ name = Expect(argument.Value).Value;
+ break;
+
+ case ToArg:
+ schemaName = Expect(argument.Value).Value;
+ break;
+ }
+ }
+
+ return new MemberBinding(schemaName, name ?? annotatedField.Name.Value);
+ }
+
+ private ArgumentVariableDefinitionCollection ReadArgumentVariableDefinitions(
+ ConfigurationDirectiveNamesContext context,
+ IReadOnlyList directiveNodes,
+ FieldDefinitionNode annotatedField)
+ {
+ var definitions = new List();
+
+ foreach (var directiveNode in directiveNodes)
+ {
+ if (directiveNode.Name.Value.EqualsOrdinal(context.VariableDirective))
+ {
+ definitions.Add(
+ ReadArgumentVariableDefinition(
+ context,
+ directiveNode,
+ annotatedField));
+ }
+ }
+
+ return new ArgumentVariableDefinitionCollection(definitions);
+ }
+
+ private ArgumentVariableDefinition ReadArgumentVariableDefinition(
+ ConfigurationDirectiveNamesContext context,
+ DirectiveNode directiveNode,
+ FieldDefinitionNode annotatedField)
+ {
+ AssertName(directiveNode, context.VariableDirective);
+ AssertArguments(directiveNode, NameArg, ArgumentArg);
+
+ string name = default!;
+ string argumentName = default!;
+
+ foreach (var argument in directiveNode.Arguments)
+ {
+ switch (argument.Name.Value)
+ {
+ case NameArg:
+ name = Expect(argument.Value).Value;
+ break;
+
+ case ArgumentArg:
+ argumentName = Expect(argument.Value).Value;
+ break;
+ }
+ }
+
+ var arg = annotatedField.Arguments.Single(t => t.Name.Value.EqualsOrdinal(argumentName));
+
+ return new ArgumentVariableDefinition(name, arg.Type, argumentName);
+ }
+
+ private static T Expect(IValueNode valueNode) where T : IValueNode
+ {
+ if (valueNode is not T casted)
+ {
+ // TODO : EXCEPTION
+ throw new InvalidOperationException("Invalid value");
+ }
+
+ return casted;
+ }
+
+ private void AssertName(DirectiveNode directive, string expectedName)
+ {
+ if (!directive.Name.Value.EqualsOrdinal(expectedName))
+ {
+ // TODO : EXCEPTION
+ throw new InvalidOperationException("INVALID DIRECTIVE NAME");
+ }
+ }
+
+ private void AssertArguments(DirectiveNode directive, params string[] expectedArguments)
+ {
+ if (directive.Arguments.Count < 0)
+ {
+ // TODO : EXCEPTION
+ throw new InvalidOperationException("INVALID ARGS");
+ }
+
+ _assert.Clear();
+
+ foreach (var argument in directive.Arguments)
+ {
+ _assert.Add(argument.Name.Value);
+ }
+
+ _assert.ExceptWith(expectedArguments);
+
+ if (_assert.Count > 0)
+ {
+ // TODO : EXCEPTION
+ throw new InvalidOperationException("INVALID ARGS");
+ }
+ }
+}
diff --git a/src/HotChocolate/Fusion/src/Core/Metadata/ServiceConfigurationToSchemaRewriter.cs b/src/HotChocolate/Fusion/src/Core/Metadata/ServiceConfigurationToSchemaRewriter.cs
new file mode 100644
index 00000000000..ebab82f3532
--- /dev/null
+++ b/src/HotChocolate/Fusion/src/Core/Metadata/ServiceConfigurationToSchemaRewriter.cs
@@ -0,0 +1,20 @@
+using HotChocolate.Language;
+using HotChocolate.Language.Visitors;
+
+namespace HotChocolate.Fusion.Metadata;
+
+internal sealed class ServiceConfigurationToSchemaRewriter
+ : SyntaxRewriter
+{
+ protected override DirectiveNode? RewriteDirective(
+ DirectiveNode node,
+ ConfigurationDirectiveNamesContext context)
+ {
+ if (context.IsConfigurationDirective(node.Name.Value))
+ {
+ return null;
+ }
+
+ return base.RewriteDirective(node, context);
+ }
+}
diff --git a/src/HotChocolate/Fusion/src/Core/Planning/QueryPlan.cs b/src/HotChocolate/Fusion/src/Core/Planning/QueryPlan.cs
index 45d571a62ae..62b9b20a842 100644
--- a/src/HotChocolate/Fusion/src/Core/Planning/QueryPlan.cs
+++ b/src/HotChocolate/Fusion/src/Core/Planning/QueryPlan.cs
@@ -13,17 +13,20 @@ internal sealed class QueryPlan
{
ExecutionNodes = executionNodes.ToArray();
RootExecutionNodes = ExecutionNodes.Where(t => t.DependsOn.Count == 0).ToArray();
- _lookup = ExecutionNodes.OfType().ToLookup(t => t.Handler.SelectionSet);
+ RequiresFetch = new HashSet(ExecutionNodes.OfType().Select(t => t.Handler.SelectionSet));
- _exports = exportDefinitions
- .GroupBy(t => t.SelectionSet, t => t.StateKey)
- .ToDictionary(t => t.Key, t => t.ToArray());
+ _lookup = ExecutionNodes.OfType().ToLookup(t => t.Handler.SelectionSet);
+ _exports = exportDefinitions.GroupBy(t => t.SelectionSet, t => t.StateKey).ToDictionary(t => t.Key, t => t.ToArray());
}
public IReadOnlyList RootExecutionNodes { get; }
public IReadOnlyList ExecutionNodes { get; }
+ // name is not really good... the selection sets that require execution of request nodes.
+ public IReadOnlySet RequiresFetch { get; }
+
+ // should we return a tree instead so that dependencies are correctly modeled?
public IEnumerable GetRequestNodes(ISelectionSet selectionSet)
=> _lookup[selectionSet];
diff --git a/src/HotChocolate/Fusion/src/Core/Utilities/JsonQueryResultFormatter.cs b/src/HotChocolate/Fusion/src/Core/Utilities/JsonQueryResultFormatter.cs
deleted file mode 100644
index 06d3f831135..00000000000
--- a/src/HotChocolate/Fusion/src/Core/Utilities/JsonQueryResultFormatter.cs
+++ /dev/null
@@ -1,447 +0,0 @@
-using System.Buffers;
-using System.Collections;
-using System.Runtime.CompilerServices;
-using System.Text;
-using System.Text.Encodings.Web;
-using System.Text.Json;
-using HotChocolate.Execution;
-using HotChocolate.Execution.Processing;
-using HotChocolate.Execution.Serialization;
-using HotChocolate.Utilities;
-using static HotChocolate.Execution.Serialization.JsonConstants;
-
-namespace HotChocolate.Fusion.Utilities;
-
-internal sealed class JsonQueryResultFormatter : IQueryResultFormatter
-{
- private readonly JsonWriterOptions _options;
-
- ///
- /// Creates a new instance of .
- ///
- ///
- /// Defines whether the underlying
- /// should pretty print the JSON which includes:
- /// indenting nested JSON tokens, adding new lines, and adding
- /// white space between property names and values.
- /// By default, the JSON is written without any extra white space.
- ///
- ///
- /// Gets or sets the encoder to use when escaping strings, or null to use the default encoder.
- ///
- public JsonQueryResultFormatter(bool indented = false, JavaScriptEncoder? encoder = null)
- {
- _options = new JsonWriterOptions { Indented = indented, Encoder = encoder };
- }
-
- public string Format(IQueryResult result)
- {
- if (result is null)
- {
- throw new ArgumentNullException(nameof(result));
- }
-
- using var arrayWriter = new ArrayWriter();
- Format(result, arrayWriter);
- return Encoding.UTF8.GetString(arrayWriter.GetInternalBuffer(), 0, arrayWriter.Length);
- }
-
- ///
- public void Format(IQueryResult result, IBufferWriter writer)
- {
- if (result is null)
- {
- throw new ArgumentNullException(nameof(result));
- }
-
- if (writer is null)
- {
- throw new ArgumentNullException(nameof(writer));
- }
-
- using var jsonWriter = new Utf8JsonWriter(writer, _options);
- WriteResult(jsonWriter, result);
- jsonWriter.Flush();
- }
-
- ///
- public async Task FormatAsync(
- IQueryResult result,
- Stream outputStream,
- CancellationToken cancellationToken = default)
- {
- if (result is null)
- {
- throw new ArgumentNullException(nameof(result));
- }
-
- if (outputStream is null)
- {
- throw new ArgumentNullException(nameof(outputStream));
- }
-
- await using var writer = new Utf8JsonWriter(outputStream, _options);
-
- WriteResult(writer, result);
-
- await writer.FlushAsync(cancellationToken).ConfigureAwait(false);
- }
-
- private void WriteResult(Utf8JsonWriter writer, IQueryResult result)
- {
- writer.WriteStartObject();
-
- WritePatchInfo(writer, result);
- WriteErrors(writer, result.Errors);
- WriteData(writer, result.Data);
- WriteExtensions(writer, result.Extensions);
- WriteHasNext(writer, result);
-
- writer.WriteEndObject();
- }
-
- private static void WritePatchInfo(
- Utf8JsonWriter writer,
- IQueryResult result)
- {
- if (result.Label is not null)
- {
- writer.WriteString("label", result.Label);
- }
-
- if (result.Path is not null)
- {
- WritePath(writer, result.Path);
- }
- }
-
- private static void WriteHasNext(
- Utf8JsonWriter writer,
- IQueryResult result)
- {
- if (result.HasNext.HasValue)
- {
- writer.WriteBoolean("hasNext", result.HasNext.Value);
- }
- }
-
- private void WriteData(
- Utf8JsonWriter writer,
- IReadOnlyDictionary? data)
- {
- if (data is not null)
- {
- writer.WritePropertyName(Data);
-
- if (data is ObjectResult resultMap)
- {
- WriteObjectResult(writer, resultMap);
- }
- else
- {
- WriteDictionary(writer, data);
- }
- }
- }
-
- private void WriteErrors(Utf8JsonWriter writer, IReadOnlyList? errors)
- {
- if (errors is { Count: > 0 })
- {
- writer.WritePropertyName(JsonConstants.Errors);
-
- writer.WriteStartArray();
-
- for (var i = 0; i < errors.Count; i++)
- {
- WriteError(writer, errors[i]);
- }
-
- writer.WriteEndArray();
- }
- }
-
- private void WriteError(Utf8JsonWriter writer, IError error)
- {
- writer.WriteStartObject();
-
- writer.WriteString(Message, error.Message);
-
- WriteLocations(writer, error.Locations);
- WritePath(writer, error.Path);
- WriteExtensions(writer, error.Extensions);
-
- writer.WriteEndObject();
- }
-
- private static void WriteLocations(Utf8JsonWriter writer, IReadOnlyList? locations)
- {
- if (locations is { Count: > 0 })
- {
- writer.WritePropertyName(Locations);
-
- writer.WriteStartArray();
-
- for (var i = 0; i < locations.Count; i++)
- {
- WriteLocation(writer, locations[i]);
- }
-
- writer.WriteEndArray();
- }
- }
-
- private static void WriteLocation(Utf8JsonWriter writer, Location location)
- {
- writer.WriteStartObject();
- writer.WriteNumber(Line, location.Line);
- writer.WriteNumber(Column, location.Column);
- writer.WriteEndObject();
- }
-
- private static void WritePath(Utf8JsonWriter writer, Path? path)
- {
- if (path is not null)
- {
- writer.WritePropertyName(JsonConstants.Path);
- WritePathValue(writer, path);
- }
- }
-
- private static void WritePathValue(Utf8JsonWriter writer, Path path)
- {
- if (path.IsRoot)
- {
- writer.WriteStartArray();
- writer.WriteEndArray();
- return;
- }
-
- writer.WriteStartArray();
-
- var list = path.ToList();
-
- for (var i = 0; i < list.Count; i++)
- {
- switch (list[i])
- {
- case string s:
- writer.WriteStringValue(s);
- break;
-
- case int n:
- writer.WriteNumberValue(n);
- break;
-
- case short n:
- writer.WriteNumberValue(n);
- break;
-
- case long n:
- writer.WriteNumberValue(n);
- break;
-
- default:
- writer.WriteStringValue(list[i].ToString());
- break;
- }
- }
-
- writer.WriteEndArray();
- }
-
- private void WriteExtensions(
- Utf8JsonWriter writer,
- IReadOnlyDictionary? dict)
- {
- if (dict is { Count: > 0 })
- {
- writer.WritePropertyName(Extensions);
- WriteDictionary(writer, dict);
- }
- }
-
- private void WriteDictionary(
- Utf8JsonWriter writer,
- IReadOnlyDictionary dict)
- {
- writer.WriteStartObject();
-
- foreach (var item in dict)
- {
- writer.WritePropertyName(item.Key);
- WriteFieldValue(writer, item.Value);
- }
-
- writer.WriteEndObject();
- }
-
- private void WriteDictionary(
- Utf8JsonWriter writer,
- Dictionary dict)
- {
- writer.WriteStartObject();
-
- foreach (var item in dict)
- {
- writer.WritePropertyName(item.Key);
- WriteFieldValue(writer, item.Value);
- }
-
- writer.WriteEndObject();
- }
-
- private void WriteObjectResult(
- Utf8JsonWriter writer,
- ObjectResult objectResult)
- {
- writer.WriteStartObject();
-
- ref var searchSpace = ref objectResult.GetReference();
-
- for(var i = 0; i < objectResult.Capacity; i++)
- {
- var field = Unsafe.Add(ref searchSpace, i);
- if (field.IsInitialized)
- {
- writer.WritePropertyName(field.Name);
- WriteFieldValue(writer, field.Value);
- }
- }
-
- writer.WriteEndObject();
- }
-
- private void WriteListResult(
- Utf8JsonWriter writer,
- ListResult list)
- {
- writer.WriteStartArray();
-
- ref var searchSpace = ref list.GetReference();
-
- for (var i = 0; i < list.Count; i++)
- {
- var element = Unsafe.Add(ref searchSpace, i);
- WriteFieldValue(writer, element);
- }
-
- writer.WriteEndArray();
- }
-
- private void WriteList(
- Utf8JsonWriter writer,
- IList list)
- {
- writer.WriteStartArray();
-
- for (var i = 0; i < list.Count; i++)
- {
- WriteFieldValue(writer, list[i]);
- }
-
- writer.WriteEndArray();
- }
-
- private void WriteFieldValue(
- Utf8JsonWriter writer,
- object? value)
- {
- if (value is null)
- {
- writer.WriteNullValue();
- return;
- }
-
- switch (value)
- {
- case ObjectResult resultMap:
- WriteObjectResult(writer, resultMap);
- break;
-
- case ListResult resultMapList:
- WriteListResult(writer, resultMapList);
- break;
-
- case JsonElement element:
- writer.WriteRawValue(element.GetRawText());
- break;
-
- case Dictionary dict:
- WriteDictionary(writer, dict);
- break;
-
- case IReadOnlyDictionary dict:
- WriteDictionary(writer, dict);
- break;
-
- case IList list:
- WriteList(writer, list);
- break;
-
- case IError error:
- WriteError(writer, error);
- break;
-
- case string s:
- writer.WriteStringValue(s);
- break;
-
- case byte b:
- writer.WriteNumberValue(b);
- break;
-
- case short s:
- writer.WriteNumberValue(s);
- break;
-
- case ushort s:
- writer.WriteNumberValue(s);
- break;
-
- case int i:
- writer.WriteNumberValue(i);
- break;
-
- case uint i:
- writer.WriteNumberValue(i);
- break;
-
- case long l:
- writer.WriteNumberValue(l);
- break;
-
- case ulong l:
- writer.WriteNumberValue(l);
- break;
-
- case float f:
- writer.WriteNumberValue(f);
- break;
-
- case double d:
- writer.WriteNumberValue(d);
- break;
-
- case decimal d:
- writer.WriteNumberValue(d);
- break;
-
- case bool b:
- writer.WriteBooleanValue(b);
- break;
-
- case Uri u:
- writer.WriteStringValue(u.ToString());
- break;
-
- case Path p:
- WritePathValue(writer, p);
- break;
-
- default:
- writer.WriteStringValue(value.ToString());
- break;
- }
- }
-}
diff --git a/src/HotChocolate/Fusion/src/Core/Utilities/JsonRequestFormatter.cs b/src/HotChocolate/Fusion/src/Core/Utilities/JsonRequestFormatter.cs
index 52af944039f..650adc149e7 100644
--- a/src/HotChocolate/Fusion/src/Core/Utilities/JsonRequestFormatter.cs
+++ b/src/HotChocolate/Fusion/src/Core/Utilities/JsonRequestFormatter.cs
@@ -1,8 +1,6 @@
using System.Buffers;
using System.Text.Encodings.Web;
using System.Text.Json;
-using HotChocolate.Fusion.Clients;
-using HotChocolate.Fusion.Execution;
using HotChocolate.Language;
using GraphQLRequest = HotChocolate.Fusion.Clients.GraphQLRequest;
diff --git a/src/HotChocolate/Fusion/test/Core.Tests/ExecutionPlanBuilderTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/ExecutionPlanBuilderTests.cs
index 605796b40f1..546e3f81347 100644
--- a/src/HotChocolate/Fusion/test/Core.Tests/ExecutionPlanBuilderTests.cs
+++ b/src/HotChocolate/Fusion/test/Core.Tests/ExecutionPlanBuilderTests.cs
@@ -10,6 +10,202 @@ namespace HotChocolate.Fusion;
public class ExecutionPlanBuilderTests
{
+ [Fact]
+ public async Task GetPersonById_With_Name_And_Bio_With_Prefixed_Directives()
+ {
+ // arrange
+ const string sdl = @"
+ type Query {
+ personById(id: ID!) : Person
+ }
+
+ type Person {
+ id: ID!
+ name: String!
+ bio: String
+ }";
+
+ const string serviceDefinition = @"
+ type Query {
+ personById(id: ID!): Person
+ @abc_variable(name: ""personId"", argument: ""id"")
+ @abc_bind(to: ""a"")
+ @abc_fetch(from: ""a"", select: ""personById(id: $personId) { ... Person }"")
+ @abc_fetch(from: ""b"", select: ""node(id: $personId) { ... on Person { ... Person } }"")
+ }
+
+ type Person
+ @abc_variable(name: ""personId"", select: ""id"" from: ""b"" type: ""ID!"")
+ @abc_variable(name: ""personId"", select: ""id"" from: ""b"" type: ""ID!"")
+ @abc_fetch(from: ""a"", select: ""personById(id: $personId) { ... Person }"")
+ @abc_fetch(from: ""b"", select: ""node(id: $personId) { ... on Person { ... Person } }"") {
+
+ id: ID!
+ @abc_bind(to: ""a"")
+ @abc_bind(to: ""b"")
+ name: String!
+ @abc_bind(to: ""a"")
+ bio: String
+ @abc_bind(to: ""b"")
+ }
+
+ schema
+ @fusion(prefix: ""abc"")
+ @abc_httpClient(name: ""a"" baseAddress: ""https://a/graphql"")
+ @abc_httpClient(name: ""b"" baseAddress: ""https://b/graphql"") {
+ query: Query
+ }";
+
+ var schema = await new ServiceCollection()
+ .AddGraphQL()
+ .AddDocumentFromString(sdl)
+ .UseField(n => n)
+ .BuildSchemaAsync();
+
+ var serviceConfig = Metadata.ServiceConfiguration.Load(serviceDefinition);
+
+ var request =
+ Parse(
+ @"query GetPersonById {
+ personById(id: 1) {
+ id
+ name
+ bio
+ }
+ }");
+
+ var operationCompiler = new OperationCompiler(new());
+ var operation = operationCompiler.Compile(
+ "abc",
+ (OperationDefinitionNode)request.Definitions.First(),
+ schema.QueryType,
+ request,
+ schema);
+
+ // act
+ var queryPlanContext = new QueryPlanContext(operation);
+ var requestPlaner = new RequestPlaner(serviceConfig);
+ var requirementsPlaner = new RequirementsPlaner();
+ var executionPlanBuilder = new ExecutionPlanBuilder(serviceConfig, schema);
+
+ requestPlaner.Plan(queryPlanContext);
+ requirementsPlaner.Plan(queryPlanContext);
+ var queryPlan = executionPlanBuilder.Build(queryPlanContext);
+
+ // assert
+ var index = 0;
+ var snapshot = new Snapshot();
+ snapshot.Add(request, "User Request");
+
+ foreach (var executionNode in queryPlan.ExecutionNodes)
+ {
+ if (executionNode is RequestNode rn)
+ {
+ snapshot.Add(rn.Handler.Document, $"Request {++index}");
+ }
+ }
+
+ await snapshot.MatchAsync();
+ }
+
+ [Fact]
+ public async Task GetPersonById_With_Name_And_Bio_With_Prefixed_Directives_PrefixedSelf()
+ {
+ // arrange
+ const string sdl = @"
+ type Query {
+ personById(id: ID!) : Person
+ }
+
+ type Person {
+ id: ID!
+ name: String!
+ bio: String
+ }";
+
+ const string serviceDefinition = @"
+ type Query {
+ personById(id: ID!): Person
+ @abc_variable(name: ""personId"", argument: ""id"")
+ @abc_bind(to: ""a"")
+ @abc_fetch(from: ""a"", select: ""personById(id: $personId) { ... Person }"")
+ @abc_fetch(from: ""b"", select: ""node(id: $personId) { ... on Person { ... Person } }"")
+ }
+
+ type Person
+ @abc_variable(name: ""personId"", select: ""id"" from: ""b"" type: ""ID!"")
+ @abc_variable(name: ""personId"", select: ""id"" from: ""b"" type: ""ID!"")
+ @abc_fetch(from: ""a"", select: ""personById(id: $personId) { ... Person }"")
+ @abc_fetch(from: ""b"", select: ""node(id: $personId) { ... on Person { ... Person } }"") {
+
+ id: ID!
+ @abc_bind(to: ""a"")
+ @abc_bind(to: ""b"")
+ name: String!
+ @abc_bind(to: ""a"")
+ bio: String
+ @abc_bind(to: ""b"")
+ }
+
+ schema
+ @abc_fusion(prefix: ""abc"", prefixSelf: true)
+ @abc_httpClient(name: ""a"" baseAddress: ""https://a/graphql"")
+ @abc_httpClient(name: ""b"" baseAddress: ""https://b/graphql"") {
+ query: Query
+ }";
+
+ var schema = await new ServiceCollection()
+ .AddGraphQL()
+ .AddDocumentFromString(sdl)
+ .UseField(n => n)
+ .BuildSchemaAsync();
+
+ var serviceConfig = Metadata.ServiceConfiguration.Load(serviceDefinition);
+
+ var request =
+ Parse(
+ @"query GetPersonById {
+ personById(id: 1) {
+ id
+ name
+ bio
+ }
+ }");
+
+ var operationCompiler = new OperationCompiler(new());
+ var operation = operationCompiler.Compile(
+ "abc",
+ (OperationDefinitionNode)request.Definitions.First(),
+ schema.QueryType,
+ request,
+ schema);
+
+ // act
+ var queryPlanContext = new QueryPlanContext(operation);
+ var requestPlaner = new RequestPlaner(serviceConfig);
+ var requirementsPlaner = new RequirementsPlaner();
+ var executionPlanBuilder = new ExecutionPlanBuilder(serviceConfig, schema);
+
+ requestPlaner.Plan(queryPlanContext);
+ requirementsPlaner.Plan(queryPlanContext);
+ var queryPlan = executionPlanBuilder.Build(queryPlanContext);
+
+ // assert
+ var index = 0;
+ var snapshot = new Snapshot();
+ snapshot.Add(request, "User Request");
+
+ foreach (var executionNode in queryPlan.ExecutionNodes)
+ {
+ if (executionNode is RequestNode rn)
+ {
+ snapshot.Add(rn.Handler.Document, $"Request {++index}");
+ }
+ }
+
+ await snapshot.MatchAsync();
+ }
+
[Fact]
public async Task GetPersonById_With_Name_And_Bio()
{
diff --git a/src/HotChocolate/Fusion/test/Core.Tests/Metadata/ConfigurationDirectiveNamesContextTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/Metadata/ConfigurationDirectiveNamesContextTests.cs
new file mode 100644
index 00000000000..f4f7094dd31
--- /dev/null
+++ b/src/HotChocolate/Fusion/test/Core.Tests/Metadata/ConfigurationDirectiveNamesContextTests.cs
@@ -0,0 +1,160 @@
+using CookieCrumble;
+using HotChocolate.Language;
+
+namespace HotChocolate.Fusion.Metadata;
+
+public class ConfigurationDirectiveNamesContextTests
+{
+ [Fact]
+ public void NewContext_DefaultDirectiveNames()
+ {
+ // act
+ var context = ConfigurationDirectiveNamesContext.Create();
+
+ // assert
+ Snapshot
+ .Create()
+ .Add(context)
+ .MatchInline(
+ @"{
+ ""VariableDirective"": ""variable"",
+ ""FetchDirective"": ""fetch"",
+ ""BindDirective"": ""bind"",
+ ""HttpDirective"": ""httpClient"",
+ ""FusionDirective"": ""fusion""
+ }");
+ }
+
+ [Fact]
+ public void NewContext_DirectiveNames_With_Prefix()
+ {
+ // act
+ var context = ConfigurationDirectiveNamesContext.Create(prefix: "def");
+
+ // assert
+ Snapshot
+ .Create()
+ .Add(context)
+ .MatchInline(
+ @"{
+ ""VariableDirective"": ""def_variable"",
+ ""FetchDirective"": ""def_fetch"",
+ ""BindDirective"": ""def_bind"",
+ ""HttpDirective"": ""def_httpClient"",
+ ""FusionDirective"": ""fusion""
+ }");
+ }
+
+ [Fact]
+ public void NewContext_DirectiveNames_With_Prefix_PrefixSelf()
+ {
+ // act
+ var context = ConfigurationDirectiveNamesContext.Create(prefix: "def", prefixSelf: true);
+
+ // assert
+ Snapshot
+ .Create()
+ .Add(context)
+ .MatchInline(
+ @"{
+ ""VariableDirective"": ""def_variable"",
+ ""FetchDirective"": ""def_fetch"",
+ ""BindDirective"": ""def_bind"",
+ ""HttpDirective"": ""def_httpClient"",
+ ""FusionDirective"": ""def_fusion""
+ }");
+ }
+
+ [Fact]
+ public void From_Document_No_Fusion_Directive()
+ {
+ // arrange
+ var document = Utf8GraphQLParser.Parse(@"schema { }");
+
+ // act
+ var context = ConfigurationDirectiveNamesContext.From(document);
+
+ // assert
+ Snapshot
+ .Create()
+ .Add(context)
+ .MatchInline(
+ @"{
+ ""VariableDirective"": ""variable"",
+ ""FetchDirective"": ""fetch"",
+ ""BindDirective"": ""bind"",
+ ""HttpDirective"": ""httpClient"",
+ ""FusionDirective"": ""fusion""
+ }");
+ }
+
+ [Fact]
+ public void From_Document_With_Fusion_Directive_No_Prefix()
+ {
+ // arrange
+ var document = Utf8GraphQLParser.Parse(@"schema @fusion(version: 1) { }");
+
+ // act
+ var context = ConfigurationDirectiveNamesContext.From(document);
+
+ // assert
+ Snapshot
+ .Create()
+ .Add(context)
+ .MatchInline(
+ @"{
+ ""VariableDirective"": ""variable"",
+ ""FetchDirective"": ""fetch"",
+ ""BindDirective"": ""bind"",
+ ""HttpDirective"": ""httpClient"",
+ ""FusionDirective"": ""fusion""
+ }");
+ }
+
+ [Fact]
+ public void From_Document_With_Fusion_Directive_With_Prefix()
+ {
+ // arrange
+ var document = Utf8GraphQLParser.Parse(@"schema @fusion(prefix: ""abc"") { }");
+
+ // act
+ var context = ConfigurationDirectiveNamesContext.From(document);
+
+ // assert
+ Snapshot
+ .Create()
+ .Add(context)
+ .MatchInline(
+ @"{
+ ""VariableDirective"": ""abc_variable"",
+ ""FetchDirective"": ""abc_fetch"",
+ ""BindDirective"": ""abc_bind"",
+ ""HttpDirective"": ""abc_httpClient"",
+ ""FusionDirective"": ""fusion""
+ }");
+ }
+
+ [Fact]
+ public void From_Document_With_Fusion_Directive_With_Prefix_PrefixSelf()
+ {
+ // arrange
+ var document = Utf8GraphQLParser.Parse(
+ @"schema @abc_fusion(prefix: ""abc"", prefixSelf: true) { }");
+
+ // act
+ var context = ConfigurationDirectiveNamesContext.From(document);
+
+ // assert
+ Snapshot
+ .Create()
+ .Add(context)
+ .MatchInline(
+ @"{
+ ""VariableDirective"": ""abc_variable"",
+ ""FetchDirective"": ""abc_fetch"",
+ ""BindDirective"": ""abc_bind"",
+ ""HttpDirective"": ""abc_httpClient"",
+ ""FusionDirective"": ""abc_fusion""
+ }");
+ }
+}
diff --git a/src/HotChocolate/Fusion/test/Core.Tests/Metadata/ServiceConfigurationToSchemaRewriterTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/Metadata/ServiceConfigurationToSchemaRewriterTests.cs
new file mode 100644
index 00000000000..dafe47a2d26
--- /dev/null
+++ b/src/HotChocolate/Fusion/test/Core.Tests/Metadata/ServiceConfigurationToSchemaRewriterTests.cs
@@ -0,0 +1,69 @@
+using CookieCrumble;
+using HotChocolate.Language;
+
+namespace HotChocolate.Fusion.Metadata;
+
+public class ServiceConfigurationToSchemaRewriterTests
+{
+ [Fact]
+ public void Remove_Configuration_Directives()
+ {
+ // arrange
+ const string serviceDefinition = @"
+ type Query {
+ personById(id: ID!): Person
+ @abc_variable(name: ""personId"", argument: ""id"")
+ @abc_bind(to: ""a"")
+ @abc_fetch(from: ""a"", select: ""personById(id: $personId) { ... Person }"")
+ @abc_fetch(from: ""b"", select: ""node(id: $personId) { ... on Person { ... Person } }"")
+ }
+
+ type Person
+ @abc_variable(name: ""personId"", select: ""id"" from: ""b"" type: ""ID!"")
+ @abc_variable(name: ""personId"", select: ""id"" from: ""b"" type: ""ID!"")
+ @abc_fetch(from: ""a"", select: ""personById(id: $personId) { ... Person }"")
+ @abc_fetch(from: ""b"", select: ""node(id: $personId) { ... on Person { ... Person } }"") {
+
+ id: ID!
+ @abc_bind(to: ""a"")
+ @abc_bind(to: ""b"")
+ name: String!
+ @abc_bind(to: ""a"")
+ bio: String
+ @abc_bind(to: ""b"")
+ }
+
+ schema
+ @fusion(prefix: ""abc"")
+ @abc_httpClient(name: ""a"" baseAddress: ""https://a/graphql"")
+ @abc_httpClient(name: ""b"" baseAddress: ""https://b/graphql"") {
+ query: Query
+ }";
+
+ var document = Utf8GraphQLParser.Parse(serviceDefinition);
+
+ // act
+ var context = ConfigurationDirectiveNamesContext.From(document);
+ var rewriter = new ServiceConfigurationToSchemaRewriter();
+ var rewritten = rewriter.Rewrite(document, context);
+
+ // assert
+ Snapshot
+ .Create()
+ .Add(rewritten)
+ .MatchInline(
+ @"type Query {
+ personById(id: ID!): Person
+ }
+
+ type Person {
+ id: ID!
+ name: String!
+ bio: String
+ }
+
+ schema {
+ query: Query
+ }");
+ }
+}
diff --git a/src/HotChocolate/Fusion/test/Core.Tests/RemoteQueryExecutorTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/RemoteQueryExecutorTests.cs
index 7a34a312fef..8fbb3486310 100644
--- a/src/HotChocolate/Fusion/test/Core.Tests/RemoteQueryExecutorTests.cs
+++ b/src/HotChocolate/Fusion/test/Core.Tests/RemoteQueryExecutorTests.cs
@@ -1,7 +1,7 @@
using CookieCrumble;
using HotChocolate.Execution;
using HotChocolate.Execution.Processing;
-using HotChocolate.Fusion.Clients;
+using HotChocolate.Execution.Serialization;
using HotChocolate.Fusion.Execution;
using HotChocolate.Fusion.Planning;
using HotChocolate.Fusion.Utilities;
@@ -64,18 +64,6 @@ public async Task Do()
var clientFactory = new MockHttpClientFactory(clients);
- const string sdl = @"
- type Query {
- personById(id: ID!) : Person
- }
-
- type Person {
- id: ID!
- name: String!
- bio: String
- friends: [Person!]
- }";
-
const string serviceConfiguration = @"
type Query {
personById(id: ID!): Person
@@ -120,10 +108,9 @@ type Person
}
}");
- var executor = await new ServiceCollection()
+ var executor = await new ServiceCollection()
.AddSingleton(clientFactory)
- .AddGraphQLServer()
- .AddGraphQLGateway(serviceConfiguration, sdl)
+ .AddFusionGatewayServer(serviceConfiguration)
.BuildRequestExecutorAsync();
// act
diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__resources__/dummy.graphql b/src/HotChocolate/Fusion/test/Core.Tests/__resources__/dummy.graphql
index d648faa4dce..2c388559021 100644
--- a/src/HotChocolate/Fusion/test/Core.Tests/__resources__/dummy.graphql
+++ b/src/HotChocolate/Fusion/test/Core.Tests/__resources__/dummy.graphql
@@ -1,24 +1,48 @@
type Query {
personById(id: ID!): Person
@variable(name: "personId", argument: "id")
- @fetch(from: "a", select: "personById(id: $personId) { ... Person }")
- @fetch(from: "b", select: "personById(id: $personId) { ... Person }")
+ @fetch(select: "personById(id: $personId) { ... Person }", from: "a")
+ @fetch(select: "personById(id: $personId) { ... Person }", from: "b")
+ persons(first: Int, last: Int) : [Person]
+ @variable(name: "first", argument: "first")
+ @variable(name: "last", argument: "last")
+ @fetch(select: "persons(first: $first, last: $last) { ... Person }", from: "a")
+ @fetch(select: "persons(first: $first, last: $last) { ... Person }", from: "b")
}
type Person
- @variable(name: "personId", select: "id", from: "a", type: "Int!")
- @variable(name: "personId", select: "id", from: "b", type: "Int!")
- @fetch(from: "a", select: "personById(id: $personId) { ... Person }")
- @fetch(from: "b", select: "personById(id: $personId) { ... Person }") {
- id: ID! @bind(to: "a") @bind(to: "b") @bind(to: "c")
- name: String! @bind(to: "a")
- bio: String @bind(to: "b")
-
- friends: [Person!] @bind(to: "a")
+ @variable(name: "personId", select: "id", from: "a", as: "Int!")
+ @variable(name: "personId", select: "id", from: "b", as: "Int!")
+ @fetch(select: "personById(id: $personId) { ... Person }", from: "a", )
+ @fetch(select: "personById(id: $personId) { ... Person }", from: "b", ) {
+ id: ID!
+ @bind(to: "a")
+ @bind(to: "b")
+ @bind(to: "c")
+ name: String!
+ @bind(to: "a")
+ bio: String
+ @bind(to: "b")
+ friends: [Person!]
+ @bind(to: "a")
}
schema
+ @fusion(version: 1)
@httpClient(name: "a", baseAddress: "https://a/graphql")
@httpClient(name: "b", baseAddress: "https://b/graphql") {
query: Query
}
+
+directive @fusion(prefix: String, version: Int! = 1, prefixSelf: Boolean! = false) on SCHEMA
+directive @fetch(from: _SchemaName, select: _FieldSyntax) repeatable on OBJECT | FIELD_DEFINITION
+directive @variable(name: String!, argument: _ArgumentName, select: _FieldSyntax, from: _SchemaName, as: _TypeSyntax) repeatable on OBJECT | FIELD_DEFINITION
+directive @bind(to: _SchemaName, as: _FieldName) repeatable on OBJECT | FIELD_DEFINITION
+directive @httpClient(name: _SchemaName, baseAddress: _URI) repeatable on SCHEMA
+
+scalar _SchemaName
+scalar _FieldName
+scalar _ArgumentName
+scalar _FieldSyntax
+scalar _TypeSyntax
+scalar _URI
diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/ExecutionPlanBuilderTests.GetPersonById_With_Name_And_Bio_With_Prefixed_Directives.snap b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/ExecutionPlanBuilderTests.GetPersonById_With_Name_And_Bio_With_Prefixed_Directives.snap
new file mode 100644
index 00000000000..b6feca2de1c
--- /dev/null
+++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/ExecutionPlanBuilderTests.GetPersonById_With_Name_And_Bio_With_Prefixed_Directives.snap
@@ -0,0 +1,31 @@
+User Request
+---------------
+query GetPersonById {
+ personById(id: 1) {
+ id
+ name
+ bio
+ }
+}
+---------------
+
+Request 1
+---------------
+query GetPersonById_1 {
+ personById(id: 1) {
+ id
+ name
+ }
+}
+---------------
+
+Request 2
+---------------
+query GetPersonById_2 {
+ node(id: 1) {
+ ... on Person {
+ bio
+ }
+ }
+}
+---------------
diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/ExecutionPlanBuilderTests.GetPersonById_With_Name_And_Bio_With_Prefixed_Directives_PrefixedSelf.snap b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/ExecutionPlanBuilderTests.GetPersonById_With_Name_And_Bio_With_Prefixed_Directives_PrefixedSelf.snap
new file mode 100644
index 00000000000..b6feca2de1c
--- /dev/null
+++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/ExecutionPlanBuilderTests.GetPersonById_With_Name_And_Bio_With_Prefixed_Directives_PrefixedSelf.snap
@@ -0,0 +1,31 @@
+User Request
+---------------
+query GetPersonById {
+ personById(id: 1) {
+ id
+ name
+ bio
+ }
+}
+---------------
+
+Request 1
+---------------
+query GetPersonById_1 {
+ personById(id: 1) {
+ id
+ name
+ }
+}
+---------------
+
+Request 2
+---------------
+query GetPersonById_2 {
+ node(id: 1) {
+ ... on Person {
+ bio
+ }
+ }
+}
+---------------
diff --git a/src/HotChocolate/Spatial/test/Data.Filters.SqlServer.Tests/HotChocolate.Spatial.Data.Filters.SqlServer.Tests.csproj b/src/HotChocolate/Spatial/test/Data.Filters.SqlServer.Tests/HotChocolate.Spatial.Data.Filters.SqlServer.Tests.csproj
index c22258eadb0..08eb8cd8e2d 100644
--- a/src/HotChocolate/Spatial/test/Data.Filters.SqlServer.Tests/HotChocolate.Spatial.Data.Filters.SqlServer.Tests.csproj
+++ b/src/HotChocolate/Spatial/test/Data.Filters.SqlServer.Tests/HotChocolate.Spatial.Data.Filters.SqlServer.Tests.csproj
@@ -21,8 +21,8 @@
-
-
+
+
diff --git a/src/HotChocolate/Spatial/test/Data.Projections.SqlServer.Tests/HotChocolate.Data.Projections.Spatial.SqlServer.Tests.csproj b/src/HotChocolate/Spatial/test/Data.Projections.SqlServer.Tests/HotChocolate.Data.Projections.Spatial.SqlServer.Tests.csproj
index 24b1d332260..149990e4649 100644
--- a/src/HotChocolate/Spatial/test/Data.Projections.SqlServer.Tests/HotChocolate.Data.Projections.Spatial.SqlServer.Tests.csproj
+++ b/src/HotChocolate/Spatial/test/Data.Projections.SqlServer.Tests/HotChocolate.Data.Projections.Spatial.SqlServer.Tests.csproj
@@ -21,8 +21,8 @@
-
-
+
+