diff --git a/docs/guide/http/validation.md b/docs/guide/http/validation.md
index 22d90a9a5..51dfaa6c8 100644
--- a/docs/guide/http/validation.md
+++ b/docs/guide/http/validation.md
@@ -419,6 +419,55 @@ public class ValidatedQuery
snippet source | anchor
+### AsParameters with [FromBody]
+
+When using `[AsParameters]` with a `[FromBody]` property, the Fluent Validation middleware will validate
+**both** the `[FromBody]` type (if it has a validator) and the `[AsParameters]` type itself. This ensures
+that validation rules on the `[AsParameters]` type are always applied, even when a `[FromBody]` property
+is present:
+
+
+
+```cs
+public static class ValidatedAsParametersWithFromBodyEndpoint
+{
+ [WolverinePost("/asparameters/validated_with_from_body")]
+ public static string Post([AsParameters] ValidatedWithFromBody query)
+ {
+ return $"{query.Name} has dog: {query.Body?.HasDog}, has cat: {query.Body?.HasCat}";
+ }
+}
+
+public class ValidatedWithFromBody
+{
+ [FromQuery]
+ public string? Name { get; set; }
+
+ [FromQuery]
+ public int Age { get; set; }
+
+ [FromBody]
+ public ValidatedQueryBody? Body { get; set; }
+
+ public class ValidatedWithFromBodyValidator : AbstractValidator
+ {
+ public ValidatedWithFromBodyValidator()
+ {
+ RuleFor(x => x.Name).NotNull();
+ RuleFor(x => x.Body).NotNull();
+ }
+ }
+
+ public class ValidatedQueryBody
+ {
+ public bool HasDog { get; set; }
+ public bool HasCat { get; set; }
+ }
+}
+```
+snippet source | anchor
+
+
## QueryString Binding
Wolverine.HTTP can apply the Fluent Validation middleware to complex types that are bound by the `[FromQuery]` behavior:
diff --git a/src/Http/Wolverine.Http.FluentValidation/Internals/HttpChainFluentValidationPolicy.cs b/src/Http/Wolverine.Http.FluentValidation/Internals/HttpChainFluentValidationPolicy.cs
index 7a226f744..5d0a9a77c 100644
--- a/src/Http/Wolverine.Http.FluentValidation/Internals/HttpChainFluentValidationPolicy.cs
+++ b/src/Http/Wolverine.Http.FluentValidation/Internals/HttpChainFluentValidationPolicy.cs
@@ -22,7 +22,20 @@ public void Apply(HttpChain chain, IServiceContainer container)
{
var validatedType = chain.HasRequestType ? chain.RequestType : chain.ComplexQueryStringType;
if (validatedType == null) return;
-
+
+ addValidationMiddleware(chain, container, validatedType);
+
+ // When using [AsParameters] with a [FromBody] property, the RequestType gets
+ // overwritten to the body property type. We still want to validate the
+ // AsParameters type itself if it has a validator registered.
+ if (chain.AsParametersType != null && chain.AsParametersType != validatedType)
+ {
+ addValidationMiddleware(chain, container, chain.AsParametersType);
+ }
+ }
+
+ private static void addValidationMiddleware(HttpChain chain, IServiceContainer container, Type validatedType)
+ {
var validatorInterface = typeof(IValidator<>).MakeGenericType(validatedType);
var registered = container.RegistrationsFor(validatorInterface);
diff --git a/src/Http/Wolverine.Http.Tests/asparameters_binding.cs b/src/Http/Wolverine.Http.Tests/asparameters_binding.cs
index eb7218254..194faa421 100644
--- a/src/Http/Wolverine.Http.Tests/asparameters_binding.cs
+++ b/src/Http/Wolverine.Http.Tests/asparameters_binding.cs
@@ -184,4 +184,45 @@ await Scenario(x =>
x.ContentTypeShouldBe("application/problem+json");
});
}
+
+ [Fact]
+ public async Task using_FluentValidation_with_AsParameters_and_FromBody_happy_path()
+ {
+ await Scenario(x =>
+ {
+ x.Post.Json(new WolverineWebApi.Forms.ValidatedWithFromBody.ValidatedQueryBody { HasDog = true, HasCat = false })
+ .ToUrl("/asparameters/validated_with_from_body")
+ .QueryString("Name", "Jeremy")
+ .QueryString("Age", "51");
+ });
+ }
+
+ [Fact]
+ public async Task using_FluentValidation_with_AsParameters_and_FromBody_should_validate_asparameters_type()
+ {
+ // Missing Name (required by the ValidatedWithFromBody validator)
+ await Scenario(x =>
+ {
+ x.Post.Json(new WolverineWebApi.Forms.ValidatedWithFromBody.ValidatedQueryBody { HasDog = true, HasCat = false })
+ .ToUrl("/asparameters/validated_with_from_body")
+ .QueryString("Age", "51");
+
+ x.StatusCodeShouldBe(400);
+ x.ContentTypeShouldBe("application/problem+json");
+ });
+ }
+
+ [Fact]
+ public async Task using_FluentValidation_with_AsParameters_and_FromBody_missing_body_should_fail()
+ {
+ // No body at all (required by the ValidatedWithFromBody validator)
+ await Scenario(x =>
+ {
+ x.Post.Url("/asparameters/validated_with_from_body")
+ .QueryString("Name", "Jeremy")
+ .QueryString("Age", "51");
+
+ x.StatusCodeShouldBe(400);
+ });
+ }
}
\ No newline at end of file
diff --git a/src/Http/Wolverine.Http/CodeGen/AsParametersBindingFrame.cs b/src/Http/Wolverine.Http/CodeGen/AsParametersBindingFrame.cs
index 65ab8b895..bb585b5f1 100644
--- a/src/Http/Wolverine.Http/CodeGen/AsParametersBindingFrame.cs
+++ b/src/Http/Wolverine.Http/CodeGen/AsParametersBindingFrame.cs
@@ -24,6 +24,7 @@ public bool TryMatch(HttpChain chain, IServiceContainer container, ParameterInfo
if (IsClassOrNullableClassNotCollection(parameter.ParameterType))
{
chain.RequestType = parameter.ParameterType;
+ chain.AsParametersType = parameter.ParameterType;
chain.IsFormData = true;
variable = new AsParametersBindingFrame(parameter.ParameterType, chain, container).Variable;
return true;
diff --git a/src/Http/Wolverine.Http/HttpChain.cs b/src/Http/Wolverine.Http/HttpChain.cs
index 8f8d27183..4b094de06 100644
--- a/src/Http/Wolverine.Http/HttpChain.cs
+++ b/src/Http/Wolverine.Http/HttpChain.cs
@@ -706,6 +706,13 @@ public HttpElementVariable GetOrCreateHeaderVariable(IFromHeaderMetadata metadat
public bool IsFormData { get; internal set; }
public Type? ComplexQueryStringType { get; set; }
+
+ ///
+ /// When using [AsParameters], this tracks the original AsParameters type even when
+ /// RequestType is overwritten by a [FromBody] property. This allows middleware like
+ /// FluentValidation to also validate the AsParameters type itself.
+ ///
+ public Type? AsParametersType { get; internal set; }
public ServiceProviderSource ServiceProviderSource { get; set; } = ServiceProviderSource.IsolatedAndScoped;
internal Variable BuildJsonDeserializationVariable()
diff --git a/src/Http/WolverineWebApi/Forms/FormEndpoints.cs b/src/Http/WolverineWebApi/Forms/FormEndpoints.cs
index 3a9d662b4..1d4ca416e 100644
--- a/src/Http/WolverineWebApi/Forms/FormEndpoints.cs
+++ b/src/Http/WolverineWebApi/Forms/FormEndpoints.cs
@@ -256,3 +256,43 @@ public ValidatedQueryValidator()
#endregion
+#region sample_using_fluent_validation_with_AsParameters_and_FromBody
+
+public static class ValidatedAsParametersWithFromBodyEndpoint
+{
+ [WolverinePost("/asparameters/validated_with_from_body")]
+ public static string Post([AsParameters] ValidatedWithFromBody query)
+ {
+ return $"{query.Name} has dog: {query.Body?.HasDog}, has cat: {query.Body?.HasCat}";
+ }
+}
+
+public class ValidatedWithFromBody
+{
+ [FromQuery]
+ public string? Name { get; set; }
+
+ [FromQuery]
+ public int Age { get; set; }
+
+ [FromBody]
+ public ValidatedQueryBody? Body { get; set; }
+
+ public class ValidatedWithFromBodyValidator : AbstractValidator
+ {
+ public ValidatedWithFromBodyValidator()
+ {
+ RuleFor(x => x.Name).NotNull();
+ RuleFor(x => x.Body).NotNull();
+ }
+ }
+
+ public class ValidatedQueryBody
+ {
+ public bool HasDog { get; set; }
+ public bool HasCat { get; set; }
+ }
+}
+
+#endregion
+