ASP.NET Core
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:
// 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();
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:
// 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()
- 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.
ValidateWithtakes the first handler argument of typeT.ValidateConstraintslooks 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.