From 41979c55d07297d17ef66ce96df41920c59a84d2 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Mon, 3 Jul 2023 11:23:52 +0200 Subject: [PATCH] Reworked fusion configuration to use observer. (#6310) --- .../Execution/Configuration/ITypeModule.cs | 5 + .../src/Execution/RequestExecutorResolver.cs | 18 ++ .../Fusion/src/Core/Clients/IGraphQLClient.cs | 15 -- .../src/Core/Clients/IGraphQLClientFactory.cs | 16 ++ .../FileWatcherTypeModule.cs | 61 ----- .../FusionRequestExecutorBuilderExtensions.cs | 229 ++++++++---------- .../GatewayConfiguration.cs | 19 ++ .../GatewayConfigurationContext.cs | 14 +- .../GatewayConfigurationFileObserver.cs | 98 ++++++++ .../GatewayConfigurationFileUtils.cs | 28 +++ .../GatewayConfigurationTypeModule.cs | 44 ++++ .../SchemaBuilderExtensions.cs | 22 ++ .../StaticGatewayConfigurationFileObserver.cs | 49 ++++ .../StaticGatewayConfigurationObserver.cs | 34 +++ .../src/Core/FusionResources.Designer.cs | 52 ++-- .../Fusion/src/Core/FusionResources.resx | 6 + .../src/Core/HotChocolate.Fusion.csproj | 4 + .../Core/Metadata/FusionGraphConfiguration.cs | 14 -- .../src/Core/Metadata/QualifiedTypeName.cs | 3 + .../Fusion/src/Core/Metadata/SubgraphInfo.cs | 13 + .../Fusion/src/Core/ThrowHelper.cs | 12 + .../test/Core.Tests/DemoIntegrationTests.cs | 132 +++++++--- .../Fusion/test/Core.Tests/ErrorTests.cs | 9 +- .../Fusion/test/Core.Tests/InterfaceTests.cs | 9 +- templates/v12/gateway/Program.cs | 5 +- 25 files changed, 631 insertions(+), 280 deletions(-) create mode 100644 src/HotChocolate/Fusion/src/Core/Clients/IGraphQLClientFactory.cs delete mode 100644 src/HotChocolate/Fusion/src/Core/DependencyInjection/FileWatcherTypeModule.cs create mode 100644 src/HotChocolate/Fusion/src/Core/DependencyInjection/GatewayConfiguration.cs create mode 100644 src/HotChocolate/Fusion/src/Core/DependencyInjection/GatewayConfigurationFileObserver.cs create mode 100644 src/HotChocolate/Fusion/src/Core/DependencyInjection/GatewayConfigurationFileUtils.cs create mode 100644 src/HotChocolate/Fusion/src/Core/DependencyInjection/GatewayConfigurationTypeModule.cs create mode 100644 src/HotChocolate/Fusion/src/Core/DependencyInjection/SchemaBuilderExtensions.cs create mode 100644 src/HotChocolate/Fusion/src/Core/DependencyInjection/StaticGatewayConfigurationFileObserver.cs create mode 100644 src/HotChocolate/Fusion/src/Core/DependencyInjection/StaticGatewayConfigurationObserver.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Metadata/QualifiedTypeName.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Metadata/SubgraphInfo.cs diff --git a/src/HotChocolate/Core/src/Execution/Configuration/ITypeModule.cs b/src/HotChocolate/Core/src/Execution/Configuration/ITypeModule.cs index 14ae6c618b3..247e95a5326 100644 --- a/src/HotChocolate/Core/src/Execution/Configuration/ITypeModule.cs +++ b/src/HotChocolate/Core/src/Execution/Configuration/ITypeModule.cs @@ -50,6 +50,11 @@ public abstract class TypeModule : ITypeModule /// public event EventHandler? TypesChanged; + internal virtual ValueTask ConfigureAsync( + ConfigurationContext context, + CancellationToken cancellationToken) + => default; + /// /// Will be called by the schema building process to add the dynamically created /// types and type extensions to the schema building process. diff --git a/src/HotChocolate/Core/src/Execution/RequestExecutorResolver.cs b/src/HotChocolate/Core/src/Execution/RequestExecutorResolver.cs index ae1a2dde4b6..8a52b3f699a 100644 --- a/src/HotChocolate/Core/src/Execution/RequestExecutorResolver.cs +++ b/src/HotChocolate/Core/src/Execution/RequestExecutorResolver.cs @@ -243,6 +243,10 @@ await OnConfigureRequestExecutorOptionsAsync(context, setup, cancellationToken) typeModuleChangeMonitor.Register(typeModule); } + // we allow newer type modules to apply configurations. + await typeModuleChangeMonitor.ConfigureAsync(context, cancellationToken) + .ConfigureAwait(false); + serviceCollection.AddSingleton( _ => new DefaultApplicationServiceProvider(_applicationServices)); @@ -519,6 +523,20 @@ public void Register(ITypeModule typeModule) _typeModules.Add(typeModule); } + internal async ValueTask ConfigureAsync( + ConfigurationContext context, + CancellationToken cancellationToken) + { + foreach (var item in _typeModules) + { + if (item is TypeModule typeModule) + { + await typeModule.ConfigureAsync(context, cancellationToken) + .ConfigureAwait(false); + } + } + } + public IAsyncEnumerable CreateTypesAsync(IDescriptorContext context) => new TypeModuleEnumerable(_typeModules, context); diff --git a/src/HotChocolate/Fusion/src/Core/Clients/IGraphQLClient.cs b/src/HotChocolate/Fusion/src/Core/Clients/IGraphQLClient.cs index bc9ab84fa0d..a535642d2ee 100644 --- a/src/HotChocolate/Fusion/src/Core/Clients/IGraphQLClient.cs +++ b/src/HotChocolate/Fusion/src/Core/Clients/IGraphQLClient.cs @@ -1,5 +1,3 @@ -using HotChocolate.Fusion.Metadata; - namespace HotChocolate.Fusion.Clients; /// @@ -28,16 +26,3 @@ Task ExecuteAsync( GraphQLRequest request, CancellationToken cancellationToken); } - -/// -/// Represents a factory for creating instances. -/// -public interface IGraphQLClientFactory -{ - /// - /// Creates a new instance. - /// - /// - /// - IGraphQLClient CreateClient(HttpClientConfiguration configuration); -} diff --git a/src/HotChocolate/Fusion/src/Core/Clients/IGraphQLClientFactory.cs b/src/HotChocolate/Fusion/src/Core/Clients/IGraphQLClientFactory.cs new file mode 100644 index 00000000000..aee4dc5a7e9 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Clients/IGraphQLClientFactory.cs @@ -0,0 +1,16 @@ +using HotChocolate.Fusion.Metadata; + +namespace HotChocolate.Fusion.Clients; + +/// +/// Represents a factory for creating instances. +/// +public interface IGraphQLClientFactory +{ + /// + /// Creates a new instance. + /// + /// + /// + IGraphQLClient CreateClient(HttpClientConfiguration configuration); +} diff --git a/src/HotChocolate/Fusion/src/Core/DependencyInjection/FileWatcherTypeModule.cs b/src/HotChocolate/Fusion/src/Core/DependencyInjection/FileWatcherTypeModule.cs deleted file mode 100644 index 1210417fbe5..00000000000 --- a/src/HotChocolate/Fusion/src/Core/DependencyInjection/FileWatcherTypeModule.cs +++ /dev/null @@ -1,61 +0,0 @@ -using HotChocolate.Execution.Configuration; -using HotChocolate.Utilities; - -namespace Microsoft.Extensions.DependencyInjection; - -internal sealed class FileWatcherTypeModule : TypeModule, IDisposable -{ - private readonly FileSystemWatcher _watcher; - - public FileWatcherTypeModule(string fileName) - { - if (fileName is null) - { - throw new ArgumentNullException(nameof(fileName)); - } - - var fullPath = Path.GetFullPath(fileName); - var directory = Path.GetDirectoryName(fullPath); - if (directory is null) - { - // TODO : resources - throw new FileNotFoundException( - "The file name must contain a directory path.", - fileName); - } - - _watcher = new FileSystemWatcher(); - _watcher.Path = directory; - _watcher.Filter = "*.*"; - - _watcher.NotifyFilter = - NotifyFilters.FileName | - NotifyFilters.DirectoryName | - NotifyFilters.Attributes | - NotifyFilters.CreationTime | - NotifyFilters.FileName | - NotifyFilters.LastWrite | - NotifyFilters.Size; - - _watcher.Created += (_, e) => - { - if (fullPath.EqualsOrdinal(e.FullPath)) - { - OnTypesChanged(); - } - }; - - _watcher.Changed += (_, e) => - { - if (fullPath.EqualsOrdinal(e.FullPath)) - { - OnTypesChanged(); - } - }; - - _watcher.EnableRaisingEvents = true; - } - - public void Dispose() - => _watcher.Dispose(); -} diff --git a/src/HotChocolate/Fusion/src/Core/DependencyInjection/FusionRequestExecutorBuilderExtensions.cs b/src/HotChocolate/Fusion/src/Core/DependencyInjection/FusionRequestExecutorBuilderExtensions.cs index 72b547e22a3..b9d8f3379bb 100644 --- a/src/HotChocolate/Fusion/src/Core/DependencyInjection/FusionRequestExecutorBuilderExtensions.cs +++ b/src/HotChocolate/Fusion/src/Core/DependencyInjection/FusionRequestExecutorBuilderExtensions.cs @@ -2,14 +2,13 @@ using HotChocolate.Execution; using HotChocolate.Execution.Configuration; using HotChocolate.Execution.Pipeline; -using HotChocolate.Fusion; using HotChocolate.Fusion.Clients; using HotChocolate.Fusion.Metadata; using HotChocolate.Fusion.Pipeline; using HotChocolate.Fusion.Planning; using HotChocolate.Language; using Microsoft.Extensions.DependencyInjection.Extensions; -using static HotChocolate.Fusion.Metadata.FusionGraphConfiguration; +using static HotChocolate.Fusion.ThrowHelper; // ReSharper disable once CheckNamespace namespace Microsoft.Extensions.DependencyInjection; @@ -22,9 +21,6 @@ public static class FusionRequestExecutorBuilderExtensions /// /// The service collection. /// - /// - /// The fusion gateway configuration document. - /// /// /// The name of the fusion graph. /// @@ -33,40 +29,63 @@ public static class FusionRequestExecutorBuilderExtensions /// /// /// is null or - /// is null. /// public static FusionGatewayBuilder AddFusionGatewayServer( this IServiceCollection services, - DocumentNode gatewayConfigurationDoc, - string? graphName = default) + string? graphName = null) { if (services is null) { throw new ArgumentNullException(nameof(services)); } - if (gatewayConfigurationDoc is null) - { - throw new ArgumentNullException(nameof(gatewayConfigurationDoc)); - } + services.AddTransient( + _ => new DefaultWebSocketConnectionFactory()); + services.TryAddSingleton( + sp => new DefaultHttpGraphQLClientFactory( + sp.GetRequiredService())); + services.TryAddSingleton( + sp => new DefaultWebSocketGraphQLSubscriptionClientFactory( + sp.GetRequiredService())); + + var builder = services + .AddGraphQLServer(graphName) + .UseField(next => next) + .AddOperationCompilerOptimizer() + .AddOperationCompilerOptimizer() + .Configure( + c => + { + c.DefaultPipelineFactory = AddDefaultPipeline; + + c.OnConfigureSchemaServicesHooks.Add( + (ctx, sc) => + { + if (!ctx.SchemaBuilder.ContainsFusionGraphConfig()) + { + throw NoConfigurationProvider(); + } + + var fusionGraphConfig = ctx.SchemaBuilder.GetFusionGraphConfig(); + sc.AddSingleton( + sp => CreateGraphQLClientFactory(sp, fusionGraphConfig)); + sc.TryAddSingleton(fusionGraphConfig); + sc.TryAddSingleton(); + }); + }); - return services.AddFusionGatewayServer( - (_, _) => new ValueTask(gatewayConfigurationDoc), - graphName: graphName); + return new FusionGatewayBuilder(builder); } /// - /// Adds a Fusion GraphQL Gateway to the service collection. + /// Specifies that the gateway configuration is loaded from a file. /// - /// - /// The service collection. + /// + /// The gateway builder. /// /// /// The path to the fusion gateway configuration file. /// - /// - /// The name of the fusion graph. - /// /// /// If set to true the fusion graph file will be watched for updates and /// the schema is rebuild whenever the file changes. @@ -75,19 +94,18 @@ public static FusionGatewayBuilder AddFusionGatewayServer( /// Returns the that can be used to configure the Gateway. /// /// - /// is null or + /// is null or /// is null or /// is equals to . /// - public static FusionGatewayBuilder AddFusionGatewayServer( - this IServiceCollection services, + public static FusionGatewayBuilder ConfigureFromFile( + this FusionGatewayBuilder builder, string gatewayConfigurationFile, - string? graphName = default, - bool watchFileForUpdates = false) + bool watchFileForUpdates = true) { - if (services is null) + if (builder is null) { - throw new ArgumentNullException(nameof(services)); + throw new ArgumentNullException(nameof(builder)); } if (string.IsNullOrEmpty(gatewayConfigurationFile)) @@ -95,97 +113,87 @@ public static FusionGatewayBuilder AddFusionGatewayServer( throw new ArgumentNullException(nameof(gatewayConfigurationFile)); } - var builder = services.AddFusionGatewayServer( - (_, ct) => LoadDocumentAsync(gatewayConfigurationFile, ct), - graphName: graphName); - if (watchFileForUpdates) { - builder.CoreBuilder.AddTypeModule(_ => new FileWatcherTypeModule(gatewayConfigurationFile)); + builder.RegisterGatewayConfiguration( + _ => new GatewayConfigurationFileObserver(gatewayConfigurationFile)); + } + else + { + builder.RegisterGatewayConfiguration( + _ => new StaticGatewayConfigurationFileObserver(gatewayConfigurationFile)); } return builder; } /// - /// Adds a Fusion GraphQL Gateway to the service collection. + /// Specifies that the gateway configuration is loaded from an in-memory GraphQL document. /// - /// - /// The service collection. - /// - /// - /// A delegate that is used to resolve the fusion gateway configuration. + /// + /// The gateway builder. /// - /// - /// The name of the fusion graph. + /// + /// The fusion gateway configuration document. /// /// /// Returns the that can be used to configure the Gateway. /// /// - /// is null or - /// is null. + /// is null or + /// is null. /// - public static FusionGatewayBuilder AddFusionGatewayServer( - this IServiceCollection services, - GatewayConfigurationResolver resolveConfig, - string? graphName = default) + public static FusionGatewayBuilder ConfigureFromDocument( + this FusionGatewayBuilder builder, + DocumentNode gatewayConfigurationDoc) { - if (services is null) + if (builder is null) { - throw new ArgumentNullException(nameof(services)); + throw new ArgumentNullException(nameof(builder)); } - if (resolveConfig is null) + if (gatewayConfigurationDoc is null) { - throw new ArgumentNullException(nameof(resolveConfig)); + throw new ArgumentNullException(nameof(gatewayConfigurationDoc)); } - services.AddTransient( - _ => new DefaultWebSocketConnectionFactory()); - services.TryAddSingleton( - sp => new DefaultHttpGraphQLClientFactory( - sp.GetRequiredService())); - services.TryAddSingleton( - sp => new DefaultWebSocketGraphQLSubscriptionClientFactory( - sp.GetRequiredService())); - - var builder = services - .AddGraphQLServer(graphName) - .UseField(next => next) - .AddOperationCompilerOptimizer() - .AddOperationCompilerOptimizer() - .Configure( - c => - { - c.DefaultPipelineFactory = AddDefaultPipeline; - - c.OnConfigureRequestExecutorOptionsHooks.Add( - new OnConfigureRequestExecutorOptionsAction( - async: async (ctx, _, ct) => - { - var rewriter = new FusionGraphConfigurationToSchemaRewriter(); - var config = await resolveConfig(new(ctx.ApplicationServices), ct); - var fusionGraphConfig = Load(config); - var schemaDoc = rewriter.Rewrite(config); + return builder + .RegisterGatewayConfiguration( + _ => new StaticGatewayConfigurationObserver(gatewayConfigurationDoc)); + } - ctx.SchemaBuilder - .AddDocument(schemaDoc) - .SetFusionGraphConfig(fusionGraphConfig); - })); + /// + /// Registers an observable Gateway configuration. + /// + /// + /// The gateway builder. + /// + /// + /// The factory that creates the observable Gateway configuration. + /// + /// + /// + /// is null or + /// is null. + /// + public static FusionGatewayBuilder RegisterGatewayConfiguration( + this FusionGatewayBuilder builder, + Func> factory) + { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } - c.OnConfigureSchemaServicesHooks.Add( - (ctx, sc) => - { - var fusionGraphConfig = ctx.SchemaBuilder.GetFusionGraphConfig(); - sc.AddSingleton( - sp => CreateGraphQLClientFactory(sp, fusionGraphConfig)); - sc.TryAddSingleton(fusionGraphConfig); - sc.TryAddSingleton(); - }); - }); + if (factory is null) + { + throw new ArgumentNullException(nameof(factory)); + } - return new FusionGatewayBuilder(builder); + builder.Services.AddSingleton(factory); + builder.Services.AddSingleton(); + builder.CoreBuilder.AddTypeModule(); + return builder; } /// @@ -380,27 +388,6 @@ IGraphQLSubscriptionClient Create(IGraphQLClientConfiguration clientConfig) return new GraphQLClientFactory(map1, map2); } - private static async ValueTask LoadDocumentAsync( - string fileName, - CancellationToken cancellationToken) - { - try - { - // We first try to load the file name as a fusion graph package. - // This might fails as a the file that was provided is a fusion - // graph document. - await using var package = FusionGraphPackage.Open(fileName, FileAccess.Read); - return await package.GetFusionGraphAsync(cancellationToken); - } - catch - { - // If we fail to load the file as a fusion graph package we will - // try to load it as a GraphQL schema document. - var sourceText = await File.ReadAllTextAsync(fileName, cancellationToken); - return Utf8GraphQLParser.Parse(sourceText); - } - } - /// /// Builds a from the specified /// . @@ -440,17 +427,3 @@ internal static ValueTask BuildSchemaAsync( CancellationToken cancellationToken = default) => builder.CoreBuilder.BuildSchemaAsync(graphName, cancellationToken); } - -static file class FileExtensions -{ - private const string _fusionGraphConfig = "HotChocolate.Fusion.FusionGraphConfig"; - - public static FusionGraphConfiguration GetFusionGraphConfig( - this ISchemaBuilder builder) - => (FusionGraphConfiguration)builder.ContextData[_fusionGraphConfig]!; - - public static ISchemaBuilder SetFusionGraphConfig( - this ISchemaBuilder builder, - FusionGraphConfiguration config) - => builder.SetContextData(_fusionGraphConfig, config); -} diff --git a/src/HotChocolate/Fusion/src/Core/DependencyInjection/GatewayConfiguration.cs b/src/HotChocolate/Fusion/src/Core/DependencyInjection/GatewayConfiguration.cs new file mode 100644 index 00000000000..1480b34346a --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/DependencyInjection/GatewayConfiguration.cs @@ -0,0 +1,19 @@ +using HotChocolate.Language; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Represents the Fusion gateway configuration. +/// +public sealed class GatewayConfiguration +{ + public GatewayConfiguration(DocumentNode document) + { + Document = document ?? throw new ArgumentNullException(nameof(document)); + } + + /// + /// Gets the Fusion gateway configuration document. + /// + public DocumentNode Document { get; } +} diff --git a/src/HotChocolate/Fusion/src/Core/DependencyInjection/GatewayConfigurationContext.cs b/src/HotChocolate/Fusion/src/Core/DependencyInjection/GatewayConfigurationContext.cs index d20a2205f1d..62546f6cc51 100644 --- a/src/HotChocolate/Fusion/src/Core/DependencyInjection/GatewayConfigurationContext.cs +++ b/src/HotChocolate/Fusion/src/Core/DependencyInjection/GatewayConfigurationContext.cs @@ -1,3 +1,5 @@ +using HotChocolate.Execution.Configuration; + namespace Microsoft.Extensions.DependencyInjection; /// @@ -11,13 +13,23 @@ public readonly struct GatewayConfigurationContext /// /// The service provider. /// - public GatewayConfigurationContext(IServiceProvider services) + /// + /// + /// + public GatewayConfigurationContext(IServiceProvider services, IReadOnlyList typeModules) { Services = services ?? throw new ArgumentNullException(nameof(services)); + TypeModules = typeModules; } /// /// Gets the service provider. /// public IServiceProvider Services { get; } + + + /// + /// Get Gateway Type Modules. + /// + public IReadOnlyList TypeModules { get; } } diff --git a/src/HotChocolate/Fusion/src/Core/DependencyInjection/GatewayConfigurationFileObserver.cs b/src/HotChocolate/Fusion/src/Core/DependencyInjection/GatewayConfigurationFileObserver.cs new file mode 100644 index 00000000000..ac3b0b3777d --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/DependencyInjection/GatewayConfigurationFileObserver.cs @@ -0,0 +1,98 @@ +using HotChocolate.Utilities; +using static HotChocolate.Fusion.FusionResources; + +namespace Microsoft.Extensions.DependencyInjection; + +internal sealed class GatewayConfigurationFileObserver : IObservable +{ + private readonly string _filename; + + public GatewayConfigurationFileObserver(string filename) + { + if (string.IsNullOrEmpty(filename)) + { + throw new ArgumentException( + GatewayConfigurationFileObserver_FileNameNullOrEmpty, + nameof(filename)); + } + + _filename = filename; + } + + public IDisposable Subscribe(IObserver observer) + { + return new FileConfigurationSession(observer, _filename); + } + + private sealed class FileConfigurationSession : IDisposable + { + private readonly IObserver _observer; + private readonly string _fileName; + private readonly FileSystemWatcher _watcher; + + public FileConfigurationSession(IObserver observer, string fileName) + { + _observer = observer; + _fileName = fileName; + var fullPath = Path.GetFullPath(fileName); + var directory = Path.GetDirectoryName(fullPath); + + if (directory is null) + { + throw new FileNotFoundException( + FileConfigurationSession_MustContainPath, + fileName); + } + + _watcher = new FileSystemWatcher(); + _watcher.Path = directory; + _watcher.Filter = "*.*"; + + _watcher.NotifyFilter = + NotifyFilters.FileName | + NotifyFilters.DirectoryName | + NotifyFilters.Attributes | + NotifyFilters.CreationTime | + NotifyFilters.FileName | + NotifyFilters.LastWrite | + NotifyFilters.Size; + + _watcher.Created += (_, e) => + { + if (fullPath.EqualsOrdinal(e.FullPath)) + { + BeginLoadConfig(); + } + }; + + _watcher.Changed += (_, e) => + { + if (fullPath.EqualsOrdinal(e.FullPath)) + { + BeginLoadConfig(); + } + }; + + _watcher.EnableRaisingEvents = true; + } + + private void BeginLoadConfig() + => Task.Run( + async () => + { + try + { + var document = await GatewayConfigurationFileUtils.LoadDocumentAsync(_fileName, default); + _observer.OnNext(new GatewayConfiguration(document)); + } + catch(Exception ex) + { + _observer.OnError(ex); + _observer.OnCompleted(); + } + }); + + public void Dispose() + => _watcher.Dispose(); + } +} diff --git a/src/HotChocolate/Fusion/src/Core/DependencyInjection/GatewayConfigurationFileUtils.cs b/src/HotChocolate/Fusion/src/Core/DependencyInjection/GatewayConfigurationFileUtils.cs new file mode 100644 index 00000000000..9a967e20f7e --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/DependencyInjection/GatewayConfigurationFileUtils.cs @@ -0,0 +1,28 @@ +using HotChocolate.Fusion; +using HotChocolate.Language; + +namespace Microsoft.Extensions.DependencyInjection; + +internal static class GatewayConfigurationFileUtils +{ + public static async ValueTask LoadDocumentAsync( + string fileName, + CancellationToken cancellationToken) + { + try + { + // We first try to load the file name as a fusion graph package. + // This might fails as a the file that was provided is a fusion + // graph document. + await using var package = FusionGraphPackage.Open(fileName, FileAccess.Read); + return await package.GetFusionGraphAsync(cancellationToken); + } + catch + { + // If we fail to load the file as a fusion graph package we will + // try to load it as a GraphQL schema document. + var sourceText = await File.ReadAllTextAsync(fileName, cancellationToken); + return Utf8GraphQLParser.Parse(sourceText); + } + } +} diff --git a/src/HotChocolate/Fusion/src/Core/DependencyInjection/GatewayConfigurationTypeModule.cs b/src/HotChocolate/Fusion/src/Core/DependencyInjection/GatewayConfigurationTypeModule.cs new file mode 100644 index 00000000000..ce69f8c2a5a --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/DependencyInjection/GatewayConfigurationTypeModule.cs @@ -0,0 +1,44 @@ +using HotChocolate; +using HotChocolate.Execution.Configuration; +using HotChocolate.Fusion; +using HotChocolate.Fusion.Metadata; +using HotChocolate.Language; + +namespace Microsoft.Extensions.DependencyInjection; + +internal sealed class GatewayConfigurationTypeModule : TypeModule +{ + private readonly TaskCompletionSource _ready = new(TaskCreationOptions.RunContinuationsAsynchronously); + private DocumentNode? _configuration; + + public GatewayConfigurationTypeModule(IObservable configuration) + { + configuration.Subscribe( + config => + { + _configuration = config.Document; + _ready.TrySetResult(); + OnTypesChanged(); + }, + error => _ready.TrySetException(error), + () => _ready.TrySetCanceled()); + } + + internal override async ValueTask ConfigureAsync(ConfigurationContext context, CancellationToken cancellationToken) + { + await _ready.Task.WaitAsync(cancellationToken).ConfigureAwait(false); + + if (_configuration is null) + { + throw ThrowHelper.UnableToLoadConfiguration(); + } + + var rewriter = new FusionGraphConfigurationToSchemaRewriter(); + var fusionGraphConfig = FusionGraphConfiguration.Load(_configuration); + var schemaDoc = rewriter.Rewrite(_configuration); + + context.SchemaBuilder + .AddDocument(schemaDoc) + .SetFusionGraphConfig(fusionGraphConfig); + } +} diff --git a/src/HotChocolate/Fusion/src/Core/DependencyInjection/SchemaBuilderExtensions.cs b/src/HotChocolate/Fusion/src/Core/DependencyInjection/SchemaBuilderExtensions.cs new file mode 100644 index 00000000000..a52662b21dd --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/DependencyInjection/SchemaBuilderExtensions.cs @@ -0,0 +1,22 @@ +using HotChocolate; +using HotChocolate.Fusion.Metadata; + +namespace Microsoft.Extensions.DependencyInjection; + +internal static class SchemaBuilderExtensions +{ + private const string _fusionGraphConfig = "HotChocolate.Fusion.FusionGraphConfig"; + + public static bool ContainsFusionGraphConfig( + this ISchemaBuilder builder) + => builder.ContextData.ContainsKey(_fusionGraphConfig); + + public static FusionGraphConfiguration GetFusionGraphConfig( + this ISchemaBuilder builder) + => (FusionGraphConfiguration)builder.ContextData[_fusionGraphConfig]!; + + public static ISchemaBuilder SetFusionGraphConfig( + this ISchemaBuilder builder, + FusionGraphConfiguration config) + => builder.SetContextData(_fusionGraphConfig, config); +} diff --git a/src/HotChocolate/Fusion/src/Core/DependencyInjection/StaticGatewayConfigurationFileObserver.cs b/src/HotChocolate/Fusion/src/Core/DependencyInjection/StaticGatewayConfigurationFileObserver.cs new file mode 100644 index 00000000000..6c09abee59b --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/DependencyInjection/StaticGatewayConfigurationFileObserver.cs @@ -0,0 +1,49 @@ +using static Microsoft.Extensions.DependencyInjection.GatewayConfigurationFileUtils; + +namespace Microsoft.Extensions.DependencyInjection; + +internal sealed class StaticGatewayConfigurationFileObserver : IObservable +{ + private readonly string _filename; + + public StaticGatewayConfigurationFileObserver(string filename) + { + if (string.IsNullOrEmpty(filename)) + { + throw new ArgumentException("Value cannot be null or empty.", nameof(filename)); + } + + _filename = filename; + } + + public IDisposable Subscribe(IObserver observer) + { + return new FileConfigurationSession(observer, _filename); + } + + private class FileConfigurationSession : IDisposable + { + public FileConfigurationSession(IObserver observer, string filename) + { + Task.Run( + async () => + { + try + { + var document = await LoadDocumentAsync(filename, default); + observer.OnNext(new GatewayConfiguration(document)); + } + catch(Exception ex) + { + observer.OnError(ex); + observer.OnCompleted(); + } + }); + } + + public void Dispose() + { + // there is nothing to dispose here. + } + } +} diff --git a/src/HotChocolate/Fusion/src/Core/DependencyInjection/StaticGatewayConfigurationObserver.cs b/src/HotChocolate/Fusion/src/Core/DependencyInjection/StaticGatewayConfigurationObserver.cs new file mode 100644 index 00000000000..e967b238181 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/DependencyInjection/StaticGatewayConfigurationObserver.cs @@ -0,0 +1,34 @@ +using HotChocolate.Language; + +namespace Microsoft.Extensions.DependencyInjection; + +internal sealed class StaticGatewayConfigurationObserver : IObservable +{ + private readonly DocumentNode _gatewayConfigDoc; + + public StaticGatewayConfigurationObserver(DocumentNode gatewayConfigDoc) + { + _gatewayConfigDoc = gatewayConfigDoc ?? + throw new ArgumentNullException(nameof(gatewayConfigDoc)); + } + + public IDisposable Subscribe(IObserver observer) + { + return new FileConfigurationSession(observer, _gatewayConfigDoc); + } + + private sealed class FileConfigurationSession : IDisposable + { + public FileConfigurationSession( + IObserver observer, + DocumentNode gatewayConfigDoc) + { + observer.OnNext(new(gatewayConfigDoc)); + } + + public void Dispose() + { + // there is nothing to dispose here. + } + } +} diff --git a/src/HotChocolate/Fusion/src/Core/FusionResources.Designer.cs b/src/HotChocolate/Fusion/src/Core/FusionResources.Designer.cs index d588008f638..f8d49d15dd5 100644 --- a/src/HotChocolate/Fusion/src/Core/FusionResources.Designer.cs +++ b/src/HotChocolate/Fusion/src/Core/FusionResources.Designer.cs @@ -9,21 +9,21 @@ namespace HotChocolate.Fusion { using System; - - + + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] [System.Diagnostics.DebuggerNonUserCodeAttribute()] [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class FusionResources { - + private static System.Resources.ResourceManager resourceMan; - + private static System.Globalization.CultureInfo resourceCulture; - + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal FusionResources() { } - + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] internal static System.Resources.ResourceManager ResourceManager { get { @@ -34,7 +34,7 @@ internal static System.Resources.ResourceManager ResourceManager { return resourceMan; } } - + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] internal static System.Globalization.CultureInfo Culture { get { @@ -44,83 +44,95 @@ internal static System.Globalization.CultureInfo Culture { resourceCulture = value; } } - + internal static string ThrowHelper_ServiceConfDocumentMustContainSchemaDef { get { return ResourceManager.GetString("ThrowHelper_ServiceConfDocumentMustContainSchemaDef", resourceCulture); } } - + internal static string ThrowHelper_ServiceConfNoClientsSpecified { get { return ResourceManager.GetString("ThrowHelper_ServiceConfNoClientsSpecified", resourceCulture); } } - + internal static string ThrowHelper_ServiceConfNoTypesSpecified { get { return ResourceManager.GetString("ThrowHelper_ServiceConfNoTypesSpecified", resourceCulture); } } - + internal static string ThrowHelper_ServiceConfInvalidValue { get { return ResourceManager.GetString("ThrowHelper_ServiceConfInvalidValue", resourceCulture); } } - + internal static string ThrowHelper_ServiceConfInvalidDirectiveName { get { return ResourceManager.GetString("ThrowHelper_ServiceConfInvalidDirectiveName", resourceCulture); } } - + internal static string ThrowHelper_ServiceConfNoDirectiveArgs { get { return ResourceManager.GetString("ThrowHelper_ServiceConfNoDirectiveArgs", resourceCulture); } } - + internal static string ThrowHelper_ServiceConfInvalidDirectiveArgs { get { return ResourceManager.GetString("ThrowHelper_ServiceConfInvalidDirectiveArgs", resourceCulture); } } - + internal static string FusionGraphConfigurationReader_ReadResolverDefinition_InvalidKindValue { get { return ResourceManager.GetString("FusionGraphConfigurationReader_ReadResolverDefinition_InvalidKindValue", resourceCulture); } } - + internal static string FusionRequestExecutorBuilderExtensions_AddFusionGatewayServer_NoSchema { get { return ResourceManager.GetString("FusionRequestExecutorBuilderExtensions_AddFusionGatewayServer_NoSchema", resourceCulture); } } - + internal static string ThrowHelper_Requirement_Is_Missing { get { return ResourceManager.GetString("ThrowHelper_Requirement_Is_Missing", resourceCulture); } } - + internal static string ThrowHelper_NoResolverInContext { get { return ResourceManager.GetString("ThrowHelper_NoResolverInContext", resourceCulture); } } - + internal static string TransportConfigurationNotSupported { get { return ResourceManager.GetString("TransportConfigurationNotSupported", resourceCulture); } } - + internal static string CreateSelection_MustBePlaceholderOrSelectExpression { get { return ResourceManager.GetString("CreateSelection_MustBePlaceholderOrSelectExpression", resourceCulture); } } + + internal static string GatewayConfigurationFileObserver_FileNameNullOrEmpty { + get { + return ResourceManager.GetString("GatewayConfigurationFileObserver_FileNameNullOrEmpty", resourceCulture); + } + } + + internal static string FileConfigurationSession_MustContainPath { + get { + return ResourceManager.GetString("FileConfigurationSession_MustContainPath", resourceCulture); + } + } } } diff --git a/src/HotChocolate/Fusion/src/Core/FusionResources.resx b/src/HotChocolate/Fusion/src/Core/FusionResources.resx index 446d2c1d2e9..c1750ce1f6d 100644 --- a/src/HotChocolate/Fusion/src/Core/FusionResources.resx +++ b/src/HotChocolate/Fusion/src/Core/FusionResources.resx @@ -57,4 +57,10 @@ Either provide a placeholder or the select expression must be a FieldNode. + + Value cannot be null or empty. + + + The file name must contain a directory path. + diff --git a/src/HotChocolate/Fusion/src/Core/HotChocolate.Fusion.csproj b/src/HotChocolate/Fusion/src/Core/HotChocolate.Fusion.csproj index ecb6f01ea0f..bd8e72ea1d6 100644 --- a/src/HotChocolate/Fusion/src/Core/HotChocolate.Fusion.csproj +++ b/src/HotChocolate/Fusion/src/Core/HotChocolate.Fusion.csproj @@ -46,4 +46,8 @@ + + + + diff --git a/src/HotChocolate/Fusion/src/Core/Metadata/FusionGraphConfiguration.cs b/src/HotChocolate/Fusion/src/Core/Metadata/FusionGraphConfiguration.cs index b5c102df564..6edfaf350ae 100644 --- a/src/HotChocolate/Fusion/src/Core/Metadata/FusionGraphConfiguration.cs +++ b/src/HotChocolate/Fusion/src/Core/Metadata/FusionGraphConfiguration.cs @@ -189,17 +189,3 @@ public static FusionGraphConfiguration Load(string sourceText) public static FusionGraphConfiguration Load(DocumentNode document) => new FusionGraphConfigurationReader().Read(document); } - -internal sealed class SubgraphInfo -{ - public SubgraphInfo(string name) - { - Name = name; - } - - public string Name { get; } - - public List Entities { get; } = new(); -} - -public readonly record struct QualifiedTypeName(string SubgraphName, string TypeName); diff --git a/src/HotChocolate/Fusion/src/Core/Metadata/QualifiedTypeName.cs b/src/HotChocolate/Fusion/src/Core/Metadata/QualifiedTypeName.cs new file mode 100644 index 00000000000..964c4dc5071 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Metadata/QualifiedTypeName.cs @@ -0,0 +1,3 @@ +namespace HotChocolate.Fusion.Metadata; + +public readonly record struct QualifiedTypeName(string SubgraphName, string TypeName); diff --git a/src/HotChocolate/Fusion/src/Core/Metadata/SubgraphInfo.cs b/src/HotChocolate/Fusion/src/Core/Metadata/SubgraphInfo.cs new file mode 100644 index 00000000000..44429c3380d --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Metadata/SubgraphInfo.cs @@ -0,0 +1,13 @@ +namespace HotChocolate.Fusion.Metadata; + +internal sealed class SubgraphInfo +{ + public SubgraphInfo(string name) + { + Name = name; + } + + public string Name { get; } + + public List Entities { get; } = new(); +} diff --git a/src/HotChocolate/Fusion/src/Core/ThrowHelper.cs b/src/HotChocolate/Fusion/src/Core/ThrowHelper.cs index a74e2f041e2..10f05bdae7b 100644 --- a/src/HotChocolate/Fusion/src/Core/ThrowHelper.cs +++ b/src/HotChocolate/Fusion/src/Core/ThrowHelper.cs @@ -64,4 +64,16 @@ public static InvalidOperationException SubscriptionsMustSubscribe() public static InvalidOperationException QueryAndMutationMustExecute() => new("A query or mutation execution plan can not be executed as a subscription."); + + public static SchemaException NoConfigurationProvider() + => new SchemaException( + SchemaErrorBuilder.New() + .SetMessage("No configuration provider registered.") + .Build()); + + public static SchemaException UnableToLoadConfiguration() + => new SchemaException( + SchemaErrorBuilder.New() + .SetMessage("Unable to load the Fusion gateway configuration.") + .Build()); } diff --git a/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs index b23e615fff8..cfeb06258bf 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs +++ b/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs @@ -82,7 +82,8 @@ public async Task Authors_And_Reviews_Query_GetUserReviews() var executor = await new ServiceCollection() .AddSingleton(demoProject.HttpClientFactory) .AddSingleton(demoProject.WebSocketConnectionFactory) - .AddFusionGatewayServer(SchemaFormatter.FormatAsDocument(fusionGraph)) + .AddFusionGatewayServer() + .ConfigureFromDocument(SchemaFormatter.FormatAsDocument(fusionGraph)) .BuildRequestExecutorAsync(); var request = Parse( @@ -132,7 +133,8 @@ public async Task Authors_And_Reviews_Query_GetUserById() var executor = await new ServiceCollection() .AddSingleton(demoProject.HttpClientFactory) .AddSingleton(demoProject.WebSocketConnectionFactory) - .AddFusionGatewayServer(SchemaFormatter.FormatAsDocument(fusionGraph)) + .AddFusionGatewayServer() + .ConfigureFromDocument(SchemaFormatter.FormatAsDocument(fusionGraph)) .BuildRequestExecutorAsync(); var request = Parse( @@ -176,7 +178,8 @@ public async Task Authors_And_Reviews_Query_GetUserById_With_Invalid_Id_Value() var executor = await new ServiceCollection() .AddSingleton(demoProject.HttpClientFactory) .AddSingleton(demoProject.WebSocketConnectionFactory) - .AddFusionGatewayServer(SchemaFormatter.FormatAsDocument(fusionGraph)) + .AddFusionGatewayServer() + .ConfigureFromDocument(SchemaFormatter.FormatAsDocument(fusionGraph)) .BuildRequestExecutorAsync(); var request = Parse( @@ -224,7 +227,8 @@ public async Task Authors_And_Reviews_Subscription_OnNewReview() var executor = await new ServiceCollection() .AddSingleton(demoProject.HttpClientFactory) .AddSingleton(demoProject.WebSocketConnectionFactory) - .AddFusionGatewayServer(SchemaFormatter.FormatAsDocument(fusionGraph)) + .AddFusionGatewayServer() + .ConfigureFromDocument(SchemaFormatter.FormatAsDocument(fusionGraph)) .BuildRequestExecutorAsync(cancellationToken: cts.Token); var request = Parse( @@ -273,7 +277,8 @@ public async Task Authors_And_Reviews_Subscription_OnNewReview_Two_Graphs() var executor = await new ServiceCollection() .AddSingleton(demoProject.HttpClientFactory) .AddSingleton(demoProject.WebSocketConnectionFactory) - .AddFusionGatewayServer(SchemaFormatter.FormatAsDocument(fusionGraph)) + .AddFusionGatewayServer() + .ConfigureFromDocument(SchemaFormatter.FormatAsDocument(fusionGraph)) .BuildRequestExecutorAsync(cancellationToken: cts.Token); var request = Parse( @@ -320,7 +325,8 @@ public async Task Authors_And_Reviews_Query_ReviewsUser() var executor = await new ServiceCollection() .AddSingleton(demoProject.HttpClientFactory) .AddSingleton(demoProject.WebSocketConnectionFactory) - .AddFusionGatewayServer(SchemaFormatter.FormatAsDocument(fusionGraph)) + .AddFusionGatewayServer() + .ConfigureFromDocument(SchemaFormatter.FormatAsDocument(fusionGraph)) .BuildRequestExecutorAsync(); var request = Parse( @@ -385,7 +391,8 @@ public async Task Authors_And_Reviews_Query_Reformat_AuthorIds() var executor = await new ServiceCollection() .AddSingleton(demoProject.HttpClientFactory) .AddSingleton(demoProject.WebSocketConnectionFactory) - .AddFusionGatewayServer(SchemaFormatter.FormatAsDocument(fusionGraph)) + .AddFusionGatewayServer() + .ConfigureFromDocument(SchemaFormatter.FormatAsDocument(fusionGraph)) .BuildRequestExecutorAsync(); var request = Parse( @@ -434,7 +441,8 @@ public async Task Authors_And_Reviews_Query_Reformat_AuthorIds_ReEncodeAllIds() var executor = await new ServiceCollection() .AddSingleton(demoProject.HttpClientFactory) .AddSingleton(demoProject.WebSocketConnectionFactory) - .AddFusionGatewayServer(SchemaFormatter.FormatAsDocument(fusionGraph)) + .AddFusionGatewayServer() + .ConfigureFromDocument(SchemaFormatter.FormatAsDocument(fusionGraph)) .BuildRequestExecutorAsync(); var request = Parse( @@ -483,7 +491,8 @@ public async Task Authors_And_Reviews_Batch_Requests() var executor = await new ServiceCollection() .AddSingleton(demoProject.HttpClientFactory) .AddSingleton(demoProject.WebSocketConnectionFactory) - .AddFusionGatewayServer(SchemaFormatter.FormatAsDocument(fusionGraph)) + .AddFusionGatewayServer() + .ConfigureFromDocument(SchemaFormatter.FormatAsDocument(fusionGraph)) .BuildRequestExecutorAsync(); var request = Parse( @@ -531,7 +540,8 @@ public async Task Authors_And_Reviews_And_Products_Query_TopProducts() var executor = await new ServiceCollection() .AddSingleton(demoProject.HttpClientFactory) .AddSingleton(demoProject.WebSocketConnectionFactory) - .AddFusionGatewayServer(SchemaFormatter.FormatAsDocument(fusionGraph)) + .AddFusionGatewayServer() + .ConfigureFromDocument(SchemaFormatter.FormatAsDocument(fusionGraph)) .BuildRequestExecutorAsync(); var request = Parse( @@ -582,7 +592,8 @@ public async Task Authors_And_Reviews_And_Products_Query_TypeName() var executor = await new ServiceCollection() .AddSingleton(demoProject.HttpClientFactory) .AddSingleton(demoProject.WebSocketConnectionFactory) - .AddFusionGatewayServer(SchemaFormatter.FormatAsDocument(fusionGraph)) + .AddFusionGatewayServer() + .ConfigureFromDocument(SchemaFormatter.FormatAsDocument(fusionGraph)) .BuildRequestExecutorAsync(); var request = Parse( @@ -634,7 +645,8 @@ public async Task Authors_And_Reviews_And_Products_With_Variables() var executor = await new ServiceCollection() .AddSingleton(demoProject.HttpClientFactory) .AddSingleton(demoProject.WebSocketConnectionFactory) - .AddFusionGatewayServer(SchemaFormatter.FormatAsDocument(fusionGraph)) + .AddFusionGatewayServer() + .ConfigureFromDocument(SchemaFormatter.FormatAsDocument(fusionGraph)) .BuildRequestExecutorAsync(); var request = Parse( @@ -680,7 +692,8 @@ public async Task Authors_And_Reviews_And_Products_Introspection() var executor = await new ServiceCollection() .AddSingleton(demoProject.HttpClientFactory) .AddSingleton(demoProject.WebSocketConnectionFactory) - .AddFusionGatewayServer(SchemaFormatter.FormatAsDocument(fusionGraph)) + .AddFusionGatewayServer() + .ConfigureFromDocument(SchemaFormatter.FormatAsDocument(fusionGraph)) .BuildRequestExecutorAsync(); var request = Parse( @@ -736,7 +749,8 @@ public async Task Fetch_User_With_Node_Field() var executor = await new ServiceCollection() .AddSingleton(demoProject.HttpClientFactory) .AddSingleton(demoProject.WebSocketConnectionFactory) - .AddFusionGatewayServer(SchemaFormatter.FormatAsDocument(fusionGraph)) + .AddFusionGatewayServer() + .ConfigureFromDocument(SchemaFormatter.FormatAsDocument(fusionGraph)) .BuildRequestExecutorAsync(); var request = Parse( @@ -788,7 +802,8 @@ public async Task Fetch_User_With_Node_Field_Pass_In_Review_Id() var executor = await new ServiceCollection() .AddSingleton(demoProject.HttpClientFactory) .AddSingleton(demoProject.WebSocketConnectionFactory) - .AddFusionGatewayServer(SchemaFormatter.FormatAsDocument(fusionGraph)) + .AddFusionGatewayServer() + .ConfigureFromDocument(SchemaFormatter.FormatAsDocument(fusionGraph)) .BuildRequestExecutorAsync(); var request = Parse( @@ -840,7 +855,8 @@ public async Task Fetch_User_With_Node_Field_Pass_In_Unknown_Id() var executor = await new ServiceCollection() .AddSingleton(demoProject.HttpClientFactory) .AddSingleton(demoProject.WebSocketConnectionFactory) - .AddFusionGatewayServer(SchemaFormatter.FormatAsDocument(fusionGraph)) + .AddFusionGatewayServer() + .ConfigureFromDocument(SchemaFormatter.FormatAsDocument(fusionGraph)) .BuildRequestExecutorAsync(); var request = Parse( @@ -890,7 +906,8 @@ public async Task Fetch_User_With_Node_Field_From_Two_Subgraphs() var executor = await new ServiceCollection() .AddSingleton(demoProject.HttpClientFactory) .AddSingleton(demoProject.WebSocketConnectionFactory) - .AddFusionGatewayServer(SchemaFormatter.FormatAsDocument(fusionGraph)) + .AddFusionGatewayServer() + .ConfigureFromDocument(SchemaFormatter.FormatAsDocument(fusionGraph)) .BuildRequestExecutorAsync(); var request = Parse( @@ -931,8 +948,6 @@ public async Task Hot_Reload() { // arrange using var demoProject = await DemoProject.CreateAsync(); - var reloadTypeModule = new ReloadTypeModule(); - var fusionGraph = await new FusionGraphComposer(logFactory: _logFactory) @@ -940,12 +955,15 @@ public async Task Hot_Reload() new[] { demoProject.Accounts.ToConfiguration(AccountsExtensionSdl), }, FusionFeatureFlags.NodeField); + var config = new HotReloadConfiguration( + new GatewayConfiguration( + SchemaFormatter.FormatAsDocument(fusionGraph))); + var services = new ServiceCollection() .AddSingleton(demoProject.HttpClientFactory) .AddSingleton(demoProject.WebSocketConnectionFactory) - .AddFusionGatewayServer((_, _) => new(SchemaFormatter.FormatAsDocument(fusionGraph))) - .CoreBuilder - .AddTypeModule(_ => reloadTypeModule) + .AddFusionGatewayServer(null) + .RegisterGatewayConfiguration(_ => config) .Services .BuildServiceProvider(); @@ -982,8 +1000,9 @@ public async Task Hot_Reload() demoProject.Accounts.ToConfiguration(AccountsExtensionSdl), }, FusionFeatureFlags.NodeField); - - reloadTypeModule.Evict(); + config.SetConfiguration( + new GatewayConfiguration( + SchemaFormatter.FormatAsDocument(fusionGraph))); result = await executorProxy.ExecuteAsync( QueryRequestBuilder @@ -1015,7 +1034,8 @@ public async Task TypeName_Field_On_QueryRoot() var executor = await new ServiceCollection() .AddSingleton(demoProject.HttpClientFactory) .AddSingleton(demoProject.WebSocketConnectionFactory) - .AddFusionGatewayServer(SchemaFormatter.FormatAsDocument(fusionGraph)) + .AddFusionGatewayServer() + .ConfigureFromDocument(SchemaFormatter.FormatAsDocument(fusionGraph)) .BuildRequestExecutorAsync(); var request = Parse( @@ -1059,7 +1079,8 @@ public async Task Forward_Nested_Variables() var executor = await new ServiceCollection() .AddSingleton(demoProject.HttpClientFactory) .AddSingleton(demoProject.WebSocketConnectionFactory) - .AddFusionGatewayServer(SchemaFormatter.FormatAsDocument(fusionGraph)) + .AddFusionGatewayServer() + .ConfigureFromDocument(SchemaFormatter.FormatAsDocument(fusionGraph)) .BuildRequestExecutorAsync(); var request = Parse( @@ -1111,7 +1132,8 @@ public async Task Forward_Nested_Node_Variables() var executor = await new ServiceCollection() .AddSingleton(demoProject.HttpClientFactory) .AddSingleton(demoProject.WebSocketConnectionFactory) - .AddFusionGatewayServer(SchemaFormatter.FormatAsDocument(fusionGraph)) + .AddFusionGatewayServer() + .ConfigureFromDocument(SchemaFormatter.FormatAsDocument(fusionGraph)) .BuildRequestExecutorAsync(); var request = Parse( @@ -1165,7 +1187,8 @@ public async Task Forward_Nested_Object_Variables() var executor = await new ServiceCollection() .AddSingleton(demoProject.HttpClientFactory) .AddSingleton(demoProject.WebSocketConnectionFactory) - .AddFusionGatewayServer(SchemaFormatter.FormatAsDocument(fusionGraph)) + .AddFusionGatewayServer() + .ConfigureFromDocument(SchemaFormatter.FormatAsDocument(fusionGraph)) .BuildRequestExecutorAsync(); var request = Parse( @@ -1222,7 +1245,8 @@ public async Task Require_Data_In_Context() var executor = await new ServiceCollection() .AddSingleton(demoProject.HttpClientFactory) .AddSingleton(demoProject.WebSocketConnectionFactory) - .AddFusionGatewayServer(SchemaFormatter.FormatAsDocument(fusionGraph)) + .AddFusionGatewayServer() + .ConfigureFromDocument(SchemaFormatter.FormatAsDocument(fusionGraph)) .BuildRequestExecutorAsync(); var request = Parse( @@ -1282,7 +1306,8 @@ public async Task Require_Data_In_Context_2() var executor = await new ServiceCollection() .AddSingleton(demoProject.HttpClientFactory) .AddSingleton(demoProject.WebSocketConnectionFactory) - .AddFusionGatewayServer(SchemaFormatter.FormatAsDocument(fusionGraph)) + .AddFusionGatewayServer() + .ConfigureFromDocument(SchemaFormatter.FormatAsDocument(fusionGraph)) .BuildRequestExecutorAsync(); var request = Parse( @@ -1321,8 +1346,51 @@ query Requires { Assert.Null(result.ExpectQueryResult().Errors); } - private class ReloadTypeModule : TypeModule + public sealed class HotReloadConfiguration : IObservable { - public void Evict() => OnTypesChanged(); + private GatewayConfiguration _configuration; + private Session? _session; + + public HotReloadConfiguration(GatewayConfiguration configuration) + { + _configuration = configuration ?? + throw new ArgumentNullException(nameof(configuration)); + } + + public void SetConfiguration(GatewayConfiguration configuration) + { + _configuration = configuration ?? + throw new ArgumentNullException(nameof(configuration)); + _session?.Update(); + } + + public IDisposable Subscribe(IObserver observer) + { + var session = _session = new Session(this, observer); + session.Update(); + return session; + } + + private sealed class Session : IDisposable + { + private readonly HotReloadConfiguration _owner; + private readonly IObserver _observer; + + public Session(HotReloadConfiguration owner, IObserver observer) + { + _owner = owner; + _observer = observer; + } + + public void Update() + { + _observer.OnNext(_owner._configuration); + } + + public void Dispose() + { + + } + } } } diff --git a/src/HotChocolate/Fusion/test/Core.Tests/ErrorTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/ErrorTests.cs index 486e3f33b71..1676205fcc3 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/ErrorTests.cs +++ b/src/HotChocolate/Fusion/test/Core.Tests/ErrorTests.cs @@ -43,7 +43,8 @@ public async Task Accounts_Offline_Author_Nullable() .AddSingleton( new ErrorFactory(demoProject.HttpClientFactory, demoProject.Accounts.Name)) .AddSingleton(demoProject.WebSocketConnectionFactory) - .AddFusionGatewayServer(SchemaFormatter.FormatAsDocument(fusionGraph)) + .AddFusionGatewayServer() + .ConfigureFromDocument(SchemaFormatter.FormatAsDocument(fusionGraph)) .BuildRequestExecutorAsync(); var request = Parse( @@ -92,7 +93,8 @@ public async Task Accounts_Offline_Author_NonNull() .AddSingleton( new ErrorFactory(demoProject.HttpClientFactory, demoProject.Accounts.Name)) .AddSingleton(demoProject.WebSocketConnectionFactory) - .AddFusionGatewayServer(SchemaFormatter.FormatAsDocument(fusionGraph)) + .AddFusionGatewayServer() + .ConfigureFromDocument(SchemaFormatter.FormatAsDocument(fusionGraph)) .BuildRequestExecutorAsync(); var request = Parse( @@ -141,7 +143,8 @@ public async Task Accounts_Offline_Reviews_ListElement_Nullable() .AddSingleton( new ErrorFactory(demoProject.HttpClientFactory, demoProject.Accounts.Name)) .AddSingleton(demoProject.WebSocketConnectionFactory) - .AddFusionGatewayServer(SchemaFormatter.FormatAsDocument(fusionGraph)) + .AddFusionGatewayServer() + .ConfigureFromDocument(SchemaFormatter.FormatAsDocument(fusionGraph)) .BuildRequestExecutorAsync(); var request = Parse( diff --git a/src/HotChocolate/Fusion/test/Core.Tests/InterfaceTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/InterfaceTests.cs index 3a02145f203..9ccf5f9771b 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/InterfaceTests.cs +++ b/src/HotChocolate/Fusion/test/Core.Tests/InterfaceTests.cs @@ -37,7 +37,8 @@ public async Task Query_Interface_List() var executor = await new ServiceCollection() .AddSingleton(demoProject.HttpClientFactory) .AddSingleton(demoProject.WebSocketConnectionFactory) - .AddFusionGatewayServer(SchemaFormatter.FormatAsDocument(fusionGraph)) + .AddFusionGatewayServer() + .ConfigureFromDocument(SchemaFormatter.FormatAsDocument(fusionGraph)) .BuildRequestExecutorAsync(); var request = Parse( @@ -86,7 +87,8 @@ public async Task Query_Interface_List_With_Fragment() var executor = await new ServiceCollection() .AddSingleton(demoProject.HttpClientFactory) .AddSingleton(demoProject.WebSocketConnectionFactory) - .AddFusionGatewayServer(SchemaFormatter.FormatAsDocument(fusionGraph)) + .AddFusionGatewayServer() + .ConfigureFromDocument(SchemaFormatter.FormatAsDocument(fusionGraph)) .BuildRequestExecutorAsync(); var request = Parse( @@ -140,7 +142,8 @@ public async Task Query_Interface_List_With_Fragment_Fetch() var executor = await new ServiceCollection() .AddSingleton(demoProject.HttpClientFactory) .AddSingleton(demoProject.WebSocketConnectionFactory) - .AddFusionGatewayServer(SchemaFormatter.FormatAsDocument(fusionGraph)) + .AddFusionGatewayServer() + .ConfigureFromDocument(SchemaFormatter.FormatAsDocument(fusionGraph)) .BuildRequestExecutorAsync(); var request = Parse( diff --git a/templates/v12/gateway/Program.cs b/templates/v12/gateway/Program.cs index 8c018036a89..f1df3c7c383 100644 --- a/templates/v12/gateway/Program.cs +++ b/templates/v12/gateway/Program.cs @@ -3,9 +3,8 @@ builder.Services.AddHttpClient(); builder.Services - .AddFusionGatewayServer( - "./gateway.fgp", - watchFileForUpdates: true); + .AddFusionGatewayServer() + .ConfigureFromFile("./gateway.fgp"); var app = builder.Build();