ASP.NET 10: Validating incoming models in Minimal APIs

profile
Tim Deschryver
timdeschryver.dev

I've been going back and forth on how to validate incoming models. Over the time this has changed from doing the validation manually and throwing exceptions or building a result object, to using libraries such as FluentValidation for this task.

For ASP.NET Controllers there's also the option to use data annotations to validate the models, but this was not possible for Minimal APIs. With the upcoming release of .NET 10 there is a new feature that allows us to use the same data annotations in Minimal APIs.

Note

This feature available from ASP.NET 10.0.0 Preview 3. Preview 4 also added support to use the data annotations with record types.

Differences between Controllers and Minimal APIs link

While the validation makes use of the same data annotations as the ASP.NET Controllers, there are a couple of differences between the two approaches.

With Controllers, the validation feature is provided out of the box, but you need to validate the model state manually within the controller endpoint using the ModelState.IsValid.

With Minimal APIs, the validation feature is opt-in, and automatically validates the model state for you. If the model state is invalid, it returns a uniform ProblemDetails response with a 400 Bad Request status code.

How to start using Validation in Minimal APIs link

To start using this feature, call the AddValidation method to register the validation services to the DI container.

Additionaly, it's also required to include a InterceptorsNamespaces to the project file.

format_quote

An interceptor is a method which can declaratively substitute a call to an interceptable method with a call to itself at compile time. This substitution occurs by having the interceptor declare the source locations of the calls that it intercepts. This provides a limited facility to change the semantics of existing code by adding new code to a compilation (e.g. in a source generator). https://github.com/dotnet/roslyn/blob/main/docs/features/interceptors.md

For each discovered model type, the interceptor generates a ValidatableTypeInfo containing the validation rules for the corresponding type. We will take a look at how this looks like later.

It will use this information to validate the model when it is bound to the request.

Model Validation link

With this technique, it's very easy to validate a model. By adding one (or multiple) of the built-in data attributes to the model properties, the validation is automatically applied.

As an example, let's take a look at the model below, which is used to create a new customer. The command receives the first and last name of the customer, and optionally a billing and shipping address.

The required attribute is used to indicate that the property is required, and the MinLength attribute is used to specify the minimum length of the string. This way we can ensure that the model is valid before it reaches the endpoint, and before we process the request.

Generated code link

An extract of generated code for the Command model includes the definition of the defined types, which are later used for the validation.

Note

If you want to see the complete generated code, you persist the generated code files to your disk by adding EmitCompilerGeneratedFiles to the project file, or by adding it to your build command. For a detailed explanation, check out Andrew Lock's blog post: Saving source generator output in source control.

The validation uses this information to look up the validation attributes for each property.

Problem Details response link

When the model is invalid, the validation will return a ProblemDetails containing a detailed collection error messages per property.

Summary link

In this blog post, we've seen how to use validation attributes to validate incoming models. While this feature was already available for ASP.NET Controllers, it an upcoming feature in ASP.NET 10, which adds support for this feature with Minimal APIs.

By enabling this feature (calling Services.AddValidation()), the models are automatically validated when they are bound to the request. If the model is invalid, a ProblemDetails response is returned, containing the the error details. This makes it efficient for your frontend to display the errors to the user.

Personally, I like this approach because it makes it easy to validate the models without having to write a lot of boilerplate code without having to use a third-party library. I also like the fact that the validation logic is close to the model, which makes it easier to understand and maintain. Of course, this is only viable for simple validation scenarios. For more complex validation, I prefer to have this logic in my application.

The attributes can be used on the form body, and also on route parameters, query parameters, and headers. While this blog post only demonstrated the basic usage of the validation attributes, there are more advanced options available, such as creating your own custom validation attributes, and changing the error message.

For the source code, see my Sandbox repository.

Feel free to update this blog post on GitHub, thanks in advance!

Join My Newsletter (WIP)

Join my weekly newsletter to receive my latest blog posts and bits, directly in your inbox.

Support me

I appreciate it if you would support me if have you enjoyed this post and found it useful, thank you in advance.

Buy Me a Coffee at ko-fi.com PayPal logo

Share this post