Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .github/workflows/build-and-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
branches:
- master
env:
VERSION: 10.0.8
VERSION: 10.0.9
jobs:
build-and-deploy:
runs-on: ubuntu-latest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ contract another application will call, so scaffolding it also means scaffolding
half of that same contract — one coherent task, not two skills chained together. **Step 2** below
determines which applies; read only the matching path once it's decided.

⚠ **Terminology**: this skill calls the customer/end-user-facing role "**Public API**" (e.g.
`Api.Platform`/`Api.Admin` in this solution — this solution's own project template for one is
⚠ **Terminology**: this skill calls the customer/end-user-facing role "**Public API**" (this solution's own project template for one is
`nanocore-api-public`), never "gateway." "Gateway" in this codebase means the Kubernetes Gateway
API resource (`nano-add-public-exposure`'s `HTTPRoute`/`Gateway`) or the network-edge/cert-manager
TLS layer in front of a cluster — an unrelated, infrastructure-level concept. Don't reuse that word
Expand Down Expand Up @@ -60,14 +59,12 @@ insufficient — not just "less convenient." Walk through this before scaffoldin
calls from the caller would let the write happen without the validation ever running. This is
the right call for a custom endpoint, not a sign to keep looking for a generic-composition way
around it.
- **The same generic-composition workaround needed at 2+ call sites is itself a signal to stop
composing and build the real custom endpoint.** A union across two entity types (e.g. "roles
owned by this tenant, plus roles reachable via its subscription plan") done as two generic calls
glued together in one Public API action is fine the first time; the same two calls duplicated
again in a second and third action is a sign the composition belongs on the *target* service as
a real custom endpoint instead — one round trip, one place the logic lives, instead of the same
non-trivial join reimplemented at every caller. Don't wait for a fourth duplicate before
promoting it.
- **A combined result built from several generic calls belongs on the owning service.** A union across
two entity types (e.g. "roles owned by this tenant, plus roles reachable via its subscription plan")
is not a fetch followed by an action, it is logic, so build it as a real custom endpoint on the
*target* service from the start — one round trip, one place the logic lives, instead of the same
non-trivial join reimplemented at every caller. The same two calls showing up in a second action is
a clear sign it was missed.
- **Before scaffolding a single-item lookup, check whether a sibling list/aggregate endpoint
already makes it redundant.** If a "get all roles for this tenant" endpoint already returns every
role with `RolePermissions` (or whatever the caller needs) populated, a separate "get one role"
Expand Down Expand Up @@ -109,15 +106,26 @@ surface, not a free side-effect — say so rather than tagging it silently.
Not always obvious from the request alone — ask if unclear, don't default to one. Getting this
wrong means the wrong constructor, the wrong dependency, and a DTO shape aimed at the wrong layer:

- **Public API controller** (e.g. `Api.Platform`/`Api.Admin` in this solution) — the action
composes one or more injected Api Clients (`.Entity`/`.Auth`/`.Audit`/`.Identity` calls, or an
existing custom client method) into one response; it has no `IRepository` of its own. Go to
**Public API path** below.
- **Public API controller** — the action composes one or more injected Api Clients
(`.Entity`/`.Auth`/`.Audit`/`.Identity` calls, or an existing custom client method) into one
response; it has no `IRepository` of its own. Go to **Public API path** below.
- **Internal service controller** — the action implements logic directly against this app's own
`IRepository`/`IEventing`, either as a custom method on an existing entity controller
(`BaseEntityController<...>` subclass) or a bare `BaseController` action with no entity backing
it at all. Go to **Internal service path** below.

**Where the logic goes.** A Public API action stays simple: fetch then act, or check then act (for
example a lookup or an ownership check, then a create, edit or delete). A check on the request, or on
what the action just fetched, that rejects with a `BadRequestException` before acting is part of that and
stays in the Public API. Complicated logic always
belongs in the internal service, as one custom action behind its Api Client: complicated rules,
deciding something from which other rows exist, or building a combined result or flags from several
calls. If a Public API action would need more than fetch-then-act or check-then-act, scaffold the
internal service endpoint instead and have the Public API call just that. Combining calls to
different services is the Public API's job. Controllers never share logic with each other — a
service puts shared logic in a repository extension method (an `IRepository` extension in the
app's `Extensions/` folder).

## Step 3 — Pin down shape and conventions

- **Endpoint shape.** HTTP verb, route segment, input shape (parameters), and response shape. Ask
Expand Down Expand Up @@ -479,6 +487,9 @@ public virtual async Task<IActionResult> MyActionAsync([FromBody][Required] MyAc
(correct base tier, naming, eventing-parameter handling). This includes the retrofit case: an
entity that already exists but has no generic controller yet still gets its full generic
controller as part of creating it here — this action doesn't replace or narrow that entitlement.
- **Return what the caller needs.** An action that changes something returns the data the caller will
show or use next (for example the finished overview), so the Public API never makes a second call
just to fetch it after the action. This is best practice, not an absolute rule.
- **Use `this.Repository`/`this.Eventing`, not the raw primary-constructor parameter, inside the
action body.** On an entity controller (`BaseEntityController<...>` and friends), the primary
constructor's `repository`/`eventing` parameters are already passed to the base constructor:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ without breaking what's already there.
generic arguments must agree with it.
6. **Database name.** Derive it from the application's own name, not the solution/repo name and
not a copy-pasted value from another app: strip any `Svc.`/`Api.`/`Web.` prefix, lowercase the
first letter of what's left, and append `Db` — e.g. `Svc.Accounts` → `accountsDb`, `Api.Admin`
→ `adminDb`. Use this exact value everywhere a database name appears for this app (local
first letter of what's left, and append `Db` — e.g. `Svc.Orders` → `ordersDb`, `Api.Backoffice`
→ `backofficeDb`. Use this exact value everywhere a database name appears for this app (local
`ConnectionString`, `POSTGRES_DB`, the SqLite file name, and the Staging/Production `SQL_NAME`
below) — one derived name, reused consistently, not decided separately per environment. Don't
reuse another app's prefix/pattern by copying its CI workflow or `appsettings.Development.json`
Expand Down
28 changes: 20 additions & 8 deletions Api.ApiClients.Audit/.claude/skills/nano-add-entity/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,21 +161,20 @@ the split layout — mappings are EF Core-only, never part of the shared API cli
```csharp
public class <Entity>Mapping : BaseEntityMapping<<Entity>>
{
public override void Configure(EntityTypeBuilder<<Entity>> builder)
protected override void ConfigureEntity(EntityTypeBuilder<<Entity>> builder)
{
ArgumentNullException.ThrowIfNull(builder);

base.Configure(builder);

builder
.Property(x => x.Name);
}
}
```

- **Always call `base.Configure(builder)`** before your own configuration — omitting it
silently breaks inherited Nano behavior (soft delete, audit, etc.). This is the single
most common mistake when writing a mapping by hand.
- **Override `ConfigureEntity`, never `Configure`.** `Configure` is not `virtual` (overriding it is
compile error CS0506); Nano applies its inherited behavior (soft delete, audit, etc.) itself
before calling `ConfigureEntity`, so there is no `base` call to make. Everything the mapping
configures goes in `ConfigureEntity`.
- **Configure every one of the entity's own properties explicitly — including navigations and
collections — never rely on EF's conventions to fill in what isn't written down.** An implicit,
convention-inferred relationship is exactly the kind of mistake that's invisible until it's a
Expand Down Expand Up @@ -205,6 +204,16 @@ public class <Entity>Mapping : BaseEntityMapping<<Entity>>
parent while dependents still exist should be a hard error instead of silently taking them
with it, `SetNull` only for a genuinely optional reference. Don't default to `Cascade`
everywhere just because it's often EF's own inferred behavior for required FKs.
- **Never give the FK scalar its own `.Property(...)` block.** `.HasForeignKey(...)` together with
`.IsRequired()` on the relationship already configures it (its nullability follows the property's
type), so a separate `.Property(x => x.ParentId).IsRequired();` (or a bare `.Property(x => x.ParentId);`)
is noise. The FK property is still declared on the entity in File 1.
- **Never leave `.WithMany()`/`.WithOne()` empty.** Every relationship has a navigation on both ends: if
the principal entity has no inverse collection/reference yet, add it to the principal's File 1 (for
example `Registrations` on the `Organization` that a `Registration` points at, or `SubmittedProducts`
when the reference is named `SubmittedByOrganization`), name the relationship in both mappings, and add
the principal-side `.HasMany(...).WithOne(...)` block described below. Don't skip the inverse because it
"isn't used" - an unnamed end is exactly what hides a missing or accidental relationship.
- **Inverse collection/reference navigation with no FK of its own** (the principal side of a
relationship whose FK is declared in the *dependent* entity's own mapping): configure it
explicitly here too, not just with a comment — `.HasMany(x => x.Children).WithOne(x => x.Parent)`
Expand Down Expand Up @@ -245,8 +254,11 @@ public class <Entity>Mapping : BaseEntityMapping<<Entity>>
`BaseEntityCreatableAndDeletableController` in File 4.
- **Index every property the entity is queried or sorted by** — one `HasIndex(...)` in the mapping for each
property File 3's query criteria filters on, and each property an ordering (`Order.By`) or a keyword
search reaches, declared at the end of `Configure`, after the properties and relationships. Foreign keys
already get an index from EF's conventions, so don't repeat those unless a composite covers them better;
search reaches, declared directly beneath the mapping of the property it indexes, not collected at the end
of `ConfigureEntity` (a composite index goes beneath the last of its properties to be mapped, after that
property's relationship block if it is a foreign key). Foreign keys already get an index from EF's
conventions, so never write a single-column `HasIndex` on a foreign key property, and none at all when a
composite index or unique index already starts with it;
don't index a low-selectivity flag (a plain `bool` or status on its own). When a list is always filtered
by one property and sorted by another (a foreign key, newest first), use one composite index in that
order (`HasIndex(x => new { x.ParentId, x.CreatedAt })`), which replaces a plain single-column index on
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,8 @@ convert, in which case its existing properties carry over as-is; don't ask for t
configures the required 1:1 relationship to the underlying `IdentityUser` row and an
`IsActive` query filter, per AGENTS.md's Data Mappings table. **If converting an existing
entity** (step 3), convert its existing mapping file the same way — just the base class;
keep every custom `Configure(...)` statement already in it, still calling
`base.Configure(builder)` first. **If creating fresh**, same `base.Configure(builder)`-first
rule as a normal mapping.
keep every custom statement already in its `ConfigureEntity(...)` as it is. **If creating
fresh**, same shape as a normal mapping (a `ConfigureEntity` override, no `base` call).
- **Query criteria** (API/Web only): exactly as the entity-scaffold skill's File 3 — nothing
identity-specific here.
- **Controller** (API/Web only, `Controllers/<Entity>sController.cs`): derive from
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,17 +232,17 @@ so a tester doesn't have to go hunting for it elsewhere:
## .Prerequisites folder

Sometimes this app's own collection needs test data that only a *different* application can
create — Api.Platform's Postman collection can't test anything before a real, finalized User
exists, but Api.Platform itself has no signup of its own; a Public API's read-only reference data
create — a Public API's Postman collection can't test anything before a real, finalized User
exists, but that Public API itself has no signup of its own; a Public API's read-only reference data
(Currencies, Product Categories) may have no Create endpoint in that app at all. When this
happens, don't just note it in a description and leave the tester to go run a different
collection by hand first — add a `.Prerequisites` folder (the leading dot sorts it first in
Postman's sidebar, ahead of Auth) with the minimum requests needed to unblock this collection's
own flow.

- **Target the actual owning application directly, never a composing Public API.** A Public API
(Api.Admin, Api.Platform) has no data of its own — it's always some internal service
(`Svc.Accounts`, `Svc.Assets`, etc.) that actually owns the entity, per AGENTS.md's Public API vs
has no data of its own — it's always some internal service
that actually owns the entity, per AGENTS.md's Public API vs
internal service section. Go straight there, not through another Public API's composed endpoint.
Use `http://{{host}}:<port>` with that service's literal host port, read from the nested service
block in this app's own `.docker/docker-compose.yml` (each nested service is mapped to a unique
Expand Down
26 changes: 13 additions & 13 deletions Api.ApiClients.Audit/.github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -1,39 +1,39 @@
# Copilot instructions — Nano Framework
# Copilot instructions - Nano Framework

This repository (and any application built on the `NanoCore`/`Nano.*` NuGet packages) uses the
**Nano framework** for API/Web/Console applications. Before making changes, check the repo root
for an `AGENTS.md` — it is the authoritative implementation reference for Nano and documents the
for an `AGENTS.md`: it is the authoritative implementation reference for Nano and documents the
exact base classes, configuration, and gotchas for that specific solution. These instructions are
a short always-on summary; `AGENTS.md` takes precedence on any conflict.

## Solution shape

A Nano app named `{name}` conventionally looks like:

- `{name}/` — the app project (`Program.cs`, `Controllers/`, `Data/` for `DbContext`+`Mappings/`,
- `{name}/`: the app project (`Program.cs`, `Controllers/`, `Data/` for `DbContext`+`Mappings/`,
`appsettings*.json`).
- `{name}.Models/` — a **separate sibling project** (not nested) holding entity models,
`Criterias/` (query criteria), and `Api/` (typed API client) — only present in a "split layout";
- `{name}.Models/`: a **separate sibling project** (not nested) holding entity models,
`Criterias/` (query criteria), and `Api/` (typed API client), only present in a "split layout";
many smaller apps use a single-project layout with everything in `{name}/` instead.
- `.tests/Tests.{name}/` — test project.
- `.docker/`, `.kubernetes/`, `.github/workflows/build-and-deploy.yml`, root `Dockerfile` — local
- `.tests/Tests.{name}/`: test project.
- `.docker/`, `.kubernetes/`, `.github/workflows/build-and-deploy.yml`, root `Dockerfile`: local
orchestration, deployment, and CI/CD.

Folder names (`Controllers/`, `Data/`, `Criterias/`, `Api/`) are convention, not a framework
requirement — Nano discovers controllers, mappings, and data providers by type via reflection, not
requirement: Nano discovers controllers, mappings, and data providers by type via reflection, not
by location.

## Conventions to follow

- **Identity type**: default to `Guid` (`BaseEntity` = `BaseEntity<Guid>`) unless the project
already consistently uses another `TIdentity` everywhere (entities, mappings, repository,
controllers, API client) — it is a cross-cutting choice, never a per-entity one.
- **Entity mappings** must call `base.Configure(builder)` before any custom configuration —
omitting it silently breaks inherited behavior (soft delete, audit, etc.).
controllers, API client): it is a cross-cutting choice, never a per-entity one.
- **Entity mappings** override `ConfigureEntity`, never `Configure` (it is not virtual). Nano
applies its inherited behavior (soft delete, audit, etc.) first, so there is no `base` call to make.
- **Controller naming is load-bearing**: `<Entity>` + literal `s` + `Controller` (naive
pluralization, e.g. `Country` → `CountrysController`, not "correct" English plurals) — Nano
pluralization, e.g. `Country` → `CountrysController`, not "correct" English plurals): Nano
derives the route from the class name.
- No manual registration is needed for mappings, controllers, startup tasks, or API clients —
- No manual registration is needed for mappings, controllers, startup tasks, or API clients:
Nano discovers them by type/assembly scanning. Don't add DI registration calls for these unless
a project's existing code clearly does otherwise.
- Match existing conventions in the project (nullable-reference style, split vs single-project
Expand Down
Loading
Loading