Files
baya-monorepo/.claude/skills/backend-feature/SKILL.md
T
2026-08-02 18:58:46 +03:30

9.5 KiB
Raw Blame History

name, description
name description
backend-feature 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
Projects, layers, the seam catalogue, startup wiring docs/rules/server/structure.md
EF Core, migrations, soft-delete, audit, config-as-rows, state machines, snapshots, uniqueness docs/rules/server/persistence.md
Anything on the money path — ledger, refunds, BNPL, payouts, invoices docs/rules/server/money.md
Auth, JWE, sessions, field encryption, tenancy docs/rules/server/identity.md
C# style, naming, async, testing docs/rules/server/conventions.md
The gate, and what "done" means 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 §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/<Area>/{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 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 §57 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:<rail>:Provider config key that falls closed to the mock. See structure.md §3.

2. The Application slice

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


3. Persistence — only if you added or changed a table

  1. One IEntityTypeConfiguration<T> in Persistence/Configuration/<Area>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<T>never hardcoded, and never re-read for an already-priced row.
dotnet ef migrations add <Name> --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.


4. The controller

[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<MyCommandResult>]
    public async Task<IActionResult> 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 §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<Program> 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 §8.


6. Docs this feature triggers — in the same change

  • docs/integration/domains/<domain>.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 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 — 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 §2.