diff --git a/docs/mediator-framework/README.md b/docs/mediator-framework/README.md index 9097ddd60..e8060bba2 100644 --- a/docs/mediator-framework/README.md +++ b/docs/mediator-framework/README.md @@ -9,16 +9,32 @@ isolated from HTTP translation, serialization and routing. ## Documents +Reference documentation (**what the framework is**) lives in this folder. +Everything that tracks **how it is being built** — plans, task boards, progress, +reviews — lives in [`progress/`](progress/README.md). + +### Reference + | Document | Purpose | | --- | --- | -| [`research.md`](research.md) | Evaluation of open-source alternatives, comparison with gRPC JSON transcoding, capability/library mapping. | | [`design.md`](design.md) | Target architecture: pure handlers, Roslyn incremental generator, the three transports, DI, error handling, user context, attachments. | -| [`implementation-plan.md`](implementation-plan.md) | Phased delivery plan with the packages to introduce. | -| [`tasks.md`](tasks.md) | Verifiable task breakdown with explicit acceptance criteria. | +| [`research.md`](research.md) | Evaluation of open-source alternatives, comparison with gRPC JSON transcoding, capability/library mapping. | | [`migration-from-mvc.md`](migration-from-mvc.md) | Incremental migration guidance, including the MVC compatibility escape hatch. | -| [`pre-release-review.md`](pre-release-review.md) | Adversarial pre-release review (DX + security), feature-gap analysis vs Ark.ReferenceProject, and recorded decisions. | -| [`tasks/README.md`](tasks/README.md) | Pre-release task board: one self-contained task document per item, organized by category, with Outcomes and Acceptance criteria. | -| [`future-improvements.md`](future-improvements.md) | Explicitly deferred post-1.0 items. | + +The end-user guide (getting started + per-feature documentation) is delivered by +task [`DOC-01`](progress/tasks/docs/DOC-01-user-documentation.md) and will live +in `docs/mediator-framework/guide/`. + +### Progress and tracking + +| Document | Purpose | +| --- | --- | +| [`progress/README.md`](progress/README.md) | Index of all delivery tracking documents. | +| [`progress/implementation-plan.md`](progress/implementation-plan.md) | Phased delivery plan with the packages to introduce. | +| [`progress/tasks.md`](progress/tasks.md) | Verifiable task breakdown with explicit acceptance criteria (epics). | +| [`progress/tasks/README.md`](progress/tasks/README.md) | Task board: one self-contained task document per item, organized by category, with Outcomes and Acceptance criteria. | +| [`progress/pre-release-review.md`](progress/pre-release-review.md) | Adversarial pre-release review (DX + security), feature-gap analysis vs Ark.ReferenceProject, and recorded decisions. | +| [`progress/future-improvements.md`](progress/future-improvements.md) | Explicitly deferred post-1.0 items. | ## Verifiable sample diff --git a/docs/mediator-framework/design.md b/docs/mediator-framework/design.md index cb2833dac..d7dbe066a 100644 --- a/docs/mediator-framework/design.md +++ b/docs/mediator-framework/design.md @@ -178,14 +178,24 @@ reconstructs the envelope (`init`-only members via object initializer / `with`) before dispatching to the pure handler. All three sources are demonstrated combined on a single sample contract and covered by tests. -### Generated multipart upload (single attachment) +### Generated multipart upload (single or multiple files) Attachment uploads are ordinary envelopes too: a `[HttpEndpoint]` contract may -declare **at most one** `IArkAttachment` property. The generator (not -hand-written startup code) emits a `multipart/form-data` endpoint for it: -route and query properties bind per the envelope rules above, the single form -file maps to `ArkAttachment` and is assigned to the attachment property. More -than one `IArkAttachment` property is a generator diagnostic (error). The +declare **at most one** attachment member, either a single `IArkAttachment` +property or a **collection** property +(`IReadOnlyList`/`IEnumerable`). The generator +(not hand-written startup code) emits a `multipart/form-data` endpoint for it: +route and query properties bind per the envelope rules above, and the form +file(s) map to `ArkAttachment` instances assigned to the attachment member. A +single-attachment contract binds the first form file and rejects a request +carrying more than one file with `400`; a collection contract binds **every** +uploaded file in form order, and an empty collection is allowed only when the +property is not required. Per-endpoint limits apply to each file +(`AllowedContentTypes`) and to the whole request (`MaxRequestBodySizeBytes`), +plus `MaxFileCount` for collections. Declaring more than one attachment member +is a generator diagnostic (error); the same rule and the same +metadata-first chunk protocol apply to gRPC uploads, where a collection member +is fed by a client stream whose metadata messages delimit the files. The `MapArkAttachmentUpload` runtime helper remains as the manual escape hatch only. Generated multipart endpoints disable antiforgery explicitly because they target bearer-token APIs; set `RequireAntiforgery = true` on the contract for a @@ -295,15 +305,39 @@ graph validation. Handlers only throw semantic domain exceptions; they never format transport errors. -### HTTP: Hellang ProblemDetails + BusinessRuleViolation +### Standard error responses on every endpoint -The Minimal API host does **not** reimplement RFC 7807 mapping. It registers -`Hellang.Middleware.ProblemDetails` and reuses the mappings that -`Ark.Tools.AspNetCore` already ships (`ArkProblemDetailsOptionsSetup`): +Every generated HTTP endpoint advertises the error contract the host actually +produces, so the OpenAPI document is complete without per-endpoint +hand-written metadata: + +- `400 Bad Request` — binding, deserialization and `ValidationException` + failures, `application/problem+json`. +- `403 Forbidden` — policy authorization failure, `application/problem+json`. + Emitted for every endpoint that is not `AllowAnonymous`; anonymous endpoints + omit it. +- `500 Internal Server Error` — unhandled exception, `application/problem+json`. + +`401 Unauthorized` is contributed by the authorization metadata of the +non-anonymous endpoints. The response type is the shared `ProblemDetails` +schema from `Ark.Tools.AspNetCore.ProblemDetails`, referenced (not duplicated) +by every operation. The generator emits this metadata; hosts do not repeat it, +and an endpoint that overrides `SuccessStatusCode`/`NullResultStatusCode` keeps +the standard error set. + +### HTTP: shared ProblemDetails package + BusinessRuleViolation + +The Minimal API host does **not** reimplement RFC 7807 mapping. Mapping lives in +the shared `Ark.Tools.AspNetCore.ProblemDetails` package (referenced by +`Ark.Tools.AspNetCore` so MVC hosts keep their existing behavior) and is wired +into a Minimal API host with `AddArkProblemDetailsExceptionHandler()` plus +`UseArkProblemDetailsExceptionHandler()` — an `IExceptionHandler`, not the +legacy Hellang middleware: - `EntityNotFoundException` → 404, `OptimisticConcurrencyException` → 409, - `UnauthorizedAccessException` → 403, `ValidationException` → 400 with field - violations. + `EntityTagMismatchException` → 412, `PolicyAuthorizationException` → 403, + `NotImplementedException` → 501, `HttpRequestException` → 503, + `ValidationException` → 400 with field violations. - `BusinessRuleViolationException` → 400: the `BusinessRuleViolation`-derived object (class name = error code; extra public properties = structured data) is serialized into the ProblemDetails `extensions`, exactly as existing MVC @@ -415,6 +449,27 @@ stream with its content type and sanitized leaf file name. Generated gRPC contra expose a server stream of metadata-first `DownloadDocumentChunk` messages followed by byte chunks; the shared chunk contracts are included in exported proto assets. +### Streaming collection responses + +A query or request whose response type is `IAsyncEnumerable` is a +**streaming collection**: the handler yields items as they are produced and the +framework never materializes the whole sequence unless the wire format forces +it. + +| Transport | Shape | +| --- | --- | +| Minimal API + JSON | native System.Text.Json streaming of a JSON **array** (`application/json`); items are written as they are yielded, the response is not buffered and no Server-Sent Events framing is used | +| Minimal API + MessagePack | the sequence is **buffered** and written as one MessagePack array; the format requires the element count in the array header, so an unknown-length top-level array cannot be streamed. The buffering ceiling and its upgrade path (a length-prefixed message stream negotiated with a distinct content type) are documented, not hidden | +| gRPC | **server-streaming** method (`IAsyncEnumerable` return in the code-first contract, `stream` in the exported `.proto`) | +| Rebus | not applicable — streaming responses are a request/response concept; a `[RebusMessage]` contract must not declare a streaming response (generator diagnostic) | + +Cancellation flows to the handler's `CancellationToken` when the client +disconnects or the gRPC call is cancelled, so producers stop early. Errors +thrown **before** the first item map to the normal error model; errors thrown +**after** streaming started cannot change the already-sent status code and are +surfaced as a truncated response (HTTP) or a non-OK trailer (gRPC), and are +always logged server-side. + ## API versioning The version is part of the contract's **lifetime**, not of its route. A @@ -561,6 +616,32 @@ targets **`net10.0` exclusively** — `[HttpEndpoint]` hosting has no `net8.0` support. The transport-neutral core and the Rebus/gRPC packages keep the repo-wide multi-targeting. +## OpenAPI operation grouping, naming and documentation + +Operations are grouped and named from the contract itself, so a developer who +writes only the handler still gets a navigable document: + +- **Tag** — defaults to the contract's **namespace** (the last namespace + segment; `Ark.Sample.Application.Greetings.GetGreetingQuery` → `Greetings`). + Overridable per contract with `[ApiTag("...")]`, and per namespace/assembly by + a host-level default. The same value groups gRPC methods when no explicit + `[ServiceGroup]` is declared, so both transports present the same taxonomy. +- **Operation name** — defaults to the contract **class name** with any + `Request`/`Query`/`Command` suffix preserved (`GetGreetingQuery` → + `GetGreetingQuery`), emitted as the OpenAPI `operationId` and as the endpoint + name (`WithName`). Versioned expansions get the version appended so + `operationId` stays unique across per-version documents. +- **Descriptions** — XML documentation is the single source of API prose. The + generators read the ``/`` of the **contract type** and emit + them as the operation summary/description, and the `` of each + contract **property** as the description of the corresponding parameter, + request-schema property and response-schema property. The same XML text is + emitted as leading comments on the messages/fields/methods of the exported + `.proto` files. Because the generators read the documentation comments from + the Roslyn symbols at compile time, cross-assembly XML files, runtime + reflection over `.xml` documentation files and per-host wiring are all + unnecessary; `GenerateDocumentationFile` is not required at runtime. + ## Developer-facing transport UIs The sample exposes the versioned OpenAPI documents as it does today and adds @@ -657,6 +738,67 @@ only to its own attribute, so adding a transport never re-runs the others. `ProjectReference` paths — including the analyzers, which reach the sample as analyzer assets of their runtime package. +## API surface snapshots + +The public wire surface must not change by accident. The framework ships a +generator that snapshots the **transport surface** — which is richer than the +C# public API — on every build and fails the build when the snapshot differs +from what is committed. + +**What is captured:** + +- HTTP: verb, expanded route template (per active version), parameter names and + sources, authorization policy, operation ID, tag. +- gRPC: service name (per version), method name, request/response message names, + field numbers (recursively, including nested messages), streaming kind. +- Rebus: message contract name and owner queue. +- Contract members: name, fully-qualified type, nullability, `[ServerSet]` flag. + Nested types are expanded recursively so a change to any nested field produces + a visible diff. + +**Workflow:** + +1. During every build the generator computes the full sorted surface and writes + it to `$(IntermediateOutputPath)/ArkApiSurface.current.txt` (the `obj/` + folder — never committed). +2. An MSBuild target compares that file byte-for-byte with the committed + `ArkApiSurface.txt` in the project directory. +3. If the files differ, or if `ArkApiSurface.txt` does not exist, the build + fails with diagnostics `ARKAPI001` / `ARKAPI002` and a message pointing to + the generated file. +4. The developer **accepts** the change by replacing the committed file with the + generated one: + ``` + cp obj/…/ArkApiSurface.current.txt ArkApiSurface.txt + git add ArkApiSurface.txt + ``` + The resulting diff is exactly the wire change, reviewable in the pull + request with no noise. + +**Diagnostics:** + +- `ARKAPI001` (error) — `ArkApiSurface.txt` is missing; create it by copying + from `obj/ArkApiSurface.current.txt`. +- `ARKAPI002` (error) — `ArkApiSurface.txt` differs from the current surface; + accept by copying `obj/ArkApiSurface.current.txt` over it. + +**CI/CD gate.** Because `ArkApiSurface.txt` must exactly match the current +surface to pass the build, CI fails automatically on any API change that has +not been explicitly committed. No separate release flag is needed; the single +snapshot is the gate at every stage of the pipeline. + +**Entry format** (deterministic, ordinal-sorted, one entry per line): + +``` +CONTRACT GreetingDto.Name : string? server-set=false +CONTRACT GreetingDto.Tags[].Value : string server-set=false +GRPC Greetings.V1/GetGreeting (GetGreetingQuery) returns (GreetingDto) unary +GRPC-FIELD GetGreetingQuery.Id = 1 : bytes +HTTP GET /api/v1/greetings/{id} -> GetGreetingQuery : GreetingDto [policy=RequireAuthenticatedUser] [op=GetGreetingQuery] [tag=Greetings] +HTTP-PARAM GetGreetingQuery.Id : System.Guid route required +REBUS RefreshGreetingCommand -> queue:greetings +``` + ## Testing strategy - `samples/Ark.MediatorFramework.Sample/test/…Sample.Tests` demonstrates **how @@ -671,6 +813,18 @@ only to its own attribute, so adding a transport never re-runs the others. adapters — independent of the sample application. Every feature that lives in a `src/` library must be unit-tested here. +## User documentation + +The framework ships a task-oriented guide under +`docs/mediator-framework/guide/`: a getting-started walkthrough (first contract, +first handler, hosting each transport) plus one page per supported feature, +each with a short example and a link to the corresponding place in +`samples/Ark.MediatorFramework.Sample`. The guide documents the supported +happy paths **and** the escape hatches (hand-written endpoint, hand-written +gRPC method, `MapArkAttachmentUpload`), and every code snippet in it is taken +from — or verified against — sample code that is compiled by the solution +build. + ## Build discipline Every implementation step — regardless of how small — ends by building the @@ -687,6 +841,6 @@ Mirroring the ReferenceProject, it separates the transport-agnostic **Application** assembly (pure contracts/handlers, store, decorator) from the **WebInterface** hosting assembly, where the selected requests/queries are exposed via endpoints and the transports (user context, Rebus) are wired. -See [`implementation-plan.md`](implementation-plan.md) for exactly which pieces -are proven in code versus specified for follow-up, and [`tasks.md`](tasks.md) +See [`progress/implementation-plan.md`](progress/implementation-plan.md) for exactly which pieces +are proven in code versus specified for follow-up, and [`progress/tasks.md`](progress/tasks.md) for acceptance criteria. diff --git a/docs/mediator-framework/progress/README.md b/docs/mediator-framework/progress/README.md new file mode 100644 index 000000000..354210a94 --- /dev/null +++ b/docs/mediator-framework/progress/README.md @@ -0,0 +1,25 @@ +# Mediator Framework — delivery tracking + +Everything in this folder tracks **how** the framework is being built. The +reference documentation (what the framework *is*) stays one level up: +[`../design.md`](../design.md), [`../research.md`](../research.md), +[`../migration-from-mvc.md`](../migration-from-mvc.md). + +| Document | Purpose | +| --- | --- | +| [`tasks/README.md`](tasks/README.md) | **Current task board.** One self-contained task document per pending item, with Outcomes and Acceptance. Start here. | +| [`tasks.md`](tasks.md) | Historical epic breakdown (Epics 1–12) with acceptance criteria. | +| [`implementation-plan.md`](implementation-plan.md) | Phased delivery plan (Phases 1–10) with step-by-step instructions. | +| [`pre-release-review.md`](pre-release-review.md) | Adversarial pre-release review (DX + security), gap analysis vs `Ark.ReferenceProject`, recorded decisions D1–D8. | +| [`future-improvements.md`](future-improvements.md) | Explicitly deferred post-1.0 items. | + +## Working agreement + +- Every task is executed in its own branch/PR with a conventional-commit title. +- Full-solution build gate on every step: + `dotnet build Ark.Tools.slnx --configuration Debug` (zero warnings) and + `dotnet test Ark.Tools.slnx --no-build --configuration Debug --minimum-expected-tests 1`. +- A task that changes framework behavior described in [`../design.md`](../design.md) + updates that document in the same PR. +- Progress is recorded in [`tasks/README.md`](tasks/README.md) (checkbox in the + execution order) — that file is the single source of truth for "what is left". diff --git a/docs/mediator-framework/future-improvements.md b/docs/mediator-framework/progress/future-improvements.md similarity index 100% rename from docs/mediator-framework/future-improvements.md rename to docs/mediator-framework/progress/future-improvements.md diff --git a/docs/mediator-framework/implementation-plan.md b/docs/mediator-framework/progress/implementation-plan.md similarity index 93% rename from docs/mediator-framework/implementation-plan.md rename to docs/mediator-framework/progress/implementation-plan.md index eb9fd09c1..cffc22d77 100644 --- a/docs/mediator-framework/implementation-plan.md +++ b/docs/mediator-framework/progress/implementation-plan.md @@ -630,9 +630,11 @@ interceptor, upload adapter, proto export) are in `src/` packages. follows the "Next implementation order" in `tasks.md` (Phase 8 wire-shape and packaging steps before the remaining Phase 7 behavioral steps), with the full-solution build gate on every step. -- **Phase 9** implements the first preview follow-up (T11.4 Rebus owner routing); - T11.2 is complete; the remaining Epic 11 work is authenticated OpenAPI UIs - and gRPCui. +- **Phase 9** (preview follow-ups, Epic 11) is complete: Rebus owner routing, + STJ source-generated metadata, authenticated OpenAPI UIs and the gRPCui + operations panel. +- **Phase 10** (release-scope extension, Epic 12 / decision D8) is specified + below and tracked per task in [`tasks/README.md`](tasks/README.md). The first build attempt on a fresh checkout failed because `--no-restore` was used before assets existed. The verified sequence is `dotnet restore @@ -717,3 +719,59 @@ client id; no browser client secret is accepted or stored. 4. The documented gRPCui command is the smoke check for listing and invoking exported unary and streaming methods from the proto set. 5. Build the full solution and run all tests. ✅ + +## Phase 10 — Release-scope extension (Epic 12, decision D8) + +Each step has a self-contained task document under [`tasks/`](tasks/README.md); +those documents hold the authoritative steps, test requirements and acceptance +criteria. Every step ends with the full-solution build gate +(`dotnet build Ark.Tools.slnx --configuration Debug`) and +`dotnet test Ark.Tools.slnx --no-build --configuration Debug --minimum-expected-tests 1`, +and updates [`../design.md`](../design.md) if behavior diverges from it. + +Order matters: wire-shape changes first (they invalidate snapshots and +documentation), the API-surface gate after them (so the first committed +snapshot is the release surface), documentation last. + +### Step 10.1 — OpenAPI tags and operation names (T12.1) + +[NET-06](tasks/aspnetcore/NET-06-openapi-tags-operation-names.md). Adds +`[ApiTag]` to the core package, emits `.WithTags`/`.WithName`, shares the +taxonomy with gRPC service grouping, diagnoses duplicate operation names. + +### Step 10.2 — Standard problem responses (T12.2) + +[FW-05](tasks/framework/FW-05-standard-problem-responses.md). Emits +400/403/500 `application/problem+json` on every generated endpoint kind and +proves it with a test that enumerates every operation of every document. + +### Step 10.3 — `IAsyncEnumerable` streaming (T12.3) + +[FW-06](tasks/framework/FW-06-async-enumerable-streaming.md). Streaming JSON +array on Minimal API, server-streaming gRPC, deliberate MessagePack buffering +with `MaxStreamedItems`, Rebus diagnostic, no SSE. + +### Step 10.4 — Multi-file uploads (T12.4) + +[FW-07](tasks/framework/FW-07-multifile-uploads.md). Attachment collection +members on multipart and gRPC streaming with per-file hardening and +`MaxFileCount`. + +### Step 10.5 — XML documentation into OpenAPI and protos (T12.5) + +[GEN-09](tasks/generator-dx/GEN-09-xml-documentation.md). Generators read +documentation comments from Roslyn symbols and emit operation/parameter/schema +descriptions plus proto leading comments. Runs after Steps 10.1–10.4 so the +documented surface is final. + +### Step 10.6 — API-surface snapshot gate (T12.6) + +[GEN-10](tasks/generator-dx/GEN-10-api-surface-snapshots.md). New analyzer + +code fix with `ArkApiSurface.Shipped.txt`/`ArkApiSurface.Unshipped.txt`; the +sample opts in and commits its snapshot, which becomes the release baseline. + +### Step 10.7 — User documentation (T12.7) + +[DOC-01](tasks/docs/DOC-01-user-documentation.md). `docs/mediator-framework/guide/` +with a getting-started walkthrough and one page per feature, snippets cited from +compiled sample code, all relative links verified. diff --git a/docs/mediator-framework/pre-release-review.md b/docs/mediator-framework/progress/pre-release-review.md similarity index 91% rename from docs/mediator-framework/pre-release-review.md rename to docs/mediator-framework/progress/pre-release-review.md index 708c923e2..5289e225b 100644 --- a/docs/mediator-framework/pre-release-review.md +++ b/docs/mediator-framework/progress/pre-release-review.md @@ -85,6 +85,7 @@ Reverse gaps (sample > ReferenceProject, keep as-is): gRPC code-first + proto ex | D5 | ProblemDetails setup | **New shared package**; `Ark.Tools.AspNetCore` references it so existing behavior is preserved as-is. | | D6 | C8 env-var auth scheme | Env var is acceptable for now; `WebApplicationFactory`-based scheme substitution recorded in [Future improvements](future-improvements.md). Malformed-bearer 401 handling is still fixed (SEC-08). | | D7 | Release gating | **All G1–G10 are release blockers.** All security items (SEC-01..SEC-08) are release blockers. **N3 (XML-docs to OpenAPI) is a release blocker.** Remaining N items are post-release. | +| D8 | 2026-07 scope extension (recorded 2026-07-26) | Seven further items are **release blockers**: OpenAPI tags + operation names (NET-06), XML documentation into OpenAPI **and** exported `.proto` produced by the generators (GEN-09, superseding NET-01's XML step), standard 400/403/500 ProblemDetails responses on every endpoint (FW-05), `IAsyncEnumerable` streaming responses on Minimal API + gRPC with deliberate MessagePack buffering and **no SSE** (FW-06), multi-file uploads bound to an attachment collection (FW-07), an API-surface snapshot gate modeled on `PublicApiAnalyzers` but covering routes/gRPC methods (GEN-10), and the user documentation guide (DOC-01). | ## Priority order @@ -93,4 +94,6 @@ Reverse gaps (sample > ReferenceProject, keep as-is): gRPC code-first + proto ex 3. Generator diagnostics (GEN-01..GEN-03, GEN-05, GEN-06). 4. SMP-01 FluentValidation + SEC-05 authorization decorator — the transport-agnostic cross-cutting story. 5. Sample parity (SMP-02..SMP-06), FW-03, FW-04 and NET-01 (blocker). -6. Post-release: NET-02..NET-05, future improvements. +6. Scope extension (D8): NET-06, FW-05, FW-06, FW-07, GEN-09, GEN-10, then DOC-01 last so the guide + documents shipped behavior. +7. Post-release: NET-02..NET-05, future improvements. diff --git a/docs/mediator-framework/tasks.md b/docs/mediator-framework/progress/tasks.md similarity index 88% rename from docs/mediator-framework/tasks.md rename to docs/mediator-framework/progress/tasks.md index 0ac1a0fcf..7b1738aa0 100644 --- a/docs/mediator-framework/tasks.md +++ b/docs/mediator-framework/progress/tasks.md @@ -333,13 +333,52 @@ Debug`) before the task is marked complete. cover generated routing and the sample sends through it without a manual type map. -## Next implementation order +## Epic 12 — Release-scope extension (2026-07, decision D8) + +Each item has a self-contained task document under +[`tasks/`](tasks/README.md); the acceptance criteria live there and are the +authoritative ones. -T9.1–T9.6 are complete. Wire-shape and packaging refinements run **before** -the behavioral-test epic so the Reqnroll scenarios are written once against -the final contracts: +- [ ] **T12.1** OpenAPI tags and operation names from the contract + ([NET-06](tasks/aspnetcore/NET-06-openapi-tags-operation-names.md)). + - *Accept:* every generated operation carries a namespace-derived tag + (overridable with `[ApiTag]`) and a unique, contract-derived `operationId`; + gRPC grouping shares the taxonomy; duplicates are a diagnostic. +- [ ] **T12.2** Standard 400/403/500 ProblemDetails responses + ([FW-05](tasks/framework/FW-05-standard-problem-responses.md)). + - *Accept:* an enumerating test over every operation of every document proves + the standard problem responses are declared and produced. +- [ ] **T12.3** `IAsyncEnumerable` streaming responses + ([FW-06](tasks/framework/FW-06-async-enumerable-streaming.md)). + - *Accept:* HTTP JSON streams a plain array without buffering and gRPC uses a + server-streaming method (both proven incremental by tests); MessagePack + buffers with a documented ceiling and item limit; no SSE. +- [ ] **T12.4** Multi-file uploads bound to an attachment collection + ([FW-07](tasks/framework/FW-07-multifile-uploads.md)). + - *Accept:* N files bind to a collection of `ArkAttachment` on multipart and + gRPC streaming with per-file hardening; single-file wire behavior unchanged. +- [ ] **T12.5** XML documentation into OpenAPI and exported `.proto` + ([GEN-09](tasks/generator-dx/GEN-09-xml-documentation.md)). + - *Accept:* contract/property XML docs appear as operation, parameter and + schema descriptions and as proto leading comments, produced by the + generators with no per-host wiring. +- [ ] **T12.6** API-surface snapshot gate + ([GEN-10](tasks/generator-dx/GEN-10-api-surface-snapshots.md)). + - *Accept:* changing a route, operation name, contract member, proto field + number or queue fails the build until `ArkApiSurface.Unshipped.txt` is + regenerated by the code fix; the sample opts in with committed snapshots. +- [ ] **T12.7** User documentation guide + ([DOC-01](tasks/docs/DOC-01-user-documentation.md)). + - *Accept:* `docs/mediator-framework/guide/` covers getting started and every + supported feature with snippets cited from compiled sample code; all + relative links resolve. + +## Next implementation order -1. **T11.1** gRPCui operations-panel tooling. +Epics 1–11 are complete. The remaining release scope is the pre-release task +board ([`tasks/README.md`](tasks/README.md)) plus **Epic 12**, executed in the +board's recommended order: SMP-04, SMP-05, SMP-06, NET-01, then T12.1–T12.6 and +finally T12.7 (documentation last, so it describes shipped behavior). ## Status legend diff --git a/docs/mediator-framework/tasks/README.md b/docs/mediator-framework/progress/tasks/README.md similarity index 81% rename from docs/mediator-framework/tasks/README.md rename to docs/mediator-framework/progress/tasks/README.md index 5dee6490b..8b407c7ab 100644 --- a/docs/mediator-framework/tasks/README.md +++ b/docs/mediator-framework/progress/tasks/README.md @@ -5,6 +5,7 @@ dedicated PR from the document alone. Every task defines **Outcomes** (what exis **Acceptance** (verifiable criteria; the PR is not mergeable until all pass). Source analysis and decisions: [`../pre-release-review.md`](../pre-release-review.md). +Delivery-tracking index: [`../README.md`](../README.md). ## Conventions for every task PR @@ -41,7 +42,19 @@ Source analysis and decisions: [`../pre-release-review.md`](../pre-release-revie | [SMP-04](sample-parity/SMP-04-optimistic-concurrency.md) | Optimistic concurrency + ETag (G6) | sample-parity | | [SMP-05](sample-parity/SMP-05-paging.md) | Paging (G7) | sample-parity | | [SMP-06](sample-parity/SMP-06-misc-parity.md) | App Insights, config layering, IClock, test infra (G9) | sample-parity | -| [NET-01](aspnetcore/NET-01-openapi-xml-docs.md) | XML-docs into OpenAPI + 3.1 verification (N3) | aspnetcore | +| [NET-01](aspnetcore/NET-01-openapi-xml-docs.md) | OpenAPI 3.1 verification, YAML, doc-UI decision (N3) | aspnetcore | + +## Release blockers — 2026-07 scope extension (decision D8) + +| Task | Title | Category | +|---|---|---| +| [NET-06](aspnetcore/NET-06-openapi-tags-operation-names.md) | OpenAPI tags and operation names from the contract | aspnetcore | +| [GEN-09](generator-dx/GEN-09-xml-documentation.md) | XML documentation into OpenAPI and exported `.proto` | generator-dx | +| [FW-05](framework/FW-05-standard-problem-responses.md) | Standard 400/403/500 ProblemDetails responses on every endpoint | framework | +| [FW-06](framework/FW-06-async-enumerable-streaming.md) | `IAsyncEnumerable` streaming responses | framework | +| [FW-07](framework/FW-07-multifile-uploads.md) | Multi-file uploads bound to an attachment collection | framework | +| [GEN-10](generator-dx/GEN-10-api-surface-snapshots.md) | API-surface snapshot gate (contracts, routes, gRPC methods) | generator-dx | +| [DOC-01](docs/DOC-01-user-documentation.md) | User documentation: getting started + feature guide | docs | ## Non-blocking (do before release if capacity allows) @@ -96,7 +109,15 @@ recent security commits `8502585`, `fd4d600`, `938567d`, and `c0fc361`. [ ] [SMP-05](sample-parity/SMP-05-paging.md) [ ] [SMP-06](sample-parity/SMP-06-misc-parity.md) [ ] [NET-01](aspnetcore/NET-01-openapi-xml-docs.md) -7. Post-release: +7. Scope extension (D8) — wire-shape items first, documentation last: + [ ] [NET-06](aspnetcore/NET-06-openapi-tags-operation-names.md) + [ ] [FW-05](framework/FW-05-standard-problem-responses.md) + [ ] [FW-06](framework/FW-06-async-enumerable-streaming.md) + [ ] [FW-07](framework/FW-07-multifile-uploads.md) + [ ] [GEN-09](generator-dx/GEN-09-xml-documentation.md) + [ ] [GEN-10](generator-dx/GEN-10-api-surface-snapshots.md) + [ ] [DOC-01](docs/DOC-01-user-documentation.md) +8. Post-release: [ ] [NET-02](aspnetcore/NET-02-openapi-operation-transformers.md) [ ] [NET-03](aspnetcore/NET-03-json-patch.md) [ ] [NET-04](aspnetcore/NET-04-auth-metrics.md) diff --git a/docs/mediator-framework/progress/tasks/aspnetcore/NET-01-openapi-xml-docs.md b/docs/mediator-framework/progress/tasks/aspnetcore/NET-01-openapi-xml-docs.md new file mode 100644 index 000000000..8e9348ab6 --- /dev/null +++ b/docs/mediator-framework/progress/tasks/aspnetcore/NET-01-openapi-xml-docs.md @@ -0,0 +1,42 @@ +# NET-01 — OpenAPI 3.1 verification, YAML and doc-UI decision (N3) + +> **Rescoped**: XML-documentation population moved to +> [GEN-09](../generator-dx/GEN-09-xml-documentation.md), which does it in the generators for both +> OpenAPI and the exported `.proto`. NET-01 keeps 3.1 verification, the YAML endpoint and the doc-UI +> decision. + +**Category**: aspnetcore · **Priority**: **Release blocker** (decision D7) · **Scope**: FRAMEWORK + SAMPLE + +## Problem + +The mediator sample uses `Microsoft.AspNetCore.OpenApi` (`AddOpenApi`) but: +1. OpenAPI **3.1** schema output of generator-emitted endpoints is unverified (nullable handling, + `IntroducedIn`/`RetiredIn` per-version docs, polymorphic contracts). +2. Both Swashbuckle.SwaggerUI and Scalar are present — decide deliberately. + +Files: `samples/Ark.MediatorFramework.Sample/src/Ark.MediatorFramework.Sample.WebInterface/SampleStartup.cs` +(OpenAPI/Scalar/Swagger wiring), framework OpenAPI helpers in +`src/mediator-framework/Ark.Tools.MediatorFramework.MinimalApi/ArkOpenApiEx.cs` and `ArkOpenApiSecurityEx.cs`. + +## Steps + +1. Verify 3.1 output: snapshot-test the generated document for one contract per feature: nullable + property, NodaTime type, polymorphic contract, versioned endpoint (`IntroducedIn`), multipart + endpoint. Fix framework transformers where the schema is wrong. +2. YAML endpoint: expose the document in YAML too if trivially supported by `MapOpenApi` (it is: + `/openapi/{documentName}.yaml`); document the route. +3. UI decision: drop Swashbuckle.SwaggerUI from the sample in favor of **Scalar only**, unless + `AddAuthorizationCodeFlow` (see `SampleStartup.cs`) depends on SwaggerUI — in that case port the + OAuth flow config to Scalar. Remove the unused package reference + lockfile entries. +4. Descriptions in OpenAPI and in the exported protos are GEN-09's responsibility; do not duplicate them here. + +## Outcomes + +- OpenAPI 3.1 correctness of generated endpoints is snapshot-verified, the YAML document is reachable and a single deliberate doc UI ships. + +## Acceptance + +- [ ] 3.1 snapshot tests for nullable/NodaTime/polymorphic/versioned/multipart schemas pass. +- [ ] YAML document reachable. +- [ ] Single doc UI (Scalar) with working OAuth flow; Swashbuckle.SwaggerUI reference removed (or a recorded, deliberate decision to keep both). +- [ ] Lockfiles updated; full solution build + tests green. diff --git a/docs/mediator-framework/tasks/aspnetcore/NET-02-openapi-operation-transformers.md b/docs/mediator-framework/progress/tasks/aspnetcore/NET-02-openapi-operation-transformers.md similarity index 100% rename from docs/mediator-framework/tasks/aspnetcore/NET-02-openapi-operation-transformers.md rename to docs/mediator-framework/progress/tasks/aspnetcore/NET-02-openapi-operation-transformers.md diff --git a/docs/mediator-framework/tasks/aspnetcore/NET-03-json-patch.md b/docs/mediator-framework/progress/tasks/aspnetcore/NET-03-json-patch.md similarity index 100% rename from docs/mediator-framework/tasks/aspnetcore/NET-03-json-patch.md rename to docs/mediator-framework/progress/tasks/aspnetcore/NET-03-json-patch.md diff --git a/docs/mediator-framework/tasks/aspnetcore/NET-04-auth-metrics.md b/docs/mediator-framework/progress/tasks/aspnetcore/NET-04-auth-metrics.md similarity index 100% rename from docs/mediator-framework/tasks/aspnetcore/NET-04-auth-metrics.md rename to docs/mediator-framework/progress/tasks/aspnetcore/NET-04-auth-metrics.md diff --git a/docs/mediator-framework/tasks/aspnetcore/NET-05-sse-transport-spike.md b/docs/mediator-framework/progress/tasks/aspnetcore/NET-05-sse-transport-spike.md similarity index 100% rename from docs/mediator-framework/tasks/aspnetcore/NET-05-sse-transport-spike.md rename to docs/mediator-framework/progress/tasks/aspnetcore/NET-05-sse-transport-spike.md diff --git a/docs/mediator-framework/progress/tasks/aspnetcore/NET-06-openapi-tags-operation-names.md b/docs/mediator-framework/progress/tasks/aspnetcore/NET-06-openapi-tags-operation-names.md new file mode 100644 index 000000000..cbf8e373f --- /dev/null +++ b/docs/mediator-framework/progress/tasks/aspnetcore/NET-06-openapi-tags-operation-names.md @@ -0,0 +1,82 @@ +# NET-06 — OpenAPI tags and operation names from the contract + +**Category**: aspnetcore · **Priority**: **Release blocker** · **Scope**: FRAMEWORK + SAMPLE + +## Problem + +Generated operations carry no tag and no operation name. `MinimalApiEndpointGenerator` emits +`.Produces<…>()` / `.WithGroupName("v1")` only +(`src/mediator-framework/Ark.Tools.MediatorFramework.MinimalApi.Generators/MinimalApiEndpointGenerator.cs`, +around the `Produces` calls). Consequences: + +- every operation lands in the default tag of the document, so Scalar/Swagger UI show one flat list; +- `operationId` is either missing or an unreadable compiler-generated name, so generated clients + (NSwag/openapi-generator) produce meaningless method names; +- gRPC service grouping (`[ServiceGroup]`) and HTTP grouping are unrelated, so the two transports + present different taxonomies for the same contracts. + +## Design + +See `docs/mediator-framework/design.md` → *OpenAPI operation grouping, naming and documentation*. + +- **Tag** default = last segment of the contract type's namespace + (`…Application.Greetings.GetGreetingQuery` → `Greetings`). +- **Operation name** default = contract type name (`GetGreetingQuery`), used for both + `WithName(...)` (endpoint name) and the OpenAPI `operationId`. +- Versioned expansion must keep `operationId` unique per document: append the version suffix + (`GetGreetingQuery_v2`) **only** when the same contract is expanded into more than one version + document. +- Override: new `[ApiTag("Greetings")]` attribute (class-level, `AttributeTargets.Class`), placed in + the transport-neutral core package `Ark.Tools.MediatorFramework` so Application assemblies do not + reference ASP.NET Core (same rule as `[BindFromQuery]`/`[ServerSet]`). +- The gRPC generator uses the same `[ApiTag]` value as the service-group fallback when + `[ServiceGroup]` is absent, so both transports group identically. An explicit `[ServiceGroup]` + still wins for gRPC. + +## Steps + +1. Add `ApiTagAttribute` (sealed, `[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, + Inherited = false)]`, single `string Name` ctor property) to + `src/mediator-framework/Ark.Tools.MediatorFramework/`, with XML docs and the standard copyright + header. +2. In `MinimalApiEndpointGenerator`, compute `tag` and `operationName` per endpoint during semantic + analysis (they belong on the endpoint model record, not in the emitter), and emit + `.WithTags("").WithName("")` on every mapped endpoint, including the + multipart, download and command paths. Escape the literals. + Docs: [`WithTags`](https://learn.microsoft.com/aspnet/core/fundamentals/openapi/aspnetcore-openapi#openapi-operation-tags), + [`WithName` / operationId](https://learn.microsoft.com/aspnet/core/fundamentals/minimal-apis/openapi#operationid). +3. In `GrpcEndpointGenerator`, fall back to `[ApiTag]` before the namespace default when grouping + methods into a service. +4. Report a generator diagnostic (next free `ARKMF0xx`, documented in `design.md`) when two contracts + in the same document/version resolve to the same operation name — a duplicate `operationId` breaks + client generation and must not be emitted silently. +5. Add `[ApiTag]` to at least one sample contract and rely on the namespace default for the others, so + both paths are demonstrated in `samples/Ark.MediatorFramework.Sample`. + +## Test coverage (required) + +- `tests/Ark.Tools.MediatorFramework.Tests/GeneratorSnapshotTests.cs`: snapshot shows `.WithTags` + and `.WithName` for a default-tag contract, an `[ApiTag]`-overridden contract and a versioned + contract (unique `operationId` per version). +- New generator test asserting the duplicate-operation-name diagnostic is reported. +- Sample test that fetches `/openapi/v1.json` and asserts: every operation has a non-empty `tags[0]` + and a unique `operationId`; the `[ApiTag]` override is honoured; the namespace default is applied + elsewhere. +- gRPC: an existing exported-proto assertion is extended to prove the `[ApiTag]` fallback groups the + method into the expected service. + +## Outcomes + +- Every generated HTTP operation carries a deterministic tag and a stable, unique `operationId` + derived from the contract, overridable with `[ApiTag]`; gRPC grouping uses the same taxonomy. + +## Acceptance + +- [ ] `ApiTagAttribute` exists in `Ark.Tools.MediatorFramework` with XML docs. +- [ ] Generated endpoints emit `.WithTags(...)` and `.WithName(...)`; snapshots updated. +- [ ] `operationId` is unique within each versioned document (tested). +- [ ] Duplicate operation names produce a documented generator diagnostic (tested). +- [ ] gRPC service grouping falls back to `[ApiTag]`; explicit `[ServiceGroup]` still wins (tested). +- [ ] `design.md` documents the defaults, the override and the new diagnostic id. +- [ ] `dotnet build Ark.Tools.slnx --configuration Debug` succeeds with zero warnings. +- [ ] `dotnet test Ark.Tools.slnx --no-build --configuration Debug --minimum-expected-tests 1` passes. diff --git a/docs/mediator-framework/progress/tasks/docs/DOC-01-user-documentation.md b/docs/mediator-framework/progress/tasks/docs/DOC-01-user-documentation.md new file mode 100644 index 000000000..5a3685043 --- /dev/null +++ b/docs/mediator-framework/progress/tasks/docs/DOC-01-user-documentation.md @@ -0,0 +1,80 @@ +# DOC-01 — User documentation: getting started + feature guide + +**Category**: docs · **Priority**: **Release blocker** · **Scope**: DOCS (+ sample references) + +## Problem + +`docs/mediator-framework/` contains design, research and delivery tracking — documents written for the +people building the framework. An adopting team has no getting-started path and no per-feature +reference; today the only usable entry point is reading `design.md` (~700 lines) and the sample source. + +## Design + +A task-oriented guide under `docs/mediator-framework/guide/`, separate from `design.md` (which stays +the architecture rationale) and from `progress/` (delivery tracking). Every page: + +- states what the feature is for, in one paragraph; +- shows the **smallest** working example (contract + attribute + what the developer gets); +- links to the exact file in `samples/Ark.MediatorFramework.Sample` that demonstrates it; +- lists the escape hatch when the feature does not fit. + +Snippets must be copied from compiled sample code, never invented, so they cannot rot silently. + +## Pages to write + +| File | Content | +| --- | --- | +| `guide/README.md` | Index + when to use this framework vs `Ark.Tools.AspNetCore` MVC. | +| `guide/getting-started.md` | New project: reference the packages, write one contract + handler, host it on Minimal API, call it; then add gRPC and Rebus by adding attributes only. Includes the SimpleInjector wiring and the run/test commands. | +| `guide/contracts-and-handlers.md` | `IRequest`/`IQuery`/`ICommand`, pure handler rules, records, `[ProtoContract]`/`[ProtoMember]` numbering rules. | +| `guide/http-endpoints.md` | `[HttpEndpoint]`: verb/template, envelope binding (route/query/body), `[BindFromQuery]`, `[ServerSet]`, status semantics (`SuccessStatusCode`, `NullResultStatusCode`), route groups. | +| `guide/grpc.md` | `[GrpcMethod]`, `[ServiceGroup]`, proto export on build, consuming the exported protos, gRPCui. | +| `guide/rebus.md` | `[RebusMessage]`, owner queues + generated routing, per-message scope, cancellation, HTTP→bus composition. | +| `guide/versioning.md` | `IntroducedIn`/`RetiredIn`, `{version}` expansion, per-version documents and gRPC services, superseding a contract. | +| `guide/errors.md` | Domain exceptions → ProblemDetails / `Google.Rpc.Status`, `BusinessRuleViolation`, standard 400/403/500 responses, what is never leaked. | +| `guide/validation-and-authorization.md` | FluentValidation decorators, the transport-agnostic policy decorator, secure-by-default endpoints and `AllowAnonymous`. | +| `guide/serialization.md` | JSON (Ark defaults, STJ source generation), MessagePack negotiation + required resolver, protobuf/NodaTime surrogates, polymorphism across the three wires. | +| `guide/attachments.md` | Upload (single + multiple files), download, limits and sanitization, gRPC streaming upload, `MapArkAttachmentUpload` escape hatch. | +| `guide/streaming.md` | `IAsyncEnumerable` responses on HTTP/gRPC, MessagePack buffering ceiling, cancellation. | +| `guide/openapi.md` | Documents per version, tags/operation names, XML documentation flow, NodaTime/polymorphism/`[ServerSet]` transformers, OAuth2 configuration, Scalar. | +| `guide/api-surface-snapshots.md` | Why the build fails on an API change, how to regenerate, how to review the diff, shipped vs unshipped. | +| `guide/testing.md` | Reqnroll behavioral tests through public interfaces, the gRPC client generated from exported protos, test auth. | +| `guide/escape-hatches.md` | Hand-written Minimal API mapping, hand-written gRPC method in the generated `partial`, hand-written `IHandleMessages<>`, MVC coexistence (links to `../migration-from-mvc.md`). | + +## Steps + +1. Write the pages above; keep each under ~150 lines and prefer a table + one snippet over prose. +2. Cross-link: `docs/mediator-framework/README.md` points to `guide/README.md` as the entry point for + users; every guide page links back to the relevant `design.md` section for rationale. +3. Add a short "Documentation" section to `samples/Ark.MediatorFramework.Sample/README.md` mapping each + sample file to the guide page it illustrates. +4. Verify every referenced sample path exists (a wrong link is a defect). +5. Write the pages **after** NET-06, GEN-09, FW-05, FW-06, FW-07 and GEN-10 are merged, so the documented + behavior is the shipped behavior. If a feature is still pending when the page is written, the page + states it explicitly rather than describing an unimplemented API. + +## Test coverage (required) + +Documentation has no unit tests; the enforced checks are: + +- A link check over `docs/mediator-framework/**/*.md` — every relative link resolves to an existing + file (run it and record the command and its output in the PR description). +- Every code snippet is copied from a file in `samples/Ark.MediatorFramework.Sample` that is compiled by + `dotnet build Ark.Tools.slnx`; the snippet's source path is cited immediately under it. +- Reviewer check: following `guide/getting-started.md` verbatim produces a running endpoint (the PR + author states they performed this walkthrough). + +## Outcomes + +- An adopting team can go from zero to a working three-transport service by following the guide, and can + look up every supported feature without reading the design or the generator source. + +## Acceptance + +- [ ] All pages in the table exist with the described content. +- [ ] Every snippet cites the compiled sample file it comes from. +- [ ] All relative links in `docs/mediator-framework/` resolve (link check performed, result in PR). +- [ ] `docs/mediator-framework/README.md` and the sample README point at the guide. +- [ ] No page documents an unimplemented behavior without flagging it as pending. +- [ ] `dotnet build Ark.Tools.slnx --configuration Debug` succeeds with zero warnings. +- [ ] `dotnet test Ark.Tools.slnx --no-build --configuration Debug --minimum-expected-tests 1` passes. diff --git a/docs/mediator-framework/tasks/framework/FW-01-icommand-support.md b/docs/mediator-framework/progress/tasks/framework/FW-01-icommand-support.md similarity index 100% rename from docs/mediator-framework/tasks/framework/FW-01-icommand-support.md rename to docs/mediator-framework/progress/tasks/framework/FW-01-icommand-support.md diff --git a/docs/mediator-framework/tasks/framework/FW-02-http-status-semantics.md b/docs/mediator-framework/progress/tasks/framework/FW-02-http-status-semantics.md similarity index 100% rename from docs/mediator-framework/tasks/framework/FW-02-http-status-semantics.md rename to docs/mediator-framework/progress/tasks/framework/FW-02-http-status-semantics.md diff --git a/docs/mediator-framework/tasks/framework/FW-03-shared-problemdetails-package.md b/docs/mediator-framework/progress/tasks/framework/FW-03-shared-problemdetails-package.md similarity index 100% rename from docs/mediator-framework/tasks/framework/FW-03-shared-problemdetails-package.md rename to docs/mediator-framework/progress/tasks/framework/FW-03-shared-problemdetails-package.md diff --git a/docs/mediator-framework/tasks/framework/FW-04-file-download.md b/docs/mediator-framework/progress/tasks/framework/FW-04-file-download.md similarity index 100% rename from docs/mediator-framework/tasks/framework/FW-04-file-download.md rename to docs/mediator-framework/progress/tasks/framework/FW-04-file-download.md diff --git a/docs/mediator-framework/progress/tasks/framework/FW-05-standard-problem-responses.md b/docs/mediator-framework/progress/tasks/framework/FW-05-standard-problem-responses.md new file mode 100644 index 000000000..c40c578dc --- /dev/null +++ b/docs/mediator-framework/progress/tasks/framework/FW-05-standard-problem-responses.md @@ -0,0 +1,79 @@ +# FW-05 — Standard 400/403/500 ProblemDetails responses on every endpoint + +**Category**: framework · **Priority**: **Release blocker** · **Scope**: FRAMEWORK + SAMPLE + +## Problem + +Generated endpoints declare only their success and null-result statuses +(`.Produces(200).Produces(404)` in +`src/mediator-framework/Ark.Tools.MediatorFramework.MinimalApi.Generators/MinimalApiEndpointGenerator.cs`). +The host **does** return RFC 7807 payloads for validation, authorization and unhandled failures via +`Ark.Tools.AspNetCore.ProblemDetails`, but the OpenAPI document never says so, so generated clients +have no error model and UI users cannot see the error contract. + +## Design + +See `docs/mediator-framework/design.md` → *Standard error responses on every endpoint*. + +Every generated HTTP endpoint declares, in addition to its success/null statuses: + +| Status | When | Content type | Schema | +| --- | --- | --- | --- | +| 400 | binding/deserialization failure, `ValidationException` | `application/problem+json` | `ProblemDetails` | +| 403 | policy authorization failure | `application/problem+json` | `ProblemDetails` | +| 500 | unhandled exception | `application/problem+json` | `ProblemDetails` | + +`401` comes from the authorization metadata of non-anonymous endpoints and is **not** duplicated here. +`403` is omitted for `AllowAnonymous = true` endpoints. If a contract declares a custom +`SuccessStatusCode`/`NullResultStatusCode` that collides with one of the standard codes, the endpoint's +own declaration wins and the standard one is skipped (no duplicate `Produces` for the same status). + +## Steps + +1. In `MinimalApiEndpointGenerator`, emit + `.ProducesProblem(StatusCodes.Status400BadRequest).ProducesProblem(StatusCodes.Status403Forbidden) + .ProducesProblem(StatusCodes.Status500InternalServerError)` (skipping collisions and skipping 403 + when anonymous) on **every** generated route: unary, multipart upload, attachment download and + command endpoints. + Docs: [`ProducesProblem`](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.http.openapiroutehandlerbuilderextensions.producesproblem), + [Minimal API responses](https://learn.microsoft.com/aspnet/core/fundamentals/minimal-apis/responses). +2. Ensure the emitted problem schema is the `Microsoft.AspNetCore.Mvc.ProblemDetails` type actually + produced by `ArkProblemDetailsExceptionHandler` + (`src/aspnetcore/Ark.Tools.AspNetCore.ProblemDetails/`), so document and runtime agree — including + the `errors` extension used by `ValidationException` and the `BusinessRuleViolation` extensions. +3. Verify the 400 path really produces `application/problem+json`, including the MessagePack + negotiation failure path in `ArkMessagePackEx` (it previously returned a bodyless 400 — if it still + does, fix it to return the shared ProblemDetails payload; this is the one behavioral fix in scope). +4. gRPC parity: no proto change is required (errors travel as `Google.Rpc.Status`), but document in + `design.md` the mapping table 400↔`InvalidArgument`, 403↔`PermissionDenied`, + 500↔`Internal`, and assert it in a test. + +## Test coverage (required) + +- Generator snapshot: every endpoint kind emits the three (or two, when anonymous) `ProducesProblem` + calls, with no duplicate status codes when `SuccessStatusCode`/`NullResultStatusCode` collide. +- Sample document test: for **every** operation in `/openapi/v1.json` and `/openapi/v2.json`, responses + contain `400` and `500` with `application/problem+json`, and `403` for non-anonymous operations. The + test enumerates operations — it must not hard-code a list, so a future endpoint cannot regress. +- Behavioral tests: a validation failure returns 400 `application/problem+json` with `errors`; a + policy failure returns 403 problem+json; a forced unhandled exception returns 500 problem+json + without leaking exception details in the non-development configuration. +- MessagePack negotiation failure returns a ProblemDetails body. +- gRPC test asserting the status-code mapping parity. + +## Outcomes + +- The OpenAPI document is a complete error contract: every operation advertises the standard problem + responses actually produced by the host, verified by an enumerating test that cannot rot. + +## Acceptance + +- [ ] All generated endpoint kinds declare 400/500 (and 403 unless anonymous) with + `application/problem+json`. +- [ ] No duplicate response declarations when a custom status collides. +- [ ] Enumerating OpenAPI test over all operations in all documents passes. +- [ ] Behavioral 400/403/500 tests assert the payload shape and the absence of leaked exception detail. +- [ ] MessagePack negotiation failure returns a ProblemDetails body. +- [ ] `design.md` records the HTTP↔gRPC status mapping table. +- [ ] `dotnet build Ark.Tools.slnx --configuration Debug` succeeds with zero warnings. +- [ ] `dotnet test Ark.Tools.slnx --no-build --configuration Debug --minimum-expected-tests 1` passes. diff --git a/docs/mediator-framework/progress/tasks/framework/FW-06-async-enumerable-streaming.md b/docs/mediator-framework/progress/tasks/framework/FW-06-async-enumerable-streaming.md new file mode 100644 index 000000000..113a494d9 --- /dev/null +++ b/docs/mediator-framework/progress/tasks/framework/FW-06-async-enumerable-streaming.md @@ -0,0 +1,89 @@ +# FW-06 — `IAsyncEnumerable` streaming responses + +**Category**: framework · **Priority**: **Release blocker** · **Scope**: FRAMEWORK + SAMPLE + +## Problem + +A handler that returns a large collection must materialize it entirely +(`IQuery>`), which buffers the whole result in server memory and delays the first byte. +Neither generator recognizes `IAsyncEnumerable` today: the Minimal API generator treats it as an +ordinary response type and the gRPC generator emits a unary method. + +## Design + +See `docs/mediator-framework/design.md` → *Streaming collection responses*. + +A contract whose response type is `IAsyncEnumerable` +(`IQuery>` / `IRequest>`) is a streaming collection: + +| Transport | Shape | +| --- | --- | +| Minimal API + JSON | native System.Text.Json streaming of a JSON array; no buffering, **no Server-Sent Events** | +| Minimal API + MessagePack | buffered into one MessagePack array (the format needs the element count in the array header) | +| gRPC | server-streaming method; `stream` in the exported `.proto` | +| Rebus | not supported — a `[RebusMessage]` contract with a streaming response is a generator error | + +`null` result handling does not apply (an `IAsyncEnumerable` is never null-mapped to 404); an empty +sequence is an empty array / an empty stream with an OK trailer. + +## Steps + +1. Core: detect `System.Collections.Generic.IAsyncEnumerable` as the response type in the shared + endpoint-model analysis of both generators; carry `IsStreaming` + the element type on the model. +2. Minimal API generator: return the `IAsyncEnumerable` from the endpoint lambda directly — + ASP.NET Core writes it as a streamed JSON array through the configured `JsonSerializerOptions`. + Declare `.Produces>(200)` so the OpenAPI schema stays an array of the element type, + not an opaque async-enumerable schema. + Docs: [Minimal API return values](https://learn.microsoft.com/aspnet/core/fundamentals/minimal-apis/responses#return-values), + [`JsonSerializer` async streaming](https://learn.microsoft.com/dotnet/standard/serialization/system-text-json/how-to#serialize-to-utf-8). +3. MessagePack path (`ArkMessagePackEx`): buffer with `await foreach` into a `List` and serialize + the list. Add a `ponytail:`-style comment naming the ceiling (unbounded buffer for very large + sequences) and the upgrade path (a length-prefixed message stream under a distinct content type). + Apply `MessagePackSecurity.UntrustedData` rules unchanged on the request side. +4. Add a per-endpoint `MaxStreamedItems` guard on `HttpEndpointAttribute` (zero = unlimited) used by + the MessagePack buffering path to fail fast with a 500 ProblemDetails instead of exhausting memory. +5. gRPC generator: emit the method as server-streaming (`IAsyncEnumerable` return on the + `[ServiceContract]` method — `protobuf-net.Grpc` maps it to a server-streaming rpc) and emit + `returns (stream …)` in the exported `.proto`. + Docs: [protobuf-net.Grpc streaming](https://protobuf-net.github.io/protobuf-net.Grpc/gettingstarted), + [gRPC server streaming](https://learn.microsoft.com/aspnet/core/grpc/basics#server-streaming-call). +6. Cancellation: pass the request `CancellationToken` into the handler call and enumerate with + `WithCancellation(ctk)`, so client disconnect stops the producer on both transports. +7. Rebus generator: report a diagnostic (next free `ARKMF0xx`) for a `[RebusMessage]` contract with a + streaming response. +8. Sample: add one streaming query exposed on HTTP + gRPC (e.g. a paged-free `GetGreetingsStream`), + yielding items with an observable delay so streaming is testable. + +## Test coverage (required) + +- Generator snapshots: streaming contract emits the streaming Minimal API mapping with an array schema, + a server-streaming gRPC method and a `stream` in the exported proto; non-streaming contracts unchanged. +- Rebus streaming diagnostic test. +- Behavioral HTTP test proving the response is **not** buffered: assert the first array element is + readable before the producer has finished (e.g. read the response stream incrementally with + `HttpCompletionOption.ResponseHeadersRead`, with a bounded timeout). +- Behavioral HTTP test asserting the JSON body is a plain array (no SSE `data:` framing, content type + `application/json`). +- MessagePack test asserting the buffered array round-trips and that `MaxStreamedItems` overflow + produces a ProblemDetails 500. +- gRPC test consuming the server stream through `Ark.MediatorFramework.Sample.GrpcClient` (generated + from the exported proto) and asserting incremental delivery and cancellation. +- Empty-sequence test on all three wires. + +## Outcomes + +- Handlers can yield results incrementally; HTTP JSON and gRPC stream them end-to-end, MessagePack + buffers deliberately with a documented ceiling and a hard item limit, and Rebus rejects the shape at + compile time. + +## Acceptance + +- [ ] `IAsyncEnumerable` responses generate streaming Minimal API and server-streaming gRPC paths. +- [ ] Incremental delivery proven by tests on both HTTP JSON and gRPC (not just correct content). +- [ ] No Server-Sent Events framing is introduced. +- [ ] MessagePack buffering documented in code + `design.md`, bounded by `MaxStreamedItems`. +- [ ] Cancellation reaches the handler when the client disconnects (tested). +- [ ] Rebus + streaming response reports a documented diagnostic. +- [ ] OpenAPI shows an array-of-element schema for streaming operations. +- [ ] `dotnet build Ark.Tools.slnx --configuration Debug` succeeds with zero warnings. +- [ ] `dotnet test Ark.Tools.slnx --no-build --configuration Debug --minimum-expected-tests 1` passes. diff --git a/docs/mediator-framework/progress/tasks/framework/FW-07-multifile-uploads.md b/docs/mediator-framework/progress/tasks/framework/FW-07-multifile-uploads.md new file mode 100644 index 000000000..9ded8549f --- /dev/null +++ b/docs/mediator-framework/progress/tasks/framework/FW-07-multifile-uploads.md @@ -0,0 +1,79 @@ +# FW-07 — Multi-file uploads bound to an attachment collection + +**Category**: framework · **Priority**: **Release blocker** · **Scope**: FRAMEWORK + SAMPLE + +## Problem + +A `[HttpEndpoint]` contract may declare **at most one** `IArkAttachment` property; more than one is +the `MultipleAttachments` diagnostic in +`src/mediator-framework/Ark.Tools.MediatorFramework.MinimalApi.Generators/MinimalApiEndpointGenerator.cs`. +There is no way to upload several files in one request, which `Ark.ReferenceProject`-style +applications need (attach N documents to one entity) — today the only workaround is N requests, which +loses atomicity. + +## Design + +See `docs/mediator-framework/design.md` → *Generated multipart upload (single or multiple files)*. + +- A contract declares **at most one attachment member**, either + `IArkAttachment` (single) or a collection of it + (`IReadOnlyList`, `IReadOnlyCollection`, `IEnumerable`). +- Single member: binds the first form file; a request with more than one file is `400`. +- Collection member: binds **every** form file, preserving form order. +- Existing hardening applies per file: `AllowedContentTypes` → `415`, filename sanitization + (`ArkAttachmentName.Sanitize`), `MaxRequestBodySizeBytes` for the whole request. New + `MaxFileCount` (zero = host default) rejects oversized batches with `400`. +- gRPC: the client-streaming upload carries a metadata message per file, so one call can deliver + several files; a metadata message ends the previous file and starts the next. + +## Steps + +1. `MinimalApiEndpointGenerator`: extend attachment detection to recognize the supported collection + shapes; keep "more than one attachment member" as the existing error diagnostic, and add a + diagnostic for an unsupported collection shape (e.g. `List` is fine to accept, + `IArkAttachment[]` too — decide and document; reject anything not constructible from a + `List`). +2. Emit the multipart lambda binding `IFormFileCollection` and projecting each file into + `ArkAttachment(file.FileName, file.ContentType, file.OpenReadStream)` (same construction as today), + assigning the resulting list to the collection member. + Docs: [Minimal API file uploads](https://learn.microsoft.com/aspnet/core/fundamentals/minimal-apis/parameter-binding#file-uploads-using-iformfile-and-iformfilecollection). +3. Enforce, before invoking the handler: `MaxFileCount`, per-file `AllowedContentTypes` (415 on the + first offending file), and the single-member "exactly one file" rule (400). +4. OpenAPI: the multipart request schema must show an array of binary strings for a collection member + (`type: array, items: {type: string, format: binary}`) and a single binary string otherwise. +5. `HttpEndpointAttribute`: add `MaxFileCount` with XML docs. +6. gRPC: extend the generated/streaming upload adapter and `UploadDocumentChunk` handling in + `src/mediator-framework/Ark.Tools.MediatorFramework/` so a stream may contain several + metadata-delimited files, exposed to the handler as the same collection. Keep single-file behavior + byte-compatible with the current wire shape (a stream with one metadata message). +7. Sample: add a multi-file upload contract + handler and expose it on HTTP and gRPC. + +## Test coverage (required) + +- Generator snapshots: single-attachment contract unchanged; collection contract emits the + `IFormFileCollection` binding; unsupported shapes and multiple attachment members produce diagnostics. +- Behavioral HTTP tests: upload 0 (when allowed), 1, and N files; assert names, content types and + content reach the handler in form order; assert sanitized file names; assert `MaxFileCount` + overflow → 400 problem+json; assert a disallowed content type → 415; assert the single-member + contract rejects a 2-file request with 400. +- OpenAPI test asserting the multipart schema shape for both single and collection members. +- gRPC test uploading two files in one client stream and asserting both reach the handler, plus a + regression test that a single-file stream behaves exactly as before. +- Security regression: a traversal-style filename (`../../evil.txt`) is reduced to its leaf name in + every file of the batch. + +## Outcomes + +- One request can carry many files, bound to a collection of `ArkAttachment`, with the same + hardening, the same handler shape and the same wire contract on HTTP multipart and gRPC streaming. + +## Acceptance + +- [ ] Collection attachment members supported on Minimal API and gRPC; single-member behavior unchanged. +- [ ] `MaxFileCount`, `AllowedContentTypes` and filename sanitization enforced per file (tested). +- [ ] Multipart OpenAPI schema shows a binary array for collections. +- [ ] gRPC multi-file client stream tested; single-file wire compatibility preserved. +- [ ] Diagnostics cover multiple attachment members and unsupported collection shapes. +- [ ] `design.md` updated (already describes the target; adjust if the implementation diverges). +- [ ] `dotnet build Ark.Tools.slnx --configuration Debug` succeeds with zero warnings. +- [ ] `dotnet test Ark.Tools.slnx --no-build --configuration Debug --minimum-expected-tests 1` passes. diff --git a/docs/mediator-framework/tasks/generator-dx/GEN-01-incremental-generators.md b/docs/mediator-framework/progress/tasks/generator-dx/GEN-01-incremental-generators.md similarity index 100% rename from docs/mediator-framework/tasks/generator-dx/GEN-01-incremental-generators.md rename to docs/mediator-framework/progress/tasks/generator-dx/GEN-01-incremental-generators.md diff --git a/docs/mediator-framework/tasks/generator-dx/GEN-02-diagnostics-for-silent-failures.md b/docs/mediator-framework/progress/tasks/generator-dx/GEN-02-diagnostics-for-silent-failures.md similarity index 100% rename from docs/mediator-framework/tasks/generator-dx/GEN-02-diagnostics-for-silent-failures.md rename to docs/mediator-framework/progress/tasks/generator-dx/GEN-02-diagnostics-for-silent-failures.md diff --git a/docs/mediator-framework/tasks/generator-dx/GEN-03-startup-handler-verification.md b/docs/mediator-framework/progress/tasks/generator-dx/GEN-03-startup-handler-verification.md similarity index 100% rename from docs/mediator-framework/tasks/generator-dx/GEN-03-startup-handler-verification.md rename to docs/mediator-framework/progress/tasks/generator-dx/GEN-03-startup-handler-verification.md diff --git a/docs/mediator-framework/tasks/generator-dx/GEN-04-remove-hardcoded-documents-proto.md b/docs/mediator-framework/progress/tasks/generator-dx/GEN-04-remove-hardcoded-documents-proto.md similarity index 100% rename from docs/mediator-framework/tasks/generator-dx/GEN-04-remove-hardcoded-documents-proto.md rename to docs/mediator-framework/progress/tasks/generator-dx/GEN-04-remove-hardcoded-documents-proto.md diff --git a/docs/mediator-framework/tasks/generator-dx/GEN-05-rebus-cancellation-token.md b/docs/mediator-framework/progress/tasks/generator-dx/GEN-05-rebus-cancellation-token.md similarity index 100% rename from docs/mediator-framework/tasks/generator-dx/GEN-05-rebus-cancellation-token.md rename to docs/mediator-framework/progress/tasks/generator-dx/GEN-05-rebus-cancellation-token.md diff --git a/docs/mediator-framework/tasks/generator-dx/GEN-06-grpc-user-context-interceptor.md b/docs/mediator-framework/progress/tasks/generator-dx/GEN-06-grpc-user-context-interceptor.md similarity index 100% rename from docs/mediator-framework/tasks/generator-dx/GEN-06-grpc-user-context-interceptor.md rename to docs/mediator-framework/progress/tasks/generator-dx/GEN-06-grpc-user-context-interceptor.md diff --git a/docs/mediator-framework/tasks/generator-dx/GEN-07-automatic-proto-export.md b/docs/mediator-framework/progress/tasks/generator-dx/GEN-07-automatic-proto-export.md similarity index 100% rename from docs/mediator-framework/tasks/generator-dx/GEN-07-automatic-proto-export.md rename to docs/mediator-framework/progress/tasks/generator-dx/GEN-07-automatic-proto-export.md diff --git a/docs/mediator-framework/tasks/generator-dx/GEN-08-from-assembly-api-names.md b/docs/mediator-framework/progress/tasks/generator-dx/GEN-08-from-assembly-api-names.md similarity index 100% rename from docs/mediator-framework/tasks/generator-dx/GEN-08-from-assembly-api-names.md rename to docs/mediator-framework/progress/tasks/generator-dx/GEN-08-from-assembly-api-names.md diff --git a/docs/mediator-framework/progress/tasks/generator-dx/GEN-09-xml-documentation.md b/docs/mediator-framework/progress/tasks/generator-dx/GEN-09-xml-documentation.md new file mode 100644 index 000000000..5d8ba6e68 --- /dev/null +++ b/docs/mediator-framework/progress/tasks/generator-dx/GEN-09-xml-documentation.md @@ -0,0 +1,88 @@ +# GEN-09 — XML documentation into OpenAPI and exported `.proto` + +**Category**: generator-dx · **Priority**: **Release blocker** · **Scope**: FRAMEWORK + SAMPLE + +Supersedes step 1 of [NET-01](../aspnetcore/NET-01-openapi-xml-docs.md) (which keeps only the +OpenAPI 3.1 verification, YAML endpoint and doc-UI decision). + +## Problem + +Contract XML documentation is invisible on the wire. OpenAPI operations and schemas have no +`summary`/`description`, and the exported `.proto` files carry no comments, so both the Scalar/Swagger +UI and any polyglot gRPC consumer see undocumented APIs. The `Microsoft.AspNetCore.OpenApi` XML +support relies on runtime/`AdditionalFiles` plumbing per host and does not cover the code-first gRPC +side at all. + +## Design + +See `docs/mediator-framework/design.md` → *OpenAPI operation grouping, naming and documentation*. + +The **generators** are the mechanism: they already have the Roslyn `ISymbol` for the contract and +every property, and `ISymbol.GetDocumentationCommentXml()` returns the documentation comment +regardless of `GenerateDocumentationFile` and regardless of which assembly declares the type. No host +wiring, no runtime XML file loading, no reflection. + +Mapping: + +| XML source | OpenAPI | `.proto` | +| --- | --- | --- | +| contract type `` | operation `summary` | leading comment on the rpc method and on the request message | +| contract type `` | operation `description` | continuation of the same leading comment | +| property `` (route/query bound) | parameter `description` | — | +| property `` (body bound) | request schema property `description` | leading comment on the message field | +| response type/property `` | response schema `description` | leading comment on the response message/field | + +## Steps + +1. Add a shared helper in each generator project (or a linked source file) that extracts the + `` and `` inner text from `ISymbol.GetDocumentationCommentXml(cancellationToken)`, + normalizes whitespace, strips the `` wrapper, resolves `` to the referenced + name and returns `null` when empty. + Docs: [`ISymbol.GetDocumentationCommentXml`](https://learn.microsoft.com/dotnet/api/microsoft.codeanalysis.isymbol.getdocumentationcommentxml), + [XML documentation tags](https://learn.microsoft.com/dotnet/csharp/language-reference/xmldoc/recommended-tags). +2. `MinimalApiEndpointGenerator`: emit `.WithSummary("…").WithDescription("…")` for the operation, and + emit parameter descriptions via the metadata the OpenAPI layer can read (extend the existing + `ArkOpenApiEx` transformer infrastructure with an `ArkDocumentationMetadata` endpoint-metadata type + carrying `(parameterName, description)` pairs and the schema-property descriptions, consumed by a + new `AddArkXmlDocumentation()` operation/schema transformer). + Docs: [Minimal API OpenAPI metadata](https://learn.microsoft.com/aspnet/core/fundamentals/openapi/aspnetcore-openapi#openapi-operation-summary-and-description). +3. `AddArkXmlDocumentation()` must be additive: it never overwrites a description already set by a + host transformer. +4. `GrpcEndpointGenerator` / proto emission: write the documentation as `//` leading comments above the + generated `rpc`, `message` and field declarations, wrapped at 100 columns, with `//` escaping of any + embedded newline. Comments must not appear inside the message body in a position that breaks the + proto syntax (verify with `Grpc.Tools` compiling the exported protos in + `samples/Ark.MediatorFramework.Sample/test/Ark.MediatorFramework.Sample.GrpcClient`). +5. Wire `AddArkXmlDocumentation()` into the sample's `ConfigureOpenApi` and document the helper in + `design.md` → *HTTP hosting helpers*. +6. Document the contract-documentation convention (summary on the contract = endpoint docs, summary on + properties = parameter docs) in the user guide backlog item (DOC-01). + +## Test coverage (required) + +- Generator snapshot tests: a documented contract produces `.WithSummary`/`.WithDescription` and the + documentation metadata; an undocumented contract produces neither (no empty strings). +- Framework test asserting the XML extraction helper: multi-line summary, ``, ``, + empty/missing comment, and a comment on an **inherited** property. +- Sample test: `/openapi/v1.json` contains the contract summary as the operation summary, the property + summary as the parameter description and as the request-schema property description. +- Proto test: the exported `.proto` for a documented contract contains the summary as a leading + comment, and the `Ark.MediatorFramework.Sample.GrpcClient` project still compiles the exported + protos (build gate covers this). + +## Outcomes + +- XML documentation authored once on the contract reaches both the OpenAPI document and the exported + `.proto`, produced by the generators without any per-host configuration. + +## Acceptance + +- [ ] Documentation extraction helper implemented and unit-tested (including inherited members). +- [ ] Operation summary/description and parameter/schema descriptions present in the OpenAPI document + (sample test asserts on the JSON document, not on UI HTML). +- [ ] Exported `.proto` files carry leading comments for services, methods, messages and fields, and + still compile with `Grpc.Tools`. +- [ ] `AddArkXmlDocumentation()` is additive and documented in `design.md`. +- [ ] NET-01 updated to drop the superseded XML-doc step. +- [ ] `dotnet build Ark.Tools.slnx --configuration Debug` succeeds with zero warnings. +- [ ] `dotnet test Ark.Tools.slnx --no-build --configuration Debug --minimum-expected-tests 1` passes. diff --git a/docs/mediator-framework/progress/tasks/generator-dx/GEN-10-api-surface-snapshots.md b/docs/mediator-framework/progress/tasks/generator-dx/GEN-10-api-surface-snapshots.md new file mode 100644 index 000000000..21613b979 --- /dev/null +++ b/docs/mediator-framework/progress/tasks/generator-dx/GEN-10-api-surface-snapshots.md @@ -0,0 +1,136 @@ +# GEN-10 — API-surface snapshot gate (contracts, routes, gRPC methods) + +**Category**: generator-dx · **Priority**: **Release blocker** · **Scope**: FRAMEWORK + SAMPLE + +## Problem + +Nothing detects a breaking wire change during development. Renaming a contract property, changing a +route template, reordering `[ProtoMember]` numbers or retiring a version compiles cleanly and only +breaks consumers at runtime. Changes to **nested** message fields — e.g., renaming a field inside a +type that is embedded in a response — are equally invisible. + +## Design + +See `docs/mediator-framework/design.md` → *API surface snapshots*. + +The framework ships a generator + MSBuild target that maintains a **single committed snapshot file** +`ArkApiSurface.txt` in the project directory. Every build recomputes the current surface and fails +if it does not match the committed file. Acceptance is a copy-replace — no manual editing, no +shipped/unshipped split. + +**On every build:** + +1. A Roslyn `IIncrementalGenerator` traverses the compilation, computes the full sorted transport + surface (HTTP, gRPC including nested fields, Rebus, contract members), and emits it as a + well-known source hint file so the content is available to the build pipeline. +2. A `buildTransitive` MSBuild target (running after `CoreCompile`) extracts the surface content + from the generated output and writes `$(IntermediateOutputPath)/ArkApiSurface.current.txt`. +3. A comparison target diffs `ArkApiSurface.current.txt` against the committed `ArkApiSurface.txt` + in `$(MSBuildProjectDirectory)`: + - If `ArkApiSurface.txt` does **not exist** → `ARKAPI001` (error). + - If the files **differ** → `ARKAPI002` (error) with a message pointing to the generated file. + - If files match → no diagnostic; build proceeds. + +**Acceptance workflow:** + +```sh +# After any API-changing build failure: +cp obj//ArkApiSurface.current.txt ArkApiSurface.txt +git add ArkApiSurface.txt +``` + +The committed diff is the full, sorted, human-readable wire surface — reviewers see exactly what +changed without reading generator output. CI fails automatically on any uncommitted surface change, +at every stage (PR builds, release builds), with no separate flag needed. + +**Entry format** (ordinal-sorted, deterministic, one entry per line): + +``` +CONTRACT GreetingDto.Name : string? server-set=false +CONTRACT GreetingDto.Tags[].Value : string server-set=false +GRPC Greetings.V1/GetGreeting (GetGreetingQuery) returns (GreetingDto) unary +GRPC-FIELD GetGreetingQuery.Id = 1 : bytes +HTTP GET /api/v1/greetings/{id} -> GetGreetingQuery : GreetingDto [policy=RequireAuthenticatedUser] [op=GetGreetingQuery] [tag=Greetings] +HTTP-PARAM GetGreetingQuery.Id : System.Guid route required +REBUS RefreshGreetingCommand -> queue:greetings +``` + +**Nested field coverage.** `CONTRACT` entries and `GRPC-FIELD` entries recurse into embedded +message types; every leaf field in the transitive closure of a contract type produces its own +line. A rename anywhere in the graph produces a visible diff. + +**Diagnostics:** + +- `ARKAPI001` (error) — `ArkApiSurface.txt` does not exist; create it by copying from + `obj/.../ArkApiSurface.current.txt`. +- `ARKAPI002` (error) — `ArkApiSurface.txt` differs from the current surface; copy + `obj/.../ArkApiSurface.current.txt` over it to accept. + +## Steps + +1. New project `src/mediator-framework/Ark.Tools.MediatorFramework.ApiSurface` (`netstandard2.0`, + packed as an analyzer/generator asset of `Ark.Tools.MediatorFramework`), following the + existing generator projects' csproj conventions. + Docs: [IIncrementalGenerator](https://learn.microsoft.com/dotnet/csharp/roslyn-sdk/source-generators-overview), + [MSBuild inline tasks](https://learn.microsoft.com/visualstudio/msbuild/msbuild-inline-tasks), + [AdditionalFiles](https://learn.microsoft.com/dotnet/csharp/roslyn-sdk/tutorials/how-to-write-csharp-analyzer-code-fix#additional-files). +2. Reuse the generators' semantic analysis to produce surface entries — extract shared surface + computation into a source-linked helper so the snapshot content cannot drift from what the + generators actually emit. +3. Surface format rules: + - Ordinal sort (deterministic across OSes and compilations). + - Version-expanded: one set of entries per active `IntroducedIn`/`RetiredIn` version. + - Recursive nested type traversal: every leaf field produces its own line, with the full + dotted path (e.g. `CONTRACT Outer.Inner.Field : type`); collection members use `[]` + notation (e.g. `CONTRACT Outer.Items[].Field : type`). + - Lines are self-contained; no references between lines. +4. `buildTransitive` targets: the generator writes the current surface to a hint file during + `CoreCompile`; a post-compile target extracts it to + `$(IntermediateOutputPath)/ArkApiSurface.current.txt`; a comparison target emits + `ARKAPI001`/`ARKAPI002` and aborts the build on mismatch or missing committed file. +5. Opt-in: the comparison target is a no-op unless `ArkApiSurface.txt` is present **or** + `$(ArkApiSurfaceEnabled)` is `true`. A project starts tracking by running the build once + (which creates `ArkApiSurface.current.txt`), copying it to `ArkApiSurface.txt`, and + committing. Document the bootstrap procedure in `design.md` and in the user guide. +6. Enable it in the sample's Application assembly with a committed `ArkApiSurface.txt`, + proving the whole loop in-repo. +7. Document the full workflow in `design.md` and in the user guide (DOC-01): + change API → build fails (`ARKAPI002`) → copy generated file → commit diff. + +## Test coverage (required) + +- Roslyn generator unit tests (`tests/Ark.Tools.MediatorFramework.Tests`): + - Surface output is identical across repeated compilations of the same input (determinism). + - Versioned contract produces one entry per active version. + - `[ServerSet]`, policy, tag, operation name and proto field numbers appear in the surface. + - Nested/embedded message fields produce `CONTRACT` and `GRPC-FIELD` entries with full paths. + - Collection member fields use `[]` notation. +- MSBuild target tests (MSBuild `UsingTask` unit tests or end-to-end): + - Missing `ArkApiSurface.txt` → `ARKAPI001`. + - Committed file differs from current surface → `ARKAPI002`. + - Committed file matches current surface → no diagnostic, build green. + - Opting in via `ArkApiSurfaceEnabled=true` with no committed file → `ARKAPI001`. +- Repository-level proof: the sample's committed `ArkApiSurface.txt` matches the current + surface; any future task that changes routes/protos causes `ARKAPI002` until the snapshot + is updated. + +## Outcomes + +- Any change to routes, operation names, nested contract members, proto field numbers or Rebus + queues fails the build immediately, with a diff-ready committed snapshot as the acceptance + step. +- CI/CD gate is automatic at every stage: PR builds, release builds, pack jobs — no separate + flag or promotion workflow needed. + +## Acceptance + +- [ ] Generator + MSBuild targets implemented, packed as build assets, no-op when not opted in. +- [ ] `ARKAPI001`/`ARKAPI002` behave as specified and are unit-tested. +- [ ] Nested type fields produce recursive `CONTRACT`/`GRPC-FIELD` entries; renaming a nested + field causes `ARKAPI002`. +- [ ] Surface is deterministic across compilations (same input = same output, bit-for-bit). +- [ ] Sample Application assembly opted in with committed `ArkApiSurface.txt`; solution build + is green with it. +- [ ] Bootstrap workflow documented in `design.md` and referenced by the user guide task. +- [ ] `dotnet build Ark.Tools.slnx --configuration Debug` succeeds with zero warnings. +- [ ] `dotnet test Ark.Tools.slnx --no-build --configuration Debug --minimum-expected-tests 1` passes. diff --git a/docs/mediator-framework/tasks/sample-parity/SMP-01-fluentvalidation.md b/docs/mediator-framework/progress/tasks/sample-parity/SMP-01-fluentvalidation.md similarity index 100% rename from docs/mediator-framework/tasks/sample-parity/SMP-01-fluentvalidation.md rename to docs/mediator-framework/progress/tasks/sample-parity/SMP-01-fluentvalidation.md diff --git a/docs/mediator-framework/tasks/sample-parity/SMP-02-sql-dapper-outbox.md b/docs/mediator-framework/progress/tasks/sample-parity/SMP-02-sql-dapper-outbox.md similarity index 100% rename from docs/mediator-framework/tasks/sample-parity/SMP-02-sql-dapper-outbox.md rename to docs/mediator-framework/progress/tasks/sample-parity/SMP-02-sql-dapper-outbox.md diff --git a/docs/mediator-framework/tasks/sample-parity/SMP-03-persisted-auditing.md b/docs/mediator-framework/progress/tasks/sample-parity/SMP-03-persisted-auditing.md similarity index 100% rename from docs/mediator-framework/tasks/sample-parity/SMP-03-persisted-auditing.md rename to docs/mediator-framework/progress/tasks/sample-parity/SMP-03-persisted-auditing.md diff --git a/docs/mediator-framework/tasks/sample-parity/SMP-04-optimistic-concurrency.md b/docs/mediator-framework/progress/tasks/sample-parity/SMP-04-optimistic-concurrency.md similarity index 100% rename from docs/mediator-framework/tasks/sample-parity/SMP-04-optimistic-concurrency.md rename to docs/mediator-framework/progress/tasks/sample-parity/SMP-04-optimistic-concurrency.md diff --git a/docs/mediator-framework/tasks/sample-parity/SMP-05-paging.md b/docs/mediator-framework/progress/tasks/sample-parity/SMP-05-paging.md similarity index 100% rename from docs/mediator-framework/tasks/sample-parity/SMP-05-paging.md rename to docs/mediator-framework/progress/tasks/sample-parity/SMP-05-paging.md diff --git a/docs/mediator-framework/tasks/sample-parity/SMP-06-misc-parity.md b/docs/mediator-framework/progress/tasks/sample-parity/SMP-06-misc-parity.md similarity index 100% rename from docs/mediator-framework/tasks/sample-parity/SMP-06-misc-parity.md rename to docs/mediator-framework/progress/tasks/sample-parity/SMP-06-misc-parity.md diff --git a/docs/mediator-framework/tasks/security/SEC-01-secure-by-default-endpoints.md b/docs/mediator-framework/progress/tasks/security/SEC-01-secure-by-default-endpoints.md similarity index 100% rename from docs/mediator-framework/tasks/security/SEC-01-secure-by-default-endpoints.md rename to docs/mediator-framework/progress/tasks/security/SEC-01-secure-by-default-endpoints.md diff --git a/docs/mediator-framework/tasks/security/SEC-02-unconditional-authorization-middleware.md b/docs/mediator-framework/progress/tasks/security/SEC-02-unconditional-authorization-middleware.md similarity index 100% rename from docs/mediator-framework/tasks/security/SEC-02-unconditional-authorization-middleware.md rename to docs/mediator-framework/progress/tasks/security/SEC-02-unconditional-authorization-middleware.md diff --git a/docs/mediator-framework/tasks/security/SEC-03-messagepack-untrusted-data.md b/docs/mediator-framework/progress/tasks/security/SEC-03-messagepack-untrusted-data.md similarity index 100% rename from docs/mediator-framework/tasks/security/SEC-03-messagepack-untrusted-data.md rename to docs/mediator-framework/progress/tasks/security/SEC-03-messagepack-untrusted-data.md diff --git a/docs/mediator-framework/tasks/security/SEC-04-server-set-binding-protection.md b/docs/mediator-framework/progress/tasks/security/SEC-04-server-set-binding-protection.md similarity index 100% rename from docs/mediator-framework/tasks/security/SEC-04-server-set-binding-protection.md rename to docs/mediator-framework/progress/tasks/security/SEC-04-server-set-binding-protection.md diff --git a/docs/mediator-framework/tasks/security/SEC-05-transport-agnostic-authorization-decorator.md b/docs/mediator-framework/progress/tasks/security/SEC-05-transport-agnostic-authorization-decorator.md similarity index 100% rename from docs/mediator-framework/tasks/security/SEC-05-transport-agnostic-authorization-decorator.md rename to docs/mediator-framework/progress/tasks/security/SEC-05-transport-agnostic-authorization-decorator.md diff --git a/docs/mediator-framework/tasks/security/SEC-06-multipart-hardening.md b/docs/mediator-framework/progress/tasks/security/SEC-06-multipart-hardening.md similarity index 100% rename from docs/mediator-framework/tasks/security/SEC-06-multipart-hardening.md rename to docs/mediator-framework/progress/tasks/security/SEC-06-multipart-hardening.md diff --git a/docs/mediator-framework/tasks/security/SEC-07-error-serialization-hardening.md b/docs/mediator-framework/progress/tasks/security/SEC-07-error-serialization-hardening.md similarity index 100% rename from docs/mediator-framework/tasks/security/SEC-07-error-serialization-hardening.md rename to docs/mediator-framework/progress/tasks/security/SEC-07-error-serialization-hardening.md diff --git a/docs/mediator-framework/tasks/security/SEC-08-test-auth-bearer-hardening.md b/docs/mediator-framework/progress/tasks/security/SEC-08-test-auth-bearer-hardening.md similarity index 100% rename from docs/mediator-framework/tasks/security/SEC-08-test-auth-bearer-hardening.md rename to docs/mediator-framework/progress/tasks/security/SEC-08-test-auth-bearer-hardening.md diff --git a/docs/mediator-framework/tasks/aspnetcore/NET-01-openapi-xml-docs.md b/docs/mediator-framework/tasks/aspnetcore/NET-01-openapi-xml-docs.md deleted file mode 100644 index ef77f19c7..000000000 --- a/docs/mediator-framework/tasks/aspnetcore/NET-01-openapi-xml-docs.md +++ /dev/null @@ -1,50 +0,0 @@ -# NET-01 — XML docs into OpenAPI + 3.1 verification (N3) - -**Category**: aspnetcore · **Priority**: **Release blocker** (decision D7) · **Scope**: FRAMEWORK + SAMPLE - -## Problem - -The mediator sample uses `Microsoft.AspNetCore.OpenApi` (`AddOpenApi`) but: -1. XML documentation comments on contracts/properties are **not** populated into the OpenAPI - document (summaries/descriptions missing) — .NET 10 supports XML-comment population for - `AddOpenApi` via the `Microsoft.AspNetCore.OpenApi` source generator (`` - + the OpenAPI XML comment support). -2. OpenAPI **3.1** schema output of generator-emitted endpoints is unverified (nullable handling, - `IntroducedIn`/`RetiredIn` per-version docs, polymorphic contracts). -3. Both Swashbuckle.SwaggerUI and Scalar are present — decide deliberately. - -Files: `samples/Ark.MediatorFramework.Sample/src/Ark.MediatorFramework.Sample.WebInterface/SampleStartup.cs` -(OpenAPI/Scalar/Swagger wiring), framework OpenAPI helpers in -`src/mediator-framework/Ark.Tools.MediatorFramework.MinimalApi/ArkOpenApiEx.cs` and `ArkOpenApiSecurityEx.cs`. - -## Steps - -1. Enable XML-doc population: ensure contract projects produce XML docs (`GenerateDocumentationFile` - is likely already on repo-wide — verify in `Directory.Build.props`) and enable the .NET 10 - `AddOpenApi` XML comment integration in the sample; verify `` from a contract type and its - properties appear as operation/schema descriptions. If XML comments must flow from a *referenced* - assembly (Application → WebInterface), verify the .NET 10 `AdditionalFiles`-based cross-assembly - support and wire it; if the framework generator must contribute operation summaries (e.g. from the - contract's XML docs), extend `ArkOpenApiEx` with a document/operation transformer that reads the - XML file — pick the mechanism that works, document it in `design.md`. -2. Verify 3.1 output: snapshot-test the generated document for one contract per feature: nullable - property, NodaTime type, polymorphic contract, versioned endpoint (`IntroducedIn`), multipart - endpoint. Fix framework transformers where the schema is wrong. -3. YAML endpoint: expose the document in YAML too if trivially supported by `MapOpenApi` (it is: - `/openapi/{documentName}.yaml`); document the route. -4. UI decision: drop Swashbuckle.SwaggerUI from the sample in favor of **Scalar only**, unless - `AddAuthorizationCodeFlow` (see `SampleStartup.cs`) depends on SwaggerUI — in that case port the - OAuth flow config to Scalar. Remove the unused package reference + lockfile entries. -5. Ensure descriptions also reach the exported protos where applicable (out of scope if non-trivial — note follow-up). - -## Outcomes - -- OpenAPI documents carry XML-doc summaries/descriptions for generated endpoints and schemas; 3.1 correctness is snapshot-verified; a single deliberate doc UI ships. - -## Acceptance - -- [ ] Contract `` visible in operation and schema descriptions (test inspects the document). -- [ ] 3.1 snapshot tests for nullable/NodaTime/polymorphic/versioned/multipart schemas pass. -- [ ] YAML document reachable. -- [ ] Single doc UI (Scalar) with working OAuth flow; Swashbuckle.SwaggerUI reference removed (or a recorded, deliberate decision to keep both). -- [ ] Lockfiles updated; full solution build + tests green. diff --git a/samples/Ark.MediatorFramework.Sample/README.md b/samples/Ark.MediatorFramework.Sample/README.md index 6af835fa1..d6579867b 100644 --- a/samples/Ark.MediatorFramework.Sample/README.md +++ b/samples/Ark.MediatorFramework.Sample/README.md @@ -92,11 +92,11 @@ hand-written proto files to copy alongside generated services. The emitted `.proto` now generates a dedicated client assembly used by the behavioral tests, and the gRPC rich-error interceptor is covered there. The 2026-07 review revisions — NodaTime via `NodaTime.Serialization.Protobuf`, -Hellang ProblemDetails with `BusinessRuleViolation` (HTTP and gRPC), gRPC +ProblemDetails with `BusinessRuleViolation` (HTTP and gRPC), gRPC client-streaming upload, version lifetime (`IntroducedIn`/`RetiredIn`) with the `/api/v{version}/…` placeholder, the per-transport package split and the framework test project under `tests/` — are specified with acceptance criteria -in [`docs/mediator-framework/tasks.md`](../../docs/mediator-framework/tasks.md) +in [`docs/mediator-framework/progress/tasks.md`](../../docs/mediator-framework/progress/tasks.md) (Epic 8) and step-by-step in -[`docs/mediator-framework/implementation-plan.md`](../../docs/mediator-framework/implementation-plan.md) +[`docs/mediator-framework/progress/implementation-plan.md`](../../docs/mediator-framework/progress/implementation-plan.md) (Phase 6).