ASP.NET Core

Shell
dotnet add package Nullean.Entities.AspNetCore

Nullean.Entities.AspNetCore validates the request body before the handler runs, turns a thrown ConstraintViolationException into the same response, and maps Result<T> and ApplyOutcome<T> to 200 or 400. Errors are keyed by Violation.Member, or by the rule's name for a rule that is not about one member, so a client can attach each message to a form field. It brings Nullean.Entities.Extensions.DependencyInjection, needs the Microsoft.AspNetCore.App shared framework, uses no reflection, and is trim and NativeAOT clean.

Register the constraints once with services.AddConstraints<T>(), then pick the hook that fits:

C#
// One endpoint, with the set named or taken from T.Constraints:
app.MapPost("/signup", (SignUp body) => Results.Created("/signup/1", body))
   .ValidateWith<RouteHandlerBuilder, SignUp>();      // 400 + errors { "Email": ["must be an email address"] }

// A whole group: every argument whose type was registered is validated, no reflection:
app.MapGroup("/api").ValidateConstraints();
Text
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json

{"title":"One or more validation errors occurred.","status":400,"errors":{"Email":["must be an email address"],"Age":["must be 18 or older"]}}

Every broken rule is reported at once, and the handler never runs. .ValidateWith(constraintSet) takes a companion's set instead of T.Constraints.

The filters validate the body only (Validate, point-in-time rules). For a change, call Apply or ApplyOrThrow in the handler so sequential rules (Transitions, Monotonic) see the before and after:

C#
// Handlers that apply a change and let a violation throw need no try/catch:
services.AddConstraintProblemDetails();
SignUp.Constraints.ApplyOrThrow(current, next);

// Or return the outcome: Result<T> and ApplyOutcome<T> map to 200 / 400.
return SignUp.Constraints.Apply(current, change).ToHttpResult();
    • app.UseExceptionHandler()
  1. ConstraintViolationException becomes a 400

Applied and Noop answer 200 and Failure answers 400. Warnings and hints of an applied change ride along in the 200 body as { "value": ..., "notices": [...] }. exception.ToProblem() returns the same 400 as an IResult, for code that catches the exception itself. The handler also answers the exception that EF Core's UseConstraints() throws from SaveChanges, so an entity changed in place gets the same 400.

services.AddControllers().AddConstraintValidation() validates registered action arguments into ModelState; an [ApiController] answers with its usual 400. ModelState.AddViolations(violations, prefix) is public for code that validates by hand; keys are prefix.Member.

.NET 10's own builder.Services.AddValidation() validates DataAnnotations on minimal API arguments. With [Constraints(ValidatableObject = true)] it runs the constraints too, with no package beyond Nullean.Entities and no registration per type. Pick by what you need:

AddValidation() + ValidatableObject = true ValidateWith / ValidateConstraints (this package)
Package none beyond Nullean.Entities Nullean.Entities.AspNetCore
Attributes and constraints attributes first; constraints only once every attribute passes every broken rule at once, attributes included (they are rules of the set)
Rules that are not about one member reported under an empty key keyed by the rule's name
Companion sets ([Constraints<T>]), Apply, Result<T> mapping, exception handler no yes
Also covers MVC, Blazor forms, Options, Validator.TryValidateObject minimal APIs and MVC (AddConstraintValidation)

Use AddValidation() when the app already validates DataAnnotations and the model can constrain itself; use these filters for a companion set, for one 400 that lists every broken rule, or for the Apply and Result<T> paths. Pick one per endpoint.

  • ValidateWith takes the first handler argument of type T. ValidateConstraints looks up each argument by its exact runtime type, so a derived type needs its own registration.
  • Only errors reject. Warnings and hints never produce a 400.
  • Rules are synchronous and pure: a rule that needs a database call belongs in the handler.

The runnable example is examples/Nullean.Entities.Integration.Examples/AspNet, and the package README on nuget.org has a complete minimal API.