Skip to main content
CodeOath
← All posts

.NET Core / Web API71 min total · 19 parts

Building REST APIs with ASP.NET Core: Routing, Middleware, and Dependency Injection

Part 7 of 19 · ~1 min

Model Validation with Data Annotations

public class CreateReservationRequest
{
    [Required]
    public DateTime StartUtc { get; set; }

    [Range(15, 120)]
    public int DurationMinutes { get; set; }

    [StringLength(280)]
    public string? Notes { get; set; }
}

Because ReservationsController carries [ApiController], a POST with DurationMinutes: 500 or a missing StartUtc never reaches Create's body — Bench hands back a 400 with a structured description of exactly which field failed, automatically, before a line of your code runs. Drop [ApiController] and the identical validation still runs and populates ModelState — it just stops checking itself, and you have to:

[HttpPost]
public IActionResult CreateManual(CreateReservationRequest request)
{
    if (!ModelState.IsValid) return BadRequest(ModelState); // needed only WITHOUT [ApiController]
    // ...
}

Data annotations handle each field on its own, but they have a blind spot Dana ran into almost immediately: nothing on CreateReservationRequest can express "this reservation has to end before the makerspace closes at 9 PM," because that depends on both StartUtc and DurationMinutes at once, and a [Range] on one property has no way to see the other. [Range(15, 120)] alone will happily accept a 120-minute laser-cutter session that starts at 8:15 PM. Reaching IValidatableObject on the model handles a case or two; past that, most real projects — Bench included, once this list grew past "one cross-field rule" — bring in a dedicated library, and FluentValidation is the usual pick.