# Validation

Validation is built in. In a form, write the rule beside the field:

```csharp
Input.Bind(() => _model.Email)
    .Validate(v => v.Contains('@') ? [] : ["Email looks wrong."])
```

That is the whole of it for a form — a rule on a field, a rule on the form for what spans fields,
either of them `async`, the simple ones kept in a value object. See
[forms — validation](https://rask.sh/docs/guides/forms-validation/).

A rule that must also hold where no form is — on a dispatched request, on a posted HTTP body — lives on
the type instead: `[Required]` and its relatives, or an `AbstractValidator<T>`. Those are also
supported in a form, and they run with nothing declared.

> Included in [`Rask.Server`](https://github.com/pal-tamas/rask/blob/main/README.md) and [`Rask.Wasm`](https://github.com/pal-tamas/rask/blob/main/README.md) — nothing to install. The
> automatic validators are **on**; an app that writes its rules inline and wants only those says so:
>
> ```csharp
> app.Configure(c => c.Validation.Off());   // or RaskValidation.AutoValidate = false; on any host
> ```
>
> Inline `.Validate(…)` rules keep running either way, and so do the two kinds a data layer registers for
> its models: the rule a property's type carries, and the rule the store owns — a unique index, a range
> that must not overlap — which a form [checks before the save](https://rask.sh/docs/guides/forms-validation/#the-database-said-no).
> Rask.Data registers both for every aggregate's generated model: a one-value value object's static
> `Validate`, and each unique index declared with `IsUnique("…")`
> ([data — what a form over the model asks by itself](https://rask.sh/docs/guides/data/#what-a-form-over-the-model-asks-by-itself)).

## Where it runs

| Where | What runs | Guide |
| --- | --- | --- |
| A `Form.Model(m)` | The inline `.Validate(…)` rules, then the model's DataAnnotations attributes, then the `AbstractValidator<T>` for it | [forms-validation.md](https://rask.sh/docs/guides/forms-validation/) |
| A dispatched query or command | The request's attributes, then its `AbstractValidator<T>`, then any `IRequestValidator<T>` you registered | below |
| A controller action or a minimal API endpoint | The bound body's attributes, then the `AbstractValidator<T>` for it — asynchronous rules included | [HTTP endpoints](#http-endpoints) |

All three share a rule written on the type: an `AbstractValidator<Order>` validates a `Form.Model(order)` while the user
types, an `Order` command when it is dispatched, **and** an `Order` posted to `/api/orders`. Write the
rules once.

## Also supported: rules on the type

Two sources, both optional, both run by a form, a dispatch and an endpoint alike.

**DataAnnotations** lives in `Rask`, the core every host depends on, so it costs no reference
at all. It covers `[Required]`, `[Range]`, `[StringLength]`, `[EmailAddress]`,
`[RegularExpression]`, custom `ValidationAttribute` subclasses and `IValidatableObject`, across the
whole reachable object graph.

**FluentValidation** is the `Rask.Validation.FluentValidation` package, referenced for you by
`Rask.Server` and `Rask.Wasm` alike. Declaring the validator is the registration — a
generator finds every `AbstractValidator<T>` at compile time and emits a `[ModuleInitializer]`. There
is no assembly scan, which is what lets a WebAssembly app use it and still publish trimmed.

A validator with constructor dependencies is resolved from the scope, so the uniqueness-check
validator — the usual reason to reach for FluentValidation — needs no extra wiring. It is built when
validation runs, not when the form renders, so a rule edited and hot-reloaded takes effect on the next
validation rather than the next page load.

### Two things to know

**Discovery is per assembly.** The generator sees the compilation it runs in, so a validator in a
referenced class library is registered only if that library also has the Rask analyzer payload and a
`Rask.Validation.FluentValidation` reference — which a plain class library does not. Keep validators
beside the app, or register them explicitly:

```csharp
RaskValidators.Register(typeof(Order), _ => new OrderValidator());
```

**A discovered validator makes the form validate asynchronously.** FluentValidation runs async, so a
model with a validator is checked through the awaitable `EditContext.Validate()` / `ValidateField(field)` —
the only ones there are: every rule, synchronous or asynchronous, runs through them, so writing an
`AbstractValidator<T>` changes nothing at the call site.

## Requests

Every dispatched request is validated before its handler runs, by a `ValidationBehavior` that
`AddRaskCqrs` registers **outermost**: a request that is not valid should never reach a transaction,
a log line saying it was handled, or the handler.

Rules that fit neither source go in an `IRequestValidator<TRequest>`, which is asynchronous by
construction:

```csharp
public sealed class NoDuplicateOrders(IOrderStore store) : IRequestValidator<PlaceOrder>
{
    public async ValueTask<IReadOnlyList<RequestValidationError>> Validate(PlaceOrder request) =>
        await store.Exists(request.Reference, Current.Cancellation)
            ? [new RequestValidationError(nameof(PlaceOrder.Reference), "That reference is already used.")]
            : [];
}

builder.Services.AddSingleton<IRequestValidator<PlaceOrder>, NoDuplicateOrders>();
```

Every validator runs and their failures are collected. This is deliberately **not** the form's
first-error-wins: a form gates per field because the user is typing into it, while a caller fixing a
request wants the whole list rather than one problem per round trip.

<a id="rejected"></a>

## A rejected request

In process, a failure reaches the caller as a `RaskValidationException` whose `Errors` are grouped by
field, with the empty key holding rules about the request as a whole. It also names its fields for a form
(`IFieldFailures`): let it out of a `Form`'s submit handler and each message is shown under the field it is
about, when the form's model is the request — the field names are the request's property paths.

Over [remote dispatch](https://rask.sh/docs/guides/cqrs/#remote-dispatch--a-client-and-a-server-raskcqrsclient--raskcqrsserver)
it becomes **400** `application/problem+json`:

```json
{
  "type": "https://github.com/pal-tamas/rask/blob/main/docs/validation.md#rejected",
  "title": "Validation failed",
  "status": 400,
  "errors": {
    "Product": ["No such product."],
    "Quantity": ["Quantity must be at least 1."]
  }
}
```

The `type` is stable across releases, so it is the right thing for a client to branch on. The client
surfaces it as a `RemoteDispatchException` with `Errors` populated.

Note that this is the one failure whose text crosses the wire. A handler exception is opaque by
default because its message is written for an operator and routinely names tables, paths and
credentials; a validation message is the opposite — it was authored to be shown to whoever sent the
request.

Inside a form nobody catches it. A submit handler that throws an exception carrying field failures
leaves each message [under its field](https://rask.sh/docs/guides/forms-validation/#the-database-said-no).

What raises it:

- a validator that rejected the request before its handler ran
- a save that violated a unique index declared with a message,
  [`IsUnique("…")`](https://rask.sh/docs/guides/data/#what-a-unique-indexs-violation-says) — under the index's fields
- a delete of a row that is still referenced, on an aggregate that says what that means —
  [`OnDelete(behavior, "…")` or `WhenReferenced("…")`](https://rask.sh/docs/guides/data/#what-deleting-a-referenced-row-says) — under the
  empty key, because it is about no field

### The browser checks first

On a WebAssembly client the request is validated **before** it is sent, so an invalid command costs a
message rather than a round trip. The server runs the same rules again and remains the authority —
the local check is a convenience, never a control, and a caller that skips it gains nothing.

Catch **both**: a rejection caught in the browser is a `RaskValidationException`, and one the browser
could not evaluate — a `MustAsync` that needs the database — comes back from the server as a
`RemoteDispatchException` whose `Errors` carry the same field map.

Events are not validated. `Publish` does not go through the request pipeline, so a rule on
an event would be enforced nowhere; put it on the command that raises the event instead.

## HTTP endpoints

An [endpoint](https://rask.sh/docs/guides/api-endpoints/) runs the same two passes, with nothing declared. Put `[Required]` on the
body type, or write an `AbstractValidator<T>` for it, and a controller action and a minimal API both
reject an invalid request with the [400 shown above](#a-rejected-request) — the same document, from
the same `type`, that a rejected dispatch sends, so one client-side handler covers every seam.

```csharp
public sealed class NewOrder
{
    [Required] public string? Reference { get; set; }
    [Range(1, 100)] public int Quantity { get; set; }
}

public sealed class NewOrderValidator : AbstractValidator<NewOrder>
{
    public NewOrderValidator(IOrderStore store) =>
        RuleFor(o => o.Reference)
            .MustAsync(async (reference, ct) => !await store.Exists(reference!, ct))
            .WithMessage("That reference is already used.");
}
```

That validator now runs in three places: in a `Form.Model(newOrder)` as each field's value arrives and on submit, on a `NewOrder`
command when it is dispatched, and on both of these:

```csharp
[HttpPost("")]
public ActionResult<Order> Place(NewOrder body) => …;      // a controller action

app.MapEndpoints(e =>
    e.MapPost("/api/orders", (NewOrder body, AppDb db) => …));   // a minimal API
```

**The asynchronous rule is the point.** MVC's `ModelState` and `Validator.TryValidateObject` are both
synchronous, so a `MustAsync` — the uniqueness check, the usual reason to reach for FluentValidation at
all — could never ride the platform's own pass. Rask adds an asynchronous filter beside it: an action
filter for controllers, an endpoint filter for minimal APIs.

Only what the **caller** sent is validated. An injected service is left alone — a `DbContext` bound as
an endpoint parameter is the container's, not the request's, and walking its object graph would be both
wrong and slow.

### Where the filter reaches

A controller is covered wherever it lives; MVC runs its filters for every action.

A minimal API is covered when it is mapped through `app.MapEndpoints(e => …)`, which is where an app
writes them. ASP.NET has no such thing as a global endpoint filter — a convention reaches only what is
mapped into the group carrying it — so an endpoint mapped somewhere else asks for the convention by
name:

```csharp
var app = raskApp.Build<App>();
app.MapPost("/api/orders", (NewOrder body) => …).RequireRaskValidation();
```

A host assembled by hand, without `RaskApp`, adds the services half itself:

```csharp
builder.Services.AddRaskApiValidation();
```

### What turning it off does, and does not do

`app.Configure(c => c.Validation.Off())` stops the Rask pass everywhere, endpoints included: the
discovered `AbstractValidator<T>` no longer runs on a controller action or a minimal API.

It deliberately leaves **ASP.NET's own** behaviour alone. `AddRaskApi` registers
`AddMvcCore().AddDataAnnotations()`, so a controller's `[Required]` and `[Range]` are still enforced by
`ModelState` exactly as in any ASP.NET app — dropping that would silently start accepting bodies the
endpoint used to reject, which is worse than a heavier registration. For a controller, then, what
turning validation off changes is the shape of the answer: on, the rejection is the Rask problem
document; off, it is MVC's own `ValidationProblemDetails`.

A minimal API has no such platform pass to fall back on — .NET 10 ships one behind `AddValidation()`,
which Rask does not switch on — so turning validation off leaves a minimal API's body unvalidated. If
that is what you want for the app but not for one endpoint, keep the battery on and take the endpoint
out of the group instead.

### On the client

A generated [API client](https://rask.sh/docs/guides/api-endpoints/#the-typed-client) surfaces the rejection as an `ApiException`
whose `Errors` hold the same field map, keyed the same way, that `RemoteDispatchException.Errors`
carries for a rejected dispatch:

```csharp
catch (ApiException ex) when (ex.Errors is { } errors)
{
    foreach (var (field, messages) in errors) { … }
}
```

## See also

- [forms-validation.md](https://rask.sh/docs/guides/forms-validation/) — inline `.Validate(…)` rules, value-object rules, async, and the validating indicator.
- [forms.md](https://rask.sh/docs/guides/forms/) — binding and the `EditContext`.
- [cqrs.md](https://rask.sh/docs/guides/cqrs/) — dispatch, pipeline behaviors, remote errors.
- [diagnostics.md](https://rask.sh/docs/guides/diagnostics/#raskval001) — RASKVAL001, RASKVAL002.
