diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index f1131ccc..88e7ecf4 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -7,7 +7,7 @@ on: branches: - master env: - VERSION: 10.0.4 + VERSION: 10.0.5 jobs: build-and-deploy: runs-on: ubuntu-latest diff --git a/Api.ApiClients.Audit/.claude/skills/nano-scaffold-entity/SKILL.md b/Api.ApiClients.Audit/.claude/skills/nano-scaffold-entity/SKILL.md new file mode 100644 index 00000000..321e1130 --- /dev/null +++ b/Api.ApiClients.Audit/.claude/skills/nano-scaffold-entity/SKILL.md @@ -0,0 +1,159 @@ +--- +name: nano-scaffold-entity +description: Scaffold a new Nano.Library entity end-to-end - data model, EF Core mapping, query criteria, and CRUD controller - following Nano framework conventions. Use when the user asks to add a new entity, resource, or CRUD endpoint to a Nano-based application (a project with an AGENTS.md describing Nano, or that references Nano.Library/NanoCore NuGet packages). +--- + +# Nano entity scaffold + +Generates the four files Nano needs for a new CRUD-capable entity: data model, EF Core +mapping, query criteria, and controller. Read `AGENTS.md` in the target repo root first if +present — it documents the exact base classes and gotchas for that specific solution; this +skill assumes the general Nano.Library conventions and defers to a project's own AGENTS.md +on any conflict. + +## Before generating anything, determine + +1. **Entity name and properties.** Ask the user if not already given in the request — need + at minimum the entity name (singular, PascalCase, e.g. `Product`) and its scalar + properties (name + type). Don't invent business fields that weren't asked for. +2. **Project layout.** Look for a `.Models` project alongside the main app + project (check the `.sln` or list sibling folders). + - **Split layout** (a `.Models` project exists, e.g. `Svc.Accounts.Models`): entity + model and query criteria go in the `.Models` project (they're part of the API client + contract other services consume); the mapping and controller go in the main app + project. + - **Single-project layout** (no `.Models` project, e.g. most Nano.Lessons): all four + files go in the one app project. +3. **Identity type.** Check the `DbContext`/`DbContextFactory` and other entities in the + project for a `TIdentity` type parameter (e.g. `BaseEntity`). If every existing + entity just uses plain `BaseEntity` (implicit `Guid`), match that. Never introduce a + non-`Guid` identity unless the project already uses one consistently — it's a + cross-cutting decision (affects the entity, mapping, controller, repository calls, + and API client), not something to add on a whim for one entity. +4. **Existing conventions.** Skim one existing entity/mapping/controller triplet in the + project (if any exist) for property style, nullable-reference usage, and namespace + layout, and match it. + +## File 1 — Data model + +Location: `Data/.cs` in the split layout's `.Models` project, or `Data/.cs` +in the single-project layout. + +```csharp +public class : BaseEntity +{ + public string Name { get; set; } = null!; + // ...other scalar properties as requested +} +``` + +- Derive from `BaseEntity` (Guid identity) or `BaseEntity` — match the project's + existing convention (see step 3 above). +- For restricted CRUD (e.g. read-only, or no delete), derive instead from + `BaseEntityReadOnly`, `BaseEntityCreatable`, `BaseEntityUpdatable`, + `BaseEntityCreatableAndUpdatable`, or `BaseEntityDeletable` — ask the user if the request + implies one of these rather than full CRUD. +- Use `required`/`= null!` per the project's existing nullable-reference style, not your own + default. + +## File 2 — Data mapping + +Location: `Data/Mappings/Mapping.cs` in the main app project (always here, even in +the split layout — mappings are EF Core-only, never part of the shared API client models). + +```csharp +public class Mapping : BaseEntityMapping<> +{ + public override void Configure(EntityTypeBuilder<> 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. +- No registration step needed — Nano auto-discovers mappings via + `ModelBuilderExtensions.MapEntities` at startup. +- Add a new EF Core migration after this file exists: `dotnet ef migrations add ` + from the app project directory (only do this if the user asked you to, or if the project's + existing workflow clearly expects a migration per entity — check `Migrations/` for + precedent first). + +## File 3 — Query criteria + +Location: `Criterias/QueryCriteria.cs` in the split layout's `.Models` project, or +`Criterias/QueryCriteria.cs` in the single-project layout. + +```csharp +public class QueryCriteria : BaseQueryCriteria +{ + public virtual string? Name { get; set; } + + public override IList GetExpressions() + { + var expressions = base.GetExpressions(); + + var expression = expressions.FirstOrDefault() ?? new CriteriaExpression(); + + if (!string.IsNullOrEmpty(this.Name)) + { + expression + .StartsWith("Name", this.Name); + } + + expressions + .Add(expression); + + return expressions; + } +} +``` + +- Only add filter properties for fields that make sense to search/filter by — don't + mechanically add one filter per scalar property on the entity. +- Every filter property must be `virtual` and nullable. +- Use the `CriteriaExpression` builder methods appropriate to each property's type + (`StartsWith`/`Contains` for strings, `EqualTo`/`GreaterThan`/etc. for numerics and dates) + — check the project's other query criteria classes for the operations actually available, + don't guess. + +## File 4 — Controller + +Location: `Controllers/sController.cs` in the main app project. + +```csharp +public class sController(ILogger<sController> logger, IRepository repository, IEventing? eventing) + : BaseEntityController<, QueryCriteria>(logger, repository, eventing) +{ + // Custom actions, if any +} +``` + +- **Naming is load-bearing, not cosmetic**: the class name must be the entity name with a + literal `s` appended, then `Controller` (e.g. `Product` → `ProductsController`, + `Country` → `CountrysController` — note this is naive `+s` pluralization, not proper + English plural rules; Nano derives the route segment from the class name). Do not + "correct" irregular plurals. +- Check whether the project actually registers an eventing provider (look for + `AddNanoEventing<...>()` in `Program.cs`). If it doesn't, drop the `IEventing? eventing` + parameter and the corresponding base-constructor argument — an unused optional eventing + dependency is harmless, but match what sibling controllers in the same project actually do. +- If the identity type isn't `Guid` (per step 3), the controller generic list needs the + identity type too: `BaseEntityController<, , QueryCriteria>`. +- No manual registration needed — Nano's MVC discovery picks up the controller + automatically from the assembly. + +## After generating + +- Show the user the four files and where they were placed; don't silently also modify + `Program.cs`, add NuGet packages, or run `dotnet ef migrations add` unless they ask — + scaffolding the entity is the task, not deciding the rest of the rollout for them. +- If the project has an existing entity with the same shape you can point to as a working + reference, mention it — it's the fastest way for the user to sanity-check the result. diff --git a/Api.ApiClients.Audit/AGENTS.md b/Api.ApiClients.Audit/AGENTS.md new file mode 100644 index 00000000..ccb36313 --- /dev/null +++ b/Api.ApiClients.Audit/AGENTS.md @@ -0,0 +1,3114 @@ +# AGENTS.md — Nano Framework + +Implementation reference for building applications with Nano. Structured to mirror the module READMEs in this +repository (`Nano.App`, `Nano.App.Api`, `Nano.App.Console`, `Nano.App.Web`, `Nano.Logging`, `Nano.Data`, +`Nano.Eventing`, `Nano.Storage`), so a section here maps 1:1 to a section there. + +--- + +## Solution Structure + +Every Nano application — Api, Web, or Console — follows the same predictable solution layout. `{name}` is the +application's own name (e.g. `Svc.Accounts`); `{name}.Models` is a **separate, sibling project**, not nested +inside `{name}/`. + +| Directory / File | API | WEB | CON | Description | +| -------------------------------------------------------- | --- | --- | --- | ------------------------------------------------------------------------------------------------------------------------- | +| `{name}.sln` | ✓ | ✓ | ✓ | The Visual Studio solution file, at the solution root. | +| `{name}/{name}.csproj` | ✓ | ✓ | ✓ | The application project file. | +| `{name}/Program.cs` | ✓ | ✓ | ✓ | Entry point — where the application is configured, built, and run. | +| `{name}/Properties/InternalsVisibleTo.cs` | ✓ | ✓ | ✓ | Exposes internal types to the test project. | +| `{name}/appsettings.json` | ✓ | ✓ | ✓ | Default application configuration. | +| `{name}/appsettings.{environment}.json` | ✓ | ✓ | ✓ | Overrides for `Development`, `Staging`, `Production`. | +| `{name}/Controllers/` | ✓ | ✓ | ✗ | Concrete controllers (conventional location, not a hard requirement). | +| `{name}/Data/` | ✓ | ✓ | ✓ | `DbContext`, `DbContextFactory`, and `Mappings/` (conventional location). | +| `{name}/Migrations/` | ✓ | ✓ | ✓ | EF Core migrations (conventional location, when a SQL data provider is used). | +| `{name}/wwwroot/` | ✓ | ✓ | ✗ | Static/dynamic web content root. | +| `{name}/Dockerfile.Local` | ✓ | ✓ | ✓ | Used by Docker Compose in `Development`; must stay in the application project folder. | +| `{name}.Models/{name}.Models.csproj` | ✓ | ✓ | ✗ | Sibling project holding entity models, query criteria, and API client (Requests/Api). Publishable as its own NuGet for sharing models + API client with consumers. Should reference at minimum `Nano.App`. | +| `{name}.Models/Data/` | ✓ | ✓ | ✗ | Entity models (conventional location). | +| `{name}.Models/Criterias/` | ✓ | ✓ | ✗ | Query criteria classes (conventional location). | +| `{name}.Models/Api/` | ✓ | ✓ | ✗ | API client + `Requests/` (conventional location, for apps exposing a typed client to consumers). | +| `.tests/Tests.{name}/Tests.{name}.csproj` | ✓ | ✓ | ✓ | Test project — empty by default, demonstrates where unit/integration tests belong. | +| `.tests/Tests.{name}/Properties/DoNotParallelize.cs` | ✓ | ✓ | ✓ | Ensures tests are not parallelized. | +| `.docker/docker-compose.dcproj` | ✓ | ✓ | ✓ | Docker Compose project used by Visual Studio for local orchestration. | +| `.docker/docker-compose.yml` | ✓ | ✓ | ✓ | Docker Compose spec for local (`Development`) orchestration. | +| `.kubernetes/configmap.yaml` | ✓ | ✓ | ✓ | Kubernetes ConfigMap. | +| `.kubernetes/autoscaler.yaml` | ✓ | ✓ | ✗ | Kubernetes Horizontal Pod Autoscaler. | +| `.kubernetes/deployment.yaml` | ✓ | ✓ | ✗ | Kubernetes Deployment. | +| `.kubernetes/service.yaml` | ✓ | ✓ | ✗ | Kubernetes Service. | +| `.kubernetes/httproute.yaml` | (✓) | (✓) | ✗ | Kubernetes HTTPRoute _(optional, public-facing apps only)_. | +| `.kubernetes/cronjob.yaml` | ✗ | ✗ | ✓ | Kubernetes CronJob (Console apps run as scheduled jobs, not long-running Deployments). | +| `.github/config/slack.yml` | ✓ | ✓ | ✓ | Build/deploy Slack notifications _(optional)_. | +| `.github/workflows/build-and-deploy.yml` | ✓ | ✓ | ✓ | CI/CD workflow — build, test, publish, deploy. | +| `Dockerfile` | ✓ | ✓ | ✓ | Container image build for `Staging`/`Production`, at the solution root. | +| `.dockerignore` / `.gitignore` | ✓ | ✓ | ✓ | Solution root. | +| `README.md` / `icon.png` / `LICENSE` | (✓) | (✓) | (✓) | Solution root, optional — used for the repo and any published NuGet packages. | + +Folder names like `Controllers/`, `Data/`, `Criterias/`, `Api/`, and `Migrations/` are convention, not a +framework requirement — Nano discovers controllers, mappings, and data providers by type, not by folder +location. As each feature section below is filled in, it will also note where new files of that kind +conventionally belong. + +**NuGet packages**: for a quick start, add `NanoCore` (all-inclusive) to `{name}.Models` only — since `{name}` +references `{name}.Models` via `ProjectReference`, every Nano package flows into the app project transitively, so +no Nano package reference is needed there directly. This is what Nano.Templates itself does. Once you know which +providers you're actually using, switch to referencing only the specific packages you need (e.g. +`Nano.Data.PostgreSQL` instead of the whole graph) — smaller dependency footprint, and it makes provider choices +explicit in the `.csproj` rather than implicit via a meta-package. + +**Non-`Guid` identity**: Nano defaults every generic surface to `Guid` via a non-generic shorthand +(`BaseEntity` = `BaseEntity`, `IRepository` = `Repository`, etc.). ⭐ It's highly +recommended to just use `Guid` throughout — it's the path every non-generic shorthand and every real example in +this doc is built around. Using a different identity type (`int`, `long`, `string`, or a custom +`IEquatable`) means threading the same `TIdentity` through **every** one of these consistently — there's no +single place that "sets" it once: + +- **Data**: `AddNanoData()`, `BaseDbContext`, every entity base class + (`BaseEntity`, etc.) and mapping base class. +- **Repository**: the concrete `Repository` registered behind `IRepository`. +- **Controllers**: `BaseEntityController` and siblings, `BaseAuthController`, + `BaseAuditController`. +- **Authentication**: `IAuthRepository`, `IAuthIdentityRepository`, `IIdentityRepository`. +- **Api Client**: `BaseApiClient`, `BaseIdentityApiClient`, and generic requests + (`DetailsRequest`, `DeleteRequest`, etc.). +- **Audit**: `AuditEntry`, `AuditEntryProperty`. +- **Identity entity models**: `IdentityUserEx`, `IdentityRole`, etc. + +Mixing identity types across these — e.g. an `int`-keyed entity registered against a `Guid`-typed repository — +doesn't compile or bind correctly. If everything stays `Guid`, none of this matters; it's only relevant the +moment one non-default identity type is chosen anywhere in the app. + +--- + +## Nano.App + +Common services shared by every Nano application type (Api, Console, Web). Transitive — never referenced +directly by an app project. + +### Environment + +Nano is environment-neutral: behavior differs only through `appsettings.{environment}.json`, never through +environment-specific code. The environment is read from `DOTNET_ENVIRONMENT` or `ASPNETCORE_ENVIRONMENT`, +defaulting to `Development`. + +| Environment | Type | Description | +| ------------- | ------ | ----------------------------- | +| `Development` | Local | Local development machine. | +| `Staging` | Cloud | Cloud Kubernetes deployment. | +| `Production` | Cloud | Cloud Kubernetes deployment. | + +### Configuration + +Standard .NET configuration providers, with precedence (later overrides earlier): + +1. `appsettings.json` +2. `appsettings.{environment}.json` +3. Command-line arguments +4. Environment variables +5. User secrets (`Development` only) + +Two deviations from stock .NET behavior: +- An **empty** configuration section is mapped with all default values — it is not treated as absent. +- Setting a section to **`null`** in an environment-specific file removes/overrides a section defined in the + base `appsettings.json` (stock .NET silently ignores a `null` override; Nano honors it as a deletion). + +### Null Logger + +If no logging provider is registered (see [Nano.Logging](#nanologging)), Nano still registers `ILoggerFactory`, +`ILogger`, and `ILogger` — backed by a `NullLogger` that discards everything. This is a safety fallback so +code that injects `ILogger` never fails to resolve, even with no logging provider configured. + +### Api Clients + +This is the mechanism for one Nano application to call another over HTTP with a typed, strongly-modeled client — +full CRUD against the target's entities, authentication, and identity management, without hand-building HTTP +requests. It's how internal services expose their models/entities to other applications (typically via a NuGet +built from their `{name}.Models` project — see [Solution Structure](#solution-structure)), and how a +publicly-exposed gateway API composes several internal services into one façade. + +**Where the code lives**: the client class and its custom request types live in the *owning* service's +`{name}.Models/Api/` project (e.g. `MyService.Models/Api/MyApi.cs`, with custom requests under +`Api/Requests/`). A consuming application references that project (or its published NuGet) and injects the +client class directly — no manual DI registration needed. + +#### Defining a client + +Derive from `BaseApiClient` (`Guid` identity), `BaseApiClient` (custom identity type), or — if the +target application has Identity configured — `BaseIdentityApiClient`/`BaseIdentityApiClient`, where `TUser` is the target's `IEntityUser` model. The constructor must take exactly `ApiClient`. + +Three shapes: + +```csharp +// Bare pass-through — no custom methods, relies entirely on the built-in .Entity/.Auth/.Audit groups +public class MyApi(ApiClient apiClient) : BaseApiClient(apiClient); +``` + +```csharp +// Custom methods only, wrapping one hand-defined request each +public class MyOtherApi(ApiClient apiClient) : BaseApiClient(apiClient) +{ + // No response — InvokeAsync + public virtual Task MyMethodAsync(MyModel model, CancellationToken cancellationToken = default) + => this.InvokeAsync(new MyRequest { Model = model }, cancellationToken); + + // Typed response — InvokeAsync; MyResponse is a plain POCO, no base type required + public virtual Task GetMyResponseAsync(MyRequest request, CancellationToken cancellationToken = default) + => this.InvokeAsync(request, cancellationToken); +} +``` + +```csharp +// Identity-backed target — adds the .Identity method group +public class MyApi(ApiClient apiClient) : BaseIdentityApiClient(apiClient) +{ + public virtual Task GetByEmailAsync(string emailAddress, CancellationToken cancellationToken = default) + => this.InvokeAsync(new GetByEmailRequest { EmailAddress = emailAddress }, cancellationToken); +} +``` + +#### Built-in method groups + +Available as properties on the client instance — no implementation needed, just call them: + +| Group | Available on | Covers | +| -------------- | ---------------------------------------- | ------------------------------------------------------------------------------------ | +| `.Entity` | `BaseApiClient` | Full CRUD against any entity of the target app: `GetAsync`, `GetManyAsync`, `QueryAsync`, `QueryFirstAsync`, `QueryCountAsync`, `CreateAsync`/`CreateOrEditAsync`/`CreateOrGetAsync`/`CreateAndGetAsync`/`CreateManyAsync`(`Bulk`), `EditAsync`/`EditAndGetAsync`/`EditManyAsync`(`Bulk`)/`EditQueryAsync`(`Bulk`), `DeleteAsync`/`DeleteManyAsync`(`Bulk`)/`DeleteQueryAsync`(`Bulk`). Mirrors the entity controller route table 1:1 — see [Controllers § Full CRUD route table](#full-crud-route-table). | +| `.Auth` | `BaseApiClient` | `LogInAsync`, `LogInRootAsync`, `LogInApiKeyAsync`, `LogInExternalAsync`, `LogInRefreshAsync`, `LogOutAsync`, `GetExternalSchemesAsync`. | +| `.Audit` | `BaseApiClient` | Read-only access to the target's `AuditEntry` log: `GetAsync`/`GetManyAsync`/`QueryAsync`/`QueryFirstAsync`/`QueryCountAsync`. | +| `.Identity` | `BaseIdentityApiClient` | Sign-up, password set/change/reset (+ token generation), email/phone change/confirm (+ token generation), roles, claims, external logins, refresh tokens, API keys. | + +An endpoint not enabled on the target application (e.g. `.Auth` when the target has no authentication configured) +returns `404` — surfaced as `null`, not an exception (see Gotchas below). + +Real usage — a controller composing multiple clients (an identity-backed `MyApi` plus a custom-methods-only +`MyOtherApi`) into one gateway endpoint: + +```csharp +public class MyUserController(ILogger logger, MyApi myApi, MyOtherApi myOtherApi) + : BaseController(logger) +{ + public virtual async Task GetMyUserAsync(Guid id, CancellationToken cancellationToken = default) + { + var entity = await myApi.Entity.GetAsync(id, cancellationToken); + return entity == null ? this.NotFound() : this.Ok(entity); + } + + public virtual async Task SignUpAsync([FromBody][Required] MyUser entity, CancellationToken cancellationToken = default) + { + var user = await myApi.Identity.SignUpAsync(new SignUpRequest { SignUp = new SignUp { User = entity } }, cancellationToken); + + await myOtherApi.MyMethodAsync(new MyModel { UserId = user.Id }, cancellationToken); + + return this.Created("signup", user); + } +} +``` + +#### Custom requests (endpoints beyond CRUD/Auth/Identity) + +1. Derive a request from `BaseRequest`, annotated with an action attribute naming the HTTP verb + relative route: + `[GetAction]`, `[PostAction]`, `[PutAction]`, `[DeleteAction]`, `[PatchAction]`, `[QueryAction]`, `[HeadAction]`, + `[OptionsAction]`, `[ConnectAction]`. +2. Annotate properties with parameter attributes: + +| Attribute | Purpose | +| ------------- | ------------------------------------------------------------------------------------------------------------------ | +| `[Route(Order = n)]` | Positional route-template substitution (`{n}` placeholders in the action's route string, filled in `Order` sequence). | +| `[Query]` | Querystring parameter (scalar types); optional `Name` override. | +| `[Body]` | The JSON request body (one complex object). | +| `[Form]` | A `multipart/form-data` field — scalar, or `IFormFile`/`FileInfo`/`FileStream`/`Stream`/`NamedStream`; complex objects need `[FromFormBody]` server-side. Mutually exclusive with `[Body]`. | +| `[Header(Name=..., ValuePrefix=...)]` | An HTTP header key/value. | + +Four shapes, covering every parameter attribute: + +```csharp +[GetAction("all")] +public class GetAllRequest : BaseRequest; // no params — controller inferred from TResponse (e.g. IEnumerable -> "MyEntities") + +[GetAction("by-name")] +public class MyQueryRequest : BaseRequest +{ + [Query] public virtual string Name { get; set; } = null!; +} + +[GetAction("{id}/file/{type}")] +public class MyFileRequest : BaseRequest +{ + [Route(Order = 0)] public virtual Guid Id { get; set; } + [Route(Order = 1)] public virtual MyEnum Type { get; set; } + + public MyFileRequest() { this.Controller = "MyEntities"; } // explicit override — route doesn't match a pluralized TResponse +} + +[PostAction("{id}/file/set")] +public class SetMyFileRequest : BaseRequest +{ + [Route] public virtual Guid Id { get; set; } + [Form] public virtual IFormFile File { get; set; } = null!; + + public SetMyFileRequest() { this.Controller = "MyEntities"; } +} +``` + +3. Add a method to the client, calling `InvokeAsync` (no response) or `InvokeAsync` + (typed response — use `NamedStream` or `Stream` for file downloads). + +**Controller resolution**: if a request doesn't set `this.Controller` explicitly in its constructor, it's +inferred as the pluralized `TResponse` type name (e.g. `IEnumerable` → `MyEntities`). Set it explicitly +whenever the route doesn't naturally match the response type, or the request has no typed response at all. + +**Keep the route string in sync with the server.** Both sides declare the same route segment independently — the +action attribute here, and `[Route(...)]` on the target controller's action — with nothing enforcing they match. +Nano's own built-in requests avoid this by referencing shared constants (`Nano.Common.Consts.ActionRoutes`) from +both sides, e.g. `BaseEntityViewController` uses `[Route(ActionRoutes.INDEX)]` while the built-in `IndexRequest` +uses `[PostAction(ActionRoutes.INDEX)]` — one string, referenced twice, so a rename can't silently break the +client without also breaking the build. For your own custom endpoints, define the route segment as a constant in +a `Consts` class inside the shared `{name}.Models` project (visible to both the owning API project and any +client-Api consumer) and reference it from both the request's action attribute and the controller's `[Route(...)]`, +instead of retyping the same literal string in two places. + +#### Configuration + +Registered automatically — no `services.AddNanoApiClient()` call needed. Every `BaseApiClient` subclass in +the entry assembly whose class name matches a key under `App:Apis` gets wired up (`HttpClient` + `ApiClient` + +the client instance) and becomes injectable. + +| Setting | Type | Default | Description | +| ---------------------------- | -------- | --------- | ------------------------------------------------------------------------------------ | +| `Host` | string | localhost | Target API host. | +| `Root` | string | api | Root path segment. | +| `Port` | int | 80 | Target port. | +| `UseSsl` | bool | false | Use HTTPS. | +| `Timeout` | TimeSpan | 00:00:30 | Request timeout. | +| `LogInRoot.Username` | string | null | Optional — auto-login as root if no inbound JWT is available to forward. | +| `LogInRoot.Password` | string | null | Optional — paired with `LogInRoot.Username`. | +| `HealthCheck.UnhealthyStatus` | enum | Unhealthy | Status reported when the target is unreachable. API/Web apps only. | + +```json +"App": { + "Apis": { + "MyApi": { + "Host": "my-service", + "Root": "api", + "Port": 8080, + "UseSsl": false, + "Timeout": "00:00:30", + "HealthCheck": { "UnhealthyStatus": "Unhealthy" } + } + } +} +``` + +⚠ The dictionary key **must exactly match the client's class name** (`MyApi` above) — this is the only +link between config and DI; there's no other place to declare which config entry a client uses. + +#### Authentication forwarding + +Outbound JWT is resolved in this order: `request.JwtTokenOverride` (explicit per-request override) → the +current inbound request's own JWT (so a call made from inside a controller/worker action transparently forwards +the caller's identity — this is how a gateway application's controllers stay authenticated end-to-end into an +internal service) → if `LogInRoot` is configured, an automatic root login (cached for the process lifetime). A +set of headers (`X-Api-Key`, `X-Forwarded-*`, request id, `Accept-Language`, timezone) is also forwarded +automatically from the inbound `HttpContext`, so locale/tenant/tracing context survives across service calls. + +Console workers (which have no inbound `HttpContext`) typically call only anonymous/unauthenticated endpoints to +avoid needing `LogInRoot` credentials — a worker with no `LogInRoot` configured at all can still call target +endpoints that are `[AllowAnonymous]`. + +#### Gotchas + +- A configured-but-never-injected client is **not** registered — Nano only wires up clients actually referenced + somewhere in the app. +- `404` responses return `null`, never throw — always null-check rather than try/catch. +- Other non-success responses throw `ProblemDetailsException` (structured `ProblemDetails`) or, if the body + isn't parseable as `ProblemDetails`, `ApiClientException` (raw body + status code). +- Every generic `.Entity` read method accepts an `includeDepth` parameter — thread your own controller's + `[FromQuery] int? includeDepth` through to it for end-to-end include-depth control, see [Include + Annotation](#include-annotation). + +### Start-Up Tasks + +One-time initialization work that must complete before the application starts accepting traffic (or, for +Console apps, before workers start) — cache warm-up, external dependency checks, or similar. Not the same +mechanism as the built-in database migration task Nano runs for a configured data provider. + +#### Defining a task + +Implement `IStartupTask` (`OnStartAsync`/`OnStopAsync`), or derive from `BaseStartupTask` to only need +`OnStartAsync` — its `OnStopAsync` defaults to `Task.CompletedTask`. + +```csharp +public class MyStartupTask(ILogger logger) : BaseStartupTask(logger) +{ + public override async Task OnStartAsync(CancellationToken cancellationToken = default) + { + // one-time init — cache warm-up, external dependency check, etc. + } + + // optional — only override if you need it; see the timing note below before relying on it + public override async Task OnStopAsync(CancellationToken cancellationToken = default) + { + // cleanup for what OnStartAsync acquired — runs right after OnStartAsync completes, not at real shutdown + } +} +``` + +No registration needed — just define the class in the entry assembly. Every non-abstract `IStartupTask` +implementation is discovered by reflection and registered `Scoped`; any other registered service, including +scoped ones, can be injected into the constructor. + +#### Execution + +All registered tasks' `OnStartAsync` run **concurrently** (`Task.WhenAll`), in one shared service scope, before +the application accepts requests. If any task throws, the exception propagates and **the application fails to +start** — a startup task is not allowed to fail silently. + +⚠ **`OnStopAsync` is not "runs at application shutdown."** Immediately after all `OnStartAsync` calls complete, +Nano's internal hosted service calls its own stop routine right away — which invokes every task's `OnStopAsync` +and decrements a shared readiness counter (`StartupTaskContext`). So `OnStopAsync` actually fires right after +`OnStartAsync` finishes, as a completion/cleanup hook — not tied to real application shutdown (though the host's +real shutdown sequence may also invoke it again). Use it for cleanup that belongs immediately after the task's +own startup work, not for logic that must run when the application actually stops. + +#### Readiness integration + +The same readiness counter backs the built-in *self* startup health check: once [Health Checks](#health-checks) +are enabled, the application isn't reported healthy/ready until every startup task's `OnStartAsync` **and** +`OnStopAsync` have completed. In Console apps, workers don't start until this same counter reaches zero — startup +tasks always run to completion before the first worker starts. + +Conventionally placed in a `Startup/` folder in the application project (not a hard requirement — discovered by +type, not location). + +### Custom Services + +Standard ASP.NET Core dependency injection — nothing Nano-specific beyond the extension point. Register anything +in the `ConfigureServices(...)` step alongside the `AddNanoX<...>()` provider calls: + +```csharp +NanoApiApplication + .ConfigureApp() + .ConfigureServices(services => + { + services.AddSingleton(); + }) + .Build() + .Run(); +``` + +### Custom Middleware + +Add middleware to the `IApplicationBuilder` delegate passed to `Build(...)`: + +```csharp +NanoApiApplication + .ConfigureApp() + .ConfigureServices(services => { /* ... */ }) + .Build(builder => + { + builder.Use((context, next) => + { + context.Response.Headers["MyHeader"] = "MyValue"; + + return next(); + }); + }) + .Run(); +``` + +⚠ Custom middleware is **appended to the end** of Nano's own middleware pipeline — it can't run earlier in the +pipeline than Nano's built-in middleware. + +⚠ Only API and Web applications support this — Console applications ignore the `Build(builder => ...)` delegate +entirely, since there's no HTTP pipeline to add middleware to. + +### Custom Configuration Section + +Define an options model, add a matching section to `appsettings.json`, and bind it with +`AddNanoConfigSection(name, out options)`: + +```csharp +public class MySectionModel +{ + // Properties... +} +``` + +```json +{ + "MySection": { } +} +``` + +```csharp +.ConfigureServices(services => +{ + services.AddNanoConfigSection("MySection", out var options); +}) +``` + +`options` is the bound instance, available immediately for further service registration in the same +`ConfigureServices` call; the section is also registered for standard `IOptions`/`IOptionsMonitor` +injection anywhere else. Binding uses the same validation as every other Nano section — +`ValidateDataAnnotationsRecursively().ValidateOnStart()` — so a `[Required]` property left unset fails at host +startup, not on first use. + +⚠ The section name must actually **exist** in configuration, even if empty (`"MySection": { }`) — an entirely +missing section throws `InvalidOperationException` at startup, it doesn't silently bind an all-defaults instance. + +Section names must not collide with Nano's own built-in sections: `App`, `Logging`, `Data`, `Eventing`, `Storage`. + +--- + +## Nano.App.Api + +`NanoApiApplication` — the ready-to-use API host template. + +### Registration + +```powershell +dotnet add package Nano.App.Api; +``` + +```csharp +NanoApiApplication + .ConfigureApp() + .ConfigureServices(x => + { + // Your services... + }) + .Build() + .Run(); +``` + +### Configuration + +The `App` section defines application-level behavior. + +| Setting | Type | Default | Description | +| ---------------------- | ---------- | ------- | --------------------------------------------------------------- | +| `Version` | string | 1.0.0.0 | Application version identifier. | +| `ShutdownTimeout` | int | 10 | Seconds to wait after SIGTERM before shutting down. | +| `Hosting` | object | default | See [Hosting](#hosting). | +| `HttpPolicyHeaders` | object | default | See [Http Policy Headers](#http-policy-headers). | +| `ResponseCache` | object | null | See [Response Cache](#response-cache). | +| `ResponseCompression` | object | null | See [Response Compression](#response-compression). | +| `Session` | object | null | See [Session](#session). | +| `TimeZone` | object | null | See [TimeZone](#timezone). | +| `Localization` | object | null | See [Localization](#localization). | +| `Documentation` | object | null | Swagger config. See [Documentation](#documentation). | +| `HealthCheck` | object | null | See [Health Checks](#health-checks). | +| `Metrics` | object | null | See [Metrics (OpenTelemetry)](#metrics-opentelemetry). | +| `VirusScan` | object | null | See [Virus Scan](#virus-scan). | +| `ErrorHandling` | object | default | See [Error Handling](#error-handling). | +| `Authentication` | object | default | See [Authentication](#authentication). | +| `Apis` | dictionary | [] | Named Nano API client configurations. See [Nano.App § Api Clients](#api-clients). | + +```json +"App": { + "Version": "1.0.0.0", + "ShutdownTimeout": 10, + "Hosting": { }, + "HttpPolicyHeaders": { }, + "ResponseCache": null, + "ResponseCompression": null, + "Session": null, + "TimeZone": null, + "Localization": null, + "Documentation": null, + "HealthCheck": null, + "VirusScan": null, + "ErrorHandling": { }, + "Authentication": { }, + "Apis": [] +} +``` + +#### Hosting + +How the API is hosted on Kestrel. + +| Setting | Type | Default | Description | +| -------------------- | ------ | ------- | -------------------------------------------------- | +| `Root` | string | api | Root route prefix for application endpoints. | +| `Http` | object | default | See [Http](#http). | +| `Https` | object | null | See [Https](#https). | +| `MultipartLimits` | object | null | See [MultiPart Limits](#multipart-limits). | + +```json +"App": { + "Hosting": { + "Root": "api", + "Http": { }, + "Https": null, + "MultipartLimits": null + } +} +``` + +##### Http + +| Setting | Type | Default | Description | +| ------------------------ | ------ | ------- | ------------------------------------------- | +| `Ports` | array | [] | List of ports for HTTP. | +| `UseHttpsRedirection` | bool | false | Enforce HTTPS redirect for all requests. | + +```json +"App": { + "Hosting": { + "Http": { + "Ports": [], + "UseHttpsRedirection": false + } + } +} +``` + +⚠ At least one HTTP or HTTPS port must be specified. Avoid the default port 80 — it may trigger security +warnings in Kubernetes. + +##### Https + +Requires at least one port plus a certificate path and password. Intended primarily for local development — +`Staging`/`Production` TLS is handled at the gateway/cert-manager level, not via this config. + +| Setting | Type | Default | Description | +| ------------------------- | ------ | ------- | -------------------------------------------- | +| `Ports` | array | [] | List of ports for HTTPS. | +| `UseHttpsRequired` | bool | false | Enforce HTTPS for all requests. | +| `Certificate.Path` | string | null | Required. File path to the certificate. | +| `Certificate.Password` | string | null | Required. Password for the certificate. | + +```json +"App": { + "Hosting": { + "Http": { "UseHttpsRedirection": true }, + "Https": { + "Ports": [4443], + "Certificate": { + "Path": "/root/.dotnet/https/localhost.pfx", + "Password": "password" + }, + "UseHttpsRequired": true + } + } +} +``` + +⚠ Avoid the default HTTPS port 443 — it may trigger security warnings in Kubernetes. Configure this only in +`appsettings.Development.json`. + +##### Routing + +No configuration — routing is fully automatic. Routes are derived from the base controller a concrete controller +derives from; API versioning is integrated into the route automatically. All routes are normalized to lowercase. + +##### MultiPart Limits + +| Setting | Type | Default | Description | +| ---------------------- | ----- | -------- | ------------------------------------------------- | +| `MaxUploadBytes` | int | 33554432 | Maximum upload size in bytes (default 32 MB). | +| `KeepAliveTimeout` | int | 00:02:10 | Timeout for slow uploads. | + +```json +"App": { + "Hosting": { + "MultipartLimits": { + "MaxUploadBytes": 33554432, + "KeepAliveTimeout": 130 + } + } +} +``` + +⚠ Leaving this `null` allows unlimited uploads — fine if limits are enforced at the orchestration level, +otherwise set explicit limits. + +#### Http Policy Headers + +Parent config object for headers such as HSTS, XSS protection, CSP, CORS, and other browser-security policies. + +| Setting | Type | Default | Description | +| ---------------------- | ------ | ------- | ---------------------------------------------------- | +| `ContentType` | object | null | See [Content Type Options](#content-type-options). | +| `ReferrerPolicy` | object | null | See [Referrer Policy](#referrer-policy). | +| `FrameOptions` | object | null | See [Frame Options](#frame-options). | +| `XssProtection` | object | null | See [Xss Protection](#xss-protection). | +| `Csp` | object | null | See [Content Security Policy (CSP)](#content-security-policy-csp). | +| `Cors` | object | null | See [Cors](#cors). | +| `Hsts` | object | null | See [Strict Transport Security (Hsts)](#strict-transport-security-hsts). | +| `Robots` | object | null | See [Robots](#robots). | +| `ForwardedHeaders` | object | null | See [Forwarded Headers](#forwarded-headers). | + +```json +"App": { + "HttpPolicyHeaders": { + "ContentType": null, + "ReferrerPolicy": null, + "FrameOptions": null, + "XssProtection": null, + "Csp": null, + "Cors": null, + "Hsts": null, + "Robots": null, + "ForwardedHeaders": null + } +} +``` + +##### Content Type Options + +Sets the `X-Content-Type-Options` response header to prevent MIME type sniffing. + +| Setting | Type | Default | Description | +| ------------- | ---- | ------- | ---------------------------------------- | +| `NoSniff` | bool | true | If true, prevents MIME type sniffing. ⭐ recommended: `true`. | + +```json +"App": { + "HttpPolicyHeaders": { + "ContentType": { "NoSniff": true } + } +} +``` + +##### Referrer Policy + +Sets the `Referrer-Policy` response header, controlling how much referrer information is sent with requests. + +| Setting | Type | Default | Description | +| --------------------------- | ---- | -------- | --------------------------------- | +| `ReferrerPolicyHeader` | enum | Disabled | See policy values below. | + +```json +"App": { + "HttpPolicyHeaders": { + "ReferrerPolicy": { "ReferrerPolicyHeader": "Disabled" } + } +} +``` + +| Policy | Description | +| ---------------------------------- | ------------ | +| `Disabled` | Header not set. | +| `NoReferrer` | No referrer information sent. | +| `NoReferrerWhenDowngrade` | Full referrer unless HTTPS → HTTP. | +| `SameOrigin` ⭐ | Full referrer for same-origin, none for cross-origin. | +| `Origin` | Only origin (no path/query) sent, always. | +| `StrictOrigin` | Origin only, and never HTTPS → HTTP. | +| `OriginWhenCrossOrigin` | Full for same-origin, origin-only for cross-origin. | +| `StrictOriginWhenCrossOrigin` | Full for same-origin, origin-only cross-origin, none HTTPS → HTTP. | +| `UnsafeUrl` | Full referrer always, including HTTPS → HTTP. Unsafe. | + +The `[ReferrerPolicy]` action/controller attribute overrides the global setting per endpoint. + +##### Frame Options + +Sets `X-Frame-Options`, guarding against clickjacking by controlling `