Conversation
…roduce IMcpProvider and IOpenApiProvider interfaces
…method, refactor OpenApiManager and McpManager to support new transport setup, and introduce tests for MCP setup composition.
…onsistency and update related tests
…me for consistency in GetSetup methods
…amline registration handling
…p-pattern # Conflicts: # src/HotChocolate/Adapters/src/Adapters.Mcp.Core/Extensions/EndpointRouteBuilderExtensions.cs # src/HotChocolate/Adapters/src/Adapters.OpenApi.AspNetCore/Extensions/OpenApiOptionsExtensions.cs
There was a problem hiding this comment.
Pull request overview
This PR refactors the OpenAPI and MCP adapters to use an options-based per-schema setup model (via IOptionsMonitor<T>), replacing prior keyed-service registrations with manager-based lookups.
Changes:
- Introduces
OpenApiSetup/McpSetupoptions andOpenApiManager/McpManagerregistries for per-schema composition and resolution. - Updates OpenAPI/MCP builder and endpoint extensions to resolve per-schema infrastructure through the managers instead of keyed DI services.
- Adds tests to validate setup composition and schema-name discovery for both adapters.
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/HotChocolate/Adapters/test/Adapters.OpenApi.Tests/Extensions/OpenApiSetupTests.cs | Adds tests asserting OpenAPI per-schema setup registration and name discovery. |
| src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/Extensions/McpSetupCompositionTests.cs | Adds tests asserting MCP setup composition across multiple AddMcp calls and name discovery. |
| src/HotChocolate/Adapters/src/Fusion.Adapters.OpenApi/Extensions/OpenApiFusionGatewayBuilderExtensions.cs | Switches OpenAPI storage registration to options-based setup; warmup uses OpenApiManager. |
| src/HotChocolate/Adapters/src/Fusion.Adapters.Mcp/Extensions/FusionGatewayBuilderExtensions.cs | Switches MCP registration to options-based setup; updates warmup task wiring. |
| src/HotChocolate/Adapters/src/Adapters.OpenApi/Extensions/OpenApiRequestExecutorBuilderExtensions.cs | Switches OpenAPI storage registration to options-based setup; warmup uses OpenApiManager. |
| src/HotChocolate/Adapters/src/Adapters.OpenApi.Core/OpenApiRegistration.cs | Adds internal container for OpenAPI per-schema runtime objects. |
| src/HotChocolate/Adapters/src/Adapters.OpenApi.Core/OpenApiManager.cs | Adds manager responsible for building/caching per-schema OpenAPI runtime objects from options. |
| src/HotChocolate/Adapters/src/Adapters.OpenApi.Core/IOpenApiProvider.cs | Adds internal provider abstraction for schema name inference. |
| src/HotChocolate/Adapters/src/Adapters.OpenApi.Core/Extensions/InternalServiceCollectionExtensions.cs | Registers OpenApiManager and options infrastructure (replacing keyed services). |
| src/HotChocolate/Adapters/src/Adapters.OpenApi.Core/Execution/DynamicEndpointMiddleware.cs | Resolves executor proxy through OpenApiManager instead of keyed DI. |
| src/HotChocolate/Adapters/src/Adapters.OpenApi.Core/Configuration/OpenApiTransportSetup.cs | Adds internal transport setup options for endpoint/document transformer factories. |
| src/HotChocolate/Adapters/src/Adapters.OpenApi.Core/Configuration/OpenApiSetup.cs | Adds public per-schema OpenAPI setup options (storage factory). |
| src/HotChocolate/Adapters/src/Adapters.OpenApi.AspNetCore/Extensions/OpenApiOptionsExtensions.cs | Resolves OpenAPI document transformer via OpenApiManager for ASP.NET OpenAPI generation. |
| src/HotChocolate/Adapters/src/Adapters.OpenApi.AspNetCore/Extensions/InternalServiceCollectionExtensions.cs | Uses PostConfigureAll to supply default OpenAPI transport factories. |
| src/HotChocolate/Adapters/src/Adapters.OpenApi.AspNetCore/Extensions/EndpointRouteBuilderExtensions.cs | Resolves endpoint data source via OpenApiManager for mapping endpoints. |
| src/HotChocolate/Adapters/src/Adapters.Mcp/Extensions/RequestExecutorBuilderExtensions.cs | Switches MCP registration to options-based setup; updates warmup task wiring. |
| src/HotChocolate/Adapters/src/Adapters.Mcp.Core/McpStorageWarmupTask.cs | Adds warmup task wrapper for starting MCP storage observer. |
| src/HotChocolate/Adapters/src/Adapters.Mcp.Core/McpRegistration.cs | Adds internal container for MCP per-schema runtime objects. |
| src/HotChocolate/Adapters/src/Adapters.Mcp.Core/McpManager.cs | Adds manager responsible for caching per-schema MCP runtime objects. |
| src/HotChocolate/Adapters/src/Adapters.Mcp.Core/IMcpProvider.cs | Adds internal provider abstraction for schema name inference. |
| src/HotChocolate/Adapters/src/Adapters.Mcp.Core/Extensions/ServiceCollectionExtensions.cs | Registers McpManager/options and adds per-schema setup composition logic. |
| src/HotChocolate/Adapters/src/Adapters.Mcp.Core/Extensions/EndpointRouteBuilderExtensions.cs | Resolves MCP handler proxy via McpManager for endpoint mapping. |
| src/HotChocolate/Adapters/src/Adapters.Mcp.Core/Configuration/McpSetup.cs | Adds public per-schema MCP setup options (modifier lists). |
Comments suppressed due to low confidence (1)
src/HotChocolate/Adapters/src/Adapters.OpenApi.AspNetCore/Extensions/EndpointRouteBuilderExtensions.cs:22
- MapOpenApiEndpoints downcasts the registration's EndpointDataSource to DynamicEndpointDataSource. If the endpoint data source is ever swapped via transport setup, this will fail at runtime. Consider changing the registration/transport setup to expose an EndpointDataSource (base type) directly, or otherwise avoid relying on this concrete cast here.
var dataSource = (DynamicEndpointDataSource)manager.Get(schemaName).EndpointDataSource;
if (!endpoints.DataSources.Contains(dataSource))
{
endpoints.DataSources.Add(dataSource);
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| var storageFactory = | ||
| setup.StorageFactory | ||
| ?? throw new InvalidOperationException( | ||
| $"No OpenAPI definition storage is registered for schema '{name}'. " | ||
| + "Call AddOpenApiDefinitionStorage(...) when configuring the GraphQL server."); | ||
|
|
||
| var storage = storageFactory(_applicationServices); | ||
| var endpointDataSource = transportSetup.EndpointDataSourceFactory!(); | ||
| var documentTransformer = transportSetup.DocumentTransformerFactory!(); | ||
| var registry = new OpenApiDefinitionRegistry(storage, documentTransformer, endpointDataSource); | ||
| var executorProxy = HttpRequestExecutorProxy.Create(_applicationServices, name); | ||
|
|
||
| return new OpenApiRegistration(registry, executorProxy, endpointDataSource, documentTransformer); |
There was a problem hiding this comment.
OpenApiManager creates and caches several disposable objects (OpenApiDefinitionRegistry is IAsyncDisposable; HttpRequestExecutorProxy ultimately implements IDisposable; DynamicEndpointDataSource implements IDisposable) but nothing disposes them when the application ServiceProvider is disposed. This is a regression from the previous DI-registered singletons and can leak event subscriptions/CTS resources. Consider creating a per-schema IServiceScope and storing it in the registration, or make OpenApiManager implement IAsyncDisposable/IDisposable and dispose the created registration components when the container shuts down.
| var storage = storageFactory(_applicationServices); | ||
| var endpointDataSource = transportSetup.EndpointDataSourceFactory!(); | ||
| var documentTransformer = transportSetup.DocumentTransformerFactory!(); |
There was a problem hiding this comment.
CreateRegistration dereferences transportSetup.EndpointDataSourceFactory and DocumentTransformerFactory with null-forgiving operators. If AddOpenApiAspNetCoreServices() was not registered (or registration order changes), this will throw a NullReferenceException without a clear error. Prefer validating these factories and throwing an InvalidOperationException with guidance on the missing registration.
| var storage = storageFactory(_applicationServices); | |
| var endpointDataSource = transportSetup.EndpointDataSourceFactory!(); | |
| var documentTransformer = transportSetup.DocumentTransformerFactory!(); | |
| var endpointDataSourceFactory = | |
| transportSetup.EndpointDataSourceFactory | |
| ?? throw new InvalidOperationException( | |
| $"No OpenAPI endpoint data source factory is registered for schema '{name}'. " | |
| + "Call AddOpenApiAspNetCoreServices() when configuring the GraphQL server."); | |
| var documentTransformerFactory = | |
| transportSetup.DocumentTransformerFactory | |
| ?? throw new InvalidOperationException( | |
| $"No OpenAPI document transformer factory is registered for schema '{name}'. " | |
| + "Call AddOpenApiAspNetCoreServices() when configuring the GraphQL server."); | |
| var storage = storageFactory(_applicationServices); | |
| var endpointDataSource = endpointDataSourceFactory(); | |
| var documentTransformer = documentTransformerFactory(); |
| var transformer = (DynamicOpenApiDocumentTransformer)manager | ||
| .Get(resolvedSchemaName) | ||
| .DocumentTransformer; | ||
|
|
||
| return transformer.TransformAsync(document, context, ct); |
There was a problem hiding this comment.
AddGraphQLTransformer casts the registration's DocumentTransformer to DynamicOpenApiDocumentTransformer. Since the registration exposes it as IDynamicOpenApiDocumentTransformer, this cast is a runtime hazard if the transport setup ever changes (InvalidCastException). Prefer storing/exposing the transformer as IOpenApiDocumentTransformer (or a combined interface) so this code can call TransformAsync without a concrete cast.
| private McpRegistration CreateRegistration(string name) | ||
| { | ||
| var executorProxy = new McpRequestExecutorProxy( | ||
| _applicationServices.GetRequiredService<IRequestExecutorProvider>(), | ||
| _applicationServices.GetRequiredService<IRequestExecutorEvents>(), | ||
| name); | ||
|
|
||
| var handlerProxy = new StreamableHttpHandlerProxy(executorProxy); | ||
|
|
||
| return new McpRegistration(executorProxy, handlerProxy); |
There was a problem hiding this comment.
McpManager creates McpRequestExecutorProxy (inherits RequestExecutorProxy and implements IDisposable) but never disposes it. This can leak the executor event subscription and CTS held by RequestExecutorProxy. Consider storing registrations as container-managed singletons (so DI disposes them), or make McpManager implement IDisposable and dispose created proxies when the ServiceProvider is disposed.
| var setup = services | ||
| .BuildServiceProvider() | ||
| .GetRequiredService<IOptionsMonitor<OpenApiSetup>>() | ||
| .Get(ISchemaDefinition.DefaultName); | ||
|
|
||
| // assert | ||
| Assert.NotNull(setup.StorageFactory); | ||
| Assert.Same(storage, setup.StorageFactory(null!)); |
There was a problem hiding this comment.
This test invokes StorageFactory with null even though the delegate is typed as Func<IServiceProvider, ...>. Passing a real IServiceProvider here (e.g., the one built in this test) would better match runtime usage and avoid masking future StorageFactory implementations that rely on the provider.
| var setup = services | |
| .BuildServiceProvider() | |
| .GetRequiredService<IOptionsMonitor<OpenApiSetup>>() | |
| .Get(ISchemaDefinition.DefaultName); | |
| // assert | |
| Assert.NotNull(setup.StorageFactory); | |
| Assert.Same(storage, setup.StorageFactory(null!)); | |
| var serviceProvider = services.BuildServiceProvider(); | |
| var setup = serviceProvider | |
| .GetRequiredService<IOptionsMonitor<OpenApiSetup>>() | |
| .Get(ISchemaDefinition.DefaultName); | |
| // assert | |
| Assert.NotNull(setup.StorageFactory); | |
| Assert.Same(storage, setup.StorageFactory(serviceProvider)); |
| foreach (var modifier in setup.ServerModifiers) | ||
| { | ||
| modifier(null!); | ||
| } |
There was a problem hiding this comment.
The test calls each ServerModifier with null (modifier(null!)). This works for the current lambdas but can hide issues if a modifier starts using the IMcpServerBuilder parameter. Consider invoking modifiers with a minimal stub/mock IMcpServerBuilder instead so the test reflects real usage more closely.
No description provided.