2 · Data and boundaries · 30 MIN
DTO validation at an API boundary
A deserialized DTO still needs explicit domain validation.
Model binding reads a JSON body into the input record, but it does not establish every business rule. In this .NET 8 minimal API example, the delegate explicitly checks a trimmed title and weekly goal. Later framework versions may offer additional validation facilities, so keep the target version clear. A preview response avoids implying persistence. Before storing an account-owned plan, authenticate the caller, derive its owner ID from the session and check authorization on the server.
Check title and goal boundaries.
Read the example
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
var app = WebApplication.CreateBuilder(args).Build();
app.MapPost("/preview", (PlanInput input) => {
var title = input.Title?.Trim();
if (title is null || title.Length < 3 || title.Length > 80 || input.WeeklyGoal < 1 || input.WeeklyGoal > 20)
return Results.BadRequest(new {error="Invalid title or weekly goal"});
return Results.Ok(new {title,weeklyGoal=input.WeeklyGoal});
});
app.Run();
public sealed record PlanInput(string? Title, int WeeklyGoal);Check the expected output
A valid body returns a trimmed preview; invalid title or weekly goal returns 400.
Your challenge
Add field-specific validation errors and enforce a request-body size limit; document whether unknown JSON fields are accepted or rejected.
Solution cost: O(m) title processing. time · O(m) input and trimmed string. space
Common trap
A non-nullable C# annotation does not establish that an HTTP client supplied a valid value.
Study the project implementation
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
var app = WebApplication.CreateBuilder(args).Build();
app.MapPost("/preview", (PlanInput input) => {
var title = input.Title?.Trim();
if (title is null || title.Length < 3 || title.Length > 80 || input.WeeklyGoal < 1 || input.WeeklyGoal > 20)
return Results.BadRequest(new {error="Invalid title or weekly goal"});
return Results.Ok(new {title,weeklyGoal=input.WeeklyGoal});
});
app.Run();
public sealed record PlanInput(string? Title, int WeeklyGoal);Further reading: Official documentation
Next lesson: Authentication and authorization are different checks →