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.FluentValidation/FluentValidationExtension.cs b/src/Http/Wolverine.Http.FluentValidation/FluentValidationExtension.cs index 51640edca..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; 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.AddSingleton(typeof(IFailureAction<>), typeof(FailureAction<>)); - - options.Services.AddSingleton(typeof(IProblemDetailSource<>), typeof(ProblemDetailSource<>)); + options.UseFluentValidationProblemDetail(); } } \ No newline at end of file diff --git a/src/Http/Wolverine.Http.FluentValidation/WolverineOptionsExtensions.cs b/src/Http/Wolverine.Http.FluentValidation/WolverineOptionsExtensions.cs new file mode 100644 index 000000000..11ef91dac --- /dev/null +++ b/src/Http/Wolverine.Http.FluentValidation/WolverineOptionsExtensions.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 WolverineOptionsExtensions +{ + 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 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..77f192d14 --- /dev/null +++ b/src/Http/Wolverine.Http.Tests/Bugs/Bug_2182_unresolved_IProblemDetailSource.cs @@ -0,0 +1,164 @@ +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.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 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 + }); + + 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.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); + }); + } + + [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.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 + { + public Validator() + { + RuleFor(x => x.Title) + .NotEmpty(); + } + } + } +} \ No newline at end of file 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..5548ef099 --- /dev/null +++ b/src/Http/Wolverine.Http.Tests/fluent_validation_problem_detail_services_registration.cs @@ -0,0 +1,87 @@ +using Microsoft.Extensions.DependencyInjection; +using Wolverine.FluentValidation; +using Wolverine.Http.FluentValidation; +using Shouldly; + +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