Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System.Collections.Concurrent;
using HotChocolate.Execution;
using HotChocolate.Features;
using HotChocolate.Utilities;
using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.AspNetCore;
Expand All @@ -15,42 +14,34 @@ internal sealed class McpRequestExecutorProxy(
string schemaName)
: RequestExecutorProxy(executorProvider, executorEvents, schemaName)
{
private McpExecutorSession? _session;
private StreamableHttpHandler? _handler;

public async ValueTask<McpExecutorSession> GetOrCreateSessionAsync(
public async ValueTask<StreamableHttpHandler> GetStreamableHttpHandlerAsync(
CancellationToken cancellationToken)
{
if (_session is not null)
if (_handler is not null)
{
return _session;
return _handler;
}

var executor = await GetExecutorAsync(cancellationToken).ConfigureAwait(false);
return executor.Features.GetRequired<McpExecutorSession>();

return executor.Schema.Services.GetRequiredService<StreamableHttpHandler>();
}

protected override void OnConfigureRequestExecutor(
IRequestExecutor newExecutor,
IRequestExecutor? oldExecutor)
{
if (oldExecutor is not null)
{
newExecutor.Features.Set(
oldExecutor.Schema.Services.GetRequiredService<ConcurrentDictionary<string, McpServer>>());
}

var session = new McpExecutorSession(newExecutor.Schema.Services.GetRequiredService<StreamableHttpHandler>());

newExecutor.Features.Set(session);
_session = session;
_handler = newExecutor.Schema.Services.GetRequiredService<StreamableHttpHandler>();
}

protected override void OnAfterRequestExecutorSwapped(
IRequestExecutor newExecutor,
IRequestExecutor oldExecutor)
{
var mcpServers =
newExecutor.Features.GetRequired<ConcurrentDictionary<string, McpServer>>();
var mcpServers = oldExecutor.Schema.Services
.GetRequiredService<ConcurrentDictionary<string, McpServer>>();

foreach (var mcpServer in mcpServers.Values)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,41 +14,41 @@ public StreamableHttpHandlerProxy(McpRequestExecutorProxy mcpRequestExecutor)

public async Task HandlePostRequestAsync(HttpContext context)
{
var session = await _mcpRequestExecutor.GetOrCreateSessionAsync(context.RequestAborted);
await session.StreamableHttpHandler.HandlePostRequestAsync(context);
var handler =
await _mcpRequestExecutor.GetStreamableHttpHandlerAsync(context.RequestAborted);
await handler.HandlePostRequestAsync(context);
}

public async Task HandleGetRequestAsync(HttpContext context)
{
var session = await _mcpRequestExecutor.GetOrCreateSessionAsync(context.RequestAborted);
var handler =
await _mcpRequestExecutor.GetStreamableHttpHandlerAsync(context.RequestAborted);

if (IsStateless(session))
if (handler.HttpServerTransportOptions.Stateless)
{
WriteMethodNotAllowed(context);

return;
}

await session.StreamableHttpHandler.HandleGetRequestAsync(context);
await handler.HandleGetRequestAsync(context);
}

public async Task HandleDeleteRequestAsync(HttpContext context)
{
var session = await _mcpRequestExecutor.GetOrCreateSessionAsync(context.RequestAborted);
var handler =
await _mcpRequestExecutor.GetStreamableHttpHandlerAsync(context.RequestAborted);

if (IsStateless(session))
if (handler.HttpServerTransportOptions.Stateless)
{
WriteMethodNotAllowed(context);

return;
}

await session.StreamableHttpHandler.HandleDeleteRequestAsync(context);
await handler.HandleDeleteRequestAsync(context);
}

private static bool IsStateless(McpExecutorSession session)
=> session.StreamableHttpHandler.HttpServerTransportOptions.Stateless;

private static void WriteMethodNotAllowed(HttpContext context)
{
context.Response.Headers.Allow = "POST";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using HotChocolate.Features;
using Microsoft.Extensions.DependencyInjection;

namespace HotChocolate.AspNetCore;
Expand All @@ -19,14 +18,13 @@ public async ValueTask<ExecutorSession> GetOrCreateSessionAsync(CancellationToke
}

var executor = await GetExecutorAsync(cancellationToken);
return executor.Features.GetRequired<ExecutorSession>();

return GetSession(executor);
}

protected override void OnConfigureRequestExecutor(IRequestExecutor newExecutor, IRequestExecutor? oldExecutor)
{
var session = new ExecutorSession(newExecutor);
newExecutor.Features.Set(session);
_session = session;
_session = GetSession(newExecutor);
}

public static HttpRequestExecutorProxy Create(IServiceProvider services, string schemaName)
Expand All @@ -35,4 +33,7 @@ public static HttpRequestExecutorProxy Create(IServiceProvider services, string
var executorEvents = services.GetRequiredService<IRequestExecutorEvents>();
return new HttpRequestExecutorProxy(executorProvider, executorEvents, schemaName);
}

private static ExecutorSession GetSession(IRequestExecutor executor)
=> executor.Schema.Services.GetService<ExecutorSession>() ?? new ExecutorSession(executor);
}
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,9 @@ private static IRequestExecutorBuilder AddGraphQLServerCore(
.Get(schemaName);
return new ExecutionConcurrencyGate(serverOptions.MaxConcurrentExecutions);
});

s.TryAddSingleton(
static sp => new ExecutorSession(sp.GetRequiredService<IRequestExecutor>()));
});

builder.Services.TryAddEnumerable(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using HotChocolate.Execution;
using Microsoft.Extensions.DependencyInjection;

namespace HotChocolate.AspNetCore;

public class HttpRequestExecutorProxyTests
{
[Fact]
public async Task GetOrCreateSessionAsync_Should_ReturnSameSession_When_ProxiesShareExecutor()
{
// arrange
var cancellationToken = TestContext.Current.CancellationToken;
await using var services = CreateServices();
using var first = HttpRequestExecutorProxy.Create(services, ISchemaDefinition.DefaultName);
using var second = HttpRequestExecutorProxy.Create(services, ISchemaDefinition.DefaultName);

// act
var firstSession = await first.GetOrCreateSessionAsync(cancellationToken);
var secondSession = await second.GetOrCreateSessionAsync(cancellationToken);

// assert
Assert.Same(firstSession, secondSession);
}

[Theory]
[InlineData(2)]
[InlineData(16)]
public async Task GetOrCreateSessionAsync_Should_ReturnOneSession_When_ProxiesStartConcurrently(
int proxyCount)
{
// arrange
var cancellationToken = TestContext.Current.CancellationToken;
var sessionCountsPerExecutor = new List<int>();

// act
for (var i = 0; i < 20; i++)
{
await using var services = CreateServices();
await services
.GetRequiredService<IRequestExecutorProvider>()
.GetExecutorAsync(cancellationToken: cancellationToken);

var proxies = new HttpRequestExecutorProxy[proxyCount];

for (var j = 0; j < proxyCount; j++)
{
proxies[j] = HttpRequestExecutorProxy.Create(
services,
ISchemaDefinition.DefaultName);
}

var start = new TaskCompletionSource(
TaskCreationOptions.RunContinuationsAsynchronously);
var sessionTasks = proxies
.Select(
async proxy =>
{
await start.Task;
return await proxy.GetOrCreateSessionAsync(cancellationToken);
})
.ToArray();
start.SetResult();
var sessions = await Task.WhenAll(sessionTasks);

sessionCountsPerExecutor.Add(sessions.Distinct().Count());

foreach (var proxy in proxies)
{
proxy.Dispose();
}
}

// assert
Assert.All(sessionCountsPerExecutor, count => Assert.Equal(1, count));
}

private static ServiceProvider CreateServices()
=> new ServiceCollection()
.AddGraphQLServer()
.AddQueryType<Query>()
.Services
.BuildServiceProvider();

public sealed class Query
{
public string Greeting => "hello";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ private static IFusionGatewayBuilder AddGraphQLGatewayServerCore(
.Get(schemaName);
return new ExecutionConcurrencyGate(serverOptions.MaxConcurrentExecutions);
});

sc.TryAddSingleton(
static sp => new ExecutorSession(sp.GetRequiredService<IRequestExecutor>()));
});

return builder;
Expand Down
Loading