ASP.NET Core Minimal API request model validation
- 7 minutes read - 1283 wordsIn this post I take a brief walk through the history of data services in ASP.NET, why the Minimal API framework was created and how you can easily implement one of the missing features of Minimal APIs: model validation.
A brief history of data APIs in ASP.NET
A long time ago, in a galaxy… when the .NET Framework was first released (that was 2002 by the way!), if you wanted to return data from a
URL in ASP.NET then ASMX is what you used. You talked to it in XML and got your data in XML using SOAP
(it was “all the rage” back then). It was simple and effective but lacked support for other protocols and the WS-* standards.
Roll forward to 2006, web service standards had evolved and WCF was released. It was designed to allow you to build interoperable data services and included support for different protocols (TCP, Named Pipes, MSMQ) as well as SOAP over HTTP/HTTPS with web service standards (WS-*) such as WS-Addressing, WS-ReliableMessaging and WS-Security. Later versions also included support for JSON payloads. It was a beast, complicated and a difficult to configure properly.
In 2012, the WebApi framework was added to ASP.NET. This allowed you to build simple, RESTful, HTTP-based services supporting both JSON and XML payloads. You could also create ODATA endpoints. It was similar in mental model and was closely aligned to ASP.NET MVC, although they were really different frameworks. WebApi was much simpler than WCF and, like much of the industry, moved away from the RPC/SOAP/XML protocols, and focused just on HTTP-based services.
All of the above were frameworks sitting on top of the original .NET Framework. When .NET Core came along in 2016, only the
MVC and WebApi frameworks got invited to the party. The basic premise and functionality stayed much the same in ASP.NET Core,
but they were unified into a single framework with one type of Controller
able to serve both MVC views and WebApi requests.
The latest step in this evolution of data APIs in .NET is Minimal APIs, released with .NET 6 in 2021, so let’s talk about those…
Why another framework?
As the WebApi framework in ASP.NET Core is unified with the MVC framework, it can be burdened with some overheads that aren’t needed if you are building lightweight, focused, HTTP-based data services. Minimal API aims to speed up and simplify both the development and runtime performance of such services by:
- reducing boilerplate code with a streamlined syntax
- lowering the bar to entry with less to learn
- allowing rapid prototyping
- providing familiarity for developers who have used similar minimal frameworks in other languages
- providing a smaller startup footprint, reduced middleware pipeline, consuming less resources
- having better suitability for serverless architectures such as AWS Lambda and Azure Functions
- lowering overheads and gaining better performance
However, Minimal APIs are an alternative to using MVC controller-based APIs for data services, not a replacement. They offer the benefits listed above because some features of MVC are left out. So if you need one of those features then you can still choose the MVC controller-based approach.
Which brings me (finally) to the subject of this post. One of those missing features is model validation.
Model validation in Minimal APIs
Essentially, it doesn’t do it. Not out-of-the-box anyway. For example, given the following very simple test case (clipped for brevity):
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var group = app.MapGroup("/people");
group.MapPost("/", (Person person) => TypedResults.Created(null as string, person));
app.Run();
record Person([Required]string Forename, [Required]string Surname);
If I POST
incomplete Person
JSON to this endpoint:
curl -X 'POST' 'http://localhost:5143/people' -H 'accept: application/json' -H 'Content-Type: application/json' \
-d '{
"surname": "string"
}'
(note the missing forename
property)
Then you would expect to get a 400 BadRequest
error response due to the Forename
property being decorated with a [Required]
attribute. But instead you get a successful 201 Created
response:
{
"forename": null,
"surname": "string"
}
So what are the options? Below I present two, one that utilises the .NET
DataAnnotations
attributes and
another that uses a more fully-featured package.
Using DataAnnotations
and MinimalApis.Extensions
MinimalApis.Extensions uses
MiniValidation which in turn leverages the DataAnnotations
attributes
to allow validation on fields/properties that are suitably decorated. After adding the MinimalApis.Extensions package you can
then enhance the previous example as follows (again clipped for brevity):
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var group = app.MapGroup("/people")
.WithParameterValidation();
group.MapPost("/", (Person person) => TypedResults.Created(null as string, person));
app.Run();
record Person([Required]string Forename, [Required]string Surname);
The WithParameterValidation()
method can be added to MapGroup()
or any .MapXXX()
method calls.
Firing the same partial JSON this time yields the following response:
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"Forename": [
"The Forename field is required."
]
}
}
Nice! The response is even formatted as RFC 7807 compliant ProblemDetails
JSON.
Multiple errors are reported together as well. For example, JSON without a Surname
property as well yields:
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"Forename": [
"The Forename field is required."
],
"Surname": [
"The Surname field is required."
]
}
}
All you need to do is decorate your request models with one or more of the myriad of DataAnnotations attributes.
You can also extend the use of validation in your project using MiniValidation, beyond what is shown above, by invoking validation
on a suitably decorated class/record manually using MiniValidator.TryValidate()
.
Using FluentValidation
FluentValidation is a popular and fully-featured .NET validation library that uses a fluent interface and lambda expressions to build strongly-typed and expressive validation rules. If your validation rules are quite complex and/or you have other specific requirements that DataAnnotations can’t support then FluentValidation is an excellent choice. I’ve personally used it on several projects.
It works by having a validator per type and defining the rules for that type using its fluent API. For example, for our Person
type:
public record Person(string Forename, string Surname);
public class PersonValidator : AbstractValidator<Person>
{
public PersonValidator()
{
RuleFor(x => x.Forename).NotNull();
RuleFor(x => x.Surname).NotNull();
}
}
FluentValidation includes many built-in validators for composing the rules for your types. You can also create your own custom validators that can be re-used across your validators in the same way as the built-in ones.
Validators need to be registered in the DI container:
builder.Services.AddScoped<IValidator<Person>, PersonValidator>();
You could then manually invoke the validation as part of the MapXXX()
lambda using code like this:
ValidationResult validationResult = await validator.ValidateAsync(person);
if (!validationResult.IsValid)
{
return Results.ValidationProblem(validationResult.ToDictionary());
}
However, to get automatic validation you can use an extension package such as
SharpGrip.FluentValidation.AutoValidation.Endpoints
.
The full sample looks like this, clipped for brevity and with the important lines highlighted:
1var builder = WebApplication.CreateBuilder(args);
2builder.Services.AddScoped<IValidator<Person>, PersonValidator>();
3builder.Services.AddFluentValidationAutoValidation();
4
5var app = builder.Build();
6
7var group = app.MapGroup("/people")
8 .AddFluentValidationAutoValidation();
9group.MapPost("/", (Person person) => TypedResults.Created(null as string, person));
10
11app.Run();
12
13public record Person(string Forename, string Surname);
14
15public class PersonValidator : AbstractValidator<Person>
16{
17 public PersonValidator()
18 {
19 RuleFor(x => x.Forename).NotNull();
20 RuleFor(x => x.Surname).NotNull();
21 }
22}
Now, as with MiniValidation, when we POST
the partial JSON we get an appropriate ProblemDetails
-shaped response:
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"Forename": [
"'Forename' must not be empty."
]
}
}
Conclusion
In this post I explored a brief history of the frameworks for building data services in .NET and why the newest framework, Minimal APIs, exists.
I presented two different ways to plug the gap of automatic model validation in Minimal APIs using the (simpler) MiniValidation and the (more fully-featured) FluentValidation packages.
You can view the full samples used in this post here.
I hope this has given you food for thought. Happy validating!