From afd8daecbc0abe7470d3d9c3f66ad922a8f5833d Mon Sep 17 00:00:00 2001 From: XL1TTE Date: Sun, 15 Feb 2026 07:44:13 +0700 Subject: [PATCH 1/6] Add UseFluentValidationProblemDetail Api I liked the idea of auto-registration for problem-detail-specific services within the extension, so I tried to avoid the necessity for additional calls. I agree that the idea of method overload is actually not good and can be confusing, so it's probably a good idea to introduce an additional API for manual configuration. There is a problem that registration could happen in another place, which is true. The FluentValidationExtension would register services if ExtensionDiscovery is in Automatic mode, which occurs before the configure call. Therefore, it's a good idea to use TryAddSingleton inside UseFluentValidationProblemDetail. I think using TryAddSingleton inside FluentValidationExtension instead of AddSingleton is also a good practice in this case. --- .../FluentValidationExtension.cs | 6 +++--- .../FluentValidationProblemDetailExtension.cs | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 src/Http/Wolverine.Http.FluentValidation/FluentValidationProblemDetailExtension.cs diff --git a/src/Http/Wolverine.Http.FluentValidation/FluentValidationExtension.cs b/src/Http/Wolverine.Http.FluentValidation/FluentValidationExtension.cs index 51640edca..79f0bcfe8 100644 --- a/src/Http/Wolverine.Http.FluentValidation/FluentValidationExtension.cs +++ b/src/Http/Wolverine.Http.FluentValidation/FluentValidationExtension.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Wolverine.Attributes; using Wolverine.FluentValidation; using Wolverine.FluentValidation.Internals; @@ -13,8 +13,8 @@ internal class FluentValidationExtension : IWolverineExtension { public void Configure(WolverineOptions options) { - options.Services.AddSingleton(typeof(IFailureAction<>), typeof(FailureAction<>)); + options.Services.TryAddSingleton(typeof(IFailureAction<>), typeof(FailureAction<>)); - options.Services.AddSingleton(typeof(IProblemDetailSource<>), typeof(ProblemDetailSource<>)); + options.Services.TryAddSingleton(typeof(IProblemDetailSource<>), typeof(ProblemDetailSource<>)); } } \ No newline at end of file diff --git a/src/Http/Wolverine.Http.FluentValidation/FluentValidationProblemDetailExtension.cs b/src/Http/Wolverine.Http.FluentValidation/FluentValidationProblemDetailExtension.cs new file mode 100644 index 000000000..6c969ffe2 --- /dev/null +++ b/src/Http/Wolverine.Http.FluentValidation/FluentValidationProblemDetailExtension.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.DependencyInjection.Extensions; +using Wolverine.FluentValidation; +using Wolverine.FluentValidation.Internals; +using Wolverine.Http.FluentValidation.Internals; + + +namespace Wolverine.Http.FluentValidation; + +public static class UseFluentValidationProblemDetailExtension +{ + public static WolverineOptions UseFluentValidationProblemDetail(this WolverineOptions options) + { + options.Services.TryAddSingleton(typeof(IFailureAction<>), typeof(FailureAction<>)); + options.Services.TryAddSingleton(typeof(IProblemDetailSource<>), typeof(ProblemDetailSource<>)); + + return options; + } +} \ No newline at end of file From 92f1c7ed1c88721645363045985295c4287c3793 Mon Sep 17 00:00:00 2001 From: XL1TTE Date: Sun, 15 Feb 2026 20:57:48 +0700 Subject: [PATCH 2/6] tests-for-problem-detail-services-registration Implemented some tests to check `IFailureAction<>` and `IProblemDetailSource<>` registration behaviour. Conclusion: UseFluentValidation method register `IFailureAction<>` service after FluentValidationExtensions do. Therefore two instances of service was added in IServiceCollection. Changed AddSingleton call to TryAddSingleton inside UseFluentValidation to fix this. --- .../WolverineFluentValidationExtensions.cs | 2 +- ...on_problem_detail_services_registration.cs | 92 +++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 src/Http/Wolverine.Http.Tests/fluent_validation_problem_detail_services_registration.cs diff --git a/src/Extensions/Wolverine.FluentValidation/WolverineFluentValidationExtensions.cs b/src/Extensions/Wolverine.FluentValidation/WolverineFluentValidationExtensions.cs index d21a0342a..60fcaceec 100644 --- a/src/Extensions/Wolverine.FluentValidation/WolverineFluentValidationExtensions.cs +++ b/src/Extensions/Wolverine.FluentValidation/WolverineFluentValidationExtensions.cs @@ -39,7 +39,7 @@ public static WolverineOptions UseFluentValidation(this WolverineOptions options return options; } - options.Services.AddSingleton(typeof(IFailureAction<>), typeof(FailureAction<>)); + options.Services.TryAddSingleton(typeof(IFailureAction<>), typeof(FailureAction<>)); options.ConfigureLazily(o => { diff --git a/src/Http/Wolverine.Http.Tests/fluent_validation_problem_detail_services_registration.cs b/src/Http/Wolverine.Http.Tests/fluent_validation_problem_detail_services_registration.cs new file mode 100644 index 000000000..82947796d --- /dev/null +++ b/src/Http/Wolverine.Http.Tests/fluent_validation_problem_detail_services_registration.cs @@ -0,0 +1,92 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Wolverine.FluentValidation; +using Wolverine.Http.FluentValidation; +using Microsoft.Extensions.Hosting; +using Shouldly; +using Wolverine.FluentValidation.Internals; +using Wolverine.Http.FluentValidation.Internals; +using Wolverine.Runtime; + +namespace Wolverine.Http.Tests; + +public class fluent_validation_problem_detail_services_registration +{ + [Fact] + public void services_registered_once_automatic_extension_discovery_without_manual_call() + { + var services = new ServiceCollection(); + services.AddWolverine(ExtensionDiscovery.Automatic, opts => { opts.UseFluentValidation(); }); + + int failureActionCount = services.Count(d => d.ServiceType == typeof(IFailureAction<>)); + int problemDetailCount = services.Count(d => d.ServiceType == typeof(IProblemDetailSource<>)); + + failureActionCount.ShouldBe(1); + problemDetailCount.ShouldBe(1); + } + + [Fact] + public void services_registered_once_automatic_extension_discovery_with_manual_call() + { + var services = new ServiceCollection(); + services.AddWolverine(ExtensionDiscovery.Automatic, opts => + { + opts.UseFluentValidation(); + opts.UseFluentValidationProblemDetail(); + }); + + int failureActionCount = services.Count(d => d.ServiceType == typeof(IFailureAction<>)); + int problemDetailCount = services.Count(d => d.ServiceType == typeof(IProblemDetailSource<>)); + + failureActionCount.ShouldBe(1); + problemDetailCount.ShouldBe(1); + } + + [Fact] + public void IProblemDetailSource_not_registered_manual_extension_discovery_without_manual_call() + { + var services = new ServiceCollection(); + services.AddWolverine(ExtensionDiscovery.ManualOnly, opts => { opts.UseFluentValidation(); }); + + int problemDetailCount = services.Count(d => d.ServiceType == typeof(IProblemDetailSource<>)); + + problemDetailCount.ShouldBe(0); + } + + [Fact] + public void services_registered_once_manual_extension_discovery_with_manual_call() + { + var services = new ServiceCollection(); + services.AddWolverine(ExtensionDiscovery.ManualOnly, opts => + { + opts.UseFluentValidation(); + opts.UseFluentValidationProblemDetail(); + }); + + int failureActionCount = services.Count(d => d.ServiceType == typeof(IFailureAction<>)); + int problemDetailCount = services.Count(d => d.ServiceType == typeof(IProblemDetailSource<>)); + + problemDetailCount.ShouldBe(1); + failureActionCount.ShouldBe(1); + } + + [Fact] + public void services_registered_once_with_multiple_service_registers_in_manual_mode() + { + var services = new ServiceCollection(); + services.AddWolverine(ExtensionDiscovery.ManualOnly, opts => + { + opts.UseFluentValidation(); + + opts.UseFluentValidationProblemDetail(); + opts.UseFluentValidationProblemDetail(); + opts.UseFluentValidationProblemDetail(); + }); + + int failureActionCount = services.Count(d => d.ServiceType == typeof(IFailureAction<>)); + int problemDetailCount = services.Count(d => d.ServiceType == typeof(IProblemDetailSource<>)); + + failureActionCount.ShouldBe(1); + problemDetailCount.ShouldBe(1); + } +} \ No newline at end of file From f6ca39fd071196290bbf7a5db8f104d5f5ff44bd Mon Sep 17 00:00:00 2001 From: XL1TTE Date: Sun, 15 Feb 2026 21:15:45 +0700 Subject: [PATCH 3/6] extension-class-name-change UseFluentValidationProblemDetailExtension -> WolverineOptionsExtensions --- .../FluentValidationProblemDetailExtension.cs | 2 +- ...fluent_validation_problem_detail_services_registration.cs | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Http/Wolverine.Http.FluentValidation/FluentValidationProblemDetailExtension.cs b/src/Http/Wolverine.Http.FluentValidation/FluentValidationProblemDetailExtension.cs index 6c969ffe2..11ef91dac 100644 --- a/src/Http/Wolverine.Http.FluentValidation/FluentValidationProblemDetailExtension.cs +++ b/src/Http/Wolverine.Http.FluentValidation/FluentValidationProblemDetailExtension.cs @@ -6,7 +6,7 @@ namespace Wolverine.Http.FluentValidation; -public static class UseFluentValidationProblemDetailExtension +public static class WolverineOptionsExtensions { public static WolverineOptions UseFluentValidationProblemDetail(this WolverineOptions options) { diff --git a/src/Http/Wolverine.Http.Tests/fluent_validation_problem_detail_services_registration.cs b/src/Http/Wolverine.Http.Tests/fluent_validation_problem_detail_services_registration.cs index 82947796d..5548ef099 100644 --- a/src/Http/Wolverine.Http.Tests/fluent_validation_problem_detail_services_registration.cs +++ b/src/Http/Wolverine.Http.Tests/fluent_validation_problem_detail_services_registration.cs @@ -1,12 +1,7 @@ using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using Wolverine.FluentValidation; using Wolverine.Http.FluentValidation; -using Microsoft.Extensions.Hosting; using Shouldly; -using Wolverine.FluentValidation.Internals; -using Wolverine.Http.FluentValidation.Internals; -using Wolverine.Runtime; namespace Wolverine.Http.Tests; From 4ab1caf24e3cbe0bea3808fce9857fea9da54a61 Mon Sep 17 00:00:00 2001 From: XL1TTE Date: Sun, 15 Feb 2026 21:24:47 +0700 Subject: [PATCH 4/6] file rename, api usage in extension --- .../FluentValidationExtension.cs | 8 +------- ...emDetailExtension.cs => WolverineOptionsExtensions.cs} | 0 2 files changed, 1 insertion(+), 7 deletions(-) rename src/Http/Wolverine.Http.FluentValidation/{FluentValidationProblemDetailExtension.cs => WolverineOptionsExtensions.cs} (100%) diff --git a/src/Http/Wolverine.Http.FluentValidation/FluentValidationExtension.cs b/src/Http/Wolverine.Http.FluentValidation/FluentValidationExtension.cs index 79f0bcfe8..dfa45b8fd 100644 --- a/src/Http/Wolverine.Http.FluentValidation/FluentValidationExtension.cs +++ b/src/Http/Wolverine.Http.FluentValidation/FluentValidationExtension.cs @@ -1,9 +1,5 @@ -using Microsoft.Extensions.DependencyInjection.Extensions; using Wolverine.Attributes; -using Wolverine.FluentValidation; -using Wolverine.FluentValidation.Internals; using Wolverine.Http.FluentValidation; -using Wolverine.Http.FluentValidation.Internals; [assembly: WolverineModule] @@ -13,8 +9,6 @@ internal class FluentValidationExtension : IWolverineExtension { public void Configure(WolverineOptions options) { - options.Services.TryAddSingleton(typeof(IFailureAction<>), typeof(FailureAction<>)); - - options.Services.TryAddSingleton(typeof(IProblemDetailSource<>), typeof(ProblemDetailSource<>)); + options.UseFluentValidationProblemDetail(); } } \ No newline at end of file diff --git a/src/Http/Wolverine.Http.FluentValidation/FluentValidationProblemDetailExtension.cs b/src/Http/Wolverine.Http.FluentValidation/WolverineOptionsExtensions.cs similarity index 100% rename from src/Http/Wolverine.Http.FluentValidation/FluentValidationProblemDetailExtension.cs rename to src/Http/Wolverine.Http.FluentValidation/WolverineOptionsExtensions.cs From 71e0aded681a65187d1fd60c1c9f50ce28192aef Mon Sep 17 00:00:00 2001 From: Dmytro Pryvedeniuk Date: Sun, 15 Feb 2026 18:38:58 +0200 Subject: [PATCH 5/6] Add tests --- ...ug_2182_unresolved_IProblemDetailSource.cs | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 src/Http/Wolverine.Http.Tests/Bugs/Bug_2182_unresolved_IProblemDetailSource.cs 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 new file mode 100644 index 000000000..c4721c235 --- /dev/null +++ b/src/Http/Wolverine.Http.Tests/Bugs/Bug_2182_unresolved_IProblemDetailSource.cs @@ -0,0 +1,157 @@ +using Alba; +using FluentValidation; +using IntegrationTests; +using Marten; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Shouldly; +using Wolverine.FluentValidation; +using Wolverine.Http.FluentValidation; + +namespace Wolverine.Http.Tests.Bugs; + +public class Bug_2182_unresolved_IProblemDetailSource +{ + 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.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")) + .ShouldThrowAsync(); + + exception.Message.ShouldBe( + "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.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); + }); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task can_compile_with_default_extension_discovery(bool useFluentValidationProblemDetail) + { + _builder.Services.AddWolverine(opts => + { + 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); + }); + } + + [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 => + { + 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 + { + public Validator() + { + RuleFor(x => x.Title) + .NotEmpty(); + } + } + } +} \ No newline at end of file From 6ebc886c91e804c23e30bcfbfb7e6ea0b215225c Mon Sep 17 00:00:00 2001 From: XL1TTE Date: Mon, 16 Feb 2026 07:28:14 +0700 Subject: [PATCH 6/6] tests fixes In my case, all tests inside Bug_2182_unresolved_IProblemDetailSource failed until I specified opts.Discovery.IncludeAssembly(typeof(Bug_2182_Endpoint.Request).Assembly) in every test case. I used the following code: ```` _builder.Services.AddWolverine(ExtensionDiscovery.ManualOnly, opts => { opts.Discovery.IncludeAssembly(typeof(Bug_2182_Endpoint).Assembly); // <- this is the fix! opts.UseFluentValidation(); // from Wolverine.FluentValidation opts.UseFluentValidationProblemDetail(); // from Wolverine.Http.FluentValidation }); ```` Six out of seven tests passed after that, but one still failed due to how AlbaHost manages exceptions. All seven tests succeeded after all changes. --- ...ug_2182_unresolved_IProblemDetailSource.cs | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 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 c4721c235..77f192d14 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 @@ -30,6 +30,7 @@ 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 }); @@ -42,14 +43,17 @@ public async Task can_not_compile_with_manual_discovery_by_default() }); }); - var exception = await host - .Scenario(x => x.Post.Json(new Bug_2182_Endpoint.Request("valid")).ToUrl("/Bug_2182")) - .ShouldThrowAsync(); + var result = await host.Scenario(x => + { + x.Post.Json(new Bug_2182_Endpoint.Request("valid")).ToUrl("/Bug_2182"); + x.StatusCodeShouldBe(500); + }); - exception.Message.ShouldBe( - "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)"); + 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] @@ -57,6 +61,7 @@ public async Task can_compile_with_manual_extension_discovery_when_problem_detai { _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 }); @@ -83,6 +88,7 @@ public async Task can_compile_with_default_extension_discovery(bool useFluentVal { _builder.Services.AddWolverine(opts => { + opts.Discovery.IncludeAssembly(typeof(Bug_2182_Endpoint.Request).Assembly); opts.UseFluentValidation(); if (useFluentValidationProblemDetail) opts.UseFluentValidationProblemDetail(); @@ -112,6 +118,7 @@ public async Task can_validate_request_with_problem_detail_middleware( { _builder.Services.AddWolverine(extensionDiscovery, opts => { + opts.Discovery.IncludeAssembly(typeof(Bug_2182_Endpoint.Request).Assembly); opts.UseFluentValidation(); if (useFluentValidationProblemDetail) opts.UseFluentValidationProblemDetail();