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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .agents/skills/configuration-and-environments/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>("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<T>("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.

Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/expose-a-use-case/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Hot Chocolate reads XML comments, so a field's `<summary>` and `<param>` 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 `<Mutation>Input` and `<Mutation>Payload`. Add `[Error<ValidationError>]` and `[Error<NotFoundError>]` 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<StringType>`, and resolve child lists on list items through a DataLoader.
Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/optional-token-auth/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
4 changes: 2 additions & 2 deletions .agents/skills/presentation-integration-tests/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Program>`) 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<Program>`) 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 |
| --- | --- | --- |
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/telemetry/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
8 changes: 4 additions & 4 deletions .agents/skills/template-authoring/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,16 @@ 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<Feature>` in `template.json`, such as `includeAuth`, with a `displayName` of "Include <feature>" and a description that ends with its default, such as "Default: off."
- A `bool` symbol named `include<Feature>` in `template.json`, such as `includeAuth`, with a `displayName` of "Include <feature>". Its description doesn't state the default, because `dotnet new --help` prints that under every option.
- A `longName` of `include-<feature>` in `dotnetcli.host.json`, so it's `--include-auth` on the command line. No short flag.
- A checkbox labelled "Include <feature>" in `ide.host.json`, whose description also ends with its default.
- A checkbox labelled "Include <feature>" 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<Feature>)` 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 `<!--#if (includeAuth) -->` and `<!--#endif -->`, 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`.

The repository itself stays all on. The Presentation project and the presentation integration tests define each toggle's constant, such as `includeAuth`, inside `<!--#if (false) -->`, 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.

Expand Down Expand Up @@ -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.
2 changes: 1 addition & 1 deletion .agents/skills/time-provider/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
28 changes: 24 additions & 4 deletions .github/workflows/build-pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
8 changes: 4 additions & 4 deletions .template.config/ide.host.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
6 changes: 3 additions & 3 deletions .template.config/template.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,13 @@
"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": {
"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",
Expand All @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
<!--#if (false) -->
- Change `.template.config/` or the template package without following `template-authoring`.
<!--#endif -->

## Skills

Expand Down
2 changes: 1 addition & 1 deletion template/template.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
<None Include="../AGENTS.md;../CLAUDE.md" Pack="true" PackagePath="content/" />
<!-- The small pipeline a generated solution starts with. The template's own Build Pipeline stays out. -->
<None Include="ci.yml" Pack="true" PackagePath="content/.github/workflows/" />
<None Include="../*.slnx;../Directory.Build.props;../Directory.Packages.props;../global.json;../NuGet.config;../Dockerfile;../docker-compose.yml;../otel-collector.yaml;../icon.png;../.*ignore;../.editorconfig;../.gitattributes" Pack="true" PackagePath="content/" />
<None Include="../*.slnx;../Directory.Build.props;../Directory.Packages.props;../global.json;../NuGet.config;../Dockerfile;../docker-compose.yml;../otel-collector.yaml;../.*ignore;../.editorconfig;../.gitattributes" Pack="true" PackagePath="content/" />
<None Include="../icon.png" Pack="true" PackagePath="images/" />
<None Include="../LICENSE.md" Pack="true" PackagePath="docs/" />
<None Include="../NUGET.md" Pack="true" PackagePath="docs/" />
Expand Down
Loading