# How a server feature is shaped

Adding a command, a query, a validator, and the controller action that reaches them.

> Last verified: 2026-07-30 against commit `d3ec723`.

---

## 1. The dispatcher is not MediatR

CQRS runs on **`martinothamar/Mediator`** — a source-generator-based dispatcher. Use `ISender` / `ICommand` /
`IQuery` from that package. Any prose anywhere that says "MediatR" is wrong; do not add MediatR types or
`IMediator`.

---

## 2. The folder shape

```
Baya.Application/Features/<Area>/
├── Commands/<VerbNoun>Command/
│   ├── <VerbNoun>Command.cs           record : IRequest<OperationResult<T>>
│   ├── <VerbNoun>Command.Handler.cs   internal sealed class : IRequestHandler<…>
│   └── <VerbNoun>Command.Validator.cs AbstractValidator<Command>  (omit when there is nothing to validate)
└── Queries/<VerbNoun>Query/
    ├── <VerbNoun>Query.cs
    ├── <VerbNoun>Query.Handler.cs
    └── <VerbNoun>Query.Result.cs      record Result(…)  ← the DTO returned
```

`Features/System/Queries/Ping/` is the minimal live example — query, handler, result — surfaced by
`Controllers/V1/PingController`.

One type per file, and the file name matches the type name.

---

## 3. The rules

- **Requests are `record`s** — immutable, value semantics.
- **Handlers are `internal sealed`** — they are never used outside the Application layer.
- **Exactly one handler per request type.** No conditional dispatch.
- **Never throw for an expected failure.** Return an `OperationResult`:

  | Factory | Maps to |
  | --- | --- |
  | `OperationResult<T>.SuccessResult(value)` | 200 |
  | `OperationResult<T>.FailureResult(errors)` | 400 — validation or business-rule failure, with field-level detail |
  | `OperationResult<T>.NotFoundResult(message)` | 404 |
  | `OperationResult.ConflictResult(message)` | 409 — idempotency, duplicate, or an illegal state transition |

  Let a genuinely *unexpected* exception propagate to the global `ExceptionHandler` middleware. Don't
  try/catch unknown exceptions in a handler, and never swallow one into a `FailureResult`.

- **Contracts the handler depends on are interfaces in `Application/Contracts/`**, implemented in
  Infrastructure. A handler never references a concrete infrastructure type.

- **Validators are FluentValidation** `AbstractValidator<TRequest>`, auto-registered from the Application
  assembly by `AddApplicationServices` and run by the `ValidateCommandBehavior` pipeline behavior before the
  handler. Validate **at the boundary** — the command or query — not deep in the domain or a repository.

  ```csharp
  public sealed class CreateOrderCommandValidator : AbstractValidator<CreateOrderCommand>
  {
      public CreateOrderCommandValidator()
      {
          RuleFor(x => x.UserId).GreaterThan(0);
          RuleFor(x => x.Items).NotEmpty().WithMessage("Order must have at least one item.");
          RuleForEach(x => x.Items).ChildRules(item =>
          {
              item.RuleFor(i => i.ProductId).GreaterThan(0);
              item.RuleFor(i => i.Quantity).InclusiveBetween(1, 100);
          });
      }
  }
  ```

  **A route-supplied id must NOT be validated in the body command.** Route values (e.g.
  `patients/update/{id}`) aren't bound into the body, so a `GreaterThan(0)` on them fails every request.

- **Pipeline order is Logging → Metrics → Validate.** A new behavior slots into that chain in
  `AddApplicationServices`, not into a handler.

---

## 4. The controller

Every controller follows this skeleton:

```csharp
[ApiVersion("1")]
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[Display(Description = "One-line description shown in Swagger")]
[Authorize(ConstantPolicies.DynamicPermission)]   // or [Authorize], or omit for public
public sealed class MyFeatureController(ISender sender) : BaseController
{
    [HttpGet("[action]")]
    [ProducesOkApiResponseType<MyQueryResult>]
    public async Task<IActionResult> GetSomething(CancellationToken ct)
        => OperationResult(await sender.Send(new MyQuery(), ct));

    [HttpPost("[action]")]
    [ProducesOkApiResponseType<MyCommandResult>]
    public async Task<IActionResult> CreateSomething(MyCommand command, CancellationToken ct)
        => OperationResult(await sender.Send(command, ct));
}
```

- **`sealed`.** Controllers are not designed for inheritance beyond `BaseController`.
- **Inject `ISender` via the primary constructor**, not `IMediator`.
- **Never call `Ok()`, `BadRequest()`, or `NotFound()` directly.** Always `base.OperationResult(result)` —
  that is what maps `OperationResult` (including 401/403/409) onto the envelope every client already parses.
- **Keep the method thin: one `Send`, one `OperationResult`.** No business logic in a controller.
- **Use `[Display(Description = "…")]`** so NSwag generates meaningful Swagger tags.
- **Pass the `CancellationToken`** from the action into `sender.Send(...)`.
- **Route segments come from `[controller]`/`[action]` tokens**, which `SnakeCaseParameterTransformer`
  converts. Never hardcode a route string — that bypasses the transformer. If a method name doesn't read
  cleanly as a URL, **rename the method**.

### Authorization — the narrowest that fits

| Attribute | When |
| --- | --- |
| *(none)* | Truly public — health check, metrics, a webhook (which is signature-verified instead) |
| `[Authorize]` | Any authenticated user |
| `[Authorize(ConstantPolicies.DynamicPermission)]` | A role/claim-gated admin action |
| `[RequireTokenWithoutAuthorization]` | A token must be present but may be expired — the refresh endpoint |

Apply at the **controller** level for a uniform policy; override at the action level only for a genuine
exception. Least privilege: an admin action gets `DynamicPermission`, not a bare `[Authorize]`.

Rate-limit the sensitive ones — see [identity.md](identity.md) §5.

---

## 5. To add a feature

1. Create the folder under `Features/<Area>/{Commands|Queries}/<VerbNoun>/`.
2. Implement the request, the handler, and a validator if it takes input.
3. Add any new dependency as an interface in `Application/Contracts/`, and implement it in Infrastructure —
   mock and real both, if it is an external rail. See [structure.md](structure.md) §3.
4. Wire a controller action to `sender.Send(...)`.
5. Add handler unit tests (NSubstitute) **and** at least one `WebApplicationFactory` integration test for the
   area: happy path 200, unauthenticated 401, validation 400. See [conventions.md](conventions.md) §5.
6. Publish the endpoint's contract to [`docs/integration/`](../../integration/index.md).

If the feature adds a table, read [persistence.md](persistence.md) first — the money, snapshot, state-machine
and soft-delete rules there are invariants, not suggestions.
