Skip to content

feat(application): application and API building blocks (handlers, validation, error responses) - #22

Merged
MendesMat merged 5 commits into
mainfrom
feat/application-building-blocks
Sep 28, 2026
Merged

MendesMat merged 5 commits into
mainfrom
feat/application-building-blocks

Conversation

@MendesMat

@MendesMat MendesMat commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

Closes #6

What

Adds the plumbing every future use case will share: the in-house command handler contract, a FluentValidation decorator that runs before it, and the translation of Result/Error failures into the Problem Details shape documented for the API.

Why

ADR-0007 (CQRS without MediatR), ADR-0008 (validate with FluentValidation) and ADR-0009 (Result pattern + Problem Details) define these building blocks; this pull request implements them. docs/api/conventions.md API-12/API-13 describe the error JSON shape, and API-14 (added here) fixes the verbatim message for an unexpected failure.

How to test

dotnet test --solution ControlService.slnx

123/123 tests pass; dotnet build ControlService.slnx -v q -clp:Summary is clean (0 warnings); dotnet format ControlService.slnx --verify-no-changes is clean.

Decisions taken along the way (the owner confirmed all of these before or during the work)

  • Error → HTTP status. A small table in the API project (ErrorStatusCodes), code → status, mirroring the ADR-0009 table. A code missing from it falls back to 500 without hiding the code/message, since that is a programming mistake, not a business outcome — kept distinct from UnexpectedErrorExceptionHandler, which handles a real unhandled exception with the generic API-14 message and never exposes the exception itself.
  • Field errors and details on Error. Error gained optional Fields (camelCase path → messages) and Details (free-form data, e.g. updatedByName) instead of a separate ValidationError subtype, so the API layer has one shape to translate.
  • Decorator registration. By hand, no Scrutor (ADR-0007 allows either). There are no concrete handlers yet to decorate, so no generic registration helper was written — that will happen per-handler when the first real command exists.
  • FluentValidation.DependencyInjectionExtensions was added to Directory.Packages.props (version matches the already-approved FluentValidation 12.1.1) as the issue asked, but no project references it yet: nothing to register.
  • ICurrentUser and TimeProvider were left out of this issue. Nothing here needs them, and ICurrentUser's shape depends on the JWT claims that only exist from issue Authentication: sign-in, sessions and the Admin's first access #8 onward; they will be introduced with the audit interceptor in issue Persistence: EF Core, PostgreSQL, audit, concurrency and seeded system records #7.
  • Test strategy for the Problem Details mapping. I originally planned a WebApplicationFactory integration test with a test-only endpoint injected through IStartupFilter. That endpoint never registered reliably under minimal hosting (repeated 404s I could not root-cause in the time available), so I switched to unit-testing ErrorResults.ToProblem(error, httpContext) directly against a DefaultHttpContext — same translation logic, no HTTP server. One real WebApplicationFactory test (RouteGroupTests) still confirms /api/v1 mounts without breaking the pipeline (API-01); that piece has no meaningful Red since no feature registers a route under it yet.
  • A discovery during the pause for the traceId test: ASP.NET Core's own AddProblemDetails() already fills the traceId extension by default (Activity.Current?.Id), so my own explicit line was dead duplication — removed in the Refactor step.

Checklist

  • Developed test-first: every behavior has a test that failed before the code existed (ADR-0033) — two cycles (test 6 and test 10) had no Red because the general rule was already covered by an earlier cycle; documented at each pause and verified by temporarily breaking the code to confirm the test would catch a regression.
  • dotnet test --solution ControlService.slnx passes locally (123/123)
  • Documentation updated: docs/api/conventions.md (API-14 and the error codes table)
  • Rule IDs covered: API-01, API-12, API-13, API-14, ADR-0007, ADR-0008, ADR-0009, ADR-0028
  • User-facing message (API-14) is the text the owner confirmed in conversation

MendesMat and others added 5 commits September 28, 2026 00:23
Error gains optional Fields and Details dictionaries so a Result
failure can hold the camelCase field messages and rule-specific data
(updatedByName, userNames) the API layer needs to build Problem
Details responses (ADR-0009).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…orator

ICommandHandler<TCommand, TResponse> is the in-house CQRS handler
contract (ADR-0007). ValidatingCommandHandler wraps it and runs a
FluentValidation validator first (ADR-0008): an invalid command
returns validation_failed and never reaches the inner handler, and
each FluentValidation failure path is converted to a camelCase field
path (emergencyContact.phone) in Error.Fields.

Removes the TEMPORARY exit-code line from
ControlService.Application.Tests.csproj, since it now has its first
tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ErrorResults.ToProblem maps a domain Error to the exact JSON shape of
ADR-0009 / API-12 / API-13: type and title from RFC 9110, the code/
message/errors/details extensions, and traceId (filled by ASP.NET
Core's own ProblemDetailsService). ErrorStatusCodes only maps the
codes exercised so far (validation_failed, not_found,
concurrency_conflict); a code missing from it falls back to 500
instead of being hidden, since that is a programming mistake, not a
business outcome.

UnexpectedErrorExceptionHandler reuses the same mapping for unhandled
exceptions, with the generic message of API-14 (docs/api/
conventions.md) and no exception detail in the response body.

Covered by unit tests against the mapping function directly
(DefaultHttpContext), not WebApplicationFactory: attempts to add a
test-only endpoint through IStartupFilter did not register reliably
under minimal hosting. RouteGroupTests keeps one real
WebApplicationFactory check that /api/v1 mounts without the pipeline
breaking (API-01); no feature registers a route under it yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
API-14 fixes the verbatim Portuguese message for an unexpected
failure (500 unexpected_error), used by
UnexpectedErrorExceptionHandler, and adds the corresponding row to
the error codes table.

FluentValidation.DependencyInjectionExtensions is added to
Directory.Packages.props (version matches FluentValidation 12.1.1)
for the first issue that registers a concrete validator; no project
references it yet, since there is no concrete validator to register.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@MendesMat
MendesMat merged commit df14bc5 into main Sep 28, 2026
5 checks passed
@MendesMat
MendesMat deleted the feat/application-building-blocks branch September 28, 2026 04:41
MendesMat added a commit that referenced this pull request Sep 28, 2026
…-0003 (#24)

## What

Marks that ADR-0008 supersedes the validation part of ADR-0003, in the
front matter of both records and in the ADR index.

## Why

Two accepted ADRs disagreed. ADR-0003 says cross-cutting validation is
applied through endpoint filters; ADR-0008, accepted later, decides that
a decorator around the command handlers runs the FluentValidation
validator, and that is what `ValidatingCommandHandler` does since #22.
The owner approved recording the relation. Following
`docs/agents/workflows/record-a-decision.md`, only the front matter and
the index change; the text of both accepted records stays as it is.

## How to test

Documentation only. Check the front matter of
`docs/adr/0003-use-minimal-apis-grouped-by-feature.md` and
`docs/adr/0008-validate-input-with-fluentvalidation.md`, and the 0003
row of `docs/adr/README.md`.

## Checklist

- [x] Developed test-first (ADR-0033): not applicable, documentation
only
- [x] `dotnet test --solution ControlService.slnx` passes locally: no
code change
- [x] Documentation updated (ADR front matter and index)
- [x] Rule IDs covered or changed: none (ADR-0003, ADR-0008)
- [x] User-facing messages: none
MendesMat added a commit that referenced this pull request Sep 28, 2026
Part of #6 (follow-ups found in the review after #22 was merged)

## What

Fixes the gaps between the error mapping merged in #22 and the
documentation, adds the real HTTP integration tests the issue asked for,
adds the missing `IQueryHandler`, and updates the agent guides.

## Why

- **Every ADR-0009 code is mapped.** The domain already returns
`link_invalid`, `system_record`, `self_deactivation` and `not_inactive`;
the table did not list them, so they would have become 500 responses.
`account_inactive` maps to 401 (during a session); the sign-in endpoint
returns its 403 explicitly.
- **An unmapped code is an unexpected failure (API-12).** It used to
return a 500 exposing the code and its message, with nothing logged. It
now throws, so the exception handler logs it and answers with
`unexpected_error` (API-14).
- **Typed result (ADR-0003).** `error.ToProblem()` returns
`ProblemHttpResult`, so endpoints can declare `Results<Ok<T>,
ProblemHttpResult>` for the OpenAPI document. Its unused `HttpContext`
parameter is gone.
- **Real integration tests (issue #6 "Done when").** The first attempt
failed because an `IStartupFilter` receives a plain
`ApplicationBuilder`, not an `IEndpointRouteBuilder`, so the test
endpoints were never mapped. With `UseRouting` + `UseEndpoints`,
`ErrorResponseTests` now checks through the whole pipeline the
documented validation JSON, the generic 500 for an exception and for an
unmapped code. That also covers the exception handler wiring in
`Program.cs`, which no test exercised before.
- **`IQueryHandler<TQuery, TResponse>`** was in the scope of #6 and was
missed.
- **Docs:** API-15 (the `validation_failed` message, which only appeared
in an example); the architecture guide describes `ToProblem`, the code
table, `account_inactive` and how a handler turns a field-less value
object error into a field error; the testing guide gains the startup
filter and `DefaultHttpContext` gotchas and loses the stale `TEMPORARY`
notes; rule 3 of `AGENTS.md` and the git workflow now cover pull request
bodies.

## How to test

```bash
dotnet test --solution ControlService.slnx
```

137/137 pass, build with 0 warnings, `dotnet format --verify-no-changes`
clean.

## Checklist

- [x] Developed test-first (ADR-0033), in autonomous mode at the owner's
request. The table and the unmapped-code behavior started Red.
`ErrorResponseTests` had no Red: it covers wiring of existing behavior;
unregistering the exception handler makes both 500 tests fail.
`IQueryHandler` is an interface with no behavior.
- [x] `dotnet test --solution ControlService.slnx` passes locally
- [x] Documentation updated (`docs/api/conventions.md`, `docs/agents/`,
`AGENTS.md`)
- [x] Rule IDs: API-12, API-13, API-14, API-15 (new), ADR-0003,
ADR-0007, ADR-0009
- [x] User-facing messages are verbatim (API-14, API-15)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Application and API building blocks: handlers, validation and error responses

1 participant