Minimal APIs have been one of the best additions to ASP.NET Core in recent years.
They are lightweight, fast and a perfect fit for many applications.
However, one key feature has been missing for years, and it is built-in validation.
With .NET 10, that finally changes. Microsoft has introduced native validation support for Minimal APIs.
Getting Started
To use the new validation features, all you need is a .NET 10 project and a quick service configuration:
builder.Services.AddValidation();
This enables the built-in validation and wires everything behind the scenes.
And that's it, you are ready to go!
At least, that's what I wish I could say. Unfortunately, the reality is a bit different.
We also need to open our .csproj file and add one special line:
<PropertyGroup>
<InterceptorsNamespaces>$(InterceptorsNamespaces);Microsoft.AspNetCore.Http.Validation.Generated</InterceptorsNamespaces>
</PropertyGroup>
As much as I dislike this extra step, I do have to admit that I really like Microsoft’s solution.
It registers the source-generated interceptor that powers the validation. Without it, the feature won't work. And since we're using source generators, the validation is extremely fast, exactly the way it should be.
Now that everything is set up, this will be the model we'll run through validation:
public sealed record CreateProductRequest(
string Name,
string Description,
decimal Price);
And here’s a simple endpoint for the example:
app.MapPost("/products", (CreateProductRequest request) =>
{
return Results.Ok("Validation successful");
});
Using Attributes
A familiar approach many developers know, though I’m personally not a big fan of, is using attributes:
public sealed record CreateProductRequest(
[Required] string Name,
[Required] string Description,
[Range(0.01, 10.0)] decimal Price);
All the well-known DataAnnotations attributes are available.
If the request body is invalid, the response looks like this:
{
"title": "One or more validation errors occurred.",
"errors": {
"Price": [
"Price should be between 0.01 and 10.0."
]
}
}
Easy and simple.
IValidatableObject
However, if you are not a big fan of attributes like me or you simply need custom validation logic, there is a great alternative.
By implementing the IValidatableObject interface, we can define validation rules for the entire object:
public sealed record CreateProductRequest(
string Name,
string Description,
decimal Price) : IValidatableObject
{
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (string.IsNullOrWhiteSpace(Name))
{
yield return new ValidationResult("Name should not be empty.", [nameof(Name)]);
}
if (string.IsNullOrWhiteSpace(Description))
{
yield return new ValidationResult("Description should not be empty.", [nameof(Description)]);
}
if (Price < 0.01m || Price > 10.0m)
{
yield return new ValidationResult("Price should be between 0.01 and 10.0.", [nameof(Price)]);
}
}
}
This approach allows for cross-field validation and gives you access to the entire object and validationContext.
Disabling Validation
Sometimes you may want to disable validation for a specific endpoint:
app.MapPost("/products-no-validation", (CreateProductRequest request) =>
{
return Results.Ok("No validation required.");
})
.DisableValidation();
DisableValidation() lets you bypass the validation pipeline for that endpoint.
Conclusion
In conclusion, while these methods may not be revolutionary, they are created to address common scenarios with more straightforward and expressive solutions.
They reduce boilerplate, improve readability and make Minimal APIs feel much more complete.
If you want to conduct additional testing, maybe with different types or simply take a look, you can find the source code here:
Source CodeI hope you enjoyed it, subscribe and get a notification when a new blog is up!
