A contact without a name is already rejected in the Kladde, since module 1.6: Contact.Create reports contact.name.required, and since
the last lesson, that arrives at the caller as ProblemDetails with a code. So why another check? Because an API has to answer two kinds
of questions. One is: Is the input usable at all? Is a required field missing, is the address not text in the form of an address, is a
page size 5000? You can check that without knowing anything about the Kladde. The other is: Is this allowed? Does another contact
already have this email address, does the appointment overlap with another one? Only the model knows that.
The first kind is checked by the edge, that is, the API itself, before a handler runs. The second stays in the model, in
Kladde.Core, where it holds for every interface. In this lesson, you get to know the validation that ASP.NET Core has brought for minimal
APIs since .NET 10, and you build a new rule into the core: Two contacts must not have the same email address.
Turning validation on
In k2-practice/2-3, create the file validation.cs:
// validation.cs: checking input at the edge
#:sdk Microsoft.NET.Sdk.Web
#:property PublishAot=false
using System.ComponentModel.DataAnnotations;
var builder = WebApplication.CreateBuilder(args);
builder.Logging.SetMinimumLevel(LogLevel.Warning);
builder.Services.AddProblemDetails();
builder.Services.AddValidation();
var app = builder.Build();
app.MapPost("/api/contacts", (ContactRequest request) => TypedResults.Created("/api/contacts/3", request));
app.MapGet("/api/contacts", ([Range(1, 100)] int size = 20) => $"Pages of {size} contacts");
app.Run();
public record ContactRequest(
[Required, MaxLength(100)] string? Name,
[EmailAddress] string? Email,
[MaxLength(100)] string? Company);
Two things are new:
builder.Services.AddValidation()turns validation on. A source generator searches at compile time for all types that handlers have as parameters and writes code that checks their attributes. At runtime, this check runs as an endpoint filter (lesson 2.3.5) before every handler.- Attributes from
System.ComponentModel.DataAnnotations, which you know from the options in lesson 2.3.4. On a record, they stand in front of the parameters inside the parentheses. A parameter of the handler can carry them as well, likesizehere.
Send a contact without a name and with a wrong address. Put the JSON into a file bad.json for this, so that the same quotation marks
apply in every shell:
{"name":"","email":"ilse-at-kranich"}
curl -i -X POST http://localhost:5000/api/contacts -H "Content-Type: application/json" --data-binary "@bad.json"
curl -i "http://localhost:5000/api/contacts?size=500"
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
Server: Kestrel
Transfer-Encoding: chunked
{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"One or more validation errors occurred.","status":400,"errors":{"Name":["The Name field is required."],"Email":["The Email field is not a valid e-mail address."]},"traceId":"00-4094b2367077a6649f7c006ccfbb0346-5b66c50c5720c0a7-00"}
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
Server: Kestrel
Transfer-Encoding: chunked
{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"One or more validation errors occurred.","status":400,"errors":{"size":["The field size must be between 1 and 100."]},"traceId":"00-c34216209d56359a0cf8283f8459f270-e0e633c24561adbd-00"}
--data-binary "@bad.json" sends the content of the file unchanged. The response is ProblemDetails again, this time in the form that ASP.NET
Core uses for validation and calls HttpValidationProblemDetails: The extension errors assigns a list of messages to each name. A form
can write each message next to its field that way.
Three details:
- The handler doesn’t run. The filter answers before it.
TypedResults.Createdsees only input that has passed the check. - The names in
errorsare those of the properties in C#,NameandEmail, not the lowercase ones from the JSON. For parameters likesize, it is the name of the parameter. - The built-in messages are in English. Every attribute takes a message of its own with
ErrorMessage; in it,{0}stands for the name, with[Range],{1}and{2}stand for the limits, and with[MaxLength],{1}stands for the length.
An endpoint that is not to be checked gets .DisableValidation(). And a check that compares two fields, such as “from” before “to” in a
search, you write by having the record implement IValidatableObject; the documentation below shows how.