# CQRS (`Rask.Cqrs`)

`Rask.Cqrs` is an opt-in, **source-generated** CQRS/mediator. You define messages — queries, commands
and events — and their handlers, then dispatch through `IDispatcher`. The generator wires every
handler at compile time, so dispatch does **no runtime reflection and no assembly scanning**. That is
the whole point: it publishes clean under the WASM/AOT trimmer, where a reflection-based mediator
cannot. It is standalone — it depends only on `Microsoft.Extensions.DependencyInjection.Abstractions`
and works in any .NET app, not just Rask.

> Rask encourages **vertical slices**: a handler owns its own data access, through [`Rask.Data`](https://rask.sh/docs/guides/data/)
> or [plain EF Core](https://rask.sh/docs/guides/data-access/), with no shared repository or service layer in between.
> CQRS is the natural way to structure those slices: each slice owns its query/command and handler.
> Reach for it when that structure earns its keep — a small app is fine calling services directly.

> 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. It is **on**; an app that does without it says so:
>
> ```csharp
> app.Configure(c => c.Cqrs.Off());
> ```

## The four message shapes

| Interface | Meaning | Handler | Returns |
| --- | --- | --- | --- |
| `IQuery<TResult>` | read, no side effects | `IQueryHandler<TQuery, TResult>` | `TResult` |
| `ICommand` | write, no value | `ICommandHandler<TCommand>` | — |
| `ICommand<TResult>` | write, returns a value | `ICommandHandler<TCommand, TResult>` | `TResult` |
| `IEvent` | event, fanned out to many | `IEventHandler<TEvent>` | — |

```csharp
public sealed record GetCounterState : IQuery<CounterState>;

public sealed class GetCounterStateHandler(CqrsCounterStore store)
    : IQueryHandler<GetCounterState, CounterState>
{
    public Task<CounterState> Handle(GetCounterState query) =>
        Task.FromResult(new CounterState(store.Count, store.Log));
}
```

## Register and dispatch

Call `AddRaskCqrs` once at startup. It is **host-agnostic** — the same call works on the Rask Server
and WASM hosts (the dispatcher captures the per-session scope on Server, the root scope on WASM), so
you never split registration per host. All discovered handlers are registered for you; you never list
them.

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

Inject `IDispatcher` and say which of the three things you are doing: `Query` asks for data,
`Send` tells the system to do something, `Publish` announces that something happened. The
result type is inferred from the message, so you never state it.

```csharp
public sealed partial class CounterView(IDispatcher dispatcher) : Component
{
    private CounterState _view = new(0, []);

    protected override async Task OnMount() =>
        _view = await dispatcher.Query(new GetCounterState(), CancellationToken);

    private async Task Increment()
    {
        await dispatcher.Send(new IncrementCounter(1), CancellationToken); // ICommand<int>
        _view = await dispatcher.Query(new GetCounterState(), CancellationToken);
    }
}
```

## Events

A command handler can publish an `IEvent`; every registered `IEventHandler` runs. Choose
`Sequential` (default) or `WhenAll` fan-out via `CqrsOptions.EventPublishStrategy`. Handlers are
matched by the event's **concrete runtime type** (a handler declared against a base type is not
invoked for a derived one — see Limitations), their run order is deterministic but not the declaration
order, so don't depend on it, and an event with no handlers reaches only its subscribers
([below](#subscribing)).

```csharp
public sealed class IncrementCounterHandler(CqrsCounterStore store, IDispatcher dispatcher)
    : ICommandHandler<IncrementCounter, int>
{
    public async Task<int> Handle(IncrementCounter command)
    {
        var value = store.IncrementBy(command.By);
        await dispatcher.Publish(new CounterIncremented(value), Current.Cancellation);
        return value;
    }
}
```

### A page that handles an event

A component can implement `IEventHandler<T>` too. **A service handler runs once per event; a component handler runs
once per open page** that has the component mounted, in that page's own session — after what the page is doing, as its
user, and rendered once:

```csharp
public sealed partial class OrdersPage : Component, IEventHandler<OrderPlaced>
{
    public async Task Handle(OrderPlaced e) => _orders = await Order.Where(o => o.Open).ToList();
}
```

The same `Publish` reaches both, and so does a domain event an aggregate raises, after its commit. A component is
never registered as a DI service. See [lifecycle](https://rask.sh/docs/guides/lifecycle/#a-page-that-handles-an-event).

### Durable handlers

A handler that must not be lost implements `IDurableHandler<T>` instead. It is written to the
[outbox](https://rask.sh/docs/guides/outbox/) and runs from there after the commit, retried until it succeeds; each handler of the event chooses:

```csharp
public sealed class RefreshDashboard : IEventHandler<OrderPlaced> { … }   // in memory, at once
public sealed class SendReceipt : IDurableHandler<OrderPlaced> { … }      // outbox: atomic, retried
```

Raised on an aggregate, the event's durable rows commit in the save's own transaction; published through the
dispatcher, they are stored in a transaction of their own. With no outbox (a browser app, or `Rask.Cqrs` on its own)
a durable handler runs in memory like any other.

### Subscribing

An event is also what a screen subscribes to. `Publish` runs its handlers and hands it to every open
subscription — a component's `QueryClient.Subscribe<T>()`, or `Subscribe` anywhere else:

```csharp
await foreach (var incremented in dispatcher.Subscribe<CounterIncremented>(ct))
    Console.WriteLine(incremented.Value);
```

An `ISubscription<T>` record says which events one page wants, admitted by its own `IWatchPolicy<T>`.
See [subscriptions](https://rask.sh/docs/guides/subscriptions/).

## Who may send it

`[Authorize]` on a handler holds wherever the request comes from: over HTTP from a browser client, and
in-process from a server page.

```csharp
[Authorize(Roles = "admin")]
public sealed class PurgeLogsHandler : ICommandHandler<PurgeLogs> { /* … */ }

try
{
    await Dispatcher.Send(new PurgeLogs());
}
catch (ForbiddenException e) when (!e.IsAuthenticated)
{
    Routes.LoginPage().Go();
}
```

A caller the handler does not admit gets `ForbiddenException` before validation runs, so they do not learn
what was wrong with a request they could not send. `[AllowAnonymous]` is never checked. Work that is nobody's —
a job, a durable handler, a hosted service — runs as the system and is not checked either; the check that
matters there was made when the user's own request enqueued it.

The attribute is read at compile time, so it has to be `[Authorize]` itself, on the handler class or a base
class. One that derives from `AuthorizeAttribute`, or sits on the `Handle` method, is a build error
([RASK101](https://rask.sh/docs/guides/diagnostics/#rask101)) rather than a check that silently never runs.

## Pipeline behaviors (decorators)

Behaviors are the extension point for cross-cutting concerns — logging, transactions, caching. You
implement `IPipelineBehavior<TRequest, TResult>` and register it.

`Rask.Cqrs` ships **one**: a `ValidationBehavior` registered outermost, which rejects an invalid
request before its handler runs. See [validation.md](https://rask.sh/docs/guides/validation/#requests). Behaviors run as an onion in **registration order** (first-registered is outermost); call `next`
to continue or return without it to short-circuit. A void `ICommand` flows through as `TResult = Unit`,
so one behavior shape covers everything.

```csharp
public sealed class DispatchLogBehavior<TRequest, TResult>(CqrsCounterStore store)
    : IPipelineBehavior<TRequest, TResult>
{
    public Task<TResult> Handle(TRequest request, RequestHandler<TResult> next)
    {
        store.Note($"⚙ dispatch {typeof(TRequest).Name}");
        return next();
    }
}

// register: open-generic wraps every request, closed wraps one request/result pair
builder.Services.AddRaskCqrs(o => o.AddOpenBehavior(typeof(DispatchLogBehavior<,>)));
```

Behaviors are code. The dispatcher's plain switches — `HandlerLifetime`, `EventPublishStrategy`,
`StopOnFirstEventException`, `ValidateRequests` — can also be set in `Rask:Cqrs` in
`appsettings.json`, and the callback runs after them. Unlike every other Rask section, `Rask:Cqrs` is read
while `AddRaskCqrs` itself runs, because those switches decide which services get registered; it comes from
the host builder's configuration, so a container that is not a host reads none.

## It all fits together

The demo below is one vertical slice — the query, the result-command, the event the command
publishes, and the logging behavior above — all dispatched reflection-free. Click **Increment** to
watch the pipeline log build up.


## Remote dispatch — a client and a server (`Rask.Cqrs.Client` / `Rask.Cqrs.Server`)

A page running in the browser reaches its server through the **same `IDispatcher` call** it already
makes in-process. There is no `HttpClient` at the call site, no `/api/*` endpoint to write, and nothing
on a message marks it as remote — you write a record and a handler exactly as above, and *where the
handler lives* decides where it runs.

One package and one line per half. Neither references the other, so a browser bundle cannot compile the
endpoint code and the server never carries the browser transport:

```csharp
// The browser half. Every message it dispatches goes to the server.
host.Services.AddRaskCqrsClient();

// The server.
builder.Services.AddRaskCqrsServer();
...
app.MapRaskCqrs();
```

**`rask new --template wasm-hosted` scaffolds all of it, in one project.** The browser app lives in `Client/`, and three
places say which half a file belongs to:

| Folder | Compiled into |
|---|---|
| `Client/**` | the browser app only — its `Program.cs`, pages, components, `wwwroot/index.html` |
| `Shared/**` | both — **message records go here** |
| everything else | the server only — **handlers go here** |

The server's `Program.cs` is `RaskApp.Create(args).Serve();` — it registers `AddRaskCqrsServer()` and maps
`MapRaskCqrs()` ahead of the browser app's fallback, so neither line is yours to write.

So a message record is shared by construction, and its handler — with the connection string, table name or
pricing rule it touches — never reaches a download anybody can read. The build generates the browser app's
project into `obj/` from `Client/` and `Shared/`; `Client/Program.cs` is what switches that on, and
`<RaskClient>false</RaskClient>` switches it off.

What is left is keeping the two transports apart, which the csproj says in one line:

```xml
<!-- The browser app gets this one; the server must not. It is the half that CALLS the endpoints the
     server answers, and a plain PackageReference would ship it into the process answering them. -->
<RaskClientPackageReference Include="Rask.Cqrs.Client" Version="..."/>
```

and the browser app registers it in its own entry point:

```csharp
// Client/Program.cs
var host = WasmHostBuilder.CreateDefault();
host.Services.AddRaskCqrsClient();
await host.Run<App>();
```

`RaskClientUsing` and `RaskClientProjectReference` do the same for a project-wide using and a project
reference the browser app needs and the server must not have.

> **Without a database, the scaffold sets `Rask:Cqrs:Server:RequireAuthenticatedUser` to `false` in
> `appsettings.json` and says why.** The default is on, and that is right for an app with accounts — but an
> app with no database has none to require, so it would answer 401 to every message, and the failure reads
> as broken transport rather than as the secure default working. Once the app has a database, delete the
> key. And because it is configuration, an environment variable can turn it off too — guard the deploy
> environment like code. A handler that carries `[Authorize]` still needs a signed-in caller with the key
> off: the attribute is the author saying so.

**A client is a pure client.** Every request message it dispatches travels; a stray client-side handler
can never quietly intercept one. Events are the deliberate exception — they fan out, so a
client's own handlers still run *and* the event travels.

**Two projects instead of one works the same way, with one extra line.** Where a bundle and its host are
separate projects, the message records have to reach both compilations — link the file rather than
duplicating it, and there is still exactly one declaration:

```xml
<!-- In the HOST's csproj. The messages live with the call sites; the handlers stay here. -->
<Compile Include="..\MyApp.Browser\Shared\RemoteMessages.cs" Link="Shared\RemoteMessages.cs"/>
```

A pair of sample projects held that arrangement end to end over cookie auth: the members page asked the
server who was calling, and the answer was the identity the *server* saw — proof the session cookie rode
the dispatch, since the message carries no name at all. They are gone, so the shape above is documented
rather than exercised.

### Two endpoints, not one per message

`GET` and `POST` on `/_rask/cqrs/request/{name}`. The verb carries what `IQuery` and `ICommand` already
declare in C#: a query is safe and idempotent so it is a GET and can be cached; anything that mutates is
a POST. So a command is **405 on GET** and cannot be triggered by a URL, a prefetch or a link scanner. A
query too long for a URL falls back to POST automatically, with an identical result.

Because the name is a route segment, logs, metrics and rate-limit partitions get it for free.
`MapRaskCqrs()` returns the endpoint group, so `.RequireRateLimiting(...)`, CORS or output caching is a
one-line addition.

A third route under the same prefix, `GET /_rask/cqrs/request/events/{name}`, serves a browser's
[subscriptions](https://rask.sh/docs/guides/subscriptions/#in-a-webassembly-front-end) as server-sent events — the same group, so the same header,
authentication and rate limit.

### It fails closed

- **Authenticated by default.** `[AllowAnonymous]` on the handler is the only way past;
  `[Authorize]` supplies a policy *and* roles, both enforced — and a handler carrying several
  `[Authorize]` attributes, its own or a base class's, has to pass every one, exactly as a page does.
  `AuthenticationSchemes` is not read: the endpoint authenticates with the app's default scheme.
- **An anonymous caller cannot enumerate your messages.** A real name and a typo get the same answer, so
  the endpoint can't be walked to discover what the app has.
- **Both verbs require the `X-Rask-Cqrs` header**, which no cross-site markup can set — adding the GET
  surface adds no cross-site trigger.
- **Handler exceptions become RFC 9457 `problem+json`** with no exception text: an exception message is
  written for an operator, not a browser, and routinely names tables, paths and credentials. The 500
  carries a `reference`, and the exception is logged at Error on `Rask.Errors` with the same one:

  ```json
  { "type": "…/docs/cqrs.md#remote-errors", "title": "Handler failed", "status": 500, "reference": "K7WQ2MZP" }
  ```

  | `Rask:Cqrs:Server:IncludeExceptionDetail` | Development | Any other environment |
  | --- | --- | --- |
  | `false` (default) | generic problem + `reference` | generic problem + `reference` |
  | `true` | adds the exception as `detail` | **ignored** — generic problem + `reference`, and a warning at startup |

  The environment is the host's `IHostEnvironment`; a host that names none is Production. The switch
  cannot take effect on a deployed server, so an environment variable cannot turn the endpoint into a view
  of your stack traces.

**A handler knows who is calling without being told.** In a Rask app with its data layer on, a remote
request's handler runs with the caller's principal ambient, as a live session's in-process dispatch does:
[`Current.UserId`](https://rask.sh/docs/guides/data/#the-current-user--current) is the caller, and a [tenant-scoped](https://rask.sh/docs/guides/multi-tenancy/)
read filters to the caller's tenant. A command therefore does not carry a user id the client could forge — the
handler reads it. A job's handler gets the user and tenant it was enqueued for, the same way.

Failure to *arrive* is the one thing remote dispatch adds to the in-process call, and it is a
`RemoteDispatchException` — a null `StatusCode` means the request never reached the server.

### `[LocalOnly]`

Keeps a message off the wire entirely, and on an **interface** covers a whole family. This matters more
than it looks: `IJob` derives from `ICommand`, so without it every job payload in the app would become an
internet-reachable endpoint. Mark a **domain event** `[LocalOnly]` too: an event travels from a browser like any
message, so an unmarked `OrderPlaced` could be published for an order nobody placed, and its handlers would run.

It is also **how a client keeps a message in-process.** "A client is a pure client" is not a figure of
speech — `AddRaskCqrsClient()` replaces the invoker for *every* request message it has a contract for, so
a handler sitting in the client project stops being reached and the message goes to the server instead.
If a client genuinely owns a message end to end — a browser-local counter, an offline queue, anything the
server has no handler for — mark it `[LocalOnly]` and it never leaves:

```csharp
[LocalOnly]                                        // stays in the browser
public sealed record IncrementLocalCounter(int By) : ICommand<int>;
```

Without it the dispatch travels, the server answers **404** (it has no handler for that name), and the
failure looks like a transport problem rather than the design decision it is.

### Files, both directions

**A message declares its file as a `IRaskFile`** — the same type a file input hands a component. So the
file a user picked is passed straight to the handler, with nothing to convert and nothing to learn:

```csharp
public sealed record AttachReceipt(int OrderId, IRaskFile File) : ICommand;

// The call site. Identical whether this page is server-rendered or running in the browser.
await dispatcher.Send(new AttachReceipt(orderId, picked));

// Download: the file the handler returned, saved by the browser.
Download.File(await dispatcher.Query(new ExportOrders(year)));
```

The handler receives a `IRaskFile` too, and reads it exactly as it would in-process:

```csharp
public sealed class AttachReceiptHandler : ICommandHandler<AttachReceipt>
{
    public async Task Handle(AttachReceipt command)
    {
        await using var stream = command.File.OpenReadStream(command.File.Size, Current.Cancellation);
        // ...
    }
}
```

Where the message runs in-process the handler simply gets the picked file. Where it travels, the
generated codec carries the bytes and hands the handler a `IRaskFile` over what arrived — so the *host*
changes and the code does not. Every host reads a `IRaskFile` in bounded slices (the browser ones
through `Blob.slice`), and a download is streamed back headers-first.

Bounded by the server's `MaxUploadBytes` and `MaxFileCount`. A chunked upload sits on the server's disk
until its message arrives, so what one caller may leave there is bounded as well: `MaxOpenUploads` (4)
uploads begun and not yet sent, holding `MaxOpenUploadBytes` (64 MB) between them. Past either, the
next chunk answers **429** until an upload is sent or lapses after `UploadSessionLifetime`. A caller is
the signed-in user; with `RequireAuthenticatedUser` off it is the address the request came from (an
IPv6 `/64` counts as one), so behind a reverse proxy wire `UseForwardedHeaders` or every visitor shares
the proxy's budget.

**`ChunkedUploadThreshold` is load-bearing in the browser, not a tuning knob.** `fetch` has no request
streaming, so a browser reads a whole request body into memory before sending it — a single-shot
multipart upload of a 500 MB file costs 500 MB *in the tab*. Above the threshold the file goes up in
bounded pieces first and the message follows carrying only the session id, which is what keeps the
request small as well as the read. Raising it raises the browser's peak memory by the same amount.

**The download direction is the one `fetch` does stream, and one property decides it.** The transport
asks for the response headers first (`HttpCompletionOption.ResponseHeadersRead`), but in the browser
that only means anything while `WasmEnableStreamingResponse` is on — the WASM SDK defaults it to `true`,
and `false` makes `BrowserHttpHandler` buffer the whole body before the caller is handed a stream. So a
constant-memory export stops being constant-memory the moment an app sets:

```xml
<!-- Don't. A FileDownload is then materialised whole in the tab. -->
<WasmEnableStreamingResponse>false</WasmEnableStreamingResponse>
```

The published bundle is checked for that flag by the browser-rung publish gate, because the default
belongs to the SDK rather than to Rask and could otherwise change under the claim (#894).

**A dropped chunk resumes.** Every chunk response echoes `X-Rask-Upload-Offset`, and a chunk that does
not follow on from what the server holds is answered `409` carrying the offset it *does* hold — 409
rather than 400 because the request is well-formed, it is just out of step. The client restarts from
that offset, bounded by a small retry count per file, so one lost chunk does not fail an upload that
the protocol can recover.

The limit is **in-session**: a browser's `File` handle dies with the page, so resuming across a reload
is not possible and the upload starts again.

### Wire codecs, and RASK053

The codecs are **source-generated** `Utf8JsonWriter`/`Utf8JsonReader` code, not reflection, so remote
dispatch publishes clean under the WASM/AOT trimmer. **RASK053** reports a message or result shape that
has no wire encoding. It reaches no existing code: codecs are generated only for a compilation that
references one of the two transport packages, so an app using `Rask.Cqrs` in-process is unconstrained.

## Diagnostics

The generator validates your handlers at compile time:

- **RASK028** (error) — a query or command has more than one handler; dispatch would be ambiguous.
- **RASK029** (warning) — a handler is an open generic or has no public constructor, so it can't be
  registered and is skipped.

A request with **no** handler is not a compile error (the handler may live in another assembly) — it
throws a clear `InvalidOperationException` at dispatch time.

## Limitations

These follow directly from the reflection-free, compile-time design — the same thing that makes it
trim-safe:

- **Handlers must live in a loaded assembly.** Registration is driven by a per-assembly
  `[ModuleInitializer]`, which runs when the CLR first loads that assembly. If your handlers are in a
  separate library that nothing touches before `AddRaskCqrs()`, they won't be registered. In practice a
  Rask/Blazor app already uses types from its handler assemblies at startup; if yours doesn't, reference
  any type from that assembly (or keep handlers in the app assembly).
- **One handler per query/command, app-wide.** RASK028 catches duplicates within a compilation. Two
  handlers for the *same* request type in *different* assemblies aren't diagnosed (no whole-program
  view) — keep each query/command's handler unique across the whole app.
- **Events match the concrete published type.** There is no base-type/polymorphic fan-out
  (that would need runtime type-hierarchy reflection). Declare a handler for the exact `IEvent`
  type you publish.

## Trimming

Every handler's constructor is preserved with a generated `[DynamicDependency]`, and dispatch is a
compile-time closed-generic map, so a WASM app using `Rask.Cqrs` publishes with **zero IL warnings**.

## Testing

Handlers are plain classes — unit-test them directly, no dispatcher required. To test wiring, register
`AddRaskCqrs` into a `ServiceCollection`, build the provider, and dispatch. See
[`tests/Rask.Cqrs.Tests`](https://github.com/pal-tamas/rask/blob/main/tests/Rask.Cqrs.Tests) and the generator tests in
[`tests/Rask.Batteries.Generators.Tests`](https://github.com/pal-tamas/rask/blob/main/tests/Rask.Batteries.Generators.Tests).

**The transport is tested with both halves in front of each other**, in
[`tests/Rask.Cqrs.Transport.Tests`](https://github.com/pal-tamas/rask/blob/main/tests/Rask.Cqrs.Transport.Tests) — the real client building the
request, the real endpoint receiving it. Note what that project cannot do, because it is a property of
the design rather than of the test: `AddRaskCqrsClient()` installs its remote invokers into the
**process-wide** `CqrsRegistry`, and the endpoint dispatches through that same registry, so a process
that registers both halves sends every message it receives straight back out. A client process and a
server process are two processes — that is the assumption, and it is worth knowing before you try to
register both in one app.
