csharp-sdk/tests/ModelContextProtocol.AspNetCore.Tests/Utils/KestrelInMemoryTransport.cs
Stephen Halter e01edc143a
Fix slow shutdown when a Streamable HTTP client is connected (#843)
* Add "Async" suffix to async methods
2025-10-10 17:50:57 -07:00

60 lines
2.2 KiB
C#

using Microsoft.AspNetCore.Connections;
using System.Collections.Concurrent;
using System.Net;
using System.Threading.Channels;
namespace ModelContextProtocol.AspNetCore.Tests.Utils;
public sealed class KestrelInMemoryTransport : IConnectionListenerFactory
{
// socket accept queues keyed by listen port.
private readonly ConcurrentDictionary<int, Channel<ConnectionContext>> _acceptQueues = [];
public KestrelInMemoryConnection CreateConnection(EndPoint endpoint)
{
var connection = new KestrelInMemoryConnection();
if (!GetAcceptQueue(endpoint).Writer.TryWrite(connection))
{
throw new IOException("The KestrelInMemoryTransport has been shut down.");
};
return connection;
}
public ValueTask<IConnectionListener> BindAsync(EndPoint endpoint, CancellationToken cancellationToken = default) =>
new(new KestrelInMemoryListener(endpoint, GetAcceptQueue(endpoint)));
private Channel<ConnectionContext> GetAcceptQueue(EndPoint endpoint) =>
_acceptQueues.GetOrAdd(GetEndpointPort(endpoint), _ => Channel.CreateUnbounded<ConnectionContext>());
private static int GetEndpointPort(EndPoint endpoint) =>
endpoint switch
{
DnsEndPoint dnsEndpoint => dnsEndpoint.Port,
IPEndPoint ipEndpoint => ipEndpoint.Port,
_ => throw new InvalidOperationException($"Unexpected endpoint type: '{endpoint.GetType()}'"),
};
private sealed class KestrelInMemoryListener(EndPoint endpoint, Channel<ConnectionContext> acceptQueue) : IConnectionListener
{
public EndPoint EndPoint => endpoint;
public async ValueTask<ConnectionContext?> AcceptAsync(CancellationToken cancellationToken = default)
{
await foreach (var item in acceptQueue.Reader.ReadAllAsync(cancellationToken))
{
return item;
}
return null;
}
public ValueTask UnbindAsync(CancellationToken cancellationToken = default)
{
acceptQueue.Writer.TryComplete();
return default;
}
public ValueTask DisposeAsync() => UnbindAsync(CancellationToken.None);
}
}