From 91560fb169bfbb04f3de6a2b2142014b2153ff05 Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Thu, 2 Oct 2025 08:11:03 -0700 Subject: [PATCH] Move options validation to constructor This works around the breaking change to BackgroundService in https://github.com/dotnet/runtime/pull/116283 which launches ExecuteAsync using Task.Run --- .../IdleTrackingBackgroundService.cs | 34 ++++++++++++------- .../StreamableHttpServerConformanceTests.cs | 8 ----- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/ModelContextProtocol.AspNetCore/IdleTrackingBackgroundService.cs b/src/ModelContextProtocol.AspNetCore/IdleTrackingBackgroundService.cs index a4ae569ba..d68f83e5d 100644 --- a/src/ModelContextProtocol.AspNetCore/IdleTrackingBackgroundService.cs +++ b/src/ModelContextProtocol.AspNetCore/IdleTrackingBackgroundService.cs @@ -4,16 +4,18 @@ namespace ModelContextProtocol.AspNetCore; -internal sealed partial class IdleTrackingBackgroundService( - StatefulSessionManager sessions, - IOptions options, - IHostApplicationLifetime appLifetime, - ILogger logger) : BackgroundService +internal sealed partial class IdleTrackingBackgroundService : BackgroundService { - // Workaround for https://github.com/dotnet/runtime/issues/91121. This is fixed in .NET 9 and later. - private readonly ILogger _logger = logger; + private readonly StatefulSessionManager _sessions; + private readonly IOptions _options; + private readonly IHostApplicationLifetime _appLifetime; + private readonly ILogger _logger; - protected override async Task ExecuteAsync(CancellationToken stoppingToken) + public IdleTrackingBackgroundService( + StatefulSessionManager sessions, + IOptions options, + IHostApplicationLifetime appLifetime, + ILogger logger) { // Still run loop given infinite IdleTimeout to enforce the MaxIdleSessionCount and assist graceful shutdown. if (options.Value.IdleTimeout != Timeout.InfiniteTimeSpan) @@ -23,14 +25,22 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) ArgumentOutOfRangeException.ThrowIfLessThan(options.Value.MaxIdleSessionCount, 0); + _sessions = sessions; + _options = options; + _appLifetime = appLifetime; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { try { - var timeProvider = options.Value.TimeProvider; + var timeProvider = _options.Value.TimeProvider; using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5), timeProvider); while (!stoppingToken.IsCancellationRequested && await timer.WaitForNextTickAsync(stoppingToken)) { - await sessions.PruneIdleSessionsAsync(stoppingToken); + await _sessions.PruneIdleSessionsAsync(stoppingToken); } } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) @@ -40,7 +50,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) { try { - await sessions.DisposeAllSessionsAsync(); + await _sessions.DisposeAllSessionsAsync(); } finally { @@ -48,7 +58,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) { // Something went terribly wrong. A very unexpected exception must be bubbling up, but let's ensure we also stop the application, // so that it hopefully gets looked at and restarted. This shouldn't really be reachable. - appLifetime.StopApplication(); + _appLifetime.StopApplication(); IdleTrackingBackgroundServiceStoppedUnexpectedly(); } } diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs index f180cdaf9..7b2be8f98 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs @@ -57,11 +57,7 @@ public async ValueTask DisposeAsync() base.Dispose(); } -#if !NET10_0 [Fact] -#else - [Fact(Skip = "https://github.com/modelcontextprotocol/csharp-sdk/issues/823")] -#endif public async Task NegativeNonInfiniteIdleTimeout_Throws_ArgumentOutOfRangeException() { Builder.Services.AddMcpServer().WithHttpTransport(options => @@ -73,11 +69,7 @@ public async Task NegativeNonInfiniteIdleTimeout_Throws_ArgumentOutOfRangeExcept Assert.Contains("IdleTimeout", ex.Message); } -#if !NET10_0 [Fact] -#else - [Fact(Skip = "https://github.com/modelcontextprotocol/csharp-sdk/issues/823")] -#endif public async Task NegativeMaxIdleSessionCount_Throws_ArgumentOutOfRangeException() { Builder.Services.AddMcpServer().WithHttpTransport(options =>