From 75dc40bcbfada3edf395beff5fa38531034d7b58 Mon Sep 17 00:00:00 2001 From: Dmytro Pryvedeniuk Date: Fri, 20 Feb 2026 00:25:27 +0200 Subject: [PATCH 1/3] Avoid flaky issues with endpoints discovery --- ...ug_2182_unresolved_IProblemDetailSource.cs | 258 +++++++++--------- .../IntegrationContext.cs | 107 +------- .../Wolverine.Http.Tests.csproj | 16 +- .../open_api_generation.cs | 2 +- 4 files changed, 151 insertions(+), 232 deletions(-) diff --git a/src/Http/Wolverine.Http.Tests/Bugs/Bug_2182_unresolved_IProblemDetailSource.cs b/src/Http/Wolverine.Http.Tests/Bugs/Bug_2182_unresolved_IProblemDetailSource.cs index 77f192d14..f71eaa32f 100644 --- a/src/Http/Wolverine.Http.Tests/Bugs/Bug_2182_unresolved_IProblemDetailSource.cs +++ b/src/Http/Wolverine.Http.Tests/Bugs/Bug_2182_unresolved_IProblemDetailSource.cs @@ -1,6 +1,7 @@ using Alba; using FluentValidation; using IntegrationTests; +using JasperFx.CodeGeneration; using Marten; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; @@ -12,153 +13,148 @@ namespace Wolverine.Http.Tests.Bugs; public class Bug_2182_unresolved_IProblemDetailSource { - private readonly WebApplicationBuilder _builder; + private readonly WebApplicationBuilder _builder; - public Bug_2182_unresolved_IProblemDetailSource() - { - _builder = WebApplication.CreateBuilder([]); - - _builder.Services.AddMarten(Servers.PostgresConnectionString); - _builder.Services.DisableAllWolverineMessagePersistence(); - _builder.Services.DisableAllExternalWolverineTransports(); - - _builder.Services.AddWolverineHttp(); - } - - [Fact] - public async Task can_not_compile_with_manual_discovery_by_default() - { - _builder.Services.AddWolverine(ExtensionDiscovery.ManualOnly, opts => - { - opts.Discovery.IncludeAssembly(typeof(Bug_2182_Endpoint.Request).Assembly); - opts.UseFluentValidation(); // from Wolverine.FluentValidation - // ExtensionDiscovery.ManualMode and Wolverine.Http.FluentValidation services are not registered - }); - - await using var host = await AlbaHost.For(_builder, app => + public Bug_2182_unresolved_IProblemDetailSource() { - app.MapWolverineEndpoints(opts => - { - opts.UseFluentValidationProblemDetailMiddleware(); - }); - }); + _builder = WebApplication.CreateBuilder([]); - var result = await host.Scenario(x => - { - x.Post.Json(new Bug_2182_Endpoint.Request("valid")).ToUrl("/Bug_2182"); - x.StatusCodeShouldBe(500); - }); - - result.ReadAsText() - .ShouldContain( - "JasperFx was unable to resolve a variable of type " + - "Wolverine.Http.FluentValidation.IProblemDetailSource " + - "as part of the method POST_Bug_2182.Handle(Microsoft.AspNetCore.Http.HttpContext httpContext)"); - } - - [Fact] - public async Task can_compile_with_manual_extension_discovery_when_problem_detail_services_are_registered() - { - _builder.Services.AddWolverine(ExtensionDiscovery.ManualOnly, opts => - { - opts.Discovery.IncludeAssembly(typeof(Bug_2182_Endpoint.Request).Assembly); - opts.UseFluentValidation(); // from Wolverine.FluentValidation - opts.UseFluentValidationProblemDetail(); // from Wolverine.Http.FluentValidation - }); + _builder.Services.AddMarten(Servers.PostgresConnectionString); + _builder.Services.DisableAllWolverineMessagePersistence(); + _builder.Services.DisableAllExternalWolverineTransports(); - await using var host = await AlbaHost.For(_builder, app => - { - app.MapWolverineEndpoints(opts => - { - opts.UseFluentValidationProblemDetailMiddleware(); - }); - }); + _builder.Services.AddWolverineHttp(); + } - await host.Scenario(x => - { - x.Post.Json(new Bug_2182_Endpoint.Request("valid")).ToUrl("/Bug_2182"); - x.StatusCodeShouldBe(200); - }); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task can_compile_with_default_extension_discovery(bool useFluentValidationProblemDetail) - { - _builder.Services.AddWolverine(opts => + [Fact] + public async Task can_not_compile_with_manual_discovery_by_default() { - opts.Discovery.IncludeAssembly(typeof(Bug_2182_Endpoint.Request).Assembly); - opts.UseFluentValidation(); - if (useFluentValidationProblemDetail) - opts.UseFluentValidationProblemDetail(); - }); + _builder.Services.AddWolverine(ExtensionDiscovery.ManualOnly, opts => + { + opts.Discovery.IncludeAssembly(typeof(Bug_2182_Endpoint.Request).Assembly); + opts.UseFluentValidation(); // from Wolverine.FluentValidation + // ExtensionDiscovery.ManualMode and Wolverine.Http.FluentValidation services are not registered + }); + + await using var host = await AlbaHost.For(_builder, app => + { + app.MapWolverineEndpoints(opts => + { + opts.UseFluentValidationProblemDetailMiddleware(); + }); + }); + + var exception = await host.Scenario(x => + { + x.Post.Json(new Bug_2182_Endpoint.Request("valid")).ToUrl("/Bug_2182"); + x.StatusCodeShouldBe(500); + }).ShouldThrowAsync(); + + exception.Message.ShouldContain( + "JasperFx was unable to resolve a variable of type " + + "Wolverine.Http.FluentValidation.IProblemDetailSource " + + "as part of the method POST_Bug_2182.Handle(Microsoft.AspNetCore.Http.HttpContext httpContext)"); + } - await using var host = await AlbaHost.For(_builder, app => + [Fact] + public async Task can_compile_with_manual_extension_discovery_when_problem_detail_services_are_registered() { - app.MapWolverineEndpoints(opts => - { - opts.UseFluentValidationProblemDetailMiddleware(); - }); - }); + _builder.Services.AddWolverine(ExtensionDiscovery.ManualOnly, opts => + { + opts.Discovery.IncludeAssembly(typeof(Bug_2182_Endpoint.Request).Assembly); + opts.UseFluentValidation(); // from Wolverine.FluentValidation + opts.UseFluentValidationProblemDetail(); // from Wolverine.Http.FluentValidation + }); + + await using var host = await AlbaHost.For(_builder, app => + { + app.MapWolverineEndpoints(opts => + { + opts.UseFluentValidationProblemDetailMiddleware(); + }); + }); + + await host.Scenario(x => + { + x.Post.Json(new Bug_2182_Endpoint.Request("valid")).ToUrl("/Bug_2182"); + x.StatusCodeShouldBe(200); + }); + } - await host.Scenario(x => - { - x.Post.Json(new Bug_2182_Endpoint.Request("valid")).ToUrl("/Bug_2182"); - x.StatusCodeShouldBe(200); - }); - } - - [Theory] - [InlineData(ExtensionDiscovery.ManualOnly, true)] - [InlineData(ExtensionDiscovery.Automatic, true)] - [InlineData(ExtensionDiscovery.Automatic, false)] - public async Task can_validate_request_with_problem_detail_middleware( - ExtensionDiscovery extensionDiscovery, bool useFluentValidationProblemDetail) - { - _builder.Services.AddWolverine(extensionDiscovery, opts => + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task can_compile_with_default_extension_discovery(bool useFluentValidationProblemDetail) { - opts.Discovery.IncludeAssembly(typeof(Bug_2182_Endpoint.Request).Assembly); - opts.UseFluentValidation(); - if (useFluentValidationProblemDetail) - opts.UseFluentValidationProblemDetail(); - }); + _builder.Services.AddWolverine(opts => + { + opts.Discovery.IncludeAssembly(typeof(Bug_2182_Endpoint.Request).Assembly); + opts.UseFluentValidation(); + if (useFluentValidationProblemDetail) + opts.UseFluentValidationProblemDetail(); + }); + + await using var host = await AlbaHost.For(_builder, app => + { + app.MapWolverineEndpoints(opts => + opts.UseFluentValidationProblemDetailMiddleware()); + }); + + await host.Scenario(x => + { + x.Post.Json(new Bug_2182_Endpoint.Request("valid")).ToUrl("/Bug_2182"); + x.StatusCodeShouldBe(200); + }); + } - await using var host = await AlbaHost.For(_builder, app => - { - app.MapWolverineEndpoints(opts => - { - opts.UseFluentValidationProblemDetailMiddleware(); - }); - }); - - var invalidRequest = new Bug_2182_Endpoint.Request(string.Empty); - var results = await host.Scenario(x => + [Theory] + [InlineData(ExtensionDiscovery.ManualOnly, true)] + [InlineData(ExtensionDiscovery.Automatic, true)] + [InlineData(ExtensionDiscovery.Automatic, false)] + public async Task can_validate_request_with_problem_detail_middleware( + ExtensionDiscovery extensionDiscovery, bool useFluentValidationProblemDetail) { - x.Post.Json(invalidRequest).ToUrl("/Bug_2182"); - x.ContentTypeShouldBe("application/problem+json"); - x.StatusCodeShouldBe(400); - }); - } + _builder.Services.AddWolverine(extensionDiscovery, opts => + { + opts.Discovery.IncludeAssembly(typeof(Bug_2182_Endpoint.Request).Assembly); + opts.UseFluentValidation(); + if (useFluentValidationProblemDetail) + opts.UseFluentValidationProblemDetail(); + }); + + await using var host = await AlbaHost.For(_builder, app => + { + app.MapWolverineEndpoints(opts => + opts.UseFluentValidationProblemDetailMiddleware()); + }); + + var invalidRequest = new Bug_2182_Endpoint.Request(string.Empty); + var results = await host.Scenario(x => + { + x.Post.Json(invalidRequest).ToUrl("/Bug_2182"); + x.ContentTypeShouldBe("application/problem+json"); + x.StatusCodeShouldBe(400); + }); + } } public static class Bug_2182_Endpoint { - [WolverinePost("/Bug_2182")] - public static IResult Post(Request value) - { - return TypedResults.Ok(value); - } - - public record Request(string Title) - { - public class Validator : AbstractValidator + [WolverinePost("/Bug_2182")] + public static IResult Post(Request value) + { + return TypedResults.Ok(value); + } + + public record Request(string Title) { - public Validator() - { - RuleFor(x => x.Title) - .NotEmpty(); - } + public class Validator : AbstractValidator + { + public Validator() + { + RuleFor(x => x.Title) + .NotEmpty(); + } + } } - } } \ No newline at end of file diff --git a/src/Http/Wolverine.Http.Tests/IntegrationContext.cs b/src/Http/Wolverine.Http.Tests/IntegrationContext.cs index c993d1f62..8bc3a7e9b 100644 --- a/src/Http/Wolverine.Http.Tests/IntegrationContext.cs +++ b/src/Http/Wolverine.Http.Tests/IntegrationContext.cs @@ -1,4 +1,3 @@ -using System.Diagnostics; using Alba; using Marten; using Microsoft.AspNetCore.Http; @@ -10,12 +9,13 @@ using Swashbuckle.AspNetCore.Swagger; using Wolverine.Tracking; using WolverineWebApi.TestSupport; +using JasperFx; namespace Wolverine.Http.Tests; public class AppFixture : IAsyncLifetime { - public IAlbaHost Host { get; private set; } + public IAlbaHost? Host { get; private set; } public async Task InitializeAsync() { @@ -24,6 +24,10 @@ public async Task InitializeAsync() // use WebApplicationFactory and/or Alba for integration testing JasperFxEnvironment.AutoStartHost = true; + // For "integration" test collection (based on this fixture) ApplicationAssembly is WolverineWebApi. + // If not set explicitly here other tests may set it to the test assembly causing issues with endpoints discovery. + JasperFxOptions.RememberedApplicationAssembly = typeof(WolverineWebApi.Program).Assembly; + #region sample_using_run_wolverine_in_solo_mode_with_extension // This is bootstrapping the actual application using @@ -47,69 +51,13 @@ public async Task InitializeAsync() #endregion } - public Task DisposeAsync() + public async Task DisposeAsync() { - if (Host != null) - { - return Host.DisposeAsync().AsTask(); - } - - return Task.CompletedTask; - } - - private async Task bootstrap(int delay) - { - if (Host != null) - { - try - { - var endpoints = Host.Services.GetRequiredService().Endpoints; - if (endpoints.Count < 5) - { - throw new Exception($"Only got {endpoints.Count} endpoints, something is missing!"); - } - - await Host.GetAsText("/trace"); - await Task.Delay(delay); - return; - } - catch (Exception e) - { - await Host.StopAsync(); - Host.Dispose(); - Host = null; - } - } - } - - public async Task ResetHost() - { - var delay = 0; - while (true) - { - if (delay > 1000) - { - throw new Exception("Will not start up, don't know why!"); - } - - try - { - await bootstrap(delay); - break; - } - catch (Exception e) - { - delay += 100; - await Task.Delay(delay); - - if (Host != null) - { - await Host.GetAsText("/trace"); - } - - break; - } - } + if (Host is null) + return; + await Host.StopAsync(); + await Host.DisposeAsync(); + Host = null; } } @@ -130,21 +78,9 @@ protected IntegrationContext(AppFixture fixture) public HttpGraph HttpChains => Host.Services.GetRequiredService().Endpoints!; - public IAlbaHost Host => _fixture.Host; + public IAlbaHost Host => _fixture.Host!; - public IDocumentStore Store - { - get - { - var store = _fixture.Host.Services.GetRequiredService(); - if (store == null) - { - _fixture.ResetHost().GetAwaiter().GetResult(); - } - - return _fixture.Host.Services.GetRequiredService(); - } - } + public IDocumentStore Store => Host.Services.GetRequiredService(); async Task IAsyncLifetime.InitializeAsync() { @@ -163,20 +99,7 @@ public Task DisposeAsync() public async Task Scenario(Action configure) { - try - { - return await Host.Scenario(configure); - } - catch (Exception e) - { - if (e.Message.Contains("but was 404")) - { - await _fixture.ResetHost(); - return await Host.Scenario(configure); - } - - throw; - } + return await Host.Scenario(configure); } // This method allows us to make HTTP calls into our system diff --git a/src/Http/Wolverine.Http.Tests/Wolverine.Http.Tests.csproj b/src/Http/Wolverine.Http.Tests/Wolverine.Http.Tests.csproj index 574ee01d6..ed657a5a7 100644 --- a/src/Http/Wolverine.Http.Tests/Wolverine.Http.Tests.csproj +++ b/src/Http/Wolverine.Http.Tests/Wolverine.Http.Tests.csproj @@ -23,22 +23,22 @@ - + - - - - + + + + - + - - + + diff --git a/src/Http/Wolverine.Http.Tests/open_api_generation.cs b/src/Http/Wolverine.Http.Tests/open_api_generation.cs index 5846eda6a..63d175d97 100644 --- a/src/Http/Wolverine.Http.Tests/open_api_generation.cs +++ b/src/Http/Wolverine.Http.Tests/open_api_generation.cs @@ -20,7 +20,7 @@ public static object[][] Chains() fixture.InitializeAsync().GetAwaiter().GetResult(); var chains = fixture - .Host + .Host! .Services .GetRequiredService() .Endpoints! From 853bf3abfe6721ce2d0b85e8d132d0f079ddf8da Mon Sep 17 00:00:00 2001 From: Dmytro Pryvedeniuk Date: Fri, 20 Feb 2026 21:05:51 +0200 Subject: [PATCH 2/3] Fix can_not_compile_with_manual_discovery_by_default test --- ...ug_2182_unresolved_IProblemDetailSource.cs | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/Http/Wolverine.Http.Tests/Bugs/Bug_2182_unresolved_IProblemDetailSource.cs b/src/Http/Wolverine.Http.Tests/Bugs/Bug_2182_unresolved_IProblemDetailSource.cs index f71eaa32f..905eb1ce7 100644 --- a/src/Http/Wolverine.Http.Tests/Bugs/Bug_2182_unresolved_IProblemDetailSource.cs +++ b/src/Http/Wolverine.Http.Tests/Bugs/Bug_2182_unresolved_IProblemDetailSource.cs @@ -39,18 +39,28 @@ public async Task can_not_compile_with_manual_discovery_by_default() await using var host = await AlbaHost.For(_builder, app => { app.MapWolverineEndpoints(opts => - { - opts.UseFluentValidationProblemDetailMiddleware(); - }); + opts.UseFluentValidationProblemDetailMiddleware()); }); - var exception = await host.Scenario(x => + // UnResolvableVariableException can be returned in 2 ways: + // either as a failed scenario result or thrown explicitly. + // The actual way is not important here, but the error itself is. + var errorMessage = string.Empty; + try { - x.Post.Json(new Bug_2182_Endpoint.Request("valid")).ToUrl("/Bug_2182"); - x.StatusCodeShouldBe(500); - }).ShouldThrowAsync(); + var result = await host.Scenario(x => + { + x.Post.Json(new Bug_2182_Endpoint.Request("valid")).ToUrl("/Bug_2182"); + x.StatusCodeShouldBe(500); + }); + errorMessage = await result.ReadAsTextAsync(); + } + catch (UnResolvableVariableException ex) + { + errorMessage = ex.Message; + } - exception.Message.ShouldContain( + errorMessage.ShouldContain( "JasperFx was unable to resolve a variable of type " + "Wolverine.Http.FluentValidation.IProblemDetailSource " + "as part of the method POST_Bug_2182.Handle(Microsoft.AspNetCore.Http.HttpContext httpContext)"); @@ -69,9 +79,7 @@ public async Task can_compile_with_manual_extension_discovery_when_problem_detai await using var host = await AlbaHost.For(_builder, app => { app.MapWolverineEndpoints(opts => - { - opts.UseFluentValidationProblemDetailMiddleware(); - }); + opts.UseFluentValidationProblemDetailMiddleware()); }); await host.Scenario(x => From d21f462b1d21583aea487f00f04f2753a3669401 Mon Sep 17 00:00:00 2001 From: Dmytro Pryvedeniuk Date: Sun, 22 Feb 2026 18:44:49 +0200 Subject: [PATCH 3/3] Stop WolverineRuntime explicitly before disposing host to avoid ObjectDisposedException --- .../Bugs/Bug_using_host_stop.cs | 82 +++++++++++++++++++ .../IntegrationContext.cs | 3 + 2 files changed, 85 insertions(+) create mode 100644 src/Http/Wolverine.Http.Tests/Bugs/Bug_using_host_stop.cs diff --git a/src/Http/Wolverine.Http.Tests/Bugs/Bug_using_host_stop.cs b/src/Http/Wolverine.Http.Tests/Bugs/Bug_using_host_stop.cs new file mode 100644 index 000000000..e025f2f45 --- /dev/null +++ b/src/Http/Wolverine.Http.Tests/Bugs/Bug_using_host_stop.cs @@ -0,0 +1,82 @@ +using Alba; +using Microsoft.AspNetCore.Builder; +using Shouldly; +using System.Reflection; +using Wolverine.Runtime; +using Wolverine.Tracking; + +namespace Wolverine.Http.Tests.Bugs; + +public class Bug_using_host_stop +{ + [Fact] + public async Task stops_wolverine_runtime_when_created_via_host_builder() + { + var builder = WebApplication.CreateBuilder([]); + builder.Services.DisableAllWolverineMessagePersistence(); + builder.Services.DisableAllExternalWolverineTransports(); + builder.Services.AddWolverine(_ => { }); + var host = await AlbaHost.For(builder, _ => { }); + var runtime = host.GetRuntime(); + var checkPoints = new bool[3]; + + checkPoints[0] = IsRunning(runtime); + await host.StopAsync(); + checkPoints[1] = IsRunning(runtime); + await host.DisposeAsync(); + checkPoints[2] = IsRunning(runtime); + + // Note WolverineRuntime is stopped when host.StopAsync() is called, + // which is expected as WolverineRuntime is IHostedService. + checkPoints.ShouldBe([true, false, false]); + } + + [Fact] + public async Task does_not_stop_wolverine_runtime_when_created_via_web_factory() + { + var builder = WebApplication.CreateBuilder([]); + builder.Services.DisableAllWolverineMessagePersistence(); + builder.Services.DisableAllExternalWolverineTransports(); + builder.Services.AddWolverine(_ => { }); + var host = await AlbaHost.For(_ => { }); + var wolverineRuntime = host.GetRuntime(); + var checkPoints = new bool[3]; + + checkPoints[0] = IsRunning(wolverineRuntime); + await host.StopAsync(); + checkPoints[1] = IsRunning(wolverineRuntime); + await host.DisposeAsync(); + checkPoints[2] = IsRunning(wolverineRuntime); + + // If you expect host.StopAsync() to stop WolverineRuntime - + // [true, false, false] - it's not the case here. + checkPoints.ShouldBe([true, true, false]); + } + + [Fact] + public async Task wolverine_runtime_can_be_stopped_explicitly_when_created_via_web_factory() + { + var builder = WebApplication.CreateBuilder([]); + builder.Services.DisableAllWolverineMessagePersistence(); + builder.Services.DisableAllExternalWolverineTransports(); + builder.Services.AddWolverine(_ => { }); + var host = await AlbaHost.For(_ => { }); + var wolverineRuntime = host.GetRuntime(); + var checkPoints = new bool[3]; + + checkPoints[0] = IsRunning(wolverineRuntime); + await host.GetRuntime().StopAsync(default); // Can be stopped explicitly. + await host.StopAsync(); + checkPoints[1] = IsRunning(wolverineRuntime); + await host.DisposeAsync(); + checkPoints[2] = IsRunning(wolverineRuntime); + + checkPoints.ShouldBe([true, false, false]); + } + + static bool IsRunning(WolverineRuntime runtime) + { + var field = typeof(WolverineRuntime).GetField("_hasStopped", BindingFlags.NonPublic | BindingFlags.Instance); + return (bool?)field?.GetValue(runtime) == false; + } +} diff --git a/src/Http/Wolverine.Http.Tests/IntegrationContext.cs b/src/Http/Wolverine.Http.Tests/IntegrationContext.cs index 8bc3a7e9b..09a44bb44 100644 --- a/src/Http/Wolverine.Http.Tests/IntegrationContext.cs +++ b/src/Http/Wolverine.Http.Tests/IntegrationContext.cs @@ -55,7 +55,10 @@ public async Task DisposeAsync() { if (Host is null) return; + + await Host.GetRuntime().StopAsync(default); await Host.StopAsync(); + await Host.DisposeAsync(); Host = null; }