--- name: backend-feature description: >- Add a feature to the Balinyaar .NET server — a command, a query, or both — end to end: the Application slice, the controller, EF configuration/migration if it touches a table, tests, and the doc updates it triggers. Use when implementing a new endpoint or extending an existing one anywhere under server/. --- # Balinyaar Backend Feature The sequence for shipping one CQRS slice, from the Application layer to a green gate. **Precedence.** This skill is the **procedure** — what order to do things in. The **rules** within each step live in `docs/rules/server/` and this skill defers to them; it doesn't restate them. | For | Read | |-----|------| | The dispatcher, folder shape, `OperationResult`, the controller skeleton, authorization | [docs/rules/server/cqrs.md](../../../archive/docs/rules/server/cqrs.md) | | Projects, layers, the seam catalogue, startup wiring | [docs/rules/server/structure.md](../../../archive/docs/rules/server/structure.md) | | EF Core, migrations, soft-delete, audit, config-as-rows, state machines, snapshots, uniqueness | [docs/rules/server/persistence.md](../../../archive/docs/rules/server/persistence.md) | | Anything on the money path — ledger, refunds, BNPL, payouts, invoices | [docs/rules/server/money.md](../../../archive/docs/rules/server/money.md) | | Auth, JWE, sessions, field encryption, tenancy | [docs/rules/server/identity.md](../../../archive/docs/rules/server/identity.md) | | C# style, naming, async, testing | [docs/rules/server/conventions.md](../../../archive/docs/rules/server/conventions.md) | | The gate, and what "done" means | [docs/rules/shared/git-and-gates.md](../../../archive/docs/rules/shared/git-and-gates.md) | **Stack:** ASP.NET Core (.NET 10), Clean Architecture, CQRS on `martinothamar/Mediator` (a source generator — **not MediatR**; there is no `IMediator` anywhere in this codebase), EF Core, FluentValidation, Mapster. --- ## 1. Scope it before writing anything - **Which area?** The Application feature areas mirror the Domain entity folders — `Identity`, `Geography`, `Catalog`, `Verification`, `Search`, `Booking` (singular, pre-payment) / `Bookings` (plural, post-payment), `Payments`, `Refunds`, `Invoices`, `Bnpl`, `Payouts`, `Reviews`, `PatientCareRecords`, `Messaging`, `PartnerCenters`, `Configuration`, `Audit`, `Analytics`, `Holidays`, `Notifications`, `SupportAlerts`. Full list and the schema-per-area mapping: [structure.md](../../../archive/docs/rules/server/structure.md) §2. - **Command or query, or both?** A command mutates; a query reads. Most features are a matched pair (create + get, or update + list). - **Find a sibling to mirror.** Grep the area's existing folder — `Features//{Commands,Queries}/` — for a feature shaped like the one you're adding. Copying a live pattern beats inventing a new one. - **Does it touch money?** (ledger, refunds, invoices, BNPL, payouts) → read [money.md](../../../archive/docs/rules/server/money.md) **first**. The invariants there (integer IRR, balanced ledger postings, webhook idempotency, snapshot-at-compute-time) are not suggestions. - **Does it add or change a table?** → read [persistence.md](../../../archive/docs/rules/server/persistence.md) §5–7 before modeling it (soft-delete filters, forward-only status machines, snapshot fields, uniqueness patterns all have a house pattern — don't reinvent one). - **Does it need a new external dependency** (a vendor, a rail)? It becomes an interface in `Application/Contracts/`, mock in `CrossCutting/Seams/`, real in `CrossCutting/Seams/Real/`, selected by a `Seams::Provider` config key that **falls closed to the mock**. See [structure.md](../../../archive/docs/rules/server/structure.md) §3. --- ## 2. The Application slice ``` Baya.Application/Features// ├── Commands/Command/ │ ├── Command.cs record : IRequest> │ ├── Command.Handler.cs internal sealed class : IRequestHandler<…> │ └── Command.Validator.cs AbstractValidator (omit when there is nothing to validate) └── Queries/Query/ ├── Query.cs ├── Query.Handler.cs └── Query.Result.cs record Result(…) ← the DTO returned ``` 1. Create the folder, one type per file, file name matching the type name. 2. The request is a `record`; the handler is `internal sealed`; return `OperationResult` — never throw for an expected failure (`SuccessResult`/`FailureResult`/`NotFoundResult`/`ConflictResult` map to 200/400/404/409). Let a genuinely unexpected exception propagate to the global `ExceptionHandler`. 3. Add a FluentValidation validator if the request takes input. **Never validate a route-supplied id in the body command** — route values aren't bound into it. 4. Query: `AsNoTracking()` + `.Select()` straight to the DTO — never hydrate an entity graph to map it in memory. Command: use `Include` only when you need navigation properties loaded to mutate the aggregate, access the DB through `IUnitOfWork`, and `CommitAsync` once at the end. Full rules and the validator/OperationResult examples: [cqrs.md](../../../archive/docs/rules/server/cqrs.md) §1–3. --- ## 3. Persistence — only if you added or changed a table 1. One `IEntityTypeConfiguration` in `Persistence/Configuration/Config/`. 2. A soft-deletable entity **must** declare `HasQueryFilter(o => !o.IsDeleted)` — without it, deleted rows leak into every query that doesn't explicitly exclude them. 3. A lifecycle `status` column is a forward-only machine: `const string` codes, a private setter, cohesive transition methods, a static allowed-edges table. The handler pre-checks and returns a clean `409` — it never throws for "already moved." 4. A row that represents a past agreement (a price, an address, a policy, a deadline) is a **snapshot** — frozen at compute time, never re-derived from a later edit to its source. 5. Money-critical constants (rates, deadlines, tolerances) are read via `IPlatformConfig.GetConfig` — **never hardcoded**, and never re-read for an already-priced row. ```bash dotnet ef migrations add --project src/Infrastructure/Baya.Infrastructure.Persistence --startup-project src/API/Baya.Web.Api dotnet ef database update --project src/Infrastructure/Baya.Infrastructure.Persistence --startup-project src/API/Baya.Web.Api ``` Full patterns, with the exact uniqueness/snapshot/state-machine tables: [persistence.md](../../../archive/docs/rules/server/persistence.md). --- ## 4. The controller ```csharp [ApiVersion("1")] [ApiController] [Route("api/v{version:apiVersion}/[controller]")] [Display(Description = "One-line description shown in Swagger")] [Authorize(ConstantPolicies.DynamicPermission)] // or [Authorize], or omit for public public sealed class MyFeatureController(ISender sender) : BaseController { [HttpPost("[action]")] [ProducesOkApiResponseType] public async Task CreateSomething(MyCommand command, CancellationToken ct) => OperationResult(await sender.Send(command, ct)); } ``` - `sealed`, inject `ISender` via the primary constructor, always `base.OperationResult(result)` — never `Ok()`/`BadRequest()`/`NotFound()` directly. - Never hardcode a route string. If the method name doesn't read cleanly as the URL segment `SnakeCaseParameterTransformer` will produce, rename the method instead. - Pick the narrowest authorization that fits: none (truly public) → `[Authorize]` (any authenticated user) → `[Authorize(ConstantPolicies.DynamicPermission)]` (role/claim-gated admin action). Table and rate-limiting notes: [cqrs.md](../../../archive/docs/rules/server/cqrs.md) §4. --- ## 5. Tests 1. **Handler unit test** (xUnit + NSubstitute + FluentAssertions), Arrange-Act-Assert, named `{MethodUnderTest}_{Scenario}_{ExpectedOutcome}`. Test the handler directly, not the controller. 2. **At least one `WebApplicationFactory` integration test** in `Baya.Test.Api` for the area, covering: happy path → 200, unauthenticated → 401, validation failure → 400 with field detail. 3. The recurring-job scheduler is dormant under `Testing`, so a background tick can't make an integration test flaky — you don't need to account for it. Examples and the full testing convention: [conventions.md](../../../archive/docs/rules/server/conventions.md) §8. --- ## 6. Docs this feature triggers — in the same change - **[`docs/integration/domains/.md`](../../../archive/docs/integration/domains/index.md)** — add the new endpoint with its verdict (`wired`/`unwired`/`phantom`), matching the client `services/` domain it belongs to. - **The OpenAPI snapshot** — regenerate `docs/integration/openapi/swagger.v1.json` per [openapi/README.md](../../../archive/docs/integration/openapi/README.md) and update its provenance table (date, commit, path/operation counts) in the same change. A snapshot with stale provenance is what that convention exists to prevent. - **[`docs/status/backlog.md`](../../../archive/docs/status/backlog.md)** — tick the row if this closes a filed item. Never delete a row; a ticked row is the record that it shipped. - **A reference file in `docs/rules/server/`** — only if the feature introduces a genuinely new reusable pattern, seam, or base class. Don't add prose for a feature that just follows the existing pattern. --- ## 7. Before you call it done Run the server gate — `dotnet build Baya.sln` (**zero new warnings**) and `dotnet test Baya.sln` — and read your diff as if reviewing the PR. Full "what done means" checklist: [git-and-gates.md](../../../archive/docs/rules/shared/git-and-gates.md) §2.