
Wolverine — the event-driven messaging and HTTP framework for .NET — provides a rich, layered set of options for validating incoming data. Whether you are building HTTP endpoints or message handlers, Wolverine meets you where you are: from zero-configuration inline checks to full Fluent Validation or Data Annotation middleware support for both command handlers and HTTP endpoints.
Let’s maybe over simplify validation scenarios say they’ll fall into two buckets:
- Run of the mill field level validation rules like required fields or value ranges. These rules are the bread and butter of dedicated validation frameworks like Fluent Validation or Microsoft’s Data Annotations markup.
- Custom validation rules that are custom to your business domain and might involve checks against the existing state of your system beyond the command messages.
Let’s first look at Wolverine’s Data Annotation integration that is completely baked into the core WolverineFx Nuget. To get started, just opt into the Data Annotations middleware for message handlers like this:
using var host = await Host.CreateDefaultBuilder() .UseWolverine(opts => { // Apply the validation middleware opts.UseDataAnnotationsValidation(); }).StartAsync();
In message handlers, this middleware will kick in for any message type that has any validation attributes as this example:
public record CreateCustomer( // you can use the attributes on a record, but you need to // add the `property` modifier to the attribute [property: Required] string FirstName, [property: MinLength(5)] string LastName, [property: PostalCodeValidator] string PostalCode) : IValidatableObject{ public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) { // you can implement `IValidatableObject` for custom // validation logic yield break; }};public class PostalCodeValidatorAttribute : ValidationAttribute{ public override bool IsValid(object? value) { // custom attributes are supported return true; }}public static class CreateCustomerHandler{ public static void Handle(CreateCustomer customer) { // do whatever you'd do here, but this won't be called // at all if the DataAnnotations Validation rules fail }}
By default for message handlers, any validation errors are logged, then the current execution is stopped through the usage of the HandlerContinuation value we’ll discuss later.
For Wolverine.HTTP integration with Data Annotations, use:
app.MapWolverineEndpoints(opts =>{ // Use Data Annotations that are built // into the Wolverine.HTTP library opts.UseDataAnnotationsValidationProblemDetailMiddleware();});
Likewise, this middleware will only apply to HTTP endpoints that have a request input model that contains data annotation attributes. In this case though, Wolverine is using the ProblemDetails specification to report validation errors back to the caller with a status code of 400 by default.
Fluent Validation Middleware
Similarly, the Fluent Validation integration works more or less the same, but requires the WolverineFx.FluentValidation package for message handlers and the WolverineFx.Http.FluentValidation package for HTTP endpoints. There are some Wolverine helpers for discovering and registering FluentValidation validators in a way that applies some Wolverine-specific performance optimizations by trying to register most validators with a Singleton lifetime just to allow Wolverine to generate more optimized code.
It is possible to override how Wolverine handles validation failures, but I’d personally recommend just using the ProblemDetails default in most cases.
I would like to note that the way that Wolverine generates code for the Fluent Validation middleware is generally going to be more efficient at runtime than the typical IoC dependent equivalents you’ll frequently find in the MediatR space.
Explicit Validation
Let’s move on to validation rules that are more specific to your own problem domain, and especially the type of validation rules that would require you to examine the state of your system by exercising some kind of data access. These kinds of rules certainly can be done with custom Fluent Validation validators, but I strongly recommend you put that kind of validation directly into your message handlers or HTTP endpoints to colocate business logic together with the actual message handler or HTTP endpoint happy path.
One of the unique features of Wolverine in comparison to the typical “IHandler of T” application frameworks in .NET is Wolverine’s built in support for a type of low code ceremony Railway Programming, and this turns out to be perfect for one off validation rules.
In message handlers we’ve long had support for returning the HandlerContinuation enum from Validate() or Before() methods as a way to signal to Wolverine to conditionally stop all additional processing:
public static class ShipOrderHandler{ // This would be called first public static async Task<(HandlerContinuation, Order?, Customer?)> LoadAsync(ShipOrder command, IDocumentSession session) { var order = await session.LoadAsync<Order>(command.OrderId); if (order == null) { return (HandlerContinuation.Stop, null, null); } var customer = await session.LoadAsync<Customer>(command.CustomerId); return (HandlerContinuation.Continue, order, customer); } // The main method becomes the "happy path", which also helps simplify it public static IEnumerable<object> Handle(ShipOrder command, Order order, Customer customer) { // use the command data, plus the related Order & Customer data to // "decide" what action to take next yield return new MailOvernight(order.Id); }}
But of course, with the example above, you could also write that with Wolverine’s declarative persistence like this:
public static class ShipOrderHandler{ // The main method becomes the "happy path", which also helps simplify it public static IEnumerable<object> Handle( ShipOrder command, // This is loaded by the OrderId on the ShipOrder command [Entity(Required = true)] Order order, // This is loaded by the CustomerId value on the ShipOrder command [Entity(Required = true)] Customer customer) { // use the command data, plus the related Order & Customer data to // "decide" what action to take next yield return new MailOvernight(order.Id); }}
In the code above, Wolverine would stop the processing if either the Order or Customer entity referenced by the command message is missing. Similarly, if this code were in an HTTP endpoint instead, Wolverine would emit a ProblemDetails with a 400 status code and a message stating the data that is missing.
If you were using the code above with the integration with Marten or Polecat, Wolverine can even emit code that uses Marten or Polecat’s batch querying functionality to make your system more efficient by eliminating database round trips.
Likewise in the HTTP space, you could also return a ProblemDetails object directly from a Validate() method like:
public class ProblemDetailsUsageEndpoint{ public ProblemDetails Validate(NumberMessage message) { if (message.Number > 5) return new ProblemDetails { Detail = "Number is bigger than 5", Status = 400 }; // All good — continue! return WolverineContinue.NoProblems; } [WolverinePost("/problems")] public static string Post(NumberMessage message) => "Ok";}
Even More Lightweight Validation!
When reviewing client code that uses the HandlerContinuation or ProblemDetails syntax, I definitely noticed the code can become verbose and noisy, especially compared to just embedding throw new InvalidOperationException("something is not right here"); code directly in the main methods — which isn’t something I’d like to see people tempted to do.
Instead, Wolverine 5.18 added a more lightweight approach that allows you to just return an array of strings from a Before/Validation() method:
public static IEnumerable<string> Validate(SimpleValidateEnumerableMessage message)
{
if (message.Number > 10)
{
yield return "Number must be 10 or less";
}
}
// or
public static string[] Validate(SimpleValidateStringArrayMessage message)
{
if (message.Number > 10)
{
return ["Number must be 10 or less"];
}
return [];
}
At runtime, Wolverine will stop a handler if there are any messages or emit a ProblemDetails response in HTTP endpoints.
Summary
Hopefully, Wolverine has you covered no matter what with options. A few practical takeaways:
- Reach for
Validate() / ValidateAsync()first whenever IoC services or database queries are involved or the validation logic is just specific to your message handler or HTTP endpoint. - Use Data Annotations middleware when your model types are already decorated with attributes and you want zero validator classes.
- Use Fluent Validation middleware when you want reusable, composable validators shared across multiple handlers or endpoints.
All three strategies generate efficient, ahead-of-time compiled middleware via Wolverine’s code generation engine, keeping the runtime overhead minimal regardless of which path you choose.










