From 4cc9b09bfdbde6f10206d97311606a6951061783 Mon Sep 17 00:00:00 2001 From: Stephen Walsh <868017+stphnwlsh@users.noreply.github.com> Date: Thu, 5 Nov 2026 17:24:52 +0000 Subject: [PATCH 1/2] Label the token options so they sit with Include authentication in the IDEs --- .agents/skills/template-authoring/SKILL.md | 2 +- .template.config/ide.host.json | 8 ++++---- .template.config/template.json | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.agents/skills/template-authoring/SKILL.md b/.agents/skills/template-authoring/SKILL.md index aec3ce0..41e2f85 100644 --- a/.agents/skills/template-authoring/SKILL.md +++ b/.agents/skills/template-authoring/SKILL.md @@ -41,7 +41,7 @@ Off means absent, not switched off. Files that only exist for a toggle go in a ` The repository itself stays all on. The Presentation project and the presentation integration tests define each toggle's constant, such as `includeAuth`, inside ``, which `dotnet new` always removes. The template's skills and `AGENTS.md` ship either way. -A toggle's inputs, such as `authority` and `audience`, are separate symbols, enabled with `isEnabled` and made required with `isRequired` only when the toggle is on. A toggle that needs another one says so in its conditions, such as `(includeTests && includeArchitectureTests)`. Verify Template checks the defaults, everything on, everything off, and each toggle flipped on its own. +A toggle's inputs, such as `authority` and `audience`, are separate symbols, enabled with `isEnabled` and made required with `isRequired` only when the toggle is on. Only `dotnet new` honours those two. Visual Studio and Rider show every input whatever the toggle says ([dotnet/templating#7860](https://github.com/dotnet/templating/issues/7860)), and Rider lists checkboxes first, then text fields in alphabetical order by label. So an input's label starts with its feature, such as "Authentication authority", to sit right under its checkbox, and its IDE description says it's only used when that box is ticked. A toggle that needs another one says so in its conditions, such as `(includeTests && includeArchitectureTests)`. Verify Template checks the defaults, everything on, everything off, and each toggle flipped on its own. The package includes `src/`, `tests/`, `docs/RUN.md`, the root build files, `AGENTS.md`, `CLAUDE.md`, the persona and the engineering skills. It also packs `template/ci.yml` as the generated solution's `.github/workflows/ci.yml`, a small pipeline with no template name in it. It leaves out the rest of `.github/`, the community files and the repository and maintainer skills, which only make sense in the template repository. Those skills are listed in `RepositorySkills` in `template/template.csproj`; a new one is added there. The pipeline fails a package over 1 MB. diff --git a/.template.config/ide.host.json b/.template.config/ide.host.json index 8be75f5..625d2ca 100644 --- a/.template.config/ide.host.json +++ b/.template.config/ide.host.json @@ -44,20 +44,20 @@ { "id": "authority", "name": { - "text": "Token authority" + "text": "Authentication authority" }, "description": { - "text": "The OAuth server that issues access tokens. Needed when authentication is on." + "text": "The OAuth server that issues access tokens. Only used when Include authentication is ticked, and needed then." }, "isVisible": true }, { "id": "audience", "name": { - "text": "Token audience (optional)" + "text": "Authentication audience (optional)" }, "description": { - "text": "Who the tokens are for, usually the API's own URL." + "text": "Who the tokens are for, usually the API's own URL. Only used when Include authentication is ticked." }, "isVisible": true } diff --git a/.template.config/template.json b/.template.config/template.json index b9544f6..b31ba60 100644 --- a/.template.config/template.json +++ b/.template.config/template.json @@ -97,7 +97,7 @@ "authority": { "type": "parameter", "datatype": "string", - "displayName": "Token authority", + "displayName": "Authentication authority", "description": "The OAuth authorization server that issues access tokens. Required with --include-auth.", "defaultValue": "", "isEnabled": "includeAuth", @@ -106,7 +106,7 @@ "audience": { "type": "parameter", "datatype": "string", - "displayName": "Token audience (optional)", + "displayName": "Authentication audience (optional)", "description": "The audience access tokens must be issued for, usually the API's own URL. Not the people in the room, who are lovely but don't carry tokens.", "defaultValue": "", "isEnabled": "includeAuth" From ce894a8e645cab814097cbdeb6e65f0b1fda85c6 Mon Sep 17 00:00:00 2001 From: Stephen Walsh <868017+stphnwlsh@users.noreply.github.com> Date: Thu, 5 Nov 2026 21:16:51 +0000 Subject: [PATCH 2/2] Keep the template out of generated solutions A generated solution no longer gets icon.png in its root, and the shipped AGENTS.md leaves out the line about .template.config. The skills now hold with authentication on or off, so an agent working in a solution created without --include-auth isn't told to add RequireAuthorization or look for AuthenticationSettings. The CLI descriptions drop "Default: off." because dotnet new --help prints the default; the IDE descriptions keep it. Verify Template now checks that nothing of the template leaks into a generated solution and that --include-auth without --authority is refused. --- .../configuration-and-environments/SKILL.md | 4 +-- .agents/skills/expose-a-use-case/SKILL.md | 2 +- .agents/skills/optional-token-auth/SKILL.md | 2 +- .../presentation-integration-tests/SKILL.md | 4 +-- .agents/skills/telemetry/SKILL.md | 2 +- .agents/skills/template-authoring/SKILL.md | 6 ++-- .agents/skills/time-provider/SKILL.md | 2 +- .github/workflows/build-pipeline.yml | 28 ++++++++++++++++--- .template.config/template.json | 2 +- AGENTS.md | 2 ++ template/template.csproj | 2 +- 11 files changed, 39 insertions(+), 17 deletions(-) diff --git a/.agents/skills/configuration-and-environments/SKILL.md b/.agents/skills/configuration-and-environments/SKILL.md index 5806870..9bd1fae 100644 --- a/.agents/skills/configuration-and-environments/SKILL.md +++ b/.agents/skills/configuration-and-environments/SKILL.md @@ -25,14 +25,14 @@ Settings that exist today: | --- | --- | | `ConnectionStrings:MovieReviews` | SQLite connection string. `Data Source=Name;Mode=Memory;Cache=Shared` keeps everything in memory. | | `Database:SeedSampleData` | Seed sample data into an empty database (`sample-data`). | -| `Authentication:Authority`, `Authentication:Audience` | Turn on token authentication (`optional-token-auth`). MCP also has `Authentication:Scopes`, published in its protected resource metadata. | +| `Authentication:Authority`, `Authentication:Audience` | Only in a solution created with `--include-auth`: turn on token authentication (`optional-token-auth`). MCP also has `Authentication:Scopes`, published in its protected resource metadata. | | `Serilog:*` | Logging (`serilog`). | ## Steps 1. Put the safe default in `appsettings.json`. If the default is "off" or empty, still write the key so people can find it. 2. Override it only in the environment files that need a different value. -3. Read a single value with `configuration.GetValue("Section:Key")`; read a group into a settings class with a `SectionName` constant and a `FromConfiguration(IConfiguration)` method, like `AuthenticationSettings`. +3. Read a single value with `configuration.GetValue("Section:Key")`; read a group into a settings class with a `SectionName` constant and a `FromConfiguration(IConfiguration)` method, like `AuthenticationSettings` in a solution with authentication. 4. Document the key in `docs/RUN.md` if someone running the app might change it. 5. Tests override settings in memory (`integration-tests`), never by editing the JSON. diff --git a/.agents/skills/expose-a-use-case/SKILL.md b/.agents/skills/expose-a-use-case/SKILL.md index db9cf24..ebb9067 100644 --- a/.agents/skills/expose-a-use-case/SKILL.md +++ b/.agents/skills/expose-a-use-case/SKILL.md @@ -27,7 +27,7 @@ Hot Chocolate reads XML comments, so a field's `` and `` tags an 1. Add the use case first (`add-a-use-case`). 2. Add the field to the feature's class in `Queries/` or `Mutations/`. The method name minus `Get` and `Async` becomes the field name (`GetMovies` is `movies`). -3. Put `[Authorize]` on every field, `version` included (`optional-token-auth`). +3. In a solution with authentication, put `[Authorize]` on every field, `version` included (`optional-token-auth`). Without it, leave the attribute out; the examples below show it. 4. Lists use `[UsePaging(ConnectionName = "...")]`, take `int? first` and `string? after`, and return `page.ToConnection()` with `after.ToOffset()` as the offset (`api-conventions`). 5. A mutation takes its input fields as plain arguments and returns the result itself. `AddMutationConventions(applyToAllMutations: true)` wraps them in `Input` and `Payload`. Add `[Error]` and `[Error]` for the failures it can raise, and `[UseMutationConvention(PayloadFieldName = "...")]` when the result isn't an entity, such as `reviewId` on `deleteReview`. 6. A new entity gets a type in `Types/`: declare each field, mark non-null strings with `NonNullType`, and resolve child lists on list items through a DataLoader. diff --git a/.agents/skills/optional-token-auth/SKILL.md b/.agents/skills/optional-token-auth/SKILL.md index 588f289..b8741ef 100644 --- a/.agents/skills/optional-token-auth/SKILL.md +++ b/.agents/skills/optional-token-auth/SKILL.md @@ -17,7 +17,7 @@ Settings are read through `AuthenticationSettings.FromConfiguration` (`Enabled` ## Steps -1. A new endpoint or group gets `.RequireAuthorization()` (or the presentation layer's equivalent). That's the default; don't skip it. +1. In a solution with authentication, a new endpoint or group gets `.RequireAuthorization()` (or the presentation layer's equivalent). That's the default; don't skip it. Without it there's nothing to call. 2. Only make something anonymous if it has to work without a token: health and the API reference. Say why in a comment. 3. When authentication is on, the template tells clients how to get a token: a bearer scheme in the OpenAPI document where a template has one, and the protected resource metadata in MCP. 4. Test both modes: one test with no authority (anonymous works), one with an authority and no token (401). diff --git a/.agents/skills/presentation-integration-tests/SKILL.md b/.agents/skills/presentation-integration-tests/SKILL.md index 2c58dc7..94bcf80 100644 --- a/.agents/skills/presentation-integration-tests/SKILL.md +++ b/.agents/skills/presentation-integration-tests/SKILL.md @@ -9,7 +9,7 @@ metadata: ## The rule -The GraphQL layer is tested through HTTP in `tests/Presentation.Tests.Integration`. `ApplicationErrorFilter`, `PageExtensions` and `AuthenticationSettings` are also tested on their own in `tests/Presentation.Tests.Unit` (`unit-tests`). `GraphQLApplication` (a `WebApplicationFactory`) starts the app with the `local` environment, its own shared-cache in-memory SQLite database and the sample data on. Each test class creates one in a field, implements `IDisposable` and disposes it, so no class shares data with another. +The GraphQL layer is tested through HTTP in `tests/Presentation.Tests.Integration`. `ApplicationErrorFilter`, `PageExtensions` and, with authentication, `AuthenticationSettings` are also tested on their own in `tests/Presentation.Tests.Unit` (`unit-tests`). `GraphQLApplication` (a `WebApplicationFactory`) starts the app with the `local` environment, its own shared-cache in-memory SQLite database and the sample data on. Each test class creates one in a field, implements `IDisposable` and disposes it, so no class shares data with another. | Piece | Where | Use it to | | --- | --- | --- | @@ -26,7 +26,7 @@ The GraphQL layer is tested through HTTP in `tests/Presentation.Tests.Integratio 4. For failures, assert `errors[0].extensions.code` (`VALIDATION`, `NOT_FOUND`) and the messages (`api-conventions`). 5. Find rows through the API (list, then pick one), not by hard-coded ids. 6. A schema change also updates the snapshot (`schema-snapshot`). -7. Authentication tests pass `Authentication:*` settings and a test signing key. See `Authentication/AuthenticationTests.cs`. +7. In a solution with authentication, its tests pass `Authentication:*` settings and a test signing key. See `Authentication/AuthenticationTests.cs`. ## Example diff --git a/.agents/skills/telemetry/SKILL.md b/.agents/skills/telemetry/SKILL.md index 7067676..34d0eb8 100644 --- a/.agents/skills/telemetry/SKILL.md +++ b/.agents/skills/telemetry/SKILL.md @@ -31,7 +31,7 @@ What each presentation layer adds: | Template | Instrumentation | | --- | --- | -| Minimal API, Web API | ASP.NET Core and HttpClient traces and metrics, runtime metrics, the authentication and authorization meters. Health endpoints aren't traced | +| Minimal API, Web API | ASP.NET Core and HttpClient traces and metrics, runtime metrics and, with authentication, its meters. Health endpoints aren't traced | | GraphQL | The same, plus Hot Chocolate's resolver and operation spans from `HotChocolate.Diagnostics` | | MCP | The same, plus the MCP SDK's own `Experimental.ModelContextProtocol` spans and `mcp.*.operation.duration` metrics | | Lambda | A span per invocation from `OpenTelemetry.Instrumentation.AWSLambda`, and runtime metrics. Traces and metrics are flushed before the function returns, because Lambda can freeze the environment as soon as it does | diff --git a/.agents/skills/template-authoring/SKILL.md b/.agents/skills/template-authoring/SKILL.md index 41e2f85..683fcf9 100644 --- a/.agents/skills/template-authoring/SKILL.md +++ b/.agents/skills/template-authoring/SKILL.md @@ -32,9 +32,9 @@ The words in those files are the first thing people read, in a terminal or a dia Every optional feature is a toggle, and every toggle has the same shape, so people learn it once: -- A `bool` symbol named `include` in `template.json`, such as `includeAuth`, with a `displayName` of "Include " and a description that ends with its default, such as "Default: off." +- A `bool` symbol named `include` in `template.json`, such as `includeAuth`, with a `displayName` of "Include ". Its description doesn't state the default, because `dotnet new --help` prints that under every option. - A `longName` of `include-` in `dotnetcli.host.json`, so it's `--include-auth` on the command line. No short flag. -- A checkbox labelled "Include " in `ide.host.json`, whose description also ends with its default. +- A checkbox labelled "Include " in `ide.host.json`, whose description ends with its default, such as "Default: off.", because the IDE dialogs don't show defaults. - One row in the options table of the README, `NUGET.md` and the docs site's template options page, which list every toggle with a Default column. Off means absent, not switched off. Files that only exist for a toggle go in a `(!include)` exclude list under `sources` in `template.json`, and code inside shared files is wrapped so `dotnet new` can leave it out. In C#, `#if includeAuth` and `#endif` start at the beginning of the line, with no brackets (the build fails IDE0047 and IDE0055 otherwise). In project files it's `` and ``, and in `appsettings.json` it's `//#if (includeAuth)` and `//#endif`. A file type the template engine doesn't know, such as the GraphQL schema snapshot, gets `#if (includeAuth)` and `#endif` lines through `SpecialCustomOperations` in `template.json`. @@ -83,4 +83,4 @@ The package includes `src/`, `tests/`, `docs/RUN.md`, the root build files, `AGE ## Check -The Test job's Verify Template step installs the packed template, generates a solution, checks its format, runs its tests and checks it has `.github/workflows/ci.yml`, and does it again with `--include-auth` where the template has it. +The Test job's Verify Template step installs the packed template, generates a solution and checks that nothing of the template leaked into it (no `includeAuth` markers, no trace of the template's own name, no `icon.png`) and that it has `.github/workflows/ci.yml`, then checks its format and runs its tests. Where the template has `--include-auth` it does it again with the toggle on, and checks that `--include-auth` without `--authority` is refused with exit code 102. diff --git a/.agents/skills/time-provider/SKILL.md b/.agents/skills/time-provider/SKILL.md index d5c1346..6096810 100644 --- a/.agents/skills/time-provider/SKILL.md +++ b/.agents/skills/time-provider/SKILL.md @@ -52,7 +52,7 @@ updatedReview.DateModified.ShouldBe(this.database.TimeProvider.GetUtcNow().UtcDa - Store local times. - Assert a date with a tolerance ("within a second of now"). Fix the clock and assert the exact value. -The one exception is a test that creates a token for the real JWT handler, which checks expiry against the system clock (`tests/Presentation.Tests.Integration/Authentication/AuthenticationTests.cs`). +The one exception, in a solution with authentication, is the test that creates a token for the real JWT handler, which checks expiry against the system clock (`tests/Presentation.Tests.Integration/Authentication/AuthenticationTests.cs`). ## Check diff --git a/.github/workflows/build-pipeline.yml b/.github/workflows/build-pipeline.yml index f3bd1e9..38813f5 100644 --- a/.github/workflows/build-pipeline.yml +++ b/.github/workflows/build-pipeline.yml @@ -220,21 +220,41 @@ jobs: run: | dotnet new install packages/*.nupkg SHORT_NAME=$(jq -r .shortName .template.config/template.json) + SOURCE_NAME=$(jq -r .sourceName .template.config/template.json) + + # Nothing of the template itself leaks into a generated solution: no toggle markers, and no trace of the + # template's own name, which dotnet new replaces everywhere. + clean() { + if grep -rqi --exclude-dir=bin --exclude-dir=obj "$2" "$1"; then + echo "::error::$1 still contains '$2'" + grep -rni --exclude-dir=bin --exclude-dir=obj "$2" "$1" + return 1 + fi + } verify() { dotnet new "$SHORT_NAME" --name "$1" --output "$1" "${@:2}" + clean "$1" includeAuth + clean "$1" "$SOURCE_NAME" + test ! -e "$1/icon.png" + test -f "$1/.github/workflows/ci.yml" dotnet format "$1" --verify-no-changes dotnet test "$1" --configuration Release - test -f "$1/.github/workflows/ci.yml" } verify Template.Verification - VARIANTS="the generated solution passes its format check, builds and passes its tests" + VARIANTS="the generated solution carries nothing of the template, passes its format check, builds and passes its tests" - # Templates with an auth option are verified a second time with it on. + # Templates with an auth option are verified a second time with it on, and --include-auth without + # --authority has to stop before anything is written (exit code 102, a missing mandatory option). if jq -e '.symbols.includeAuth' .template.config/template.json > /dev/null; then verify Template.Authentication --include-auth --authority https://login.example.com --audience https://api.example.com - VARIANTS="the generated solution passes its format check, builds and passes its tests with and without --include-auth" + set +e + dotnet new "$SHORT_NAME" --name Template.NoAuthority --output Template.NoAuthority --include-auth + NO_AUTHORITY=$? + set -e + test "$NO_AUTHORITY" -eq 102 || { echo "::error::--include-auth without --authority exited $NO_AUTHORITY, not 102"; exit 1; } + VARIANTS="$VARIANTS with and without --include-auth, and --include-auth without --authority is refused" fi echo "### Template" >> "$GITHUB_STEP_SUMMARY" diff --git a/.template.config/template.json b/.template.config/template.json index b31ba60..1625474 100644 --- a/.template.config/template.json +++ b/.template.config/template.json @@ -91,7 +91,7 @@ "type": "parameter", "datatype": "bool", "displayName": "Include authentication", - "description": "Add JWT bearer token authentication. Leave it off and the API has no auth code at all, which is fine on your machine and brave anywhere else. Default: off.", + "description": "Add JWT bearer token authentication. Leave it off and the API has no auth code at all, which is fine on your machine and brave anywhere else.", "defaultValue": "false" }, "authority": { diff --git a/AGENTS.md b/AGENTS.md index c90f855..3d154f5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -51,7 +51,9 @@ Not set. Replace this line with where your team plans work, such as a GitHub pro - Suppress a warning instead of fixing it, unless a skill says that exact suppression is ours. - Add a package with a commercial or paid licence, such as MediatR, MassTransit or AutoMapper. - Commit secrets, connection strings with passwords, or tokens. + - Change `.template.config/` or the template package without following `template-authoring`. + ## Skills diff --git a/template/template.csproj b/template/template.csproj index 7532e1d..593a5af 100644 --- a/template/template.csproj +++ b/template/template.csproj @@ -43,7 +43,7 @@ - +