Skip to content

The search box in the website knows all the secrets—try it!

For any queries, join our Discord Channel to reach us faster.

JasperFx Logo

JasperFx provides formal support for Wolverine and other JasperFx libraries. Please check our Support Plans for more details.

Validation within Wolverine.HTTP

INFO

You can of course use completely custom Wolverine middleware for validation, and once again, returning the ProblemDetails object or WolverineContinue.NoProblems to communicate validation errors is our main recommendation in that case.

Wolverine.HTTP has direct support for utilizing validation within HTTP endpoint that all revolve around the ProblemDetails specification.

  1. Using one off Validate() or ValidateAsync() methods embedded directly in your endpoint types that return ProblemDetails. This is our recommendation for any validation logic like data lookups that would require you to utilize IoC services or database calls.
  2. Fluent Validation middleware through the separate WolverineFx.Http.FluentValidation Nuget
  3. Data Annotations middleware that is an option you have to explicitly configure within Wolverine.HTTP application

TIP

We very strongly recommend using the one off ValidateAsync() method for any validation that requires you to use an IoC' service rather than trying to use the Fluent Validation IValidator interface. Especially if that validation logic is specific to that HTTP endpoint.

Using ProblemDetails

Wolverine has some first class support for the ProblemDetails specification in its HTTP middleware model. Wolverine also has a Fluent Validation middleware package for HTTP endpoints, but it's frequently valuable to write one off, explicit validation for certain endpoints.

Consider this contrived sample endpoint with explicit validation being done in a "Before" middleware method:

cs
public class ProblemDetailsUsageEndpoint
{
    public ProblemDetails Validate(NumberMessage message)
    {
        // If the number is greater than 5, fail with a
        // validation message
        if (message.Number > 5)
            return new ProblemDetails
            {
                Detail = "Number is bigger than 5",
                Status = 400
            };

        // All good, keep on going!
        return WolverineContinue.NoProblems;
    }

    [WolverinePost("/problems")]
    public static string Post(NumberMessage message)
    {
        return "Ok";
    }
}

public record NumberMessage(int Number);

snippet source | anchor

Wolverine.Http now (as of 1.2.0) has a convention that sees a return value of ProblemDetails and looks at that as a "continuation" to tell the http handler code what to do next. One of two things will happen:

  1. If the ProblemDetails return value is the same instance as WolverineContinue.NoProblems, just keep going
  2. Otherwise, write the ProblemDetails out to the HTTP response and exit the HTTP request handling

To make that clearer, here's the generated code:

csharp
    public class POST_problems : Wolverine.Http.HttpHandler
    {
        private readonly Wolverine.Http.WolverineHttpOptions _wolverineHttpOptions;

        public POST_problems(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions) : base(wolverineHttpOptions)
        {
            _wolverineHttpOptions = wolverineHttpOptions;
        }

        public override async System.Threading.Tasks.Task Handle(Microsoft.AspNetCore.Http.HttpContext httpContext)
        {
            var problemDetailsUsageEndpoint = new WolverineWebApi.ProblemDetailsUsageEndpoint();
            var (message, jsonContinue) = await ReadJsonAsync<WolverineWebApi.NumberMessage>(httpContext);
            if (jsonContinue == Wolverine.HandlerContinuation.Stop) return;
            
            var problemDetails = problemDetailsUsageEndpoint.Before(message);
            if (!(ReferenceEquals(problemDetails, Wolverine.Http.WolverineContinue.NoProblems)))
            {
                await Microsoft.AspNetCore.Http.Results.Problem(problemDetails).ExecuteAsync(httpContext).ConfigureAwait(false);
                return;
            }

            var result_of_Post = WolverineWebApi.ProblemDetailsUsageEndpoint.Post(message);
            await WriteString(httpContext, result_of_Post);
        }

    }

And for more context, here's the matching "happy path" and "sad path" tests for the endpoint above:

cs
[Fact]
public async Task continue_happy_path()
{
    // Should be good
    await Scenario(x =>
    {
        x.Post.Json(new NumberMessage(3)).ToUrl("/problems");
    });
}

[Fact]
public async Task stop_with_problems_if_middleware_trips_off()
{
    // This is the "sad path" that should spawn a ProblemDetails
    // object
    var result = await Scenario(x =>
    {
        x.Post.Json(new NumberMessage(10)).ToUrl("/problems");
        x.StatusCodeShouldBe(400);
        x.ContentTypeShouldBe("application/problem+json");
    });
}

snippet source | anchor

Lastly, if Wolverine sees the existence of a ProblemDetails return value in any middleware, Wolverine will fill in OpenAPI metadata for the "application/problem+json" content type and a status code of 400. This behavior can be easily overridden with your own metadata if you need to use a different status code like this:

csharp
    // Use 418 as the status code instead
    [ProducesResponseType(typeof(ProblemDetails), 418)]

Using ProblemDetails with Marten aggregates

Of course, if you are using Marten's aggregates within your Wolverine http handlers, you also want to be able to validation using the aggregate's details in your middleware and this is perfectly possible like this:

cs
[AggregateHandler]
public static ProblemDetails Before(IShipOrder command, Order order)
{
    if (order.IsShipped())
    {
        return new ProblemDetails
        {
            Detail = "Order already shipped",
            Status = 428
        };
    }
    return WolverineContinue.NoProblems;
}

snippet source | anchor

ProblemDetails Within Message Handlers 3.0

ProblemDetails can be used within message handlers as well with similar rules. See this example from the tests:

cs
public static class NumberMessageHandler
{
    public static ProblemDetails Validate(NumberMessage message)
    {
        if (message.Number > 5)
        {
            return new ProblemDetails
            {
                Detail = "Number is bigger than 5",
                Status = 400
            };
        }
        
        // All good, keep on going!
        return WolverineContinue.NoProblems;
    }

    // This "Before" method would only be utilized as
    // an HTTP endpoint
    [WolverineBefore(MiddlewareScoping.HttpEndpoints)]
    public static void BeforeButOnlyOnHttp(HttpContext context)
    {
        Debug.WriteLine("Got an HTTP request for " + context.TraceIdentifier);
        CalledBeforeOnlyOnHttpEndpoints = true;
    }

    // This "Before" method would only be utilized as
    // a message handler
    [WolverineBefore(MiddlewareScoping.MessageHandlers)]
    public static void BeforeButOnlyOnMessageHandlers()
    {
        CalledBeforeOnlyOnMessageHandlers = true;
    }

    // Look at this! You can use this as an HTTP endpoint too!
    [WolverinePost("/problems2")]
    public static void Handle(NumberMessage message)
    {
        Debug.WriteLine("Handled " + message);
        Handled = true;
    }

    // These properties are just a cheap trick in Wolverine internal tests
    public static bool Handled { get; set; }
    public static bool CalledBeforeOnlyOnMessageHandlers { get; set; }
    public static bool CalledBeforeOnlyOnHttpEndpoints { get; set; }
}

snippet source | anchor

This functionality was added so that some handlers could be both an endpoint and message handler without having to duplicate code or delegate to the handler through an endpoint.

Data Annotations 5.9

WARNING

While it is possible to access the IoC Services via ValidationContext, we recommend instead using a more explicit Validate or ValidateAsync() method directly in your message handler class for the data input.

Wolverine.Http has a separate package called WolverineFx.Http.DataAnnotationsValidation that provides a simple middleware to use Data Annotation Attributes in your endpoints.

To get started, add this one line of code to your Wolverine.HTTP configuration:

csharp
app.MapWolverineEndpoints(opts =>
{
    // Use Data Annotations that are built
    // into the Wolverine.HTTP library
    opts.UseDataAnnotationsValidationProblemDetailMiddleware();

});

This middleware will kick in for any HTTP endpoint where the request type has any property decorated with a ValidationAttribute or which implements the IValidatableObject interface.

Any validation errors detected will cause the HTTP request to fail with a ProblemDetails response.

For an example, consider this input model that will be a request type in your application:

cs
public record CreateAccount(
    // don't forget the property prefix on records
    [property: Required] string AccountName,
    [property: Reference] string Reference
) : IValidatableObject
{
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (AccountName.Equals("invalid", StringComparison.InvariantCultureIgnoreCase))
        {
            yield return new("AccountName is invalid", [nameof(AccountName)]);
        }
    }
}

snippet source | anchor

As long as the Data Annotations middleware is active, the CreateAccount model would be validated if used as the request body like this:

cs
[WolverinePost("/validate/account")]
public static string Post(
    
    // In this case CreateAccount is being posted
    // as JSON
    CreateAccount account)
{
    return "Got a new account";
}

snippet source | anchor

or even like this:

cs
[WolverinePost("/validate/account2")]
public static string Post2([FromQuery] CreateAccount customer)
{
    return "Got a new account";
}

snippet source | anchor

Fluent Validation Middleware

WARNING

If you need to use IoC services in a Fluent Validation IValidator that might force Wolverine to use a service locator pattern in the generated code (basically from AddScoped<T>(s => build it at runtime)), we recommend instead using a more explicit Validate or ValidateAsync() method directly in your HTTP endpoint class for the data input.

Wolverine.Http has a separate package called WolverineFx.Http.FluentValidation that provides a simple middleware for using Fluent Validation in your HTTP endpoints.

To get started, install that Nuget reference:

bash
dotnet add package WolverineFx.Http.FluentValidation

Next, let's assume that you have some Fluent Validation validators registered in your application container for the request types of your HTTP endpoints -- and the UseFluentValidation method from the WolverineFx.FluentValidation package will help find these validators and register them in a way that optimizes this middleware usage.

Next, add this one single line of code to your Wolverine.Http bootstrapping:

csharp
opts.UseFluentValidationProblemDetailMiddleware();

as shown in context below in an application shown below:

cs
app.MapWolverineEndpoints(opts =>
{
    // This is strictly to test the endpoint policy

    opts.ConfigureEndpoints(httpChain =>
    {
        // The HttpChain model is a configuration time
        // model of how the HTTP endpoint handles requests

        // This adds metadata for OpenAPI
        httpChain.WithMetadata(new CustomMetadata());
    });

    // more configuration for HTTP...

    // Opting into the Fluent Validation middleware from
    // Wolverine.Http.FluentValidation
    opts.UseFluentValidationProblemDetailMiddleware();
    
    // Or instead, you could use Data Annotations that are built
    // into the Wolverine.HTTP library
    opts.UseDataAnnotationsValidationProblemDetailMiddleware();

snippet source | anchor

AsParameters Binding

The Fluent Validation middleware can also be used against the [AsParameters] input of an HTTP endpoint:

cs
public static class ValidatedAsParametersEndpoint
{
    [WolverineGet("/asparameters/validated")]
    public static string Get([AsParameters] ValidatedQuery query)
    {
        return $"{query.Name} is {query.Age}";
    }
}

public class ValidatedQuery
{
    [FromQuery]
    public string? Name { get; set; }
    
    public int Age { get; set; }

    public class ValidatedQueryValidator : AbstractValidator<ValidatedQuery>
    {
        public ValidatedQueryValidator()
        {
            RuleFor(x => x.Name).NotNull();
        }
    }
}

snippet source | anchor

QueryString Binding 5.0

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<CreateCustomer>
    {
        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

Released under the MIT License.