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:
public class ProblemDetailsUsageEndpoint
{
public ProblemDetails Before(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);
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:
- If the
ProblemDetails
return value is the same instance asWolverineContinue.NoProblems
, just keep going - Otherwise, write the
ProblemDetails
out to the HTTP response and exit the HTTP request handling
To make that clearer, here's the generated code:
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:
[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");
});
}
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:
// 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:
[AggregateHandler]
public static ProblemDetails Before(IShipOrder command, Order order)
{
if (order.IsShipped())
{
return new ProblemDetails
{
Detail = "Order already shipped",
Status = 428
};
}
return WolverineContinue.NoProblems;
}
Within Message Handlers 3.0
ProblemDetails
can be used within message handlers as well with similar rules. See this example from the tests:
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;
}
// 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;
}
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.