9.7 KiB
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 §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 inCrossCutting/Seams/, real inCrossCutting/Seams/Real/, selected by aSeams:<rail>:Providerconfig 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
- Create the folder, one type per file, file name matching the type name.
- The request is a
record; the handler isinternal sealed; returnOperationResult<T>— never throw for an expected failure (SuccessResult/FailureResult/NotFoundResult/ConflictResultmap to 200/400/404/409). Let a genuinely unexpected exception propagate to the globalExceptionHandler. - 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.
- Query:
AsNoTracking()+.Select()straight to the DTO — never hydrate an entity graph to map it in memory. Command: useIncludeonly when you need navigation properties loaded to mutate the aggregate, access the DB throughIUnitOfWork, andCommitAsynconce at the end.
Full rules and the validator/OperationResult examples: cqrs.md §1–3.
3. Persistence — only if you added or changed a table
- One
IEntityTypeConfiguration<T>inPersistence/Configuration/<Area>Config/. - A soft-deletable entity must declare
HasQueryFilter(o => !o.IsDeleted)— without it, deleted rows leak into every query that doesn't explicitly exclude them. - A lifecycle
statuscolumn is a forward-only machine:const stringcodes, a private setter, cohesive transition methods, a static allowed-edges table. The handler pre-checks and returns a clean409— it never throws for "already moved." - 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.
- 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, injectISendervia the primary constructor, alwaysbase.OperationResult(result)— neverOk()/BadRequest()/NotFound()directly.- Never hardcode a route string. If the method name doesn't read cleanly as the URL segment
SnakeCaseParameterTransformerwill 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
- Handler unit test (xUnit + NSubstitute + FluentAssertions), Arrange-Act-Assert, named
{MethodUnderTest}_{Scenario}_{ExpectedOutcome}. Test the handler directly, not the controller. - At least one
WebApplicationFactory<Program>integration test inBaya.Test.Apifor the area, covering: happy path → 200, unauthenticated → 401, validation failure → 400 with field detail. - 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 clientservices/domain it belongs to.- The OpenAPI snapshot — regenerate
docs/integration/openapi/swagger.v1.jsonper 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.