Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions docs/guide/http/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,55 @@ public class ValidatedQuery
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Forms/FormEndpoints.cs#L230-L257' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_fluent_validation_with_asparameters' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

### AsParameters with [FromBody] <Badge type="tip" text="5.25" />

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:

<!-- snippet: sample_using_fluent_validation_with_AsParameters_and_FromBody -->
<a id='snippet-sample_using_fluent_validation_with_asparameters_and_frombody'></a>
```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<ValidatedWithFromBody>
{
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; }
}
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Forms/FormEndpoints.cs' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_fluent_validation_with_asparameters_and_frombody' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

## QueryString Binding <Badge type="tip" text="5.0" />

Wolverine.HTTP can apply the Fluent Validation middleware to complex types that are bound by the `[FromQuery]` behavior:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
41 changes: 41 additions & 0 deletions src/Http/Wolverine.Http.Tests/asparameters_binding.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 7 additions & 0 deletions src/Http/Wolverine.Http/HttpChain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,13 @@ public HttpElementVariable GetOrCreateHeaderVariable(IFromHeaderMetadata metadat

public bool IsFormData { get; internal set; }
public Type? ComplexQueryStringType { get; set; }

/// <summary>
/// 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.
/// </summary>
public Type? AsParametersType { get; internal set; }
public ServiceProviderSource ServiceProviderSource { get; set; } = ServiceProviderSource.IsolatedAndScoped;

internal Variable BuildJsonDeserializationVariable()
Expand Down
40 changes: 40 additions & 0 deletions src/Http/WolverineWebApi/Forms/FormEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ValidatedWithFromBody>
{
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

Loading