diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.Warmup.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.Warmup.cs new file mode 100644 index 00000000000..2dde0cfb591 --- /dev/null +++ b/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.Warmup.cs @@ -0,0 +1,22 @@ +using System; +using HotChocolate.AspNetCore.Utilities; +using HotChocolate.Execution.Configuration; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static partial class HotChocolateAspNetCoreServiceCollectionExtensions + { + public static IRequestExecutorBuilder InitializeOnStartup( + this IRequestExecutorBuilder builder) + { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Services.AddHostedService(); + builder.Services.AddSingleton(new WarmupSchema(builder.Name)); + return builder; + } + } +} diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.cs index 3cfa751c3bf..6da5f9d3bcb 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ using System; using Microsoft.Extensions.DependencyInjection.Extensions; using HotChocolate; +using HotChocolate.AspNetCore; using HotChocolate.AspNetCore.Utilities; using HotChocolate.Execution.Configuration; using HotChocolate.Language; diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Utilities/ExecutorWarmupTask.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Utilities/ExecutorWarmupTask.cs new file mode 100644 index 00000000000..42d73c21402 --- /dev/null +++ b/src/HotChocolate/AspNetCore/src/AspNetCore/Utilities/ExecutorWarmupTask.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using HotChocolate.Execution; +using Microsoft.Extensions.Hosting; + +namespace HotChocolate.AspNetCore.Utilities +{ + internal class ExecutorWarmupTask : BackgroundService + { + private readonly IRequestExecutorResolver _executorResolver; + private readonly HashSet _schemaNames; + + public ExecutorWarmupTask( + IRequestExecutorResolver executorResolver, + IEnumerable schemas) + { + if (executorResolver is null) + { + throw new ArgumentNullException(nameof(executorResolver)); + } + + if (schemas is null) + { + throw new ArgumentNullException(nameof(schemas)); + } + + _executorResolver = executorResolver; + _schemaNames = new HashSet(schemas.Select(t => t.SchemaName)); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + foreach (NameString schemaName in _schemaNames) + { + await _executorResolver + .GetRequestExecutorAsync(schemaName, stoppingToken) + .ConfigureAwait(false); + } + } + } +} diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Utilities/WarmupSchema.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Utilities/WarmupSchema.cs new file mode 100644 index 00000000000..adce77f7087 --- /dev/null +++ b/src/HotChocolate/AspNetCore/src/AspNetCore/Utilities/WarmupSchema.cs @@ -0,0 +1,12 @@ +namespace HotChocolate.AspNetCore.Utilities +{ + internal class WarmupSchema + { + public WarmupSchema(NameString schemaName) + { + SchemaName = schemaName; + } + + public NameString SchemaName { get; } + } +} diff --git a/src/HotChocolate/Core/src/Execution/Configuration/NamedRequestExecutorFactoryOptions.cs b/src/HotChocolate/Core/src/Execution/Configuration/ConfigureRequestExecutorSetup.cs similarity index 53% rename from src/HotChocolate/Core/src/Execution/Configuration/NamedRequestExecutorFactoryOptions.cs rename to src/HotChocolate/Core/src/Execution/Configuration/ConfigureRequestExecutorSetup.cs index 03a93d2b5d3..c68d727f185 100644 --- a/src/HotChocolate/Core/src/Execution/Configuration/NamedRequestExecutorFactoryOptions.cs +++ b/src/HotChocolate/Core/src/Execution/Configuration/ConfigureRequestExecutorSetup.cs @@ -1,38 +1,32 @@ using System; +using System.ComponentModel.Design; namespace HotChocolate.Execution.Configuration { - public sealed class NamedRequestExecutorFactoryOptions - : INamedRequestExecutorFactoryOptions + public sealed class ConfigureRequestExecutorSetup + : IConfigureRequestExecutorSetup { - private readonly Action _configure; + private readonly Action _configure; - public NamedRequestExecutorFactoryOptions( + public ConfigureRequestExecutorSetup( NameString schemaName, - Action configure) + Action configure) { SchemaName = schemaName.EnsureNotEmpty(nameof(schemaName)); _configure = configure ?? throw new ArgumentNullException(nameof(configure)); } - /* - public NamedRequestExecutorFactoryOptions( + public ConfigureRequestExecutorSetup( NameString schemaName, - RequestExecutorFactoryOptions options) + RequestExecutorSetup options) { SchemaName = schemaName.EnsureNotEmpty(nameof(schemaName)); - _configure = o => - { - options.Pipeline - - - } + _configure = options.CopyTo; } - */ public NameString SchemaName { get; } - public void Configure(RequestExecutorFactoryOptions options) + public void Configure(RequestExecutorSetup options) { if (options is null) { diff --git a/src/HotChocolate/Core/src/Execution/Configuration/DefaultRequestExecutorOptionsMonitor.cs b/src/HotChocolate/Core/src/Execution/Configuration/DefaultRequestExecutorOptionsMonitor.cs index 78bbe0b27f6..16decf6ae0a 100644 --- a/src/HotChocolate/Core/src/Execution/Configuration/DefaultRequestExecutorOptionsMonitor.cs +++ b/src/HotChocolate/Core/src/Execution/Configuration/DefaultRequestExecutorOptionsMonitor.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -10,38 +9,43 @@ namespace HotChocolate.Execution.Configuration { internal sealed class DefaultRequestExecutorOptionsMonitor : IRequestExecutorOptionsMonitor - , IDisposable + , IDisposable { private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); - private readonly IOptionsMonitor _optionsMonitor; + private readonly IOptionsMonitor _optionsMonitor; private readonly IRequestExecutorOptionsProvider[] _optionsProviders; - private readonly ConcurrentDictionary - _options = new ConcurrentDictionary(); + private readonly Dictionary> _configs = + new Dictionary>(); private readonly List _disposables = new List(); - private readonly List> _listeners = - new List>(); + private readonly List> _listeners = new List>(); private bool _initialized; private bool _disposed; public DefaultRequestExecutorOptionsMonitor( - IOptionsMonitor optionsMonitor, + IOptionsMonitor optionsMonitor, IEnumerable optionsProviders) { _optionsMonitor = optionsMonitor; _optionsProviders = optionsProviders.ToArray(); } - public async ValueTask GetAsync( + public async ValueTask GetAsync( NameString schemaName, CancellationToken cancellationToken = default) { await InitializeAsync(cancellationToken).ConfigureAwait(false); - RequestExecutorFactoryOptions options = _optionsMonitor.Get(schemaName); + var options = new RequestExecutorSetup(); + _optionsMonitor.Get(schemaName).CopyTo(options); - if (_options.TryGetValue(schemaName, out INamedRequestExecutorFactoryOptions? o)) + if (_configs.TryGetValue( + schemaName, + out List? configurations)) { - o.Configure(options); + foreach (IConfigureRequestExecutorSetup configuration in configurations) + { + configuration.Configure(options); + } } return options; @@ -55,17 +59,27 @@ private async ValueTask InitializeAsync(CancellationToken cancellationToken) if (!_initialized) { + _configs.Clear(); + foreach (IRequestExecutorOptionsProvider provider in _optionsProviders) { _disposables.Add(provider.OnChange(OnChange)); - IEnumerable allOptions = + IEnumerable allConfigurations = await provider.GetOptionsAsync(cancellationToken) .ConfigureAwait(false); - foreach (NamedRequestExecutorFactoryOptions options in allOptions) + foreach (IConfigureRequestExecutorSetup configuration in allConfigurations) { - _options[options.SchemaName] = options; + if (!_configs.TryGetValue( + configuration.SchemaName, + out List? configurations)) + { + configurations = new List(); + _configs.Add(configuration.SchemaName, configurations); + } + + configurations.Add(configuration); } } @@ -76,21 +90,18 @@ await provider.GetOptionsAsync(cancellationToken) } } - public IDisposable OnChange(Action listener) => + public IDisposable OnChange(Action listener) => new Session(this, listener); - private void OnChange(INamedRequestExecutorFactoryOptions changes) + private void OnChange(IConfigureRequestExecutorSetup changes) { - _options[changes.SchemaName] = changes; - - RequestExecutorFactoryOptions options = _optionsMonitor.Get(changes.SchemaName); - changes.Configure(options); + _initialized = false; lock (_listeners) { - foreach (Action listener in _listeners) + foreach (Action listener in _listeners) { - listener.Invoke(options, changes.SchemaName); + listener.Invoke(changes.SchemaName); } } } @@ -113,11 +124,11 @@ public void Dispose() private class Session : IDisposable { private readonly DefaultRequestExecutorOptionsMonitor _monitor; - private readonly Action _listener; + private readonly Action _listener; public Session( DefaultRequestExecutorOptionsMonitor monitor, - Action listener) + Action listener) { lock (monitor._listeners) { diff --git a/src/HotChocolate/Core/src/Execution/Configuration/INamedRequestExecutorFactoryOptions.cs b/src/HotChocolate/Core/src/Execution/Configuration/IConfigureRequestExecutorSetup.cs similarity index 71% rename from src/HotChocolate/Core/src/Execution/Configuration/INamedRequestExecutorFactoryOptions.cs rename to src/HotChocolate/Core/src/Execution/Configuration/IConfigureRequestExecutorSetup.cs index 702e7f65b78..4cf420dd0be 100644 --- a/src/HotChocolate/Core/src/Execution/Configuration/INamedRequestExecutorFactoryOptions.cs +++ b/src/HotChocolate/Core/src/Execution/Configuration/IConfigureRequestExecutorSetup.cs @@ -3,10 +3,10 @@ namespace HotChocolate.Execution.Configuration { /// - /// Represents something that configures the . + /// Represents something that configures the . /// - public interface INamedRequestExecutorFactoryOptions - : IConfigureOptions + public interface IConfigureRequestExecutorSetup + : IConfigureOptions { /// /// The schema name to which this instance provides configurations to. diff --git a/src/HotChocolate/Core/src/Execution/Configuration/IRequestExecutorOptionsMonitor.cs b/src/HotChocolate/Core/src/Execution/Configuration/IRequestExecutorOptionsMonitor.cs index 6927a76d799..ec2e0f0b1d9 100644 --- a/src/HotChocolate/Core/src/Execution/Configuration/IRequestExecutorOptionsMonitor.cs +++ b/src/HotChocolate/Core/src/Execution/Configuration/IRequestExecutorOptionsMonitor.cs @@ -5,28 +5,28 @@ namespace HotChocolate.Execution.Configuration { /// - /// Used for notifications when instances change. + /// Used for notifications when instances change. /// public interface IRequestExecutorOptionsMonitor { /// - /// Returns a configured + /// Returns a configured /// instance with the given name. /// - ValueTask GetAsync( + ValueTask GetAsync( NameString schemaName, CancellationToken cancellationToken); /// /// Registers a listener to be called whenever a named - /// changes. + /// changes. /// /// - /// The action to be invoked when has changed. + /// The action to be invoked when has changed. /// /// /// An which should be disposed to stop listening for changes. /// - IDisposable OnChange(Action listener); + IDisposable OnChange(Action listener); } } diff --git a/src/HotChocolate/Core/src/Execution/Configuration/IRequestExecutorOptionsProvider.cs b/src/HotChocolate/Core/src/Execution/Configuration/IRequestExecutorOptionsProvider.cs index d3e4baa33e6..252fd5729c1 100644 --- a/src/HotChocolate/Core/src/Execution/Configuration/IRequestExecutorOptionsProvider.cs +++ b/src/HotChocolate/Core/src/Execution/Configuration/IRequestExecutorOptionsProvider.cs @@ -19,19 +19,19 @@ public interface IRequestExecutorOptionsProvider /// /// Returns the configuration options of this provider. /// - ValueTask> GetOptionsAsync( + ValueTask> GetOptionsAsync( CancellationToken cancellationToken); /// /// Registers a listener to be called whenever a named - /// changes. + /// changes. /// /// - /// The action to be invoked when has changed. + /// The action to be invoked when has changed. /// /// /// An which should be disposed to stop listening for changes. /// - IDisposable OnChange(Action listener); + IDisposable OnChange(Action listener); } } diff --git a/src/HotChocolate/Core/src/Execution/Configuration/RequestExecutorFactoryOptions.cs b/src/HotChocolate/Core/src/Execution/Configuration/RequestExecutorFactoryOptions.cs deleted file mode 100644 index cdb45ddc064..00000000000 --- a/src/HotChocolate/Core/src/Execution/Configuration/RequestExecutorFactoryOptions.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.Extensions.DependencyInjection; -using HotChocolate.Execution.Options; - -namespace HotChocolate.Execution.Configuration -{ - public class RequestExecutorFactoryOptions - { - public ISchema? Schema { get; set; } - - public ISchemaBuilder? SchemaBuilder { get; set; } - - public RequestExecutorOptions? RequestExecutorOptions { get; set; } - - public IList SchemaBuilderActions { get; } = - new List(); - - public IList RequestExecutorOptionsActions { get; } = - new List(); - - public IList Pipeline { get; } = - new List(); - - public IList> SchemaServices { get; } = - new List>(); - - public IList OnRequestExecutorCreated { get; } = - new List(); - - public IList OnRequestExecutorEvicted { get; } = - new List(); - } -} diff --git a/src/HotChocolate/Core/src/Execution/Configuration/RequestExecutorSetup.cs b/src/HotChocolate/Core/src/Execution/Configuration/RequestExecutorSetup.cs new file mode 100644 index 00000000000..2b074faed31 --- /dev/null +++ b/src/HotChocolate/Core/src/Execution/Configuration/RequestExecutorSetup.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Execution.Options; + +namespace HotChocolate.Execution.Configuration +{ + public class RequestExecutorSetup + { + private readonly List _schemaBuilderActions = + new List(); + private readonly List _requestExecutorOptionsActions = + new List(); + private readonly List _pipeline = + new List(); + private readonly List> _schemaServices = + new List>(); + private readonly List _onRequestExecutorCreated = + new List(); + private readonly List _onRequestExecutorEvicted = + new List(); + + public ISchema? Schema { get; set; } + + public ISchemaBuilder? SchemaBuilder { get; set; } + + public RequestExecutorOptions? RequestExecutorOptions { get; set; } + + public IList SchemaBuilderActions => + _schemaBuilderActions; + + public IList RequestExecutorOptionsActions => + _requestExecutorOptionsActions; + + public IList Pipeline => + _pipeline; + + public IList> SchemaServices => + _schemaServices; + + public IList OnRequestExecutorCreated => + _onRequestExecutorCreated; + + public IList OnRequestExecutorEvicted => + _onRequestExecutorEvicted; + + public void CopyTo(RequestExecutorSetup options) + { + options.Schema = Schema; + options.SchemaBuilder = SchemaBuilder; + options.RequestExecutorOptions = RequestExecutorOptions; + options._schemaBuilderActions.AddRange(_schemaBuilderActions); + options._requestExecutorOptionsActions.AddRange(_requestExecutorOptionsActions); + options._pipeline.AddRange(_pipeline); + options._schemaServices.AddRange(_schemaServices); + options._onRequestExecutorCreated.AddRange(_onRequestExecutorCreated); + options._onRequestExecutorEvicted.AddRange(_onRequestExecutorEvicted); + } + } +} diff --git a/src/HotChocolate/Core/src/Execution/Configuration/SchemaBuilderAction.cs b/src/HotChocolate/Core/src/Execution/Configuration/SchemaBuilderAction.cs index 7ce4f4523ab..678c62900e1 100644 --- a/src/HotChocolate/Core/src/Execution/Configuration/SchemaBuilderAction.cs +++ b/src/HotChocolate/Core/src/Execution/Configuration/SchemaBuilderAction.cs @@ -7,21 +7,27 @@ namespace HotChocolate.Execution.Configuration public readonly struct SchemaBuilderAction { public SchemaBuilderAction( - Action action) + Action action) { Action = action ?? throw new ArgumentNullException(nameof(action)); AsyncAction = default; } public SchemaBuilderAction( - Func asyncAction) + Func asyncAction) { Action = default; AsyncAction = asyncAction ?? throw new ArgumentNullException(nameof(asyncAction)); } - public Action? Action { get; } + public Action? Action + { + get; + } - public Func? AsyncAction { get; } + public Func? AsyncAction + { + get; + } } } diff --git a/src/HotChocolate/Core/src/Execution/DependencyInjection/InternalServiceCollectionExtensions.cs b/src/HotChocolate/Core/src/Execution/DependencyInjection/InternalServiceCollectionExtensions.cs index caa472108cc..54bf2d5df0d 100644 --- a/src/HotChocolate/Core/src/Execution/DependencyInjection/InternalServiceCollectionExtensions.cs +++ b/src/HotChocolate/Core/src/Execution/DependencyInjection/InternalServiceCollectionExtensions.cs @@ -23,8 +23,8 @@ internal static class InternalServiceCollectionExtensions { services.TryAddSingleton( sp => new DefaultRequestExecutorOptionsMonitor( - sp.GetRequiredService>(), - sp.GetRequiredService>())); + sp.GetRequiredService>(), + sp.GetServices())); return services; } diff --git a/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorBuilderExtensions.cs b/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorBuilderExtensions.cs index 8b2e0cd56f4..4c6dac3fe32 100644 --- a/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorBuilderExtensions.cs +++ b/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorBuilderExtensions.cs @@ -44,7 +44,7 @@ public static partial class RequestExecutorBuilderExtensions return Configure( builder, options => options.SchemaBuilderActions.Add( - new SchemaBuilderAction(configureSchema))); + new SchemaBuilderAction((sp, sb) => configureSchema(sb)))); } /// @@ -77,7 +77,7 @@ public static partial class RequestExecutorBuilderExtensions return Configure( builder, options => options.SchemaBuilderActions.Add( - new SchemaBuilderAction(configureSchema))); + new SchemaBuilderAction((sp, sb, ct) => configureSchema(sb, ct)))); } /// @@ -113,8 +113,8 @@ public static partial class RequestExecutorBuilderExtensions return Configure( builder, - (services, options) => options.SchemaBuilderActions.Add( - new SchemaBuilderAction(b => configureSchema(services, b)))); + options => options.SchemaBuilderActions.Add( + new SchemaBuilderAction(configureSchema))); } /// @@ -150,8 +150,8 @@ public static partial class RequestExecutorBuilderExtensions return Configure( builder, - (services, options) => options.SchemaBuilderActions.Add( - new SchemaBuilderAction((b, ct) => configureSchema(services, b, ct)))); + options => options.SchemaBuilderActions.Add( + new SchemaBuilderAction(configureSchema))); } /// @@ -407,7 +407,7 @@ public static partial class RequestExecutorBuilderExtensions internal static IRequestExecutorBuilder Configure( this IRequestExecutorBuilder builder, - Action configure) + Action configure) { builder.Services.Configure(builder.Name, configure); return builder; @@ -415,10 +415,10 @@ public static partial class RequestExecutorBuilderExtensions internal static IRequestExecutorBuilder Configure( this IRequestExecutorBuilder builder, - Action configure) + Action configure) { - builder.Services.AddTransient>( - services => new ConfigureNamedOptions( + builder.Services.AddTransient>( + services => new ConfigureNamedOptions( builder.Name, options => configure(services, options))); diff --git a/src/HotChocolate/Core/src/Execution/RequestExecutorResolver.cs b/src/HotChocolate/Core/src/Execution/RequestExecutorResolver.cs index 38d8d67da5f..c1bca382e95 100644 --- a/src/HotChocolate/Core/src/Execution/RequestExecutorResolver.cs +++ b/src/HotChocolate/Core/src/Execution/RequestExecutorResolver.cs @@ -39,7 +39,7 @@ internal sealed class RequestExecutorResolver throw new ArgumentNullException(nameof(optionsMonitor)); _applicationServices = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); - _optionsMonitor.OnChange((options, name) => EvictRequestExecutor(name)); + _optionsMonitor.OnChange(EvictRequestExecutor); } public async ValueTask GetRequestExecutorAsync( @@ -74,7 +74,7 @@ internal sealed class RequestExecutorResolver if (!_executors.TryGetValue(schemaName, out RegisteredExecutor? re)) { - RequestExecutorFactoryOptions options = + RequestExecutorSetup options = await _optionsMonitor.GetAsync(schemaName, cancellationToken) .ConfigureAwait(false); @@ -129,7 +129,7 @@ private void BeginRunEvictionEvents(RegisteredExecutor registeredExecutor) Task.Run(async () => { foreach (OnRequestExecutorEvictedAction action in - registeredExecutor.FactoryOptions.OnRequestExecutorEvicted) + registeredExecutor.Setup.OnRequestExecutorEvicted) { action.Action?.Invoke(registeredExecutor.Executor); @@ -146,7 +146,7 @@ private void BeginRunEvictionEvents(RegisteredExecutor registeredExecutor) private async Task CreateSchemaServicesAsync( NameString schemaName, - RequestExecutorFactoryOptions options, + RequestExecutorSetup options, CancellationToken cancellationToken) { var lazy = new SchemaBuilder.LazySchema(); @@ -233,17 +233,17 @@ await CreateSchemaAsync(schemaName, options, combinedServices, cancellationToken private async ValueTask CreateSchemaAsync( NameString schemaName, - RequestExecutorFactoryOptions options, + RequestExecutorSetup options, IServiceProvider serviceProvider, CancellationToken cancellationToken) { - if (options.Schema is { }) + if (options.Schema is not null) { AssertSchemaNameValid(options.Schema, schemaName); return options.Schema; } - var schemaBuilder = options.SchemaBuilder ?? new SchemaBuilder(); + ISchemaBuilder schemaBuilder = options.SchemaBuilder ?? new SchemaBuilder(); schemaBuilder.AddServices(serviceProvider); @@ -251,12 +251,13 @@ await CreateSchemaAsync(schemaName, options, combinedServices, cancellationToken { if (action.Action is { } configure) { - configure(schemaBuilder); + configure(serviceProvider, schemaBuilder); } if (action.AsyncAction is { } configureAsync) { - await configureAsync(schemaBuilder, cancellationToken).ConfigureAwait(false); + await configureAsync(serviceProvider, schemaBuilder, cancellationToken) + .ConfigureAwait(false); } } @@ -278,7 +279,7 @@ private void AssertSchemaNameValid(ISchema schema, NameString expectedSchemaName } private async ValueTask CreateExecutorOptionsAsync( - RequestExecutorFactoryOptions options, + RequestExecutorSetup options, CancellationToken cancellationToken) { var executorOptions = options.RequestExecutorOptions ?? new RequestExecutorOptions(); @@ -339,12 +340,12 @@ private class RegisteredExecutor IRequestExecutor executor, IServiceProvider services, IDiagnosticEvents diagnosticEvents, - RequestExecutorFactoryOptions factoryOptions) + RequestExecutorSetup setup) { Executor = executor; Services = services; DiagnosticEvents = diagnosticEvents; - FactoryOptions = factoryOptions; + Setup = setup; } public IRequestExecutor Executor { get; } @@ -353,7 +354,7 @@ private class RegisteredExecutor public IDiagnosticEvents DiagnosticEvents { get; } - public RequestExecutorFactoryOptions FactoryOptions { get; } + public RequestExecutorSetup Setup { get; } } private sealed class SetSchemaNameInterceptor : TypeInterceptor diff --git a/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/FilterVisitorTestBase.cs b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/FilterVisitorTestBase.cs index 4d8692d2b91..c5321052765 100644 --- a/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/FilterVisitorTestBase.cs +++ b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/FilterVisitorTestBase.cs @@ -92,7 +92,7 @@ public class FilterVisitorTestBase ISchema schema = builder.Create(); return new ServiceCollection() - .Configure( + .Configure( Schema.DefaultName, o => o.Schema = schema) .AddGraphQL() diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/ProjectionVisitorTestBase.cs b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/ProjectionVisitorTestBase.cs index ba8fcf90333..8caa981ecea 100644 --- a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/ProjectionVisitorTestBase.cs +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/ProjectionVisitorTestBase.cs @@ -121,7 +121,7 @@ public class ProjectionVisitorTestBase ISchema schema = builder.Create(); return new ServiceCollection() - .Configure( + .Configure( Schema.DefaultName, o => o.Schema = schema) .AddGraphQL() diff --git a/src/HotChocolate/Data/test/Data.Sorting.SqlLite.Tests/SortVisitorTestBase.cs b/src/HotChocolate/Data/test/Data.Sorting.SqlLite.Tests/SortVisitorTestBase.cs index 6bbad970b6c..574394c77db 100644 --- a/src/HotChocolate/Data/test/Data.Sorting.SqlLite.Tests/SortVisitorTestBase.cs +++ b/src/HotChocolate/Data/test/Data.Sorting.SqlLite.Tests/SortVisitorTestBase.cs @@ -74,7 +74,7 @@ public class SortVisitorTestBase ISchema? schema = builder.Create(); return new ServiceCollection() - .Configure( + .Configure( Schema.DefaultName, o => o.Schema = schema) .AddGraphQL() diff --git a/src/HotChocolate/Language/src/Language/Legacy/Visitors/QuerySyntaxWalker.cs b/src/HotChocolate/Language/src/Language/Legacy/Visitors/QuerySyntaxWalker.cs index 3e28091f0ed..07d5933f9d9 100644 --- a/src/HotChocolate/Language/src/Language/Legacy/Visitors/QuerySyntaxWalker.cs +++ b/src/HotChocolate/Language/src/Language/Legacy/Visitors/QuerySyntaxWalker.cs @@ -150,8 +150,8 @@ public class QuerySyntaxWalker VisitName(node.Name, context); VisitMany( - node.Directives, - context, + node.Directives, + context, VisitDirective); } @@ -165,8 +165,8 @@ public class QuerySyntaxWalker } VisitMany( - node.Directives, - context, + node.Directives, + context, VisitDirective); if (node.SelectionSet != null) diff --git a/src/HotChocolate/Stitching/src/Stitching.Abstractions/RemoteSchemaDefinition.cs b/src/HotChocolate/Stitching/src/Stitching.Abstractions/RemoteSchemaDefinition.cs index 312c6857153..e3c43dd0402 100644 --- a/src/HotChocolate/Stitching/src/Stitching.Abstractions/RemoteSchemaDefinition.cs +++ b/src/HotChocolate/Stitching/src/Stitching.Abstractions/RemoteSchemaDefinition.cs @@ -36,16 +36,4 @@ public class RemoteSchemaDefinition /// public IReadOnlyList ExtensionDocuments { get; } } - - /* - extend schema - @_removeType(name: "abc" schema: "{optional}") - @_renameType(name: "abc", newName: "def" schema: "{optional}") - @_renameField(type: "abc" name: "abc", newName: "def" schema: "{optional}") - @_renameArgument(type: "abc" field: "abc" name: "abc", newName: "def" schema: "{optional}") - @_removeQueryType(schema: "{optional}") - @_removeMutationType(schema: "{optional}") - @_removeSubscriptionType(schema: "{optional}") - @_rewriteType(name: "abc", newName: "def" schema: "{optional}") - */ } diff --git a/src/HotChocolate/Stitching/src/Stitching.Redis/Class1.cs b/src/HotChocolate/Stitching/src/Stitching.Redis/Class1.cs deleted file mode 100644 index 0805fc00a32..00000000000 --- a/src/HotChocolate/Stitching/src/Stitching.Redis/Class1.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using HotChocolate.Execution.Configuration; -using HotChocolate.Execution.Options; -using Microsoft.Extensions.DependencyInjection; - -namespace HotChocolate.Stitching.Redis -{ - public class RedisExecutorOptionsProvider : IRequestExecutorOptionsProvider - { - private NameString _schemaName; - - public RedisExecutorOptionsProvider(NameString schemaName) - { - _schemaName = schemaName; - } - - public async ValueTask> GetOptionsAsync( - CancellationToken cancellationToken) - { - IEnumerable schemaDefinitions = - await GetSchemaDefinitionsAsync(cancellationToken) - .ConfigureAwait(false); - - var list = new List(); - var serviceCollection = new ServiceCollection(); - IRequestExecutorBuilder builder = serviceCollection.AddGraphQL(); - - foreach (RemoteSchemaDefinition schemaDefinition in schemaDefinitions) - { - builder.AddRemoteSchema( - schemaDefinition.Name, - (sp, ct) => new ValueTask(schemaDefinition)); - - IServiceProvider services = serviceCollection.BuildServiceProvider(); - IRequestExecutorOptionsMonitor optionsMonitor = - services.GetRequiredService(); - - RequestExecutorFactoryOptions options = - await optionsMonitor.GetAsync(schemaDefinition.Name, cancellationToken) - .ConfigureAwait(false); - // list.Add(new NamedRequestExecutorFactoryOptions(schemaDefinition.Name, options)); - } - - return list; - } - - public IDisposable OnChange(Action listener) - { - throw new NotImplementedException(); - } - - - private async ValueTask> GetSchemaDefinitionsAsync( - CancellationToken cancellationToken) => throw new NotImplementedException(); - } -} diff --git a/src/HotChocolate/Stitching/src/Stitching.Redis/DependencyInjection/HotChocolateStitchingRedisPublishSchemaDefinitionDescriptorExtensions.cs b/src/HotChocolate/Stitching/src/Stitching.Redis/DependencyInjection/HotChocolateStitchingRedisPublishSchemaDefinitionDescriptorExtensions.cs new file mode 100644 index 00000000000..1917a24a469 --- /dev/null +++ b/src/HotChocolate/Stitching/src/Stitching.Redis/DependencyInjection/HotChocolateStitchingRedisPublishSchemaDefinitionDescriptorExtensions.cs @@ -0,0 +1,30 @@ +using System; +using HotChocolate; +using HotChocolate.Stitching.Redis; +using HotChocolate.Stitching.SchemaDefinitions; +using StackExchange.Redis; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class HotChocolateStitchingRedisPublishSchemaDefinitionDescriptorExtensions + { + public static IPublishSchemaDefinitionDescriptor PublishToRedis( + this IPublishSchemaDefinitionDescriptor descriptor, + NameString configurationName, + Func connectionFactory) + { + if (connectionFactory is null) + { + throw new ArgumentNullException(nameof(connectionFactory)); + } + + configurationName.EnsureNotEmpty(nameof(configurationName)); + + return descriptor.SetSchemaDefinitionPublisher(sp => + { + IConnectionMultiplexer connection = connectionFactory(sp); + return new RedisSchemaDefinitionPublisher(configurationName, connection); + }); + } + } +} diff --git a/src/HotChocolate/Stitching/src/Stitching.Redis/DependencyInjection/HotChocolateStitchingRedisRequestExecutorBuilderExtensions.cs b/src/HotChocolate/Stitching/src/Stitching.Redis/DependencyInjection/HotChocolateStitchingRedisRequestExecutorBuilderExtensions.cs new file mode 100644 index 00000000000..e874b76df43 --- /dev/null +++ b/src/HotChocolate/Stitching/src/Stitching.Redis/DependencyInjection/HotChocolateStitchingRedisRequestExecutorBuilderExtensions.cs @@ -0,0 +1,42 @@ +using System; +using Microsoft.Extensions.DependencyInjection.Extensions; +using HotChocolate; +using HotChocolate.Execution.Configuration; +using HotChocolate.Stitching.Redis; +using HotChocolate.Stitching.Requests; +using StackExchange.Redis; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class HotChocolateStitchingRedisRequestExecutorBuilderExtensions + { + public static IRequestExecutorBuilder AddRemoteSchemasFromRedis( + this IRequestExecutorBuilder builder, + NameString configurationName, + Func connectionFactory) + { + if (connectionFactory is null) + { + throw new ArgumentNullException(nameof(connectionFactory)); + } + + configurationName.EnsureNotEmpty(nameof(configurationName)); + + builder.Services.AddSingleton(sp => + { + IConnectionMultiplexer connection = connectionFactory(sp); + IDatabase database = connection.GetDatabase(); + ISubscriber subscriber = connection.GetSubscriber(); + return new RedisExecutorOptionsProvider( + builder.Name, configurationName, database, subscriber); + }); + + // Last but not least, we will setup the stitching context which will + // provide access to the remote executors which in turn use the just configured + // request executor proxies to send requests to the downstream services. + builder.Services.TryAddScoped(); + + return builder; + } + } +} diff --git a/src/HotChocolate/Stitching/src/Stitching.Redis/Stitching.Redis.csproj b/src/HotChocolate/Stitching/src/Stitching.Redis/HotChocolate.Stitching.Redis.csproj similarity index 89% rename from src/HotChocolate/Stitching/src/Stitching.Redis/Stitching.Redis.csproj rename to src/HotChocolate/Stitching/src/Stitching.Redis/HotChocolate.Stitching.Redis.csproj index 7d591b1ede4..86e52d832cd 100644 --- a/src/HotChocolate/Stitching/src/Stitching.Redis/Stitching.Redis.csproj +++ b/src/HotChocolate/Stitching/src/Stitching.Redis/HotChocolate.Stitching.Redis.csproj @@ -2,7 +2,6 @@ HotChocolate.Stitching.Redis - HotChocolate.Stitching.Redis HotChocolate.Stitching.Redis Contains the Hot Chocolate GraphQL schema stitching layer. enable diff --git a/src/HotChocolate/Stitching/src/Stitching.Redis/RedisExecutorOptionsProvider.cs b/src/HotChocolate/Stitching/src/Stitching.Redis/RedisExecutorOptionsProvider.cs new file mode 100644 index 00000000000..15001e7beb4 --- /dev/null +++ b/src/HotChocolate/Stitching/src/Stitching.Redis/RedisExecutorOptionsProvider.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Execution.Configuration; +using HotChocolate.Language; +using StackExchange.Redis; + +namespace HotChocolate.Stitching.Redis +{ + internal class RedisExecutorOptionsProvider : IRequestExecutorOptionsProvider + { + private readonly NameString _schemaName; + private readonly NameString _configurationName; + private readonly IDatabase _database; + private readonly List _listeners = new List(); + + public RedisExecutorOptionsProvider( + NameString schemaName, + NameString configurationName, + IDatabase database, + ISubscriber subscriber) + { + _schemaName = schemaName; + _configurationName = configurationName; + _database = database; + subscriber.Subscribe(configurationName.Value).OnMessage(OnChangeMessageAsync); + } + + public async ValueTask> GetOptionsAsync( + CancellationToken cancellationToken) + { + IEnumerable schemaDefinitions = + await GetSchemaDefinitionsAsync(cancellationToken) + .ConfigureAwait(false); + + var factoryOptions = new List(); + + foreach (RemoteSchemaDefinition schemaDefinition in schemaDefinitions) + { + await CreateFactoryOptionsAsync( + schemaDefinition, + factoryOptions, + cancellationToken) + .ConfigureAwait(false); + } + + return factoryOptions; + } + + public IDisposable OnChange(Action listener) => + new OnChangeListener(_listeners, listener); + + private async Task OnChangeMessageAsync(ChannelMessage message) + { + string schemaName = message.Message; + + RemoteSchemaDefinition schemaDefinition = + await GetRemoteSchemaDefinitionAsync(schemaName) + .ConfigureAwait(false); + + var factoryOptions = new List(); + await CreateFactoryOptionsAsync(schemaDefinition, factoryOptions, default) + .ConfigureAwait(false); + + lock (_listeners) + { + foreach (OnChangeListener listener in _listeners) + { + foreach (IConfigureRequestExecutorSetup options in factoryOptions) + { + listener.OnChange(options); + } + } + } + } + + private async ValueTask> GetSchemaDefinitionsAsync( + CancellationToken cancellationToken) + { + RedisValue[] items = await _database.SetMembersAsync(_configurationName.Value) + .ConfigureAwait(false); + + var schemaDefinitions = new List(); + + foreach (var schemaName in items.Select(t => (string)t)) + { + cancellationToken.ThrowIfCancellationRequested(); + + RemoteSchemaDefinition schemaDefinition = + await GetRemoteSchemaDefinitionAsync(schemaName) + .ConfigureAwait(false); + + schemaDefinitions.Add(schemaDefinition); + } + + return schemaDefinitions; + } + + private async Task CreateFactoryOptionsAsync( + RemoteSchemaDefinition schemaDefinition, + IList factoryOptions, + CancellationToken cancellationToken) + { + await using ServiceProvider services = + new ServiceCollection() + .AddGraphQL(_schemaName) + .AddRemoteSchema( + schemaDefinition.Name, + (sp, ct) => new ValueTask(schemaDefinition)) + .Services + .BuildServiceProvider(); + + IRequestExecutorOptionsMonitor optionsMonitor = + services.GetRequiredService(); + + RequestExecutorSetup options = + await optionsMonitor.GetAsync(schemaDefinition.Name, cancellationToken) + .ConfigureAwait(false); + + factoryOptions.Add(new ConfigureRequestExecutorSetup(schemaDefinition.Name, options)); + + options = + await optionsMonitor.GetAsync(_schemaName, cancellationToken) + .ConfigureAwait(false); + + factoryOptions.Add(new ConfigureRequestExecutorSetup(_schemaName, options)); + } + + private async Task GetRemoteSchemaDefinitionAsync(string schemaName) + { + string key = $"{_configurationName}.{schemaName}"; + var json = (byte[])await _database.StringGetAsync(key).ConfigureAwait(false); + SchemaDefinitionDto? dto = JsonSerializer.Deserialize(json); + + return new RemoteSchemaDefinition( + dto.Name, + Utf8GraphQLParser.Parse(dto.Document), + dto.ExtensionDocuments.Select(Utf8GraphQLParser.Parse)); + } + + private sealed class OnChangeListener : IDisposable + { + private readonly List _listeners; + private readonly Action _onChange; + + public OnChangeListener( + List listeners, + Action onChange) + { + _listeners = listeners; + _onChange = onChange; + + lock (_listeners) + { + _listeners.Add(this); + } + } + + public void OnChange(IConfigureRequestExecutorSetup options) => + _onChange(options); + + public void Dispose() + { + lock (_listeners) + { + _listeners.Remove(this); + } + } + } + } +} diff --git a/src/HotChocolate/Stitching/src/Stitching.Redis/RedisSchemaDefinitionPublisher.cs b/src/HotChocolate/Stitching/src/Stitching.Redis/RedisSchemaDefinitionPublisher.cs new file mode 100644 index 00000000000..7a3f4ccfa98 --- /dev/null +++ b/src/HotChocolate/Stitching/src/Stitching.Redis/RedisSchemaDefinitionPublisher.cs @@ -0,0 +1,54 @@ +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using HotChocolate.Stitching.SchemaDefinitions; +using StackExchange.Redis; + +namespace HotChocolate.Stitching.Redis +{ + public class RedisSchemaDefinitionPublisher : ISchemaDefinitionPublisher + { + private readonly IConnectionMultiplexer _connection; + private readonly NameString _configurationName; + + public RedisSchemaDefinitionPublisher( + NameString configurationName, + IConnectionMultiplexer connection) + { + _connection = connection; + _configurationName = configurationName; + } + + public async ValueTask PublishAsync( + RemoteSchemaDefinition schemaDefinition, + CancellationToken cancellationToken = default) + { + string key = $"{_configurationName}.{schemaDefinition.Name}"; + string json = SerializeSchemaDefinition(schemaDefinition); + + IDatabase database = _connection.GetDatabase(); + await database.StringSetAsync(key, json).ConfigureAwait(false); + await database.SetAddAsync(_configurationName.Value, schemaDefinition.Name.Value) + .ConfigureAwait(false); + + ISubscriber subscriber = _connection.GetSubscriber(); + await subscriber.PublishAsync(_configurationName.Value, schemaDefinition.Name.Value) + .ConfigureAwait(false); + } + + private string SerializeSchemaDefinition(RemoteSchemaDefinition schemaDefinition) + { + var dto = new SchemaDefinitionDto + { + Name = schemaDefinition.Name.Value, + Document = schemaDefinition.Document.ToString(false), + }; + + dto.ExtensionDocuments.AddRange( + schemaDefinition.ExtensionDocuments.Select(t => t.ToString()).ToList()); + + return JsonSerializer.Serialize(dto); + } + } +} diff --git a/src/HotChocolate/Stitching/src/Stitching.Redis/SchemaDefinitionDto.cs b/src/HotChocolate/Stitching/src/Stitching.Redis/SchemaDefinitionDto.cs new file mode 100644 index 00000000000..4e056dba590 --- /dev/null +++ b/src/HotChocolate/Stitching/src/Stitching.Redis/SchemaDefinitionDto.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using HotChocolate.Execution.Options; + +namespace HotChocolate.Stitching.Redis +{ + internal sealed class SchemaDefinitionDto + { + public string Name { get; set; } + + public string? Document { get; set; } + + public List ExtensionDocuments { get; set; } = new List(); + } +} diff --git a/src/HotChocolate/Stitching/src/Stitching/Utilities/ExtractFieldQuerySyntaxRewriter.Context.cs b/src/HotChocolate/Stitching/src/Stitching/Delegation/ExtractFieldQuerySyntaxRewriter.Context.cs similarity index 97% rename from src/HotChocolate/Stitching/src/Stitching/Utilities/ExtractFieldQuerySyntaxRewriter.Context.cs rename to src/HotChocolate/Stitching/src/Stitching/Delegation/ExtractFieldQuerySyntaxRewriter.Context.cs index 77fe4328d1b..3d09454a052 100644 --- a/src/HotChocolate/Stitching/src/Stitching/Utilities/ExtractFieldQuerySyntaxRewriter.Context.cs +++ b/src/HotChocolate/Stitching/src/Stitching/Delegation/ExtractFieldQuerySyntaxRewriter.Context.cs @@ -3,7 +3,7 @@ using HotChocolate.Language; using HotChocolate.Types; -namespace HotChocolate.Stitching.Utilities +namespace HotChocolate.Stitching.Delegation { public partial class ExtractFieldQuerySyntaxRewriter { diff --git a/src/HotChocolate/Stitching/src/Stitching/Utilities/ExtractFieldQuerySyntaxRewriter.cs b/src/HotChocolate/Stitching/src/Stitching/Delegation/ExtractFieldQuerySyntaxRewriter.cs similarity index 98% rename from src/HotChocolate/Stitching/src/Stitching/Utilities/ExtractFieldQuerySyntaxRewriter.cs rename to src/HotChocolate/Stitching/src/Stitching/Delegation/ExtractFieldQuerySyntaxRewriter.cs index cd7328482b7..5e7c5fd6127 100644 --- a/src/HotChocolate/Stitching/src/Stitching/Utilities/ExtractFieldQuerySyntaxRewriter.cs +++ b/src/HotChocolate/Stitching/src/Stitching/Delegation/ExtractFieldQuerySyntaxRewriter.cs @@ -2,10 +2,11 @@ using System.Collections.Generic; using System.Linq; using HotChocolate.Language; +using HotChocolate.Stitching.Utilities; using HotChocolate.Types; using HotChocolate.Utilities; -namespace HotChocolate.Stitching.Utilities +namespace HotChocolate.Stitching.Delegation { public partial class ExtractFieldQuerySyntaxRewriter : QuerySyntaxRewriter @@ -112,7 +113,7 @@ public partial class ExtractFieldQuerySyntaxRewriter FieldNode current = node; if (field.TryGetSourceDirective(context.Schema, - out SourceDirective sourceDirective)) + out SourceDirective? sourceDirective)) { if (current.Alias == null) { @@ -162,7 +163,7 @@ public partial class ExtractFieldQuerySyntaxRewriter FieldNode current = node; - for (int i = 0; i < _rewriters.Length; i++) + for (var i = 0; i < _rewriters.Length; i++) { current = _rewriters[i].OnRewriteField( context.Schema, diff --git a/src/HotChocolate/Stitching/src/Stitching/Utilities/ExtractedField.cs b/src/HotChocolate/Stitching/src/Stitching/Delegation/ExtractedField.cs similarity index 94% rename from src/HotChocolate/Stitching/src/Stitching/Utilities/ExtractedField.cs rename to src/HotChocolate/Stitching/src/Stitching/Delegation/ExtractedField.cs index dff3e50f5d5..6774a0c7b2f 100644 --- a/src/HotChocolate/Stitching/src/Stitching/Utilities/ExtractedField.cs +++ b/src/HotChocolate/Stitching/src/Stitching/Delegation/ExtractedField.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using HotChocolate.Language; -namespace HotChocolate.Stitching.Utilities +namespace HotChocolate.Stitching.Delegation { public class ExtractedField { diff --git a/src/HotChocolate/Stitching/src/Stitching/DependencyInjection/HotChocolateStitchingRequestExecutorExtensions.DomainServices.cs b/src/HotChocolate/Stitching/src/Stitching/DependencyInjection/HotChocolateStitchingRequestExecutorExtensions.DomainServices.cs index a80fa817c3f..a8b44a5a24e 100644 --- a/src/HotChocolate/Stitching/src/Stitching/DependencyInjection/HotChocolateStitchingRequestExecutorExtensions.DomainServices.cs +++ b/src/HotChocolate/Stitching/src/Stitching/DependencyInjection/HotChocolateStitchingRequestExecutorExtensions.DomainServices.cs @@ -13,10 +13,15 @@ public static partial class HotChocolateStitchingRequestExecutorExtensions var descriptor = new PublishSchemaDefinitionDescriptor(builder); configure(descriptor); + var typeInterceptor = new SchemaDefinitionTypeInterceptor(!descriptor.HasPublisher); + var schemaInterceptor = new SchemaDefinitionSchemaInterceptor(descriptor); + builder .AddType() - .TryAddTypeInterceptor() - .TryAddSchemaInterceptor(new SchemaDefinitionSchemaInterceptor(descriptor)); + .TryAddTypeInterceptor(typeInterceptor) + .TryAddSchemaInterceptor(schemaInterceptor) + .ConfigureOnRequestExecutorCreatedAsync( + async (sp, executor, ct) => await descriptor.PublishAsync(sp, ct)); return builder; } diff --git a/src/HotChocolate/Stitching/src/Stitching/ErrorHelper.cs b/src/HotChocolate/Stitching/src/Stitching/ErrorHelper.cs new file mode 100644 index 00000000000..383f1551c59 --- /dev/null +++ b/src/HotChocolate/Stitching/src/Stitching/ErrorHelper.cs @@ -0,0 +1,18 @@ +using System.Net; + +namespace HotChocolate.Stitching +{ + public static class ErrorHelper + { + public static IError HttpRequestClient_HttpError( + HttpStatusCode statusCode, + string? responseBody) => + ErrorBuilder.New() + .SetMessage( + "HTTP error {0} while fetching from downstream service.", + statusCode) + .SetCode(ErrorCodes.Stitching.HttpRequestException) + .SetExtension("response", responseBody) + .Build(); + } +} diff --git a/src/HotChocolate/Stitching/src/Stitching/Pipeline/HttpQueryRequest.cs b/src/HotChocolate/Stitching/src/Stitching/Pipeline/HttpQueryRequest.cs deleted file mode 100644 index a241029dec9..00000000000 --- a/src/HotChocolate/Stitching/src/Stitching/Pipeline/HttpQueryRequest.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Collections.Generic; - -namespace HotChocolate.Stitching.Utilities -{ - internal class HttpQueryRequest : IHttpQueryRequest - { - public string Id { get; set; } - public string Query { get; set; } - public string OperationName { get; set; } - public IReadOnlyDictionary Variables { get; set; } - } -} diff --git a/src/HotChocolate/Stitching/src/Stitching/Pipeline/HttpRequestClient.cs b/src/HotChocolate/Stitching/src/Stitching/Pipeline/HttpRequestClient.cs index ec13d763fdb..cda4fedc46f 100644 --- a/src/HotChocolate/Stitching/src/Stitching/Pipeline/HttpRequestClient.cs +++ b/src/HotChocolate/Stitching/src/Stitching/Pipeline/HttpRequestClient.cs @@ -1,13 +1,14 @@ using System; -using System.Buffers; using System.Collections.Generic; using System.IO; using System.Net.Http; +using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using HotChocolate.Execution; using HotChocolate.Language; +using HotChocolate.Stitching.Properties; using HotChocolate.Utilities; #nullable enable @@ -45,8 +46,10 @@ internal class HttpRequestClient NameString targetSchema, CancellationToken cancellationToken = default) { + using var writer = new ArrayWriter(); + HttpRequestMessage requestMessage = - await CreateRequestAsync(request, targetSchema, cancellationToken) + await CreateRequestAsync(writer, request, targetSchema, cancellationToken) .ConfigureAwait(false); return await FetchAsync( @@ -71,12 +74,12 @@ await CreateRequestAsync(request, targetSchema, cancellationToken) .SendAsync(requestMessage, cancellationToken) .ConfigureAwait(false); - // TODO : we should rework this an try to inspect the payload for graphql errors. - responseMessage.EnsureSuccessStatusCode(); - IQueryResult result = - await ParseResponseMessageAsync(responseMessage, cancellationToken) - .ConfigureAwait(false); + responseMessage.IsSuccessStatusCode + ? await ParseResponseMessageAsync(responseMessage, cancellationToken) + .ConfigureAwait(false) + : await ParseErrorResponseMessageAsync(responseMessage, cancellationToken) + .ConfigureAwait(false); return await _requestInterceptor.OnReceivedResultAsync( targetSchema, @@ -86,14 +89,6 @@ await ParseResponseMessageAsync(responseMessage, cancellationToken) cancellationToken) .ConfigureAwait(false); } - catch (HttpRequestException ex) - { - IError error = _errorHandler.CreateUnexpectedError(ex) - .SetCode(ErrorCodes.Stitching.HttpRequestException) - .Build(); - - return QueryResultBuilder.CreateError(error); - } catch(Exception ex) { IError error = _errorHandler.CreateUnexpectedError(ex) @@ -109,10 +104,10 @@ await ParseResponseMessageAsync(responseMessage, cancellationToken) } internal static async ValueTask CreateRequestMessageAsync( + ArrayWriter writer, IQueryRequest request, CancellationToken cancellationToken) { - using var writer = new ArrayWriter(); await using var jsonWriter = new Utf8JsonWriter(writer, _jsonWriterOptions); WriteJsonRequest(writer, jsonWriter, request); @@ -130,31 +125,77 @@ await ParseResponseMessageAsync(responseMessage, cancellationToken) return requestMessage; } + private static async ValueTask ParseErrorResponseMessageAsync( + HttpResponseMessage responseMessage, + CancellationToken cancellationToken) + { +#if NET5_0 + await using Stream stream = await responseMessage.Content + .ReadAsStreamAsync(cancellationToken) + .ConfigureAwait(false); +#else + using Stream stream = await responseMessage.Content + .ReadAsStreamAsync() + .ConfigureAwait(false); +#endif + + try + { + return await ParseResponseMessageAsync(responseMessage, cancellationToken) + .ConfigureAwait(false); + } + catch + { + string? responseBody = null; + + if (stream.Length > 0) + { + var buffer = new byte[stream.Length]; + stream.Seek(0, SeekOrigin.Begin); + await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken) + .ConfigureAwait(false); + responseBody = Encoding.UTF8.GetString(buffer, 0, buffer.Length); + } + + return QueryResultBuilder.CreateError( + ErrorHelper.HttpRequestClient_HttpError( + responseMessage.StatusCode, + responseBody)); + } + } + internal static async ValueTask ParseResponseMessageAsync( HttpResponseMessage responseMessage, CancellationToken cancellationToken) { +#if NET5_0 + await using Stream stream = await responseMessage.Content + .ReadAsStreamAsync(cancellationToken) + .ConfigureAwait(false); +#else using Stream stream = await responseMessage.Content .ReadAsStreamAsync() .ConfigureAwait(false); +#endif IReadOnlyDictionary response = await BufferHelper.ReadAsync( - stream, - ParseResponse, - cancellationToken) + stream, + ParseResponse, + cancellationToken) .ConfigureAwait(false); return HttpResponseDeserializer.Deserialize(response); } private async ValueTask CreateRequestAsync( + ArrayWriter writer, IQueryRequest request, NameString targetSchema, CancellationToken cancellationToken = default) { HttpRequestMessage requestMessage = - await CreateRequestMessageAsync(request, cancellationToken) + await CreateRequestMessageAsync(writer, request, cancellationToken) .ConfigureAwait(false); await _requestInterceptor @@ -169,7 +210,7 @@ await _requestInterceptor Utf8GraphQLRequestParser.ParseResponse(buffer.AsSpan(0, bytesBuffered))!; private static void WriteJsonRequest( - IBufferWriter writer, + ArrayWriter writer, Utf8JsonWriter jsonWriter, IQueryRequest request) { @@ -186,7 +227,7 @@ await _requestInterceptor } private static void WriteJsonRequestVariables( - IBufferWriter writer, + ArrayWriter writer, Utf8JsonWriter jsonWriter, IReadOnlyDictionary? variables) { @@ -207,7 +248,7 @@ await _requestInterceptor } private static void WriteValue( - IBufferWriter writer, + ArrayWriter writer, Utf8JsonWriter jsonWriter, object? value) { @@ -251,17 +292,11 @@ await _requestInterceptor break; case IntValueNode i: - jsonWriter.WriteNumberValue(0); - jsonWriter.Flush(); - writer.Advance(-1); - writer.Write(i.AsSpan()); + WriterNumber(i.AsSpan(), jsonWriter, writer); break; case FloatValueNode f: - jsonWriter.WriteNumberValue(0); - jsonWriter.Flush(); - writer.Advance(-1); - writer.Write(f.AsSpan()); + WriterNumber(f.AsSpan(), jsonWriter, writer); break; case BooleanValueNode b: @@ -270,9 +305,27 @@ await _requestInterceptor default: throw new NotSupportedException( - "Unknown variable value kind."); + StitchingResources.HttpRequestClient_UnknownVariableValueKind); } } } + + private static void WriterNumber( + ReadOnlySpan number, + Utf8JsonWriter jsonWriter, + ArrayWriter arrayWriter) + { + jsonWriter.WriteNumberValue(0); + jsonWriter.Flush(); + arrayWriter.GetInternalBuffer()[arrayWriter.Length - 1] = number[0]; + + if (number.Length > 1) + { + number = number.Slice(1); + Span span = arrayWriter.GetSpan(number.Length); + number.CopyTo(span); + arrayWriter.Advance(number.Length); + } + } } } diff --git a/src/HotChocolate/Stitching/src/Stitching/Pipeline/HttpStitchingRequestInterceptor.cs b/src/HotChocolate/Stitching/src/Stitching/Pipeline/HttpStitchingRequestInterceptor.cs index 5a767555143..9d7941148b2 100644 --- a/src/HotChocolate/Stitching/src/Stitching/Pipeline/HttpStitchingRequestInterceptor.cs +++ b/src/HotChocolate/Stitching/src/Stitching/Pipeline/HttpStitchingRequestInterceptor.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; using HotChocolate.Execution; -namespace HotChocolate.Stitching +namespace HotChocolate.Stitching.Pipeline { public class HttpStitchingRequestInterceptor : IHttpStitchingRequestInterceptor { diff --git a/src/HotChocolate/Stitching/src/Stitching/Pipeline/IHttpQueryRequest.cs b/src/HotChocolate/Stitching/src/Stitching/Pipeline/IHttpQueryRequest.cs deleted file mode 100644 index aa0aaafb6d7..00000000000 --- a/src/HotChocolate/Stitching/src/Stitching/Pipeline/IHttpQueryRequest.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Collections.Generic; - -namespace HotChocolate.Stitching -{ - public interface IHttpQueryRequest - { - string OperationName { get; } - string Id { get; } - string Query { get; } - IReadOnlyDictionary Variables { get; } - } -} diff --git a/src/HotChocolate/Stitching/src/Stitching/Pipeline/IHttpStitchingRequestInterceptor.cs b/src/HotChocolate/Stitching/src/Stitching/Pipeline/IHttpStitchingRequestInterceptor.cs index 42dff467f12..60dcd7de684 100644 --- a/src/HotChocolate/Stitching/src/Stitching/Pipeline/IHttpStitchingRequestInterceptor.cs +++ b/src/HotChocolate/Stitching/src/Stitching/Pipeline/IHttpStitchingRequestInterceptor.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; using HotChocolate.Execution; -namespace HotChocolate.Stitching +namespace HotChocolate.Stitching.Pipeline { public interface IHttpStitchingRequestInterceptor { diff --git a/src/HotChocolate/Stitching/src/Stitching/Properties/StitchingResources.Designer.cs b/src/HotChocolate/Stitching/src/Stitching/Properties/StitchingResources.Designer.cs index 94d8234eb54..3190793a5c8 100644 --- a/src/HotChocolate/Stitching/src/Stitching/Properties/StitchingResources.Designer.cs +++ b/src/HotChocolate/Stitching/src/Stitching/Properties/StitchingResources.Designer.cs @@ -278,5 +278,11 @@ internal class StitchingResources { return ResourceManager.GetString("ThrowHelper_BufferedRequest_OperationNotFound", resourceCulture); } } + + internal static string HttpRequestClient_UnknownVariableValueKind { + get { + return ResourceManager.GetString("HttpRequestClient_UnknownVariableValueKind", resourceCulture); + } + } } } diff --git a/src/HotChocolate/Stitching/src/Stitching/Properties/StitchingResources.resx b/src/HotChocolate/Stitching/src/Stitching/Properties/StitchingResources.resx index 6f36e091957..31ded81c199 100644 --- a/src/HotChocolate/Stitching/src/Stitching/Properties/StitchingResources.resx +++ b/src/HotChocolate/Stitching/src/Stitching/Properties/StitchingResources.resx @@ -234,4 +234,7 @@ The provided remote query does not contain the specified operation.\r\n\r\n`{0}` + + Unknown variable value kind. + diff --git a/src/HotChocolate/Stitching/src/Stitching/Requests/MergeRequestHelper.cs b/src/HotChocolate/Stitching/src/Stitching/Requests/MergeRequestHelper.cs index 86e73c3c3eb..1ba3ecb70b4 100644 --- a/src/HotChocolate/Stitching/src/Stitching/Requests/MergeRequestHelper.cs +++ b/src/HotChocolate/Stitching/src/Stitching/Requests/MergeRequestHelper.cs @@ -107,7 +107,7 @@ internal class MergeRequestHelper variableValues, requestPrefix); - bool isAutoGenerated = + var isAutoGenerated = bufferedRequest.Request.ContextData?.ContainsKey(IsAutoGenerated) ?? false; bufferedRequest.Aliases = rewriter.AddQuery( @@ -159,7 +159,7 @@ internal class MergeRequestHelper { foreach (IError error in mergedResult.Errors) { - if (TryResolveField(error, aliases, out string? responseName)) + if (TryResolveField(error, aliases, out var responseName)) { handledErrors.Add(error); result.AddError(RewriteError(error, responseName)); @@ -201,7 +201,7 @@ private static IError RewriteError(IError error, string responseName) [NotNullWhen(true)] out string? responseName) { if (GetRoot(error.Path) is NamePathSegment root && - aliases.TryGetValue(root.Name, out string? s)) + aliases.TryGetValue(root.Name, out var s)) { responseName = s; return true; @@ -235,7 +235,7 @@ private static Path ReplaceRoot(Path path, string responseName) try { - int i = path.Depth; + var i = path.Depth; Path? current = path; do diff --git a/src/HotChocolate/Stitching/src/Stitching/Requests/MergeRequestRewriter.cs b/src/HotChocolate/Stitching/src/Stitching/Requests/MergeRequestRewriter.cs index 534d876521e..a967701ba7b 100644 --- a/src/HotChocolate/Stitching/src/Stitching/Requests/MergeRequestRewriter.cs +++ b/src/HotChocolate/Stitching/src/Stitching/Requests/MergeRequestRewriter.cs @@ -8,7 +8,6 @@ namespace HotChocolate.Stitching.Requests internal class MergeRequestRewriter : QuerySyntaxRewriter { private static readonly NameNode _defaultName = new NameNode("exec_batch"); - private static readonly HashSet _emptySet = new HashSet(); private readonly List _fields = new List(); private readonly Dictionary _variables = diff --git a/src/HotChocolate/Stitching/src/Stitching/Requests/StitchingContext.cs b/src/HotChocolate/Stitching/src/Stitching/Requests/StitchingContext.cs index 326768fbd37..c239b21dc99 100644 --- a/src/HotChocolate/Stitching/src/Stitching/Requests/StitchingContext.cs +++ b/src/HotChocolate/Stitching/src/Stitching/Requests/StitchingContext.cs @@ -16,12 +16,12 @@ public class StitchingContext : IStitchingContext IBatchScheduler batchScheduler, IRequestContextAccessor requestContextAccessor) { - if (batchScheduler == null) + if (batchScheduler is null) { throw new ArgumentNullException(nameof(batchScheduler)); } - if (requestContextAccessor == null) + if (requestContextAccessor is null) { throw new ArgumentNullException(nameof(requestContextAccessor)); } diff --git a/src/HotChocolate/Stitching/src/Stitching/SchemaDefinitions/IPublishSchemaDefinitionDescriptor.cs b/src/HotChocolate/Stitching/src/Stitching/SchemaDefinitions/IPublishSchemaDefinitionDescriptor.cs index 88e97917df3..3e42fbb462e 100644 --- a/src/HotChocolate/Stitching/src/Stitching/SchemaDefinitions/IPublishSchemaDefinitionDescriptor.cs +++ b/src/HotChocolate/Stitching/src/Stitching/SchemaDefinitions/IPublishSchemaDefinitionDescriptor.cs @@ -1,4 +1,6 @@ +using System; using System.Reflection; +using HotChocolate.Execution.Configuration; namespace HotChocolate.Stitching.SchemaDefinitions { @@ -25,6 +27,9 @@ public interface IPublishSchemaDefinitionDescriptor IPublishSchemaDefinitionDescriptor AddTypeExtensionsFromString( string schemaSdl); + IPublishSchemaDefinitionDescriptor SetSchemaDefinitionPublisher( + Func publisherFactory); + IPublishSchemaDefinitionDescriptor IgnoreRootTypes(); IPublishSchemaDefinitionDescriptor IgnoreType( diff --git a/src/HotChocolate/Stitching/src/Stitching/SchemaDefinitions/ISchemaDefinitionPublisher.cs b/src/HotChocolate/Stitching/src/Stitching/SchemaDefinitions/ISchemaDefinitionPublisher.cs new file mode 100644 index 00000000000..41b6f7caaaf --- /dev/null +++ b/src/HotChocolate/Stitching/src/Stitching/SchemaDefinitions/ISchemaDefinitionPublisher.cs @@ -0,0 +1,12 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace HotChocolate.Stitching.SchemaDefinitions +{ + public interface ISchemaDefinitionPublisher + { + ValueTask PublishAsync( + RemoteSchemaDefinition schemaDefinition, + CancellationToken cancellationToken = default); + } +} diff --git a/src/HotChocolate/Stitching/src/Stitching/SchemaDefinitions/PublishSchemaDefinitionDescriptor.cs b/src/HotChocolate/Stitching/src/Stitching/SchemaDefinitions/PublishSchemaDefinitionDescriptor.cs index ba48c103d47..f0a1c69ef38 100644 --- a/src/HotChocolate/Stitching/src/Stitching/SchemaDefinitions/PublishSchemaDefinitionDescriptor.cs +++ b/src/HotChocolate/Stitching/src/Stitching/SchemaDefinitions/PublishSchemaDefinitionDescriptor.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Reflection; +using System.Threading; using System.Threading.Tasks; using HotChocolate.Execution.Configuration; using HotChocolate.Language; @@ -15,13 +16,17 @@ public class PublishSchemaDefinitionDescriptor : IPublishSchemaDefinitionDescrip private readonly IRequestExecutorBuilder _builder; private readonly string _key = Guid.NewGuid().ToString(); private readonly List _schemaDirectives = new List(); + private Func? _publisherFactory; private NameString _name; + private RemoteSchemaDefinition? _schemaDefinition; public PublishSchemaDefinitionDescriptor(IRequestExecutorBuilder builder) { _builder = builder; } + public bool HasPublisher => _publisherFactory is not null; + public IPublishSchemaDefinitionDescriptor SetName(NameString name) { _name = name; @@ -47,7 +52,6 @@ public IPublishSchemaDefinitionDescriptor AddTypeExtensionsFromFile(string fileN .ReadAllBytesAsync(fileName, ct) .ConfigureAwait(false); #endif - s.AddTypeExtensions(Utf8GraphQLParser.Parse(content), _key); }); @@ -65,16 +69,14 @@ public IPublishSchemaDefinitionDescriptor AddTypeExtensionsFromFile(string fileN if (stream is null) { - // todo : throw helper - throw new SchemaException( - SchemaErrorBuilder.New() - .SetMessage( - "The resource `{0}` was not found!", - key) - .Build()); + throw ThrowHelper.PublishSchemaDefinitionDescriptor_ResourceNotFound(key); } +#if NET5_0 + await using (stream) +#else using (stream) +#endif { var buffer = new byte[stream.Length]; await stream.ReadAsync(buffer, 0, buffer.Length, ct).ConfigureAwait(false); @@ -96,6 +98,13 @@ public IPublishSchemaDefinitionDescriptor AddTypeExtensionsFromString(string sch return this; } + public IPublishSchemaDefinitionDescriptor SetSchemaDefinitionPublisher( + Func publisherFactory) + { + _publisherFactory = publisherFactory; + return this; + } + public IPublishSchemaDefinitionDescriptor IgnoreRootTypes() { _schemaDirectives.Add(new DirectiveNode(DirectiveNames.RemoveRootTypes)); @@ -151,10 +160,25 @@ public IPublishSchemaDefinitionDescriptor IgnoreRootTypes() extensionDocuments.Add(new DocumentNode(new[] { schemaExtension })); } - return new RemoteSchemaDefinition( + _schemaDefinition = new RemoteSchemaDefinition( _name.HasValue ? _name : schema.Name, schema.ToDocument(), extensionDocuments); + + return _schemaDefinition; + } + + public async ValueTask PublishAsync( + IServiceProvider applicationServices, + CancellationToken cancellationToken = default) + { + if (_publisherFactory is not null && + _schemaDefinition is not null) + { + ISchemaDefinitionPublisher publisher = _publisherFactory(applicationServices); + await publisher.PublishAsync(_schemaDefinition, cancellationToken) + .ConfigureAwait(false); + } } } } diff --git a/src/HotChocolate/Stitching/src/Stitching/SchemaDefinitions/SchemaDefinitionTypeInterceptor.cs b/src/HotChocolate/Stitching/src/Stitching/SchemaDefinitions/SchemaDefinitionTypeInterceptor.cs index 9c1c049ccd3..461a244b5d8 100644 --- a/src/HotChocolate/Stitching/src/Stitching/SchemaDefinitions/SchemaDefinitionTypeInterceptor.cs +++ b/src/HotChocolate/Stitching/src/Stitching/SchemaDefinitions/SchemaDefinitionTypeInterceptor.cs @@ -11,14 +11,23 @@ namespace HotChocolate.Stitching.SchemaDefinitions { internal class SchemaDefinitionTypeInterceptor : TypeInterceptor { + private readonly bool _publishOnSchema; + + public SchemaDefinitionTypeInterceptor(bool publishOnSchema) + { + _publishOnSchema = publishOnSchema; + } + public override void OnBeforeCompleteType( ITypeCompletionContext completionContext, DefinitionBase? definition, IDictionary contextData) { // when we are visiting the query type we will add the schema definition field. - if ((completionContext.IsQueryType ?? false) && - definition is ObjectTypeDefinition objectTypeDefinition) + if (_publishOnSchema && + (completionContext.IsQueryType ?? false) && + definition is ObjectTypeDefinition objectTypeDefinition && + !objectTypeDefinition.Fields.Any(t => t.Name.Equals(SchemaDefinitionField))) { ObjectFieldDefinition typeNameField = objectTypeDefinition.Fields.First( t => t.Name.Equals(IntrospectionFields.TypeName) && t.IsIntrospectionField); diff --git a/src/HotChocolate/Stitching/src/Stitching/ThrowHelper.cs b/src/HotChocolate/Stitching/src/Stitching/ThrowHelper.cs index 1a46e068a64..6b27b2f043b 100644 --- a/src/HotChocolate/Stitching/src/Stitching/ThrowHelper.cs +++ b/src/HotChocolate/Stitching/src/Stitching/ThrowHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using HotChocolate.Language; using HotChocolate.Stitching.Properties; using static HotChocolate.Stitching.Properties.StitchingResources; @@ -57,5 +58,22 @@ internal static class ThrowHelper .SetPath(path) .AddLocation(fieldSelection) .Build()); + + public static SchemaException PublishSchemaDefinitionDescriptor_ResourceNotFound( + string key) => + new SchemaException( + SchemaErrorBuilder.New() + .SetMessage( + "The resource `{0}` was not found!", + key) + .Build()); + + public static SchemaException IntrospectionHelper_UnableToFetchSchemaDefinition( + IReadOnlyList errors) => + new SchemaException( + SchemaErrorBuilder.New() + .SetMessage("Unable to fetch schema definition.") + .SetExtension("errors", errors) + .Build()); } } diff --git a/src/HotChocolate/Stitching/src/Stitching/Utilities/CopySchemaDefinitionTypeInterceptor.cs b/src/HotChocolate/Stitching/src/Stitching/Utilities/CopySchemaDefinitionTypeInterceptor.cs index cf1f8fef645..1c06041b363 100644 --- a/src/HotChocolate/Stitching/src/Stitching/Utilities/CopySchemaDefinitionTypeInterceptor.cs +++ b/src/HotChocolate/Stitching/src/Stitching/Utilities/CopySchemaDefinitionTypeInterceptor.cs @@ -13,8 +13,8 @@ public class CopySchemaDefinitionTypeInterceptor : TypeInterceptor { if (definition is SchemaTypeDefinition) { - contextData[typeof(RemoteSchemaDefinition).FullName] = - completionContext.ContextData[typeof(RemoteSchemaDefinition).FullName]; + contextData[typeof(RemoteSchemaDefinition).FullName!] = + completionContext.ContextData[typeof(RemoteSchemaDefinition).FullName!]; } } } diff --git a/src/HotChocolate/Stitching/src/Stitching/Utilities/FieldDependency.cs b/src/HotChocolate/Stitching/src/Stitching/Utilities/FieldDependency.cs index 7a1dc0d12ef..f47f726375e 100644 --- a/src/HotChocolate/Stitching/src/Stitching/Utilities/FieldDependency.cs +++ b/src/HotChocolate/Stitching/src/Stitching/Utilities/FieldDependency.cs @@ -16,6 +16,7 @@ public FieldDependency(NameString typeName, NameString fieldName) } public NameString TypeName { get; } + public NameString FieldName { get; } public bool Equals(FieldDependency other) @@ -24,7 +25,7 @@ public bool Equals(FieldDependency other) && other.FieldName.Equals(FieldName); } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (obj is null) { diff --git a/src/HotChocolate/Stitching/src/Stitching/Utilities/FieldDependencyResolver.cs b/src/HotChocolate/Stitching/src/Stitching/Utilities/FieldDependencyResolver.cs index 9253e488910..508bebc782b 100644 --- a/src/HotChocolate/Stitching/src/Stitching/Utilities/FieldDependencyResolver.cs +++ b/src/HotChocolate/Stitching/src/Stitching/Utilities/FieldDependencyResolver.cs @@ -42,7 +42,10 @@ public FieldDependencyResolver(ISchema schema) var context = Context.New(declaringType, GetFragments(document)); - VisitSelectionSet(field.SelectionSet, context); + if (field.SelectionSet is not null) + { + VisitSelectionSet(field.SelectionSet, context); + } return context.Dependencies; } @@ -136,7 +139,7 @@ protected override void VisitField(FieldNode node, Context context) { if (type.Fields.TryGetField( fieldName, - out IOutputField dependency)) + out IOutputField? dependency)) { context.Dependencies.Add(new FieldDependency( type.Name, dependency.Name)); @@ -173,7 +176,7 @@ protected override void VisitField(FieldNode node, Context context) base.VisitFragmentSpread(node, context); if (context.Fragments.TryGetValue(node.Name.Value, - out FragmentDefinitionNode fragment)) + out FragmentDefinitionNode? fragment)) { VisitFragmentDefinition(fragment, context); } diff --git a/src/HotChocolate/Stitching/src/Stitching/Utilities/IQueryDelegationRewriter.cs b/src/HotChocolate/Stitching/src/Stitching/Utilities/IQueryDelegationRewriter.cs index 5b27f1fe49f..d51eb1bcd68 100644 --- a/src/HotChocolate/Stitching/src/Stitching/Utilities/IQueryDelegationRewriter.cs +++ b/src/HotChocolate/Stitching/src/Stitching/Utilities/IQueryDelegationRewriter.cs @@ -1,7 +1,7 @@ using HotChocolate.Language; using HotChocolate.Types; -namespace HotChocolate.Stitching +namespace HotChocolate.Stitching.Utilities { /// /// This interface provides the query delegation rewriter hooks. @@ -23,8 +23,8 @@ public interface IQueryDelegationRewriter /// /// The current output field on which this selection set is declared. /// - /// - /// The list of selections that shall be added to the delegation query. + /// + /// The field selection syntax node. /// FieldNode OnRewriteField( NameString targetSchemaName, diff --git a/src/HotChocolate/Stitching/src/Stitching/Utilities/IntrospectionHelper.cs b/src/HotChocolate/Stitching/src/Stitching/Utilities/IntrospectionHelper.cs index 536427cbaf2..b09a0f545d2 100644 --- a/src/HotChocolate/Stitching/src/Stitching/Utilities/IntrospectionHelper.cs +++ b/src/HotChocolate/Stitching/src/Stitching/Utilities/IntrospectionHelper.cs @@ -92,21 +92,23 @@ private static bool ProvidesSchemaDefinition(DocumentNode schemaDocument) private async ValueTask FetchSchemaDefinitionAsync( CancellationToken cancellationToken) { + using var writer = new ArrayWriter(); + IQueryRequest request = QueryRequestBuilder.New() .SetQuery( - $@"query GetSchemaDefinition($configuration: String!) {{ - {SchemaDefinitionFieldNames.SchemaDefinitionField}(configuration: $configuration) {{ - name - document - extensionDocuments - }} - }}") - .SetVariableValue("configuration", new StringValueNode(_configuration.Value)) + $@"query GetSchemaDefinition($c: String!) {{ + {SchemaDefinitionFieldNames.SchemaDefinitionField}(configuration: $c) {{ + name + document + extensionDocuments + }} + }}") + .SetVariableValue("c", new StringValueNode(_configuration.Value)) .Create(); HttpRequestMessage requestMessage = await HttpRequestClient - .CreateRequestMessageAsync(request, cancellationToken) + .CreateRequestMessageAsync(writer, request, cancellationToken) .ConfigureAwait(false); HttpResponseMessage responseMessage = await _httpClient @@ -119,15 +121,12 @@ private static bool ProvidesSchemaDefinition(DocumentNode schemaDocument) if (result.Errors is { Count: > 1 }) { - // TODO : throw helper - throw new SchemaException( - SchemaErrorBuilder.New() - .SetMessage("Unable to fetch schema definition.") - .SetExtension("errors", result.Errors) - .Build()); + throw ThrowHelper.IntrospectionHelper_UnableToFetchSchemaDefinition(result.Errors); } - if (result.Data[SchemaDefinitionFieldNames.SchemaDefinitionField] is IReadOnlyDictionary o && + if (result.Data is not null && + result.Data[SchemaDefinitionFieldNames.SchemaDefinitionField] + is IReadOnlyDictionary o && o.TryGetValue("name", out object? n) && n is StringValueNode name && o.TryGetValue("document", out object? d) && diff --git a/src/HotChocolate/Stitching/src/Stitching/Utilities/QueryDelegationRewriterBase.cs b/src/HotChocolate/Stitching/src/Stitching/Utilities/QueryDelegationRewriterBase.cs index 19ce1cf2989..7956ed72cef 100644 --- a/src/HotChocolate/Stitching/src/Stitching/Utilities/QueryDelegationRewriterBase.cs +++ b/src/HotChocolate/Stitching/src/Stitching/Utilities/QueryDelegationRewriterBase.cs @@ -1,7 +1,7 @@ using HotChocolate.Language; using HotChocolate.Types; -namespace HotChocolate.Stitching +namespace HotChocolate.Stitching.Utilities { public class QueryDelegationRewriterBase : IQueryDelegationRewriter @@ -19,8 +19,8 @@ public class QueryDelegationRewriterBase /// /// The current output field on which this selection set is declared. /// - /// - /// The list of selections that shall be added to the delegation query. + /// + /// The field selection syntax node. /// public virtual FieldNode OnRewriteField( NameString targetSchemaName, diff --git a/src/HotChocolate/Stitching/src/Stitching/Utilities/StitchingTypeInterceptor.cs b/src/HotChocolate/Stitching/src/Stitching/Utilities/StitchingTypeInterceptor.cs index 7f6155bcb9c..ffb8bc45bf5 100644 --- a/src/HotChocolate/Stitching/src/Stitching/Utilities/StitchingTypeInterceptor.cs +++ b/src/HotChocolate/Stitching/src/Stitching/Utilities/StitchingTypeInterceptor.cs @@ -10,7 +10,7 @@ namespace HotChocolate.Stitching.Utilities { - public class StitchingTypeInterceptor : TypeInterceptor + internal class StitchingTypeInterceptor : TypeInterceptor { private readonly HashSet<(NameString, NameString)> _handledExternalFields = new HashSet<(NameString, NameString)>(); diff --git a/src/HotChocolate/Stitching/src/Stitching/WellKnownContextData.cs b/src/HotChocolate/Stitching/src/Stitching/WellKnownContextData.cs index f8f6a88ef51..6179fa86e98 100644 --- a/src/HotChocolate/Stitching/src/Stitching/WellKnownContextData.cs +++ b/src/HotChocolate/Stitching/src/Stitching/WellKnownContextData.cs @@ -2,7 +2,7 @@ namespace HotChocolate.Stitching { internal static class WellKnownContextData { - public const string IsAutoGenerated = "__hc_IsAutoGenerated"; + public const string IsAutoGenerated = "HotChocolate.Stitching.IsAutoGenerated"; public const string SchemaName = "HotChocolate.Stitching.SchemaName"; public const string RemoteExecutors = "HotChocolate.Stitching.Executor"; public const string TypeMergeRules = "HotChocolate.Stitching.TypeMergeRules"; diff --git a/src/HotChocolate/Stitching/test/Stitching.Tests/HotChocolate.Stitching.Tests.csproj b/src/HotChocolate/Stitching/test/Stitching.Tests/HotChocolate.Stitching.Tests.csproj index 55a8be3407b..b6852bda4ec 100644 --- a/src/HotChocolate/Stitching/test/Stitching.Tests/HotChocolate.Stitching.Tests.csproj +++ b/src/HotChocolate/Stitching/test/Stitching.Tests/HotChocolate.Stitching.Tests.csproj @@ -9,11 +9,13 @@ + + diff --git a/src/HotChocolate/Stitching/test/Stitching.Tests/Integration/FederatedRedisSchemaTests.cs b/src/HotChocolate/Stitching/test/Stitching.Tests/Integration/FederatedRedisSchemaTests.cs new file mode 100644 index 00000000000..1da595aca34 --- /dev/null +++ b/src/HotChocolate/Stitching/test/Stitching.Tests/Integration/FederatedRedisSchemaTests.cs @@ -0,0 +1,342 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using ChilliCream.Testing; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.AspNetCore.Utilities; +using HotChocolate.Execution; +using HotChocolate.Stitching.Schemas.Accounts; +using HotChocolate.Stitching.Schemas.Inventory; +using HotChocolate.Stitching.Schemas.Products; +using HotChocolate.Stitching.Schemas.Reviews; +using HotChocolate.Types; +using Snapshooter.Xunit; +using Squadron; +using StackExchange.Redis; +using Xunit; + +namespace HotChocolate.Stitching.Integration +{ + public class FederatedRedisSchemaTests + : IClassFixture + , IClassFixture + { + private const string _accounts = "accounts"; + private const string _inventory = "inventory"; + private const string _products = "products"; + private const string _reviews = "reviews"; + + private readonly ConnectionMultiplexer _connection; + + public FederatedRedisSchemaTests(StitchingTestContext context, RedisResource redisResource) + { + Context = context; + _connection = redisResource.GetConnection(); + } + + private StitchingTestContext Context { get; } + + [Fact] + public async Task AutoMerge_Schema() + { + // arrange + NameString configurationName = "C" + Guid.NewGuid().ToString("N"); + IHttpClientFactory httpClientFactory = CreateDefaultRemoteSchemas(configurationName); + + IDatabase database = _connection.GetDatabase(); + for (int i = 0; i < 10; i++) + { + if (await database.SetLengthAsync(configurationName.Value) == 4) + { + break; + } + + await Task.Delay(150); + } + + // act + ISchema schema = + await new ServiceCollection() + .AddSingleton(httpClientFactory) + .AddGraphQL() + .AddQueryType(d => d.Name("Query")) + .AddRemoteSchemasFromRedis(configurationName, s => _connection) + .ModifyOptions(o => o.SortFieldsByName = true) + .BuildSchemaAsync(); + + // assert + schema.Print().MatchSnapshot(); + } + + [Fact] + public async Task AutoMerge_HotReload_Schema() + { + // arrange + NameString configurationName = "C" + Guid.NewGuid().ToString("N"); + var schemaDefinitionV2 = FileResource.Open("AccountSchemaDefinition.json"); + IHttpClientFactory httpClientFactory = CreateDefaultRemoteSchemas(configurationName); + + IDatabase database = _connection.GetDatabase(); + for (var i = 0; i < 10; i++) + { + if (await database.SetLengthAsync(configurationName.Value) == 4) + { + break; + } + + await Task.Delay(150); + } + + IRequestExecutorResolver executorResolver = + new ServiceCollection() + .AddSingleton(httpClientFactory) + .AddGraphQL() + .AddQueryType(d => d.Name("Query")) + .AddRemoteSchemasFromRedis(configurationName, s => _connection) + .Services + .BuildServiceProvider() + .GetRequiredService(); + + await executorResolver.GetRequestExecutorAsync(); + var raised = false; + + executorResolver.RequestExecutorEvicted += (sender, args) => + { + if (args.Name.Equals(Schema.DefaultName)) + { + raised = true; + } + }; + + // act + Assert.False(raised, "eviction was raised before act."); + await database.StringSetAsync($"{configurationName}.{_accounts}", schemaDefinitionV2); + await _connection.GetSubscriber().PublishAsync(configurationName.Value, _accounts); + + for (var i = 0; i < 10; i++) + { + if (raised) + { + break; + } + + await Task.Delay(150); + } + + // assert + Assert.True(raised, "schema evicted."); + IRequestExecutor executor = await executorResolver.GetRequestExecutorAsync(); + ObjectType type = executor.Schema.GetType("User"); + Assert.True(type.Fields.ContainsField("foo"), "foo field exists."); + } + + [Fact] + public async Task AutoMerge_Execute() + { + // arrange + NameString configurationName = "C" + Guid.NewGuid().ToString("N"); + IHttpClientFactory httpClientFactory = CreateDefaultRemoteSchemas(configurationName); + + IDatabase database = _connection.GetDatabase(); + for (int i = 0; i < 10; i++) + { + if (await database.SetLengthAsync(configurationName.Value) == 4) + { + break; + } + + await Task.Delay(150); + } + + IRequestExecutor executor = + await new ServiceCollection() + .AddSingleton(httpClientFactory) + .AddGraphQL() + .AddQueryType(d => d.Name("Query")) + .AddRemoteSchemasFromRedis(configurationName, s => _connection) + .BuildRequestExecutorAsync(); + + // act + IExecutionResult result = await executor.ExecuteAsync( + @"{ + me { + id + name + reviews { + body + product { + upc + } + } + } + }"); + + // assert + result.ToJson().MatchSnapshot(); + } + + [Fact] + public async Task AutoMerge_AddLocal_Field_Execute() + { + // arrange + NameString configurationName = "C" + Guid.NewGuid().ToString("N"); + IHttpClientFactory httpClientFactory = CreateDefaultRemoteSchemas(configurationName); + + IDatabase database = _connection.GetDatabase(); + for (int i = 0; i < 10; i++) + { + if (await database.SetLengthAsync(configurationName.Value) == 4) + { + break; + } + + await Task.Delay(150); + } + + IRequestExecutor executor = + await new ServiceCollection() + .AddSingleton(httpClientFactory) + .AddGraphQL(configurationName) + .AddQueryType(d => d.Name("Query").Field("local").Resolve("I am local.")) + .AddRemoteSchemasFromRedis(configurationName, s => _connection) + .BuildRequestExecutorAsync(configurationName); + + // act + IExecutionResult result = await executor.ExecuteAsync( + @"{ + me { + id + name + reviews { + body + product { + upc + } + } + } + local + }"); + + // assert + result.ToJson().MatchSnapshot(); + } + + public TestServer CreateAccountsService(NameString configurationName) => + Context.ServerFactory.Create( + services => services + .AddRouting() + .AddHttpRequestSerializer(HttpResultSerialization.JsonArray) + .AddGraphQLServer() + .AddAccountsSchema() + .InitializeOnStartup() + .PublishSchemaDefinition(c => c + .SetName(_accounts) + .IgnoreRootTypes() + .AddTypeExtensionsFromString( + @"extend type Query { + me: User! @delegate(path: ""user(id: 1)"") + } + + extend type Review { + author: User @delegate(path: ""user(id: $fields:authorId)"") + }") + .PublishToRedis(configurationName, sp => _connection)), + app => app + .UseWebSockets() + .UseRouting() + .UseEndpoints(endpoints => endpoints.MapGraphQL("/"))); + + public TestServer CreateInventoryService(NameString configurationName) => + Context.ServerFactory.Create( + services => services + .AddRouting() + .AddHttpRequestSerializer(HttpResultSerialization.JsonArray) + .AddGraphQLServer() + .AddInventorySchema() + .InitializeOnStartup() + .PublishSchemaDefinition(c => c + .SetName(_inventory) + .IgnoreRootTypes() + .AddTypeExtensionsFromString( + @"extend type Product { + inStock: Boolean + @delegate(path: ""inventoryInfo(upc: $fields:upc).isInStock"") + + shippingEstimate: Int + @delegate(path: ""shippingEstimate(price: $fields:price weight: $fields:weight)"") + }") + .PublishToRedis(configurationName, sp => _connection)), + app => app + .UseWebSockets() + .UseRouting() + .UseEndpoints(endpoints => endpoints.MapGraphQL("/"))); + + public TestServer CreateProductsService(NameString configurationName) => + Context.ServerFactory.Create( + services => services + .AddRouting() + .AddHttpRequestSerializer(HttpResultSerialization.JsonArray) + .AddGraphQLServer() + .AddProductsSchema() + .InitializeOnStartup() + .PublishSchemaDefinition(c => c + .SetName(_products) + .IgnoreRootTypes() + .AddTypeExtensionsFromString( + @"extend type Query { + topProducts(first: Int = 5): [Product] @delegate + } + + extend type Review { + product: Product @delegate(path: ""product(upc: $fields:upc)"") + }") + .PublishToRedis(configurationName, sp => _connection)), + app => app + .UseWebSockets() + .UseRouting() + .UseEndpoints(endpoints => endpoints.MapGraphQL("/"))); + + public TestServer CreateReviewsService(NameString configurationName) => + Context.ServerFactory.Create( + services => services + .AddRouting() + .AddHttpRequestSerializer(HttpResultSerialization.JsonArray) + .AddGraphQLServer() + .AddReviewSchema() + .InitializeOnStartup() + .PublishSchemaDefinition(c => c + .SetName(_reviews) + .IgnoreRootTypes() + .AddTypeExtensionsFromString( + @"extend type User { + reviews: [Review] + @delegate(path:""reviewsByAuthor(authorId: $fields:id)"") + } + + extend type Product { + reviews: [Review] + @delegate(path:""reviewsByProduct(upc: $fields:upc)"") + }") + .PublishToRedis(configurationName, sp => _connection)), + app => app + .UseWebSockets() + .UseRouting() + .UseEndpoints(endpoints => endpoints.MapGraphQL("/"))); + + public IHttpClientFactory CreateDefaultRemoteSchemas(NameString configurationName) + { + var connections = new Dictionary + { + { _accounts, CreateAccountsService(configurationName).CreateClient() }, + { _inventory, CreateInventoryService(configurationName).CreateClient() }, + { _products, CreateProductsService(configurationName).CreateClient() }, + { _reviews, CreateReviewsService(configurationName).CreateClient() }, + }; + + return StitchingTestContext.CreateRemoteSchemas(connections); + } + } +} diff --git a/src/HotChocolate/Stitching/test/Stitching.Tests/Integration/FederatedSchemaTests.cs b/src/HotChocolate/Stitching/test/Stitching.Tests/Integration/FederatedSchemaTests.cs index 126da45ea4e..a7f88d24128 100644 --- a/src/HotChocolate/Stitching/test/Stitching.Tests/Integration/FederatedSchemaTests.cs +++ b/src/HotChocolate/Stitching/test/Stitching.Tests/Integration/FederatedSchemaTests.cs @@ -18,10 +18,10 @@ namespace HotChocolate.Stitching.Integration { public class FederatedSchemaTests : IClassFixture { - public const string Accounts = "accounts"; - public const string Inventory = "inventory"; - public const string Products = "products"; - public const string Reviews = "reviews"; + private const string _accounts = "accounts"; + private const string _inventory = "inventory"; + private const string _products = "products"; + private const string _reviews = "reviews"; public FederatedSchemaTests(StitchingTestContext context) { @@ -42,10 +42,10 @@ public async Task AutoMerge_Schema() .AddSingleton(httpClientFactory) .AddGraphQL() .AddQueryType(d => d.Name("Query")) - .AddRemoteSchema(Accounts) - .AddRemoteSchema(Inventory) - .AddRemoteSchema(Products) - .AddRemoteSchema(Reviews) + .AddRemoteSchema(_accounts) + .AddRemoteSchema(_inventory) + .AddRemoteSchema(_products) + .AddRemoteSchema(_reviews) .BuildSchemaAsync(); // assert @@ -63,19 +63,19 @@ public async Task AutoMerge_Execute() .AddSingleton(httpClientFactory) .AddGraphQL() .AddQueryType(d => d.Name("Query")) - .AddRemoteSchema(Accounts) - .AddRemoteSchema(Inventory) - .AddRemoteSchema(Products) - .AddRemoteSchema(Reviews) + .AddRemoteSchema(_accounts) + .AddRemoteSchema(_inventory) + .AddRemoteSchema(_products) + .AddRemoteSchema(_reviews) .BuildRequestExecutorAsync(); - + // act IExecutionResult result = await executor.ExecuteAsync( @"{ me { id - name - reviews { + name + reviews { body product { upc @@ -99,19 +99,19 @@ public async Task AutoMerge_AddLocal_Field_Execute() .AddSingleton(httpClientFactory) .AddGraphQL() .AddQueryType(d => d.Name("Query").Field("local").Resolve("I am local.")) - .AddRemoteSchema(Accounts) - .AddRemoteSchema(Inventory) - .AddRemoteSchema(Products) - .AddRemoteSchema(Reviews) + .AddRemoteSchema(_accounts) + .AddRemoteSchema(_inventory) + .AddRemoteSchema(_products) + .AddRemoteSchema(_reviews) .BuildRequestExecutorAsync(); - + // act IExecutionResult result = await executor.ExecuteAsync( @"{ me { id - name - reviews { + name + reviews { body product { upc @@ -131,7 +131,18 @@ public async Task AutoMerge_AddLocal_Field_Execute() .AddRouting() .AddHttpRequestSerializer(HttpResultSerialization.JsonArray) .AddGraphQLServer() - .AddAccountsSchema(), + .AddAccountsSchema() + .PublishSchemaDefinition(c => c + .SetName(_accounts) + .IgnoreRootTypes() + .AddTypeExtensionsFromString( + @"extend type Query { + me: User! @delegate(path: ""user(id: 1)"") + } + + extend type Review { + author: User @delegate(path: ""user(id: $fields:authorId)"") + }")), app => app .UseWebSockets() .UseRouting() @@ -143,7 +154,17 @@ public async Task AutoMerge_AddLocal_Field_Execute() .AddRouting() .AddHttpRequestSerializer(HttpResultSerialization.JsonArray) .AddGraphQLServer() - .AddInventorySchema(), + .AddInventorySchema() + .PublishSchemaDefinition(c => c + .SetName(_inventory) + .IgnoreRootTypes() + .AddTypeExtensionsFromString( + @"extend type Product { + inStock: Boolean + @delegate(path: ""inventoryInfo(upc: $fields:upc).isInStock"") + shippingEstimate: Int + @delegate(path: ""shippingEstimate(price: $fields:price weight: $fields:weight)"") + }")), app => app .UseWebSockets() .UseRouting() @@ -155,7 +176,18 @@ public async Task AutoMerge_AddLocal_Field_Execute() .AddRouting() .AddHttpRequestSerializer(HttpResultSerialization.JsonArray) .AddGraphQLServer() - .AddProductsSchema(), + .AddProductsSchema() + .PublishSchemaDefinition(c => c + .SetName(_products) + .IgnoreRootTypes() + .AddTypeExtensionsFromString( + @"extend type Query { + topProducts(first: Int = 5): [Product] @delegate + } + + extend type Review { + product: Product @delegate(path: ""product(upc: $fields:upc)"") + }")), app => app .UseWebSockets() .UseRouting() @@ -167,7 +199,20 @@ public async Task AutoMerge_AddLocal_Field_Execute() .AddRouting() .AddHttpRequestSerializer(HttpResultSerialization.JsonArray) .AddGraphQLServer() - .AddReviewSchema(), + .AddReviewSchema() + .PublishSchemaDefinition(c => c + .SetName(_reviews) + .IgnoreRootTypes() + .AddTypeExtensionsFromString( + @"extend type User { + reviews: [Review] + @delegate(path:""reviewsByAuthor(authorId: $fields:id)"") + } + + extend type Product { + reviews: [Review] + @delegate(path:""reviewsByProduct(upc: $fields:upc)"") + }")), app => app .UseWebSockets() .UseRouting() @@ -177,10 +222,10 @@ public IHttpClientFactory CreateDefaultRemoteSchemas() { var connections = new Dictionary { - { Accounts, CreateAccountsService().CreateClient() }, - { Inventory, CreateInventoryService().CreateClient() }, - { Products, CreateProductsService().CreateClient() }, - { Reviews, CreateReviewsService().CreateClient() }, + { _accounts, CreateAccountsService().CreateClient() }, + { _inventory, CreateInventoryService().CreateClient() }, + { _products, CreateProductsService().CreateClient() }, + { _reviews, CreateReviewsService().CreateClient() }, }; return StitchingTestContext.CreateRemoteSchemas(connections); diff --git a/src/HotChocolate/Stitching/test/Stitching.Tests/Integration/__snapshots__/FederatedRedisSchemaTests.AutoMerge_AddLocal_Field_Execute.snap b/src/HotChocolate/Stitching/test/Stitching.Tests/Integration/__snapshots__/FederatedRedisSchemaTests.AutoMerge_AddLocal_Field_Execute.snap new file mode 100644 index 00000000000..eff8ca1d1f6 --- /dev/null +++ b/src/HotChocolate/Stitching/test/Stitching.Tests/Integration/__snapshots__/FederatedRedisSchemaTests.AutoMerge_AddLocal_Field_Execute.snap @@ -0,0 +1,23 @@ +{ + "data": { + "me": { + "id": 1, + "name": "Ada Lovelace", + "reviews": [ + { + "body": "Love it!", + "product": { + "upc": 1 + } + }, + { + "body": "Too expensive.", + "product": { + "upc": 2 + } + } + ] + }, + "local": "I am local." + } +} diff --git a/src/HotChocolate/Stitching/test/Stitching.Tests/Integration/__snapshots__/FederatedRedisSchemaTests.AutoMerge_Execute.snap b/src/HotChocolate/Stitching/test/Stitching.Tests/Integration/__snapshots__/FederatedRedisSchemaTests.AutoMerge_Execute.snap new file mode 100644 index 00000000000..c4678d52031 --- /dev/null +++ b/src/HotChocolate/Stitching/test/Stitching.Tests/Integration/__snapshots__/FederatedRedisSchemaTests.AutoMerge_Execute.snap @@ -0,0 +1,22 @@ +{ + "data": { + "me": { + "id": 1, + "name": "Ada Lovelace", + "reviews": [ + { + "body": "Love it!", + "product": { + "upc": 1 + } + }, + { + "body": "Too expensive.", + "product": { + "upc": 2 + } + } + ] + } + } +} diff --git a/src/HotChocolate/Stitching/test/Stitching.Tests/Integration/__snapshots__/FederatedRedisSchemaTests.AutoMerge_Schema.snap b/src/HotChocolate/Stitching/test/Stitching.Tests/Integration/__snapshots__/FederatedRedisSchemaTests.AutoMerge_Schema.snap new file mode 100644 index 00000000000..bee8003b0d0 --- /dev/null +++ b/src/HotChocolate/Stitching/test/Stitching.Tests/Integration/__snapshots__/FederatedRedisSchemaTests.AutoMerge_Schema.snap @@ -0,0 +1,68 @@ +schema { + query: Query +} + +type InventoryInfo @source(name: "InventoryInfo", schema: "inventory") { + isInStock: Boolean! + upc: Int! +} + +type Product @source(name: "Product", schema: "products") { + inStock: Boolean @delegate(path: "inventoryInfo(upc: $fields:upc).isInStock", schema: "inventory") + name: String + price: Int! + reviews: [Review] @delegate(path: "reviewsByProduct(upc: $fields:upc)", schema: "reviews") + shippingEstimate: Int @delegate(path: "shippingEstimate(price: $fields:price weight: $fields:weight)", schema: "inventory") + upc: Int! + weight: Int! +} + +type Query { + me: User! @delegate(path: "user(id: 1)", schema: "accounts") + topProducts(first: Int = 5): [Product] @delegate(schema: "products") +} + +type Review @source(name: "Review", schema: "reviews") { + author: User @delegate(path: "user(id: $fields:authorId)", schema: "accounts") + authorId: Int! + body: String + id: Int! + product: Product @delegate(path: "product(upc: $fields:upc)", schema: "products") + upc: Int! +} + +type User @source(name: "User", schema: "accounts") { + birthdate: DateTime! + id: Int! + name: String + reviews: [Review] @delegate(path: "reviewsByAuthor(authorId: $fields:id)", schema: "reviews") + username: String +} + +directive @computed("Specifies the fields on which a computed field is dependent on." dependantOn: [Name!]) on FIELD_DEFINITION + +"The `@defer` directive may be provided for fragment spreads and inline fragments to inform the executor to delay the execution of the current fragment to indicate deprioritization of the current fragment. A query with `@defer` directive will cause the request to potentially return multiple responses, where non-deferred data is delivered in the initial response and data deferred is delivered in a subsequent response. `@include` and `@skip` take precedence over `@defer`." +directive @defer("Deferred when true." if: Boolean "If this argument label has a value other than null, it will be passed on to the result of this defer directive. This label is intended to give client applications a way to identify to which fragment a deferred result belongs to." label: String) on FRAGMENT_SPREAD | INLINE_FRAGMENT + +directive @delegate(path: String "The name of the schema to which this field shall be delegated to." schema: Name!) on FIELD_DEFINITION + +"The @deprecated directive is used within the type system definition language to indicate deprecated portions of a GraphQL service’s schema,such as deprecated fields on a type or deprecated enum values." +directive @deprecated("Deprecations include a reason for why it is deprecated, which is formatted using Markdown syntax (as specified by CommonMark)." reason: String = "No longer supported") on FIELD_DEFINITION | ENUM_VALUE + +"Directs the executor to include this field or fragment only when the `if` argument is true." +directive @include("Included when true." if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +"Directs the executor to skip this field or fragment when the `if` argument is true." +directive @skip("Skipped when true." if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +"Annotates the original name of a type." +directive @source("The original name of the annotated type." name: Name! "The name of the schema to which this type belongs to." schema: Name!) repeatable on ENUM | OBJECT | INTERFACE | UNION | INPUT_OBJECT | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM_VALUE + +"The `@stream` directive may be provided for a field of `List` type so that the backend can leverage technology such as asynchronous iterators to provide a partial list in the initial response, and additional list items in subsequent responses. `@include` and `@skip` take precedence over `@stream`." +directive @stream("Streamed when true." if: Boolean! "The initial elements that shall be send down to the consumer." initialCount: Int! "If this argument label has a value other than null, it will be passed on to the result of this stream directive. This label is intended to give client applications a way to identify to which fragment a streamed result belongs to." label: String) on FIELD + +"The `DateTime` scalar represents an ISO-8601 compliant date time type." +scalar DateTime + +"The name scalar represents a valid GraphQL name as specified in the spec and can be used to refer to fields or types." +scalar Name diff --git a/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Accounts/AccountsSchemaRequestExecutorBuilderExtensions.cs b/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Accounts/AccountsSchemaRequestExecutorBuilderExtensions.cs index cdc40fe7e56..3039c352d05 100644 --- a/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Accounts/AccountsSchemaRequestExecutorBuilderExtensions.cs +++ b/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Accounts/AccountsSchemaRequestExecutorBuilderExtensions.cs @@ -12,19 +12,7 @@ public static class AccountsSchemaRequestExecutorBuilderExtensions .AddSingleton(); return builder - .AddGraphQLServer() - .AddQueryType() - .PublishSchemaDefinition(c => c - .SetName("accounts") - .IgnoreRootTypes() - .AddTypeExtensionsFromString( - @"extend type Query { - me: User! @delegate(path: ""user(id: 1)"") - } - - extend type Review { - author: User @delegate(path: ""user(id: $fields:authorId)"") - }")); + .AddQueryType(); } } } diff --git a/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Inventory/InventorySchemaRequestExecutorBuilderExtensions.cs b/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Inventory/InventorySchemaRequestExecutorBuilderExtensions.cs index 769402a1489..b089cb0ec89 100644 --- a/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Inventory/InventorySchemaRequestExecutorBuilderExtensions.cs +++ b/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Inventory/InventorySchemaRequestExecutorBuilderExtensions.cs @@ -12,18 +12,7 @@ public static class InventorySchemaRequestExecutorBuilderExtensions .AddSingleton(); return builder - .AddQueryType() - .PublishSchemaDefinition(c => c - .SetName("inventory") - .IgnoreRootTypes() - .AddTypeExtensionsFromString( - @"extend type Product { - inStock: Boolean - @delegate(path: ""inventoryInfo(upc: $fields:upc).isInStock"") - shippingEstimate: Int - @delegate(path: ""shippingEstimate(price: $fields:price weight: $fields:weight)"") - } - ")); + .AddQueryType(); } } } diff --git a/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Products/ProductsSchemaRequestExecutorBuilderExtensions.cs b/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Products/ProductsSchemaRequestExecutorBuilderExtensions.cs index 5e5a4f250f2..ec9e45a5e94 100644 --- a/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Products/ProductsSchemaRequestExecutorBuilderExtensions.cs +++ b/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Products/ProductsSchemaRequestExecutorBuilderExtensions.cs @@ -12,18 +12,7 @@ public static class ProductsSchemaRequestExecutorBuilderExtensions .AddSingleton(); return builder - .AddQueryType() - .PublishSchemaDefinition(c => c - .SetName("products") - .IgnoreRootTypes() - .AddTypeExtensionsFromString( - @"extend type Query { - topProducts(first: Int = 5): [Product] @delegate - } - - extend type Review { - product: Product @delegate(path: ""product(upc: $fields:upc)"") - }")); + .AddQueryType(); } } } diff --git a/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Reviews/ReviewsSchemaRequestExecutorBuilderExtensions.cs b/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Reviews/ReviewsSchemaRequestExecutorBuilderExtensions.cs index f31bfae3ba4..1555210155d 100644 --- a/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Reviews/ReviewsSchemaRequestExecutorBuilderExtensions.cs +++ b/src/HotChocolate/Stitching/test/Stitching.Tests/Schemas/Reviews/ReviewsSchemaRequestExecutorBuilderExtensions.cs @@ -12,20 +12,7 @@ public static class ReviewSchemaRequestExecutorBuilderExtensions .AddSingleton(); return builder - .AddQueryType() - .PublishSchemaDefinition(c => c - .SetName("reviews") - .IgnoreRootTypes() - .AddTypeExtensionsFromString( - @"extend type User { - reviews: [Review] - @delegate(path:""reviewsByAuthor(authorId: $fields:id)"") - } - - extend type Product { - reviews: [Review] - @delegate(path:""reviewsByProduct(upc: $fields:upc)"") - }")); + .AddQueryType(); } } } diff --git a/src/HotChocolate/Stitching/test/Stitching.Tests/__resources__/AccountSchemaDefinition.json b/src/HotChocolate/Stitching/test/Stitching.Tests/__resources__/AccountSchemaDefinition.json new file mode 100644 index 00000000000..ef95845a8c2 --- /dev/null +++ b/src/HotChocolate/Stitching/test/Stitching.Tests/__resources__/AccountSchemaDefinition.json @@ -0,0 +1,8 @@ +{ + "Name": "accounts", + "Document": "schema { query: Query } type Query { users: [User] user(id: Int!): User } type User { id: Int! name: String birthdate: DateTime! username: String foo: String } type _SchemaDefinition { name: String! document: String! extensionDocuments: [String!]! } \u0022The \u0060@defer\u0060 directive may be provided for fragment spreads and inline fragments to inform the executor to delay the execution of the current fragment to indicate deprioritization of the current fragment. A query with \u0060@defer\u0060 directive will cause the request to potentially return multiple responses, where non-deferred data is delivered in the initial response and data deferred is delivered in a subsequent response. \u0060@include\u0060 and \u0060@skip\u0060 take precedence over \u0060@defer\u0060.\u0022 directive @defer(\u0022If this argument label has a value other than null, it will be passed on to the result of this defer directive. This label is intended to give client applications a way to identify to which fragment a deferred result belongs to.\u0022 label: String \u0022Deferred when true.\u0022 if: Boolean) on FRAGMENT_SPREAD | INLINE_FRAGMENT \u0022The @deprecated directive is used within the type system definition language to indicate deprecated portions of a GraphQL service\u2019s schema,such as deprecated fields on a type or deprecated enum values.\u0022 directive @deprecated(\u0022Deprecations include a reason for why it is deprecated, which is formatted using Markdown syntax (as specified by CommonMark).\u0022 reason: String = \u0022No longer supported\u0022) on FIELD_DEFINITION | ENUM_VALUE \u0022Directs the executor to include this field or fragment only when the \u0060if\u0060 argument is true.\u0022 directive @include(\u0022Included when true.\u0022 if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT \u0022Directs the executor to skip this field or fragment when the \u0060if\u0060 argument is true.\u0022 directive @skip(\u0022Skipped when true.\u0022 if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT \u0022The \u0060@stream\u0060 directive may be provided for a field of \u0060List\u0060 type so that the backend can leverage technology such as asynchronous iterators to provide a partial list in the initial response, and additional list items in subsequent responses. \u0060@include\u0060 and \u0060@skip\u0060 take precedence over \u0060@stream\u0060.\u0022 directive @stream(\u0022If this argument label has a value other than null, it will be passed on to the result of this stream directive. This label is intended to give client applications a way to identify to which fragment a streamed result belongs to.\u0022 label: String \u0022The initial elements that shall be send down to the consumer.\u0022 initialCount: Int! \u0022Streamed when true.\u0022 if: Boolean!) on FIELD \u0022The \u0060DateTime\u0060 scalar represents an ISO-8601 compliant date time type.\u0022 scalar DateTime", + "ExtensionDocuments": [ + "extend type Query {\n me: User! @delegate(path: \u0022user(id: 1)\u0022)\n}\n\nextend type Review {\n author: User @delegate(path: \u0022user(id: $fields:authorId)\u0022)\n}", + "extend schema @_removeRootTypes {\n\n}" + ] +} diff --git a/src/HotChocolate/Utilities/src/Utilities/BufferHelper.cs b/src/HotChocolate/Utilities/src/Utilities/BufferHelper.cs index 7f8bcc95d98..ae484a28125 100644 --- a/src/HotChocolate/Utilities/src/Utilities/BufferHelper.cs +++ b/src/HotChocolate/Utilities/src/Utilities/BufferHelper.cs @@ -12,7 +12,7 @@ public static class BufferHelper Stream stream, Func handle, CancellationToken cancellationToken) => - ReadAsync(stream, handle, null, cancellationToken); + ReadAsync(stream, handle, null, cancellationToken); public static async Task ReadAsync( Stream stream, @@ -31,7 +31,7 @@ public static class BufferHelper if (bytesRemaining == 0) { - var next = ArrayPool.Shared.Rent(buffer.Length * 2); + byte[] next = ArrayPool.Shared.Rent(buffer.Length * 2); Buffer.BlockCopy(buffer, 0, next, 0, buffer.Length); ArrayPool.Shared.Return(buffer); buffer = next; diff --git a/src/HotChocolate/Utilities/src/Utilities/Properties/InternalsVisibleTo.cs b/src/HotChocolate/Utilities/src/Utilities/Properties/InternalsVisibleTo.cs index 7d94f36f7cb..c50af641c98 100644 --- a/src/HotChocolate/Utilities/src/Utilities/Properties/InternalsVisibleTo.cs +++ b/src/HotChocolate/Utilities/src/Utilities/Properties/InternalsVisibleTo.cs @@ -5,5 +5,6 @@ [assembly: InternalsVisibleTo("HotChocolate.Core")] [assembly: InternalsVisibleTo("HotChocolate.Types")] [assembly: InternalsVisibleTo("HotChocolate.Stitching")] +[assembly: InternalsVisibleTo("HotChocolate.Stitching.Redis")] [assembly: InternalsVisibleTo("HotChocolate.Execution")] [assembly: InternalsVisibleTo("HotChocolate.Utilities.Tests")]