diff --git a/docs/guide/durability/marten/event-sourcing.md b/docs/guide/durability/marten/event-sourcing.md index 3dfba1118..20a1116ed 100644 --- a/docs/guide/durability/marten/event-sourcing.md +++ b/docs/guide/durability/marten/event-sourcing.md @@ -914,16 +914,114 @@ own rules about value type identifiers](https://martendb.io/documents/identity.h For a message handler, let's start with this example identifier type and aggregate from the Wolverine tests: -snippet: sample_strong_typed_identifier_with_aggregate + + +```cs +[StronglyTypedId(Template.Guid)] +public readonly partial struct LetterId; + +public class StrongLetterAggregate +{ + public StrongLetterAggregate() + { + } + + public LetterId Id { get; set; } + + public int ACount { get; set; } + public int BCount { get; set; } + public int CCount { get; set; } + public int DCount { get; set; } + + public void Apply(AEvent _) => ACount++; + public void Apply(BEvent _) => BCount++; + public void Apply(CEvent _) => CCount++; + public void Apply(DEvent _) => DCount++; +} +``` +snippet source | anchor + And now let's use that identifier type in message handlers: -snippet: sample_using_strong_typed_identifier_with_aggregate_handler_workflow + + +```cs +public record IncrementStrongA(LetterId Id); + +public record AddFrom(LetterId Id1, LetterId Id2); + +public record IncrementBOnBoth(LetterId Id1, LetterId Id2); + +public record FetchCounts(LetterId Id); + +public static class StrongLetterHandler +{ + public static StrongLetterAggregate Handle(FetchCounts counts, + [ReadAggregate] StrongLetterAggregate aggregate) => aggregate; + + public static AEvent Handle(IncrementStrongA command, [WriteAggregate] StrongLetterAggregate aggregate) + { + return new(); + } + + public static void Handle( + IncrementBOnBoth command, + [WriteAggregate(nameof(IncrementBOnBoth.Id1))] IEventStream stream1, + [WriteAggregate(nameof(IncrementBOnBoth.Id2))] IEventStream stream2 + ) + { + stream1.AppendOne(new BEvent()); + stream2.AppendOne(new BEvent()); + } + + public static IEnumerable Handle( + AddFrom command, + [WriteAggregate(nameof(AddFrom.Id1))] StrongLetterAggregate _, + [ReadAggregate(nameof(AddFrom.Id2))] StrongLetterAggregate readOnly) + { + for (int i = 0; i < readOnly.ACount; i++) + { + yield return new AEvent(); + } + + for (int i = 0; i < readOnly.BCount; i++) + { + yield return new BEvent(); + } + + for (int i = 0; i < readOnly.CCount; i++) + { + yield return new CEvent(); + } + + for (int i = 0; i < readOnly.DCount; i++) + { + yield return new DEvent(); + } + } +} +``` +snippet source | anchor + And also in some of the equivalent Wolverine.HTTP endpoints: -snippet: sample_using_strong_typed_id_as_route_argument + + +```cs +[WolverineGet("/sti/aggregate/longhand/{id}")] +public static ValueTask Handle2(LetterId id, IDocumentSession session) => + session.Events.FetchLatest(id.Value); + +// This is an equivalent to the endpoint above +[WolverineGet("/sti/aggregate/{id}")] +public static StrongLetterAggregate Handle( + [ReadAggregate] StrongLetterAggregate aggregate) => aggregate; +``` +snippet source | anchor + tools do this for you, and value types generated by these tools are -legal route argument variables for Wolverine.HTTP now. \ No newline at end of file +legal route argument variables for Wolverine.HTTP now. diff --git a/docs/guide/http/fluentvalidation.md b/docs/guide/http/fluentvalidation.md index ab714e035..e27ef056d 100644 --- a/docs/guide/http/fluentvalidation.md +++ b/docs/guide/http/fluentvalidation.md @@ -82,3 +82,46 @@ public class ValidatedQuery ``` snippet source | anchor + +## QueryString Binding + +Wolverine.HTTP can apply the Fluent Validation middleware to complex types that are bound by the `[FromQuery]` behavior: + + + +```cs +public record CreateCustomer +( + string FirstName, + string LastName, + string PostalCode +) +{ + public class CreateCustomerValidator : AbstractValidator + { + public CreateCustomerValidator() + { + RuleFor(x => x.FirstName).NotNull(); + RuleFor(x => x.LastName).NotNull(); + RuleFor(x => x.PostalCode).NotNull(); + } + } +} + +public static class CreateCustomerEndpoint +{ + [WolverinePost("/validate/customer")] + public static string Post(CreateCustomer customer) + { + return "Got a new customer"; + } + + [WolverinePost("/validate/customer2")] + public static string Post2([FromQuery] CreateCustomer customer) + { + return "Got a new customer"; + } +} +``` +snippet source | anchor + diff --git a/docs/introduction/from-mediatr.md b/docs/introduction/from-mediatr.md index db3d9c14d..5079acd18 100644 --- a/docs/introduction/from-mediatr.md +++ b/docs/introduction/from-mediatr.md @@ -186,9 +186,15 @@ public static class CreateCustomerEndpoint { return "Got a new customer"; } + + [WolverinePost("/validate/customer2")] + public static string Post2([FromQuery] CreateCustomer customer) + { + return "Got a new customer"; + } } ``` -snippet source | anchor +snippet source | anchor In the application bootstrapping, I've added this option: diff --git a/src/Http/Wolverine.Http.FluentValidation/Internals/HttpChainFluentValidationPolicy.cs b/src/Http/Wolverine.Http.FluentValidation/Internals/HttpChainFluentValidationPolicy.cs index ab79add57..7a226f744 100644 --- a/src/Http/Wolverine.Http.FluentValidation/Internals/HttpChainFluentValidationPolicy.cs +++ b/src/Http/Wolverine.Http.FluentValidation/Internals/HttpChainFluentValidationPolicy.cs @@ -12,7 +12,7 @@ internal class HttpChainFluentValidationPolicy : IHttpPolicy { public void Apply(IReadOnlyList chains, GenerationRules rules, IServiceContainer container) { - foreach (var chain in chains.Where(x => x.HasRequestType)) + foreach (var chain in chains) { Apply(chain, container); } @@ -20,7 +20,10 @@ public void Apply(IReadOnlyList chains, GenerationRules rules, IServi public void Apply(HttpChain chain, IServiceContainer container) { - var validatorInterface = typeof(IValidator<>).MakeGenericType(chain.RequestType!); + var validatedType = chain.HasRequestType ? chain.RequestType : chain.ComplexQueryStringType; + if (validatedType == null) return; + + var validatorInterface = typeof(IValidator<>).MakeGenericType(validatedType); var registered = container.RegistrationsFor(validatorInterface); @@ -30,7 +33,7 @@ public void Apply(HttpChain chain, IServiceContainer container) var method = typeof(FluentValidationHttpExecutor).GetMethod(nameof(FluentValidationHttpExecutor.ExecuteOne))! - .MakeGenericMethod(chain.RequestType); + .MakeGenericMethod(validatedType); var methodCall = new MethodCall(typeof(FluentValidationHttpExecutor), method) { @@ -38,7 +41,7 @@ public void Apply(HttpChain chain, IServiceContainer container) }; var maybeResult = new MaybeEndWithResultFrame(methodCall.ReturnVariable!); - chain.Middleware.InsertRange(0, new Frame[]{methodCall,maybeResult}); + chain.Middleware.InsertRange(0, [methodCall, maybeResult]); } else if (registered.Count() > 1) { @@ -46,11 +49,11 @@ public void Apply(HttpChain chain, IServiceContainer container) var method = typeof(FluentValidationHttpExecutor).GetMethod(nameof(FluentValidationHttpExecutor.ExecuteMany))! - .MakeGenericMethod(chain.RequestType); + .MakeGenericMethod(validatedType); var methodCall = new MethodCall(typeof(FluentValidationHttpExecutor), method); var maybeResult = new MaybeEndWithResultFrame(methodCall.ReturnVariable!); - chain.Middleware.InsertRange(0, new Frame[]{methodCall,maybeResult}); + chain.Middleware.InsertRange(0, [methodCall, maybeResult]); } } } \ No newline at end of file diff --git a/src/Http/Wolverine.Http.Tests/fluent_validation_middleware.cs b/src/Http/Wolverine.Http.Tests/fluent_validation_middleware.cs index b16e5fd2e..b3d33d2e1 100644 --- a/src/Http/Wolverine.Http.Tests/fluent_validation_middleware.cs +++ b/src/Http/Wolverine.Http.Tests/fluent_validation_middleware.cs @@ -58,6 +58,41 @@ public async Task one_validator_sad_path() // in the request var problems = results.ReadAsJson(); } + + [Fact] + public async Task one_validator_happy_path_on_complex_query_string_argument() + { + // Succeeds w/ a 200 + var result = await Scenario(x => + { + x.Post.Url("/validate/customer2") + .QueryString(nameof(CreateCustomer.FirstName), "Creed") + .QueryString(nameof(CreateCustomer.LastName), "Humphrey") + .QueryString(nameof(CreateCustomer.PostalCode), "11111") ; + x.ContentTypeShouldBe("text/plain"); + }); + } + + [Fact] + public async Task one_validator_sad_path_on_complex_query_string_argument() + { + var createCustomer = new CreateCustomer(null, "Humphrey", "11111"); + + var results = await Scenario(x => + { + x.Post.Url("/validate/customer2") + .QueryString(nameof(CreateCustomer.FirstName), "Creed") + //.QueryString(nameof(CreateCustomer.LastName), "Humphrey") + .QueryString(nameof(CreateCustomer.PostalCode), "11111") ; + x.ContentTypeShouldBe("application/problem+json"); + x.StatusCodeShouldBe(400); + }); + + // Just proving that we have HttpValidationProblemDetails content + // in the request + var problems = results.ReadAsJson(); + } + [Fact] public async Task one_validator_sad_path_in_different_assembly() { diff --git a/src/Http/WolverineWebApi/Internal/Generated/WolverineHandlers/DELETE_validate_user_compound.cs b/src/Http/WolverineWebApi/Internal/Generated/WolverineHandlers/DELETE_validate_user_compound.cs index c84587c49..dd74149b3 100644 --- a/src/Http/WolverineWebApi/Internal/Generated/WolverineHandlers/DELETE_validate_user_compound.cs +++ b/src/Http/WolverineWebApi/Internal/Generated/WolverineHandlers/DELETE_validate_user_compound.cs @@ -14,14 +14,14 @@ namespace Internal.Generated.WolverineHandlers public sealed class DELETE_validate_user_compound : Wolverine.Http.HttpHandler { private readonly Wolverine.Http.WolverineHttpOptions _wolverineHttpOptions; - private readonly Wolverine.Http.FluentValidation.IProblemDetailSource _problemDetailSource; private readonly FluentValidation.IValidator _validator; + private readonly Wolverine.Http.FluentValidation.IProblemDetailSource _problemDetailSource; - public DELETE_validate_user_compound(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions, Wolverine.Http.FluentValidation.IProblemDetailSource problemDetailSource, FluentValidation.IValidator validator) : base(wolverineHttpOptions) + public DELETE_validate_user_compound(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions, FluentValidation.IValidator validator, Wolverine.Http.FluentValidation.IProblemDetailSource problemDetailSource) : base(wolverineHttpOptions) { _wolverineHttpOptions = wolverineHttpOptions; - _problemDetailSource = problemDetailSource; _validator = validator; + _problemDetailSource = problemDetailSource; } diff --git a/src/Http/WolverineWebApi/Internal/Generated/WolverineHandlers/POST_validate_customer.cs b/src/Http/WolverineWebApi/Internal/Generated/WolverineHandlers/POST_validate_customer.cs index 553b60b12..8dc0eae7a 100644 --- a/src/Http/WolverineWebApi/Internal/Generated/WolverineHandlers/POST_validate_customer.cs +++ b/src/Http/WolverineWebApi/Internal/Generated/WolverineHandlers/POST_validate_customer.cs @@ -14,14 +14,14 @@ namespace Internal.Generated.WolverineHandlers public sealed class POST_validate_customer : Wolverine.Http.HttpHandler { private readonly Wolverine.Http.WolverineHttpOptions _wolverineHttpOptions; - private readonly Wolverine.Http.FluentValidation.IProblemDetailSource _problemDetailSource; private readonly FluentValidation.IValidator _validator; + private readonly Wolverine.Http.FluentValidation.IProblemDetailSource _problemDetailSource; - public POST_validate_customer(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions, Wolverine.Http.FluentValidation.IProblemDetailSource problemDetailSource, FluentValidation.IValidator validator) : base(wolverineHttpOptions) + public POST_validate_customer(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions, FluentValidation.IValidator validator, Wolverine.Http.FluentValidation.IProblemDetailSource problemDetailSource) : base(wolverineHttpOptions) { _wolverineHttpOptions = wolverineHttpOptions; - _problemDetailSource = problemDetailSource; _validator = validator; + _problemDetailSource = problemDetailSource; } diff --git a/src/Http/WolverineWebApi/Internal/Generated/WolverineHandlers/POST_validate_user.cs b/src/Http/WolverineWebApi/Internal/Generated/WolverineHandlers/POST_validate_user.cs index 17cde05c0..3fc64e758 100644 --- a/src/Http/WolverineWebApi/Internal/Generated/WolverineHandlers/POST_validate_user.cs +++ b/src/Http/WolverineWebApi/Internal/Generated/WolverineHandlers/POST_validate_user.cs @@ -14,14 +14,14 @@ namespace Internal.Generated.WolverineHandlers public sealed class POST_validate_user : Wolverine.Http.HttpHandler { private readonly Wolverine.Http.WolverineHttpOptions _wolverineHttpOptions; - private readonly System.Collections.Generic.IEnumerable> _validatorIEnumerable; private readonly Wolverine.Http.FluentValidation.IProblemDetailSource _problemDetailSource; + private readonly System.Collections.Generic.IEnumerable> _validatorIEnumerable; - public POST_validate_user(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions, System.Collections.Generic.IEnumerable> validatorIEnumerable, Wolverine.Http.FluentValidation.IProblemDetailSource problemDetailSource) : base(wolverineHttpOptions) + public POST_validate_user(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions, Wolverine.Http.FluentValidation.IProblemDetailSource problemDetailSource, System.Collections.Generic.IEnumerable> validatorIEnumerable) : base(wolverineHttpOptions) { _wolverineHttpOptions = wolverineHttpOptions; - _validatorIEnumerable = validatorIEnumerable; _problemDetailSource = problemDetailSource; + _validatorIEnumerable = validatorIEnumerable; } diff --git a/src/Http/WolverineWebApi/Validation/CreateCustomerEndpoint.cs b/src/Http/WolverineWebApi/Validation/CreateCustomerEndpoint.cs index 0d6ed9f05..48cc0033e 100644 --- a/src/Http/WolverineWebApi/Validation/CreateCustomerEndpoint.cs +++ b/src/Http/WolverineWebApi/Validation/CreateCustomerEndpoint.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using FluentValidation; +using Microsoft.AspNetCore.Mvc; using Wolverine.Http; namespace WolverineWebApi.Validation; @@ -31,6 +32,12 @@ public static string Post(CreateCustomer customer) { return "Got a new customer"; } + + [WolverinePost("/validate/customer2")] + public static string Post2([FromQuery] CreateCustomer customer) + { + return "Got a new customer"; + } } #endregion