Skip to footer content
Iron Academy Logo
Learn C#
Learn C#

Other Categories

Data Validation in .NET 10 Minimal APIs

Tim Corey
~10m

Minimal APIs have always been the lean alternative to controller-based ASP.NET Core, but for a long time they had a notable gap: no built-in support for validating incoming data. You either wired up manual checks inside every handler, or you leaned on third-party libraries. .NET 10 closes that gap with first-class data annotation validation for query strings, headers, and request bodies, all without extra packages.

This deep dive into .NET 10 Minimal APIs, based on Tim Corey's walkthrough, explains how to register the validation service, annotate model classes, and avoid common access-modifier pitfalls that can break your setup.

The Setup: A Simple Minimal API

[0:44 - 1:35] The demo begins with a bare-bones ASP.NET Core Minimal API project running on .NET 10. The surface area is intentionally small: two POST endpoints, each accepting a different model.

app.MapPost("/person", (Person person) => Results.Ok(person));
app.MapPost("/login", (LoginModel login) => Results.Ok(login));
app.MapPost("/person", (Person person) => Results.Ok(person));
app.MapPost("/login", (LoginModel login) => Results.Ok(login));

The Person model carries basic identity fields. LoginModel handles credentials: an email address, a password, and a confirm-password field. Both are sent as JSON bodies. At this point there is no input checking at all; the API accepts whatever it receives, including empty strings and malformed email addresses.

Scalar (the modern OpenAPI UI that ships with .NET 10) is used to fire test requests directly from the browser, making it easy to see exactly what the API returns before and after validation is wired up.

Registering the Validation Service

[2:36 - 3:06] Before any validation attributes take effect, you need to opt in at the service level. A single call in your service registration block enables enforcement across all Minimal API endpoints:

builder.Services.AddValidation();
builder.Services.AddValidation();

That one line is the entire configuration step. There is no middleware to add, no pipeline stage to hook into. The framework takes over automatically once the service is registered. If you skip this call, your data annotation attributes will be present on the model but never evaluated, and requests pass through regardless of what they contain.

This is worth keeping in mind as a first debugging step: if validation silently does nothing, AddValidation() is usually the missing piece.

Adding Validation to a Class Model

[3:00 - 3:55] With the service registered, adding validation to a class-based model is a matter of decorating properties with data annotation attributes:

public class Person
{
    [Required]
    public string FirstName { get; set; }

    [Required]
    public string LastName { get; set; }
}
public class Person
{
    [Required]
    public string FirstName { get; set; }

    [Required]
    public string LastName { get; set; }
}

After adding the System.ComponentModel.DataAnnotations using directive at the top of the file, marking FirstName and LastName as [Required] ensures they are validated. Sending a POST request with an empty body through Scalar now returns a 400 Bad Request with a structured error response:

{
  "errors": {
    "FirstName": ["The FirstName field is required."],
    "LastName": ["The LastName field is required."]
  }
}

No custom error handling, no filter attributes. The framework produces that response automatically, and the check runs before your handler body executes, so you never need to guard against null inside the endpoint logic.

Applying Validation to a Record

[4:29 - 5:30] The same attributes work on C# records, but the syntax differs slightly because record properties are typically defined in the primary constructor rather than as separate member declarations.

public record LoginModel(
    [Required] [EmailAddress] string Email,
    [Required] string Password,
    [Required] string ConfirmPassword
);
public record LoginModel(
    [Required] [EmailAddress] string Email,
    [Required] string Password,
    [Required] string ConfirmPassword
);

Attributes on constructor parameters are applied to the generated properties, so [Required] and [EmailAddress] behave exactly as they would on a class. Sending a request with a malformed email, say "notanemail", now returns a 400 that identifies the Email field as invalid.

The [Compare] attribute can also be used to enforce that ConfirmPassword matches Password. On a record, attribute targets need an explicit nudge because the compiler must know you mean the generated member, not the constructor parameter itself:

[property: Compare(nameof(Password))]
string ConfirmPassword
[property: Compare(nameof(Password))]
string ConfirmPassword

The [property:] target tells the compiler to attach the attribute to the generated member rather than the parameter. Without it, [Compare] compiles but never runs during the check. This is the trickiest part of working with records vs classes in this context: class properties accept attributes naturally, while record parameters need the explicit target for anything that operates at the member level.

The Public Access Modifier Requirement

[7:51 - 9:00] A common pitfall is easy to miss and produces no error message when you hit it. The validation system uses reflection to inspect your model types at runtime; for reflection to find a type's members, the type itself must be marked public.

// Validation will NOT run; the class is internal by default
class Person { ... }

// Validation runs correctly
public class Person { ... }
// Validation will NOT run; the class is internal by default
class Person { ... }

// Validation runs correctly
public class Person { ... }

The same rule applies to records. If your model is declared without an access modifier, C# defaults to internal, and the service skips it entirely. Your endpoint still receives the request, the handler still executes, and no error is returned; the checks just silently do nothing.

This is an ASP.NET Core behavior, not specific to Minimal APIs, but it surfaces more often here because these projects tend to be compact and developers sometimes define models inline or in the same file as Program.cs without thinking about visibility.

A quick way to audit your project: any model type passed into an endpoint handler should be explicitly marked public. If it is not, no number of attributes will make the framework fire.

What You Can Validate

The built-in data annotation attributes cover the most common scenarios without any extra work:

  • [Required] rejects null or empty values
  • [EmailAddress] validates the format of an email string
  • [Compare] checks that two properties match, useful for password confirmation
  • [Range] enforces numeric or date boundaries
  • [StringLength] caps string length with an optional minimum
  • [RegularExpression] validates against a custom pattern

All of these work across query string parameters, request headers, and JSON bodies. The same model class or record can be bound from different sources without changing any of its attributes.

Conclusion

[7:46 - end] With AddValidation() registered and your models decorated, Minimal APIs enforce input constraints automatically across classes and records alike. To get this working in .NET 10, simply call builder.Services.AddValidation() once, decorate your model properties with standard data annotation attributes, and ensure every model type is marked public.

The [property:] target on records and the public modifier requirement are the only real gotchas. They are easy to overlook and produce silent failures rather than compile errors, so keep them on your checklist whenever input checks appear to do nothing.

Watch the full video on Tim Corey's YouTube channel for the complete source code walkthrough.

Hero Worlddot related to Data Validation in .NET 10 Minimal APIs
Hero Affiliate related to Data Validation in .NET 10 Minimal APIs

Earn More by Sharing What You Love

Do you create content for developers working with .NET, C#, Java, Python, or Node.js? Turn your expertise into extra income!

Iron Support Team

We're online 24 hours, 5 days a week.
Chat
Email
Call Me