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
56 changes: 56 additions & 0 deletions docs/guide/handlers/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -525,3 +525,59 @@ public static class PingHandler
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Samples/PingPongWithRabbitMq/Ponger/PingHandler.cs#L6-L35' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_PingHandler-1' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

## Validation

Wolverine provides several options for validating messages before they are processed by your handler logic. These range from lightweight, inline validation to full integration with popular validation libraries.

### Lightweight Validation with String Messages

The simplest approach is to add a `Validate` or `ValidateAsync` method to your handler class that returns validation error messages as strings. If any messages are returned, Wolverine will log them and abort the handler execution.

Supported return types are `IEnumerable<string>`, `string[]`, `Task<string[]>`, and `ValueTask<string[]>`.

For **message handlers**, Wolverine will:
1. Check if there are any validation messages
2. If none, continue processing
3. If there are messages, log each one as a warning using `ILogger`
4. Abort the handler by returning early

<!-- snippet: sample_simple_validation_ienumerable -->
<!-- endSnippet -->

You can also use synchronous `string[]` returns:

<!-- snippet: sample_simple_validation_string_array -->
<!-- endSnippet -->

Or asynchronous validation:

<!-- snippet: sample_simple_validation_async -->
<!-- endSnippet -->

For **HTTP endpoints**, the behavior is different — Wolverine will create a `ProblemDetails` response with a 400 status code containing the validation messages:

<!-- snippet: sample_simple_validation_http_ienumerable -->
<!-- endSnippet -->

### Validation with HandlerContinuation

For more control, your `Validate` method can return a `HandlerContinuation` to explicitly signal whether processing should continue or stop:

<!-- snippet: sample_sending_messages_in_before_middleware -->
<!-- endSnippet -->

### Validation with ProblemDetails

In HTTP endpoints (and message handlers with the Wolverine.Http package), you can return a `ProblemDetails` object for richer validation responses:

<!-- snippet: sample_ProblemDetailsUsageEndpoint -->
<!-- endSnippet -->

### FluentValidation Integration

For more complex validation scenarios, Wolverine integrates with [FluentValidation](/guide/handlers/fluent-validation). This allows you to use FluentValidation's full feature set including rule builders, conditional rules, and custom validators.

### Data Annotations Integration

Wolverine also supports [.NET Data Annotations](/guide/handlers/dataannotations-validation) for validation, allowing you to use standard `[Required]`, `[Range]`, and other validation attributes on your message types.

Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using Alba;
using Shouldly;
using WolverineWebApi;

namespace Wolverine.Http.Tests;

public class simple_validation_in_http_endpoints : IntegrationContext
{
public simple_validation_in_http_endpoints(AppFixture fixture) : base(fixture)
{
}

[Fact]
public async Task happy_path_with_ienumerable_string_validate()
{
await Scenario(x =>
{
x.Post.Json(new SimpleValidateHttpEnumerableMessage(3)).ToUrl("/simple-validation/ienumerable");
});
}

[Fact]
public async Task sad_path_with_ienumerable_string_validate()
{
await Scenario(x =>
{
x.Post.Json(new SimpleValidateHttpEnumerableMessage(20)).ToUrl("/simple-validation/ienumerable");
x.StatusCodeShouldBe(400);
x.ContentTypeShouldBe("application/problem+json");
});
}

[Fact]
public async Task happy_path_with_string_array_validate()
{
await Scenario(x =>
{
x.Post.Json(new SimpleValidateHttpStringArrayMessage(3)).ToUrl("/simple-validation/string-array");
});
}

[Fact]
public async Task sad_path_with_string_array_validate()
{
await Scenario(x =>
{
x.Post.Json(new SimpleValidateHttpStringArrayMessage(20)).ToUrl("/simple-validation/string-array");
x.StatusCodeShouldBe(400);
x.ContentTypeShouldBe("application/problem+json");
});
}

[Fact]
public async Task happy_path_with_async_string_array_validate()
{
await Scenario(x =>
{
x.Post.Json(new SimpleValidateHttpAsyncMessage(3)).ToUrl("/simple-validation/async");
});
}

[Fact]
public async Task sad_path_with_async_string_array_validate()
{
await Scenario(x =>
{
x.Post.Json(new SimpleValidateHttpAsyncMessage(20)).ToUrl("/simple-validation/async");
x.StatusCodeShouldBe(400);
x.ContentTypeShouldBe("application/problem+json");
});
}

[Fact]
public async Task happy_path_with_valuetask_string_array_validate()
{
await Scenario(x =>
{
x.Post.Json(new SimpleValidateHttpValueTaskMessage(3)).ToUrl("/simple-validation/valuetask");
});
}

[Fact]
public async Task sad_path_with_valuetask_string_array_validate()
{
await Scenario(x =>
{
x.Post.Json(new SimpleValidateHttpValueTaskMessage(20)).ToUrl("/simple-validation/valuetask");
x.StatusCodeShouldBe(400);
x.ContentTypeShouldBe("application/problem+json");
});
}
}
56 changes: 56 additions & 0 deletions src/Http/Wolverine.Http/CodeGen/SimpleValidationHttpFrame.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using JasperFx.CodeGeneration;
using JasperFx.CodeGeneration.Frames;
using JasperFx.CodeGeneration.Model;
using JasperFx.Core.Reflection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace Wolverine.Http.CodeGen;

/// <summary>
/// Frame that generates validation code for HTTP endpoints.
/// Creates a ProblemDetails with status 400 and writes it to the response if validation messages exist.
/// </summary>
internal class SimpleValidationHttpFrame : AsyncFrame
{
private static int _count;
private readonly Variable _variable;
private Variable? _context;

public SimpleValidationHttpFrame(Variable variable)
{
_variable = variable;
_variable.OverrideName(_variable.Usage + ++_count);
uses.Add(_variable);
}

public override IEnumerable<Variable> FindVariables(IMethodVariables chain)
{
_context = chain.FindVariable(typeof(HttpContext));
yield return _context;
}

public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
{
writer.WriteComment("Check for any simple validation messages and abort with ProblemDetails if any exist");
writer.Write(
$"var validationMessages{_count} = {_variable.Usage}.ToArray();");
writer.Write(
$"BLOCK:if (validationMessages{_count}.Length > 0)");
writer.Write(
$"var problemDetails{_count} = new {typeof(ProblemDetails).FullNameInCode()}();");
writer.Write(
$"problemDetails{_count}.{nameof(ProblemDetails.Status)} = 400;");
writer.Write(
$"problemDetails{_count}.{nameof(ProblemDetails.Title)} = \"Validation failed\";");
writer.Write(
$"problemDetails{_count}.{nameof(ProblemDetails.Extensions)}[\"errors\"] = validationMessages{_count};");
writer.Write(
$"await {nameof(HttpHandler.WriteProblems)}(problemDetails{_count}, {_context!.Usage}).ConfigureAwait(false);");
writer.Write("return;");
writer.FinishBlock();
writer.BlankLine();

Next?.GenerateCode(method, writer);
}
}
6 changes: 6 additions & 0 deletions src/Http/Wolverine.Http/HttpChain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,12 @@ public override bool HasAttribute<T>()
return HasRequestType ? RequestType : ComplexQueryStringType;
}

public override Frame? CreateSimpleValidationFrame(Variable variable)
{
Metadata.Produces(400, contentType: "application/problem+json");
return new SimpleValidationHttpFrame(variable);
}

public override Frame[] AddStopConditionIfNull(Variable variable)
{
return [new SetStatusCodeAndReturnIfEntityIsNullFrame(variable)];
Expand Down
90 changes: 90 additions & 0 deletions src/Http/WolverineWebApi/SimpleValidationUsage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using System.Diagnostics;
using Wolverine.Http;

namespace WolverineWebApi;

#region sample_simple_validation_http_ienumerable

public record SimpleValidateHttpEnumerableMessage(int Number);

public static class SimpleValidationHttpEnumerableEndpoint
{
public static IEnumerable<string> Validate(SimpleValidateHttpEnumerableMessage message)
{
if (message.Number > 10)
{
yield return "Number must be 10 or less";
}
}

[WolverinePost("/simple-validation/ienumerable")]
public static string Post(SimpleValidateHttpEnumerableMessage message) => "Ok";
}

#endregion

#region sample_simple_validation_http_string_array

public record SimpleValidateHttpStringArrayMessage(int Number);

public static class SimpleValidationHttpStringArrayEndpoint
{
public static string[] Validate(SimpleValidateHttpStringArrayMessage message)
{
if (message.Number > 10)
{
return ["Number must be 10 or less"];
}

return [];
}

[WolverinePost("/simple-validation/string-array")]
public static string Post(SimpleValidateHttpStringArrayMessage message) => "Ok";
}

#endregion

#region sample_simple_validation_http_async

public record SimpleValidateHttpAsyncMessage(int Number);

public static class SimpleValidationHttpAsyncEndpoint
{
public static Task<string[]> ValidateAsync(SimpleValidateHttpAsyncMessage message)
{
if (message.Number > 10)
{
return Task.FromResult(new[] { "Number must be 10 or less" });
}

return Task.FromResult(Array.Empty<string>());
}

[WolverinePost("/simple-validation/async")]
public static string Post(SimpleValidateHttpAsyncMessage message) => "Ok";
}

#endregion

#region sample_simple_validation_http_valuetask

public record SimpleValidateHttpValueTaskMessage(int Number);

public static class SimpleValidationHttpValueTaskEndpoint
{
public static ValueTask<string[]> ValidateAsync(SimpleValidateHttpValueTaskMessage message)
{
if (message.Number > 10)
{
return new ValueTask<string[]>(new[] { "Number must be 10 or less" });
}

return new ValueTask<string[]>(Array.Empty<string>());
}

[WolverinePost("/simple-validation/valuetask")]
public static string Post(SimpleValidateHttpValueTaskMessage message) => "Ok";
}

#endregion
Loading
Loading