diff --git a/astro.config.mjs b/astro.config.mjs
index 38e68b9..25a27a3 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -1,6 +1,7 @@
// @ts-check
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
+import { satteri } from '@astrojs/markdown-satteri';
import starlightLinksValidator from 'starlight-links-validator';
import starlightLlmsTxt from 'starlight-llms-txt';
import starlightSidebarTopics from 'starlight-sidebar-topics';
@@ -46,10 +47,10 @@ const sidebars = {
},
],
mcp: [
- { label: 'Start here', items: ['mcp/getting-started', 'mcp/guides/run-locally'] },
+ { label: 'Start here', items: ['mcp/getting-started', 'mcp/guides/run-locally', 'mcp/guides/connect-a-client'] },
{
label: 'Guides',
- items: ['mcp/guides/make-it-yours', 'mcp/guides/connect-a-client', 'mcp/guides/add-a-tool', 'mcp/guides/test-your-changes', 'mcp/guides/database', 'mcp/guides/secure-the-server', 'mcp/guides/ship-the-container', 'mcp/guides/set-up-ci', 'mcp/guides/work-with-a-coding-agent'],
+ items: ['mcp/guides/make-it-yours', 'mcp/guides/add-a-tool', 'mcp/guides/test-your-changes', 'mcp/guides/database', 'mcp/guides/secure-the-server', 'mcp/guides/ship-the-container', 'mcp/guides/set-up-ci', 'mcp/guides/work-with-a-coding-agent'],
},
{ label: 'Reference', items: [{ autogenerate: { directory: 'mcp/reference' } }] },
{
@@ -61,7 +62,7 @@ const sidebars = {
{ label: 'Start here', items: ['lambda/getting-started', 'lambda/guides/run-locally'] },
{
label: 'Guides',
- items: ['lambda/guides/make-it-yours', 'lambda/guides/add-a-function', 'lambda/guides/test-your-changes', 'lambda/guides/database', 'lambda/guides/deploy-to-aws', 'lambda/guides/secure-the-function', 'lambda/guides/set-up-ci', 'lambda/guides/work-with-a-coding-agent'],
+ items: ['lambda/guides/make-it-yours', 'lambda/guides/add-a-function', 'lambda/guides/test-your-changes', 'lambda/guides/database', 'lambda/guides/secure-the-function', 'lambda/guides/deploy-to-aws', 'lambda/guides/set-up-ci', 'lambda/guides/work-with-a-coding-agent'],
},
{ label: 'Reference', items: [{ autogenerate: { directory: 'lambda/reference' } }] },
{
@@ -77,9 +78,26 @@ const sidebars = {
],
};
+/* Marks inline code longer than a short name, usually a file path, so the stylesheet can let it wrap
+ like text instead of sitting on a line of its own. */
+const LONG_CODE = 28;
+const markLongCode = {
+ name: 'mark-long-code',
+ element: {
+ filter: ['code'],
+ visit(node, ctx) {
+ if (ctx.parent(node)?.tagName === 'pre') return;
+ if (ctx.textContent(node).length <= LONG_CODE) return;
+ const cls = node.properties?.className;
+ ctx.setProperty(node, 'className', Array.isArray(cls) ? [...cls, 'cl-long'] : ['cl-long']);
+ },
+ },
+};
+
export default defineConfig({
site: 'https://leanmeancleanapps.github.io',
trailingSlash: 'always',
+ markdown: { processor: satteri({ hastPlugins: [markLongCode] }) },
integrations: [
starlight({
title: 'Lean Mean Clean Apps',
diff --git a/package-lock.json b/package-lock.json
index b4f7471..fe8015d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6,6 +6,7 @@
"": {
"name": "lean-mean-clean-apps-site",
"dependencies": {
+ "@astrojs/markdown-satteri": "0.4.6",
"@astrojs/starlight": "0.42.8",
"@fontsource-variable/bricolage-grotesque": "5.3.0",
"@fontsource-variable/inter": "5.3.0",
diff --git a/package.json b/package.json
index 4e217bf..5be10f9 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
"sync-brand": "node scripts/sync-brand.mjs"
},
"dependencies": {
+ "@astrojs/markdown-satteri": "0.4.6",
"@astrojs/starlight": "0.42.8",
"@fontsource-variable/bricolage-grotesque": "5.3.0",
"@fontsource-variable/inter": "5.3.0",
diff --git a/src/components/docs/Footer.astro b/src/components/docs/Footer.astro
index dec5fb3..aff029b 100644
--- a/src/components/docs/Footer.astro
+++ b/src/components/docs/Footer.astro
@@ -18,8 +18,8 @@ const community = 'https://github.com/LeanMeanCleanApps/.github/blob/main';
Sponsor
- Made by the Lean Mean Clean Apps team with Clean Architecture, strong coffee and a healthy
- disregard for pomp and ceremony. Not affiliated with Microsoft, AWS, Uncle Bob or anyone with a marketing budget.
+ Made by the Lean Mean Clean Apps team with strong coffee and a healthy disregard for pomp and
+ ceremony. Not affiliated with Uncle Bob, a certain billionaire's chest piece, or anyone with a marketing budget.
diff --git a/src/components/docs/Header.astro b/src/components/docs/Header.astro
index 2609144..0a95ef4 100644
--- a/src/components/docs/Header.astro
+++ b/src/components/docs/Header.astro
@@ -39,7 +39,7 @@ const isBrand = key === brand.key;
)
}
-
+
@@ -126,10 +126,9 @@ const isBrand = key === brand.key;
background: var(--sl-color-gray-6);
}
- /* On a phone the organisation keeps only its mark and GitHub waits in the menu. */
+ /* On a phone the organisation keeps only its mark, and on the smallest screens it gives its space to the title. */
@media (max-width: 50rem) {
- .cl-org span,
- .cl-icon {
+ .cl-org span {
display: none;
}
@@ -137,4 +136,10 @@ const isBrand = key === brand.key;
margin-inline-end: 0.25rem;
}
}
+
+ @media (max-width: 30rem) {
+ .cl-org {
+ display: none;
+ }
+ }
diff --git a/src/components/docs/Hero.astro b/src/components/docs/Hero.astro
index bd96bcb..78fb470 100644
--- a/src/components/docs/Hero.astro
+++ b/src/components/docs/Hero.astro
@@ -18,11 +18,11 @@ const isBrand = key === brand.key;
const match = /^(.*?),?\s*(minus the ceremony\.?)$/i.exec(title);
const [lead, accent] = match ? [match[1].trim() + ',', match[2].replace(/\.?$/, '.')] : [title, ''];
-const solution = template?.key === 'lambda' ? 'WorksOnMyMachine.Reviews' : 'WorksOnMyMachine.Movies';
+const solution = 'WorksOnMyMachine.Movies';
const commands = template
? [
`dotnet new install ${template.package}`,
- `dotnet new ${template.shortName} --name ${solution} --authorName "Your Name"`,
+ `dotnet new ${template.shortName} --name ${solution}`,
`cd ${solution} && docker compose up -d`,
]
: [];
diff --git a/src/components/docs/Sidebar.astro b/src/components/docs/Sidebar.astro
index 8cb162c..ead6e56 100644
--- a/src/components/docs/Sidebar.astro
+++ b/src/components/docs/Sidebar.astro
@@ -1,7 +1,7 @@
---
-// The five templates and the brand sit at the top of the sidebar, each with its ring logo, and the sidebar below belongs to the one you're in.
+// The five templates and the brand sit at the top of the sidebar, one line each with its ring logo, and the sidebar below belongs to the one you're in.
import Default from '@astrojs/starlight/components/Sidebar.astro';
-import { brand, templates } from '../../data/templates';
+import { brand } from '../../data/templates';
const { topics } = Astro.locals.starlightSidebarTopics;
const keyOf = (link: string) => link.split('/').filter(Boolean)[0];
@@ -12,7 +12,6 @@ const keyOf = (link: string) => link.split('/').filter(Boolean)[0];
{
topics.map((topic) => {
const key = keyOf(topic.link);
- const t = key === brand.key ? brand : templates.find((x) => x.key === key);
const logo = key === brand.key ? '/logos/org-spectrum-small-tile.svg' : `/logos/${key}-small.svg`;
const hue = key === brand.key ? 'org' : key;
return (
@@ -24,7 +23,6 @@ const keyOf = (link: string) => link.split('/').filter(Boolean)[0];
>
{topic.label}
- {t && {t.subject} }
);
@@ -55,7 +53,7 @@ const keyOf = (link: string) => link.split('/').filter(Boolean)[0];
grid-template-columns: 1.5rem 1fr;
column-gap: 0.6rem;
align-items: center;
- padding: 0.35rem 0.6rem;
+ padding: 0.3rem 0.6rem;
border-radius: 0.75rem;
border: 1px solid transparent;
color: var(--sl-color-gray-2);
@@ -65,18 +63,10 @@ const keyOf = (link: string) => link.split('/').filter(Boolean)[0];
}
.cl-topics img {
- grid-row: span 2;
width: 1.5rem;
height: 1.5rem;
}
- .cl-topics small {
- grid-column: 2;
- font-weight: 400;
- font-size: var(--sl-text-xs);
- color: var(--sl-color-gray-3);
- }
-
.cl-topics a:hover {
color: var(--sl-color-white);
border-color: color-mix(in srgb, var(--cl-topic-hue) 45%, transparent);
diff --git a/src/content/docs/brand/index.mdx b/src/content/docs/brand/index.mdx
index b059987..ecb7e2b 100644
--- a/src/content/docs/brand/index.mdx
+++ b/src/content/docs/brand/index.mdx
@@ -1,7 +1,9 @@
---
-title: Lean Mean Clean Apps – Brand
+title: Brand kit
description: Logos, colours, type and social templates for every Lean Mean Clean Apps template.
template: splash
+editUrl: false
+lastUpdated: false
hero:
title: Everything that makes us look this clean
tagline: Logos, colours, type and social templates for every Lean Mean Clean Apps template. Take what you need and put it everywhere.
diff --git a/src/content/docs/graphql/explanation/architecture.mdx b/src/content/docs/graphql/explanation/architecture.mdx
index 14f9df1..7c11fcb 100644
--- a/src/content/docs/graphql/explanation/architecture.mdx
+++ b/src/content/docs/graphql/explanation/architecture.mdx
@@ -45,7 +45,7 @@ There's a little CQRS in the style of the commands and queries, but reads and wr
In textbook Clean Architecture, Presentation never references Infrastructure. Here it does, so it can wire up dependency injection without a fourth project whose only job is to call two methods.
-To keep that from turning into a free-for-all, every class in Infrastructure is `internal`. Presentation can call `AddInfrastructure` and nothing else. If you try to new up a repository from a resolver, the compiler will politely decline.
+To keep that from turning into a free-for-all, every class in Infrastructure except `DependencyInjection` is `internal`. Presentation can call `AddInfrastructure` and nothing else. If you try to new up a repository from a resolver, the compiler will politely decline.
## What happens on a query
diff --git a/src/content/docs/graphql/explanation/decisions.mdx b/src/content/docs/graphql/explanation/decisions.mdx
index c98eccd..1653b22 100644
--- a/src/content/docs/graphql/explanation/decisions.mdx
+++ b/src/content/docs/graphql/explanation/decisions.mdx
@@ -60,4 +60,4 @@ The one rule we bend is letting Presentation reference Infrastructure for depend
## Tests that don't share
-Every test class gets its own in-memory SQLite database, built from the real migrations and the same fixed seed. Tests can run in any order, in parallel, forever, and still agree with each other.
+Every test gets its own in-memory SQLite database, built from the real migrations and the same fixed seed. Tests can run in any order, in parallel, forever, and still agree with each other.
diff --git a/src/content/docs/graphql/explanation/how-its-built.mdx b/src/content/docs/graphql/explanation/how-its-built.mdx
index 5480490..9d197b7 100644
--- a/src/content/docs/graphql/explanation/how-its-built.mdx
+++ b/src/content/docs/graphql/explanation/how-its-built.mdx
@@ -1,36 +1,17 @@
---
title: "How it's built"
-description: "The pipeline, the gates and the scanners that keep every pull request honest."
+description: "How the template repository itself is run: the rules on main, the monthly release and where these docs come from."
sidebar:
order: 3
---
-import ReportCard from '../../../../components/docs/ReportCard.astro';
+This page is about the template repository, not your solution. Your solution starts with a simpler pipeline, and [Set up CI](/graphql/guides/set-up-ci/) covers what it runs, along with every check the template runs on itself and what each one needs if you want to borrow it.
-This page is about how we build the template itself. Your solution starts with a simpler pipeline, covered in [Set up CI](/graphql/guides/set-up-ci/), and you can borrow any of the checks below when you're ready.
-
-These are templates we build for fun, but the pipeline doesn't know that. Every pull request goes through the same checks, and some of them are allowed to say no.
-
-**Gatekeepers** fail the build and block the merge. **Tattletales** report what they find and leave the decision to a human.
-
-## The checks
-
-
+These are templates we build for fun, but the repository doesn't know that. Every pull request goes through the same checks, and some of them are allowed to say no.
## The rules on main
-- Every change goes through a pull request, and every check above must pass.
+- Every change goes through a pull request, and every check must pass.
- Only the maintainer can merge. Pull requests from anyone are welcome, but the green button has one owner.
- Commits must be signed, and merges are squashed so the history stays readable.
- Release tags can't be moved or deleted once they exist, so a version always means the same code.
diff --git a/src/content/docs/graphql/getting-started.mdx b/src/content/docs/graphql/getting-started.mdx
index 5416646..599039c 100644
--- a/src/content/docs/graphql/getting-started.mdx
+++ b/src/content/docs/graphql/getting-started.mdx
@@ -13,7 +13,7 @@ By the end of this page you'll have your own solution, a GraphQL API running on
You need the [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0). [Docker](https://www.docker.com/products/docker-desktop) is optional, but it gets you the Aspire dashboard for the logs, traces and metrics, and a container you can ship, so it's worth having around.
-## Make it yours
+## Create and run your solution
diff --git a/src/content/docs/graphql/guides/add-a-field.mdx b/src/content/docs/graphql/guides/add-a-field.mdx
index 1990b25..98ce4f1 100644
--- a/src/content/docs/graphql/guides/add-a-field.mdx
+++ b/src/content/docs/graphql/guides/add-a-field.mdx
@@ -7,7 +7,7 @@ sidebar:
import { Steps, Aside } from '@astrojs/starlight/components';
-This guide adds `bestReview` to the `Movie` type: the highest rated review of the movie, or null when it has none. `Movie` already carries its reviews, so this is pure Presentation work, a resolver on the type. For a field that needs new data, add a query to `src/Application` first and send it through Mediator the way the existing resolvers do.
+This guide adds `bestReview` to the `Movie` type: the highest rated review of the movie, or null when it has none. `Movie` already carries its reviews, so this is pure Presentation work, a resolver on the type. For a field that needs new data, add a query to `src/Application` first and send it through Mediator the way the existing resolvers do. [Make it yours](/graphql/guides/make-it-yours/) walks through a whole new entity, from the Application layer to the tests.
Every object type in `src/Presentation/GraphQL/Types` is a `static partial class` with `[ObjectType]`. Hot Chocolate's source generator turns the `Configure` method and any public static methods into the schema at build time, and `AddPresentationTypes()` in `ProgramExtensions.cs` registers the lot. Nothing is discovered by reflection at startup.
@@ -63,7 +63,7 @@ This guide adds `bestReview` to the `Movie` type: the highest rated review of th
curl "https://localhost:7331/graphql?sdl" > tests/Presentation.Tests.Integration/GraphQL/Schema/schema.graphql
```
- The diff is the one field, described:
+ The snapshot is copied to the test output when the project builds, so run `dotnet test` without `--no-build` or it keeps comparing the old copy. The diff is the one field, described:
```graphql ins={3-4}
"A movie that can be reviewed."
@@ -93,7 +93,7 @@ This guide adds `bestReview` to the `Movie` type: the highest rated review of th
public async Task Movie_ShouldReturn_BestReview()
{
// Arrange
- using var client = Application.CreateClient();
+ using var client = this.application.CreateClient();
using var setupResponse = await client.GetAsync("/graphql?query={movies(first:50){nodes{id,reviews{id}}}}");
var setupResult = (await setupResponse.Content.ReadAsStringAsync()).Deserialize();
var movieId = setupResult.Data.Movies.Nodes.First(m => m.Reviews.Length > 0).Id;
diff --git a/src/content/docs/graphql/guides/database.mdx b/src/content/docs/graphql/guides/database.mdx
index fca1b49..621002e 100644
--- a/src/content/docs/graphql/guides/database.mdx
+++ b/src/content/docs/graphql/guides/database.mdx
@@ -32,7 +32,7 @@ Set `ConnectionStrings__MovieReviews`. To keep everything in memory, which is wh
```bash
docker run --rm -p 7330:7330 -e DOTNET_ENVIRONMENT=docker \
-e ConnectionStrings__MovieReviews="Data Source=MovieReviews;Mode=Memory;Cache=Shared" \
- acme-movies:local
+ worksonmymachine-movies:local
```
## Add a migration
@@ -41,15 +41,21 @@ After you change the data model in `src/Infrastructure`:
```bash
dotnet tool install --global dotnet-ef
-dotnet ef migrations add AddDirectors --project ./src/Infrastructure --output-dir Databases/MovieReviews/Migrations
+dotnet ef migrations add AddDirectors \
+ --project ./src/Infrastructure \
+ --output-dir Databases/MovieReviews/Migrations \
+ --namespace WorksOnMyMachine.Movies.Infrastructure.Databases.MovieReviews.Migrations
+sed -i '1s/^\xEF\xBB\xBF//' src/Infrastructure/Databases/MovieReviews/Migrations/*.cs
```
-The next start applies it.
+Without `--namespace` the migration lands in a namespace of its own, and the `sed` strips the byte order mark the tool adds. On macOS, use `sed -i ''`. Then make `// ` the first line of the new `_AddDirectors.cs`, as it is in the other migrations, so the analysers and `dotnet format` leave it alone. Swap `WorksOnMyMachine.Movies` for your solution's name.
+
+The next start applies it, and [Make it yours](/graphql/guides/make-it-yours/) adds a whole entity this way.
- Every test class builds its own in-memory SQLite database from the same migrations and the same seed, so `dotnet test` never touches `movie-reviews.db`. You can run the tests while the API is running and neither will notice.
+ Every test builds its own in-memory SQLite database from the same migrations and the same seed, so `dotnet test` never touches `movie-reviews.db`. You can run the tests while the API is running and neither will notice.
## Swap SQLite for something real
-Everything database-shaped lives in Infrastructure, and every class there is `internal`. To move to PostgreSQL, SQL Server or anything else, implement the repository interfaces from Application (`IMoviesRepository`, `IAuthorsRepository` and `IReviewsRepository`) against the new store and register them in `Infrastructure/DependencyInjection.cs`. Application and Presentation don't need to know it happened. That's the whole sales pitch of Clean Architecture in one paragraph.
+Everything database-shaped lives in Infrastructure, and every class there but `DependencyInjection` is `internal`. To move to PostgreSQL, SQL Server or anything else, implement the repository interfaces from Application (`IMoviesRepository`, `IAuthorsRepository` and `IReviewsRepository`) against the new store and register them in `Infrastructure/DependencyInjection.cs`. Application and Presentation don't need to know it happened. That's the whole sales pitch of Clean Architecture in one paragraph.
diff --git a/src/content/docs/graphql/guides/make-it-yours.mdx b/src/content/docs/graphql/guides/make-it-yours.mdx
index bc3a819..b0cf8e3 100644
--- a/src/content/docs/graphql/guides/make-it-yours.mdx
+++ b/src/content/docs/graphql/guides/make-it-yours.mdx
@@ -17,6 +17,8 @@ Go the other way round. Build your first slice beside the sample domain, prove i
The code below assumes you named the solution `WorksOnMyMachine.Movies`. Swap in your own name wherever you see it.
+## Build the new slice
+
1. **Add the entity and use cases to `src/Application`** (`add-a-use-case`).
@@ -297,6 +299,12 @@ The code below assumes you named the solution `WorksOnMyMachine.Movies`. Swap in
Without `--namespace` the migration lands in a namespace of its own. The `sed` strips the byte order mark the tool adds. On macOS, use `sed -i ''`. Then make `// ` the first line of the new `_AddCinemas.cs`, as it is in the other migrations, so the analysers leave generated code alone.
+
+
+## Expose and test it
+
+
+
4. **Put it on the schema in `src/Presentation`** (`expose-a-use-case` and `xml-docs-and-openapi`).
A type says what a cinema looks like to a client, a query lists them and a mutation creates one. The XML comments become the descriptions in the schema, so Nitro and every client's tooling show them.
@@ -395,14 +403,12 @@ The code below assumes you named the solution `WorksOnMyMachine.Movies`. Swap in
Each new test has a sample test that already does the same job for movies or reviews. Copy it and rename it, and you have the shape, the names and the house style without having to think about them.
- | New test | Copy from |
- | --- | --- |
- | `tests/Application.Tests.Unit/Cinemas/Queries/GetCinemas/GetCinemasHandlerTests.cs` | `Movies/Queries/GetMovies/GetMoviesHandlerTests.cs` |
- | `tests/Application.Tests.Unit/Cinemas/Queries/GetCinemas/GetCinemasValidatorTests.cs` | `Movies/Queries/GetMovies/GetMoviesValidatorTests.cs` |
- | `tests/Application.Tests.Unit/Cinemas/Commands/CreateCinema/CreateCinemaHandlerTests.cs` | `Reviews/Commands/CreateReview/CreateReviewHandlerTests.cs` |
- | `tests/Application.Tests.Unit/Cinemas/Commands/CreateCinema/CreateCinemaValidatorTests.cs` | `Reviews/Commands/CreateReview/CreateReviewValidatorTests.cs` |
- | A `Cinemas` region in `tests/Infrastructure.Tests.Integration/Databases/MovieReviews/EntityFrameworkMovieReviewsRepositoryTests.cs` | The `Movies` region: search, paging and create |
- | `tests/Presentation.Tests.Integration/GraphQL/Cinemas/CinemaTests.cs` and `CinemaData.cs` | `Movies/MovieTests.cs` for the list, `Reviews/ReviewTests.cs` for the mutation and its validation error |
+ - `tests/Application.Tests.Unit/Cinemas/Queries/GetCinemas/GetCinemasHandlerTests.cs`, copied from `Movies/Queries/GetMovies/GetMoviesHandlerTests.cs`.
+ - `tests/Application.Tests.Unit/Cinemas/Queries/GetCinemas/GetCinemasValidatorTests.cs`, copied from `Movies/Queries/GetMovies/GetMoviesValidatorTests.cs`.
+ - `tests/Application.Tests.Unit/Cinemas/Commands/CreateCinema/CreateCinemaHandlerTests.cs`, copied from `Reviews/Commands/CreateReview/CreateReviewHandlerTests.cs`.
+ - `tests/Application.Tests.Unit/Cinemas/Commands/CreateCinema/CreateCinemaValidatorTests.cs`, copied from `Reviews/Commands/CreateReview/CreateReviewValidatorTests.cs`.
+ - A `Cinemas` region in `tests/Infrastructure.Tests.Integration/Databases/MovieReviews/EntityFrameworkMovieReviewsRepositoryTests.cs`, copied from the `Movies` region: search, paging and create.
+ - `tests/Presentation.Tests.Integration/GraphQL/Cinemas/CinemaTests.cs` and `CinemaData.cs`, copied from `Movies/MovieTests.cs` for the list, `Reviews/ReviewTests.cs` for the mutation and its validation error.
The repository tests need some cinemas. Add three to `AddTestData` in `tests/Infrastructure.Tests.Integration/Databases/MovieReviews/Extensions/MovieReviewsCollectionData.cs`, named the same way as the sample rows:
@@ -437,6 +443,12 @@ The code below assumes you named the solution `WorksOnMyMachine.Movies`. Swap in
Both domains now run side by side. Stop here if you're keeping the sample for reference, which is a perfectly respectable choice.
+
+
+## Take out the sample
+
+
+
6. **Take out the sample movies, authors and reviews** (`sample-data` and `database-migrations`).
This step is mostly deleting, but the template's own tests use the sample as their test subject, so a few of them change instead of going. Work through it layer by layer and build as you go. The compiler finds what's left faster than any list.
@@ -471,17 +483,14 @@ The code below assumes you named the solution `WorksOnMyMachine.Movies`. Swap in
- `EntityFrameworkMovieReviewsRepositoryTests.cs`: the `Authors`, `Movies` and `Reviews` regions.
- `MovieReviewsCollectionData.cs`: the author, movie and review rows.
- **Tests to change:** these test the template's plumbing, and they used the sample as their subject. Point them at cinemas.
-
- | Test | Change |
- | --- | --- |
- | `Common/Behaviours/LoggingBehaviourTests.cs` and `ValidationBehaviourTests.cs` | Use `CreateCinemaCommand`, `CreateCinemaValidator` and `Cinema` in place of `GetAuthorByIdQuery`, its validator and `Author`. The invalid case is an empty `Name`, so the expected property is `Name`. The logged message names `CreateCinemaCommand`. |
- | `Common/Exceptions/NotFoundExceptionTests.cs` | Use `EntityType.Cinema` and a `Cinema`. The message becomes "The Cinema with the supplied id was not found." |
- | `MovieReviewsConfigurationTests.cs` | Check a cinema loads, ignoring case: look up `"one"` and expect `"One"`. |
- | `DependencyInjectionTests.cs` | Expect `ICinemasRepository`, 5 seeded cinemas and none when seeding is off. Use `GetCinemas` for the concurrency and same-sample-data checks. |
- | `GraphQL/Common/Data.cs` and `Payload.cs` | Keep `cinemas`, `version` and `createCinema` in `Data`, and drop `ReviewPayload` and `DeleteReviewPayload`. |
- | `GraphQL/Schema/schema.graphql` | Copy the new SDL in again, as in step 5. `NotFoundError` goes with the review mutations, because nothing left can raise it. |
- | `Telemetry/TelemetryTests.cs` | Send `{cinemas(first:5){nodes{id}}}` and expect a `GetCinemasQuery` span. |
- | `Authentication/AuthenticationTests.cs`, only with `--include-auth` | Send `{cinemas{nodes{id}}}` in place of the movie, author and review queries, and check `Cinemas` and `Version` come back empty. |
+ - **`Common/Behaviours/LoggingBehaviourTests.cs` and `ValidationBehaviourTests.cs`:** Use `CreateCinemaCommand`, `CreateCinemaValidator` and `Cinema` in place of `GetAuthorByIdQuery`, its validator and `Author`. The invalid case is an empty `Name`, so the expected property is `Name`. The logged message names `CreateCinemaCommand`.
+ - **`Common/Exceptions/NotFoundExceptionTests.cs`:** Use `EntityType.Cinema` and a `Cinema`. The message becomes "The Cinema with the supplied id was not found."
+ - **`MovieReviewsConfigurationTests.cs`:** Check a cinema loads, ignoring case: look up `"one"` and expect `"One"`.
+ - **`DependencyInjectionTests.cs`:** Expect `ICinemasRepository`, 5 seeded cinemas and none when seeding is off. Use `GetCinemas` for the concurrency and same-sample-data checks.
+ - **`GraphQL/Common/Data.cs` and `Payload.cs`:** Keep `cinemas`, `version` and `createCinema` in `Data`, and drop `ReviewPayload` and `DeleteReviewPayload`.
+ - **`GraphQL/Schema/schema.graphql`:** Copy the new SDL in again, as in step 5. `NotFoundError` goes with the review mutations, because nothing left can raise it.
+ - **`Telemetry/TelemetryTests.cs`:** Send `{cinemas(first:5){nodes{id}}}` and expect a `GetCinemasQuery` span.
+ - **`Authentication/AuthenticationTests.cs`, only with `--include-auth`:** Send `{cinemas{nodes{id}}}` in place of the movie, author and review queries, and check `Cinemas` and `Version` come back empty.
7. **Check it the way CI will** (`coverage`).
@@ -501,4 +510,4 @@ The code below assumes you named the solution `WorksOnMyMachine.Movies`. Swap in
## What's still called "movie reviews"
-The database plumbing keeps its sample name: the `Databases/MovieReviews` folder, `MovieReviewsDbContext`, `EntityFrameworkMovieReviewsRepository`, the `MovieReviews` connection string and the `movie-reviews.db` file. They all work as they are. Everything in `src/Infrastructure` is `internal`, so renaming them later is a refactor that never reaches Application or Presentation. Do it when the name starts to bother you, and change the connection string in every `appsettings.*.json` and in `docker-compose.yml` at the same time.
+The database plumbing keeps its sample name: the `Databases/MovieReviews` folder, `MovieReviewsDbContext`, `EntityFrameworkMovieReviewsRepository`, the `MovieReviews` connection string and the `movie-reviews.db` file. They all work as they are. Everything in `src/Infrastructure` apart from `DependencyInjection` is `internal`, so renaming them later is a refactor that never reaches Application or Presentation. Do it when the name starts to bother you, and change the connection string in every `appsettings.*.json` and in `docker-compose.yml` at the same time.
diff --git a/src/content/docs/graphql/guides/run-locally.mdx b/src/content/docs/graphql/guides/run-locally.mdx
index 0b66cb9..947c0c1 100644
--- a/src/content/docs/graphql/guides/run-locally.mdx
+++ b/src/content/docs/graphql/guides/run-locally.mdx
@@ -28,8 +28,8 @@ There are two ready-made environments. `local` is for `dotnet run`: sample data
```bash
- docker build . --target run --tag acme-movies:local
- docker run --rm -p 7330:7330 -e DOTNET_ENVIRONMENT=docker acme-movies:local
+ docker build . --target run --tag worksonmymachine-movies:local
+ docker run --rm -p 7330:7330 -e DOTNET_ENVIRONMENT=docker worksonmymachine-movies:local
```
Without Compose there's no collector, so the logs stay on the console and nothing else is exported. Set `OTEL_EXPORTER_OTLP_ENDPOINT` to send them to a collector of your own.
@@ -89,7 +89,7 @@ mutation {
dotnet test
```
-Each test class builds its own in-memory SQLite database from the migrations and the same fixed seed. No test can change what another one sees, and nothing touches `movie-reviews.db`. One of the tests compares `/graphql?sdl` with `tests/Presentation.Tests.Integration/GraphQL/Schema/schema.graphql`, so a schema change you didn't mean to make fails the build before anyone's client notices.
+The tests build their own in-memory SQLite databases from the same migrations and seed, so nothing touches `movie-reviews.db`. [Test your changes](/graphql/guides/test-your-changes/) covers where a test goes, how the tests are written and how to check coverage before CI does.
## Run just one part of the Dockerfile
@@ -112,7 +112,7 @@ A failing test doesn't fail the build, so the results still come out. Check `out
## Stamp a version
```bash
-docker build . --target run --tag acme-movies:latest \
+docker build . --target run --tag worksonmymachine-movies:latest \
--build-arg VERSION_PREFIX=1.2.3 --build-arg VERSION_SUFFIX=beta
```
diff --git a/src/content/docs/graphql/guides/set-up-ci.mdx b/src/content/docs/graphql/guides/set-up-ci.mdx
index eef467a..f554683 100644
--- a/src/content/docs/graphql/guides/set-up-ci.mdx
+++ b/src/content/docs/graphql/guides/set-up-ci.mdx
@@ -1,25 +1,47 @@
---
title: "Set up CI"
-description: "Your solution starts with a simple pipeline. Here's what it runs and how to add more."
+description: "Your solution starts with a simple pipeline on purpose. Here's what it runs, and the checks you can add when your process calls for them."
sidebar:
order: 7
---
import { Steps } from '@astrojs/starlight/components';
+import ReportCard from '../../../../components/docs/ReportCard.astro';
-Whichever way you made your solution, it starts with a simple pipeline: format, build, and test with a 95% coverage bar. Those are the same commands as in `AGENTS.md`, so the pipeline and your laptop agree on what "green" means. It needs no secrets, no accounts and no paid features. Everything else is yours to add when you want it.
+Your solution starts with a simple pipeline: format, build, and test with a 95% coverage bar. Those are the same commands as in `AGENTS.md`, so the pipeline and your laptop agree on what "green" means. It needs no secrets, no accounts and no paid features, and it's green on the first push.
-## Made with `dotnet new`
+It's minimal on purpose. We don't know your review process, your security team or your deployment target, so the pipeline stops where guessing would start. Everything else on this page is a check you can add when your process calls for it.
+
+## What runs
You get `.github/workflows/ci.yml`, one GitHub Actions job that restores, checks formatting, builds, runs every test and checks the coverage total. Push to GitHub and it runs.
Not on GitHub Actions? Ask your coding agent to move it. It works as Jacques, so it uses the `ci-for-your-solution` skill in `.agents/skills` to port the same commands to Azure Pipelines, GitLab, or the Jenkins box under someone's desk, then breaks something on purpose to prove the new pipeline notices.
-## Made with "Use this template"
+## Adding more
+
+The `ci-for-your-solution` skill lists the checks you can add, what each one catches and what it needs, from a vulnerable package check that needs nothing to CodeQL, which needs Advanced Security on a private repository. Ask your agent to add one and it will agree it with you first.
+
+For a worked example of each, look at the template's own [Build Pipeline](https://github.com/LeanMeanCleanApps/CleanGraphQLApi/blob/main/.github/workflows/build-pipeline.yml). Every pull request to the template goes through these checks, and each one is a job you can lift into `ci.yml`. **Gatekeepers** fail the build and block the merge. **Tattletales** report what they find and leave the decision to a human.
+
+
-Your copy has our Build Pipeline. Outside the LeanMeanCleanApps organisation it runs the format check, build and tests, and the Summary shows the rest as not run. That covers CodeQL, Dependency Review, the container scan, and the steps that pack, verify and publish the template. So a private copy goes green without GitHub Advanced Security, and it never tries to publish to NuGet on your behalf.
+Borrow freely, but leave out the jobs that pack, verify and publish the template. You're shipping the API, not a template. Whatever you add, run the same commands locally that the pipeline runs, and make the checks you rely on required on `main`.
-### After you copy
+## If you used "Use this template"
+
+A copy made with GitHub's **Use this template** button has the whole Build Pipeline rather than `ci.yml`. Outside the LeanMeanCleanApps organisation it runs the format check, build and tests, and the Summary shows the rest as not run: CodeQL, Dependency Review, the container scan, and the steps that pack, verify and publish the template. So a private copy goes green without GitHub Advanced Security, and it never tries to publish to NuGet on your behalf. Three things to change after you copy:
@@ -30,9 +52,3 @@ Your copy has our Build Pipeline. Outside the LeanMeanCleanApps organisation it
3. Fill in **Where we track work** in `AGENTS.md`, so agents put their plans and handoffs where your team will see them.
-
-## Adding more
-
-The `ci-for-your-solution` skill lists the checks you can add, what each one catches and what it needs, from a vulnerable package check that needs nothing to CodeQL, which needs Advanced Security on a private repository. Ask your agent to add one and it will agree it with you first.
-
-For a worked example of every check, read our [Build Pipeline](https://github.com/LeanMeanCleanApps/CleanGraphQLApi/blob/main/.github/workflows/build-pipeline.yml). Borrow freely, but leave out the jobs that pack and publish the template. You're shipping an app, not a template.
diff --git a/src/content/docs/graphql/guides/ship-the-container.mdx b/src/content/docs/graphql/guides/ship-the-container.mdx
index 630e6e8..fe16af1 100644
--- a/src/content/docs/graphql/guides/ship-the-container.mdx
+++ b/src/content/docs/graphql/guides/ship-the-container.mdx
@@ -12,7 +12,7 @@ The `run` stage of the `Dockerfile` is the whole API: one image, built on `mcr.m
## Build it
```bash
-docker build . --target run --tag acme-movies:1.2.3 \
+docker build . --target run --tag worksonmymachine-movies:1.2.3 \
--build-arg VERSION_PREFIX=1.2.3
```
@@ -32,13 +32,13 @@ For production, pick a name like `production`, add an `appsettings.production.js
## Put it behind HTTPS
-The container only listens on HTTP. Outside the `local` and `Development` environments it sends an HSTS header and redirects HTTP to HTTPS, which means it expects something in front of it to end TLS: a load balancer, a reverse proxy or your platform's ingress. Tell it so:
+The container only listens on HTTP, and it expects something in front of it to end TLS: a load balancer, a reverse proxy or your platform's ingress. Outside the `local` and `Development` environments it sends an HSTS header, and it redirects HTTP to HTTPS once it knows an HTTPS port to send people to, such as `ASPNETCORE_HTTPS_PORT=443`. Tell it about the proxy too:
```bash
ASPNETCORE_FORWARDEDHEADERS_ENABLED=true
```
-Without that, the API thinks every request arrived over plain HTTP and redirects it, and the proxy sends it back, and the two of them go round and round until someone's pager goes off.
+Without that, the API thinks every request arrived over plain HTTP. Give it an HTTPS port in that state and it redirects the proxy's requests, the proxy sends them back, and the two of them go round and round until someone's pager goes off.
## Wire up the health checks
diff --git a/src/content/docs/graphql/guides/test-your-changes.mdx b/src/content/docs/graphql/guides/test-your-changes.mdx
index 8ed2941..c679b0e 100644
--- a/src/content/docs/graphql/guides/test-your-changes.mdx
+++ b/src/content/docs/graphql/guides/test-your-changes.mdx
@@ -14,7 +14,7 @@ Your solution starts with four test projects and a pipeline that fails under 95%
| Project | What it tests | How |
| --- | --- | --- |
| `tests/Application.Tests.Unit` | Handlers, validators and pipeline behaviours | xUnit, Shouldly and NSubstitute, with the repositories substituted |
-| `tests/Infrastructure.Tests.Integration` | The repositories against a real database | A private in-memory SQLite database per test class, built from the migrations and the fixed seed |
+| `tests/Infrastructure.Tests.Integration` | The repositories against a real database | A private in-memory SQLite database per test, built from the migrations and the fixed seed |
| `tests/Presentation.Tests.Unit` | The error filter, cursors and settings | Plain unit tests, no host |
| `tests/Presentation.Tests.Integration` | Real GraphQL requests for every query and mutation, authentication, health checks, and the schema snapshot | `WebApplicationFactory` with a private database |
@@ -71,4 +71,5 @@ The last table it prints is the merged total. `Program.cs`, the EF Core migratio
## Where to next
- [Add a field](/graphql/guides/add-a-field/) ends with a test for the new field.
+- [Make it yours](/graphql/guides/make-it-yours/) adds a whole entity, then clears the sample out without dropping under the coverage gate.
- [Work with a coding agent](/graphql/guides/work-with-a-coding-agent/): the `test-design`, `unit-tests`, `integration-tests` and `coverage` skills hold these same rules, so an agent writes tests the same way you would.
diff --git a/src/content/docs/graphql/guides/work-with-a-coding-agent.mdx b/src/content/docs/graphql/guides/work-with-a-coding-agent.mdx
index 429d1a7..0553dc2 100644
--- a/src/content/docs/graphql/guides/work-with-a-coding-agent.mdx
+++ b/src/content/docs/graphql/guides/work-with-a-coding-agent.mdx
@@ -23,11 +23,11 @@ The skills are written for this solution, not for .NET in general. `add-a-use-ca
## Get started
1. Open the solution in your agent of choice. Most read `AGENTS.md` without being asked. If yours doesn't, start with "Read AGENTS.md".
-2. Ask for a change the way you'd ask a colleague: "Add a query that lists the reviews an author wrote, with paging, and put it on a query."
+2. Ask for a change the way you'd ask a colleague: "Add a query that lists the reviews an author wrote, with paging, and expose it in the schema."
3. For anything bigger than a few lines, Jacques agrees a plan first: the open decisions one at a time, each with a recommendation, then the scope in a paragraph. Answer those and the work starts.
4. Before it says it's done, the agent runs `dotnet build`, `dotnet test` and `dotnet format --verify-no-changes`, the same checks as CI.
-## Make it yours
+## Tune it for your team
- **Where we track work.** `AGENTS.md` has a line that says "Not set". Replace it with your GitHub project, Jira board or Azure Boards, and agents put their plans and handoffs there instead of asking each time.
- **Change a rule, change the skill.** When your team decides to do something differently, update the matching `SKILL.md` in the same pull request. A skill that disagrees with the code is worse than no skill at all.
diff --git a/src/content/docs/graphql/index.mdx b/src/content/docs/graphql/index.mdx
index 67b50ba..54c9ab8 100644
--- a/src/content/docs/graphql/index.mdx
+++ b/src/content/docs/graphql/index.mdx
@@ -2,6 +2,8 @@
title: Clean GraphQL API
description: A .NET 10 dotnet new template for a GraphQL API on Hot Chocolate, built with Clean Architecture minus the ceremony.
template: splash
+editUrl: false
+lastUpdated: false
prev: false
next: false
hero:
diff --git a/src/content/docs/graphql/reference/configuration.mdx b/src/content/docs/graphql/reference/configuration.mdx
index aa489b3..0012586 100644
--- a/src/content/docs/graphql/reference/configuration.mdx
+++ b/src/content/docs/graphql/reference/configuration.mdx
@@ -51,7 +51,7 @@ The app always collects [OpenTelemetry](https://opentelemetry.io) traces and met
| `OTEL_EXPORTER_OTLP_ENDPOINT` | none | Where to send traces, metrics and logs. Nothing is exported until it's set. Docker Compose sets it to the `otel-collector` service |
| `OTEL_EXPORTER_OTLP_PROTOCOL` | `grpc` | `grpc` or `http/protobuf` |
| `OTEL_EXPORTER_OTLP_HEADERS` | none | Headers for every export, such as an API key for a hosted backend |
-| `OTEL_SERVICE_NAME` | `CleanGraphQLApi` | The `service.name` on everything sent |
+| `OTEL_SERVICE_NAME` | Your solution's name | The `service.name` on everything sent |
Every span, metric and log also carries `service.version`, the version stamped into the build, and `deployment.environment.name`.
diff --git a/src/content/docs/graphql/reference/project-structure.mdx b/src/content/docs/graphql/reference/project-structure.mdx
index 72b2722..19f5142 100644
--- a/src/content/docs/graphql/reference/project-structure.mdx
+++ b/src/content/docs/graphql/reference/project-structure.mdx
@@ -63,4 +63,4 @@ import { FileTree } from '@astrojs/starlight/components';
| --- | --- | --- |
| Presentation | Application, Infrastructure | References Infrastructure only to wire up dependency injection |
| Application | none | Owns the entities, the repository interfaces and the rules |
-| Infrastructure | Application | Every class is `internal` |
+| Infrastructure | Application | Every class but `DependencyInjection` is `internal` |
diff --git a/src/content/docs/lambda/explanation/architecture.mdx b/src/content/docs/lambda/explanation/architecture.mdx
index 43eaaed..82e5fe7 100644
--- a/src/content/docs/lambda/explanation/architecture.mdx
+++ b/src/content/docs/lambda/explanation/architecture.mdx
@@ -1,6 +1,6 @@
---
-title: Architecture
-description: The three layers, the one rule we bend, and what happens when the function is invoked.
+title: "Architecture"
+description: "The three layers, the one rule we bend, and what happens when the function is invoked."
sidebar:
order: 1
---
@@ -45,7 +45,7 @@ There's a little CQRS in the style of the commands and queries, but reads and wr
In textbook Clean Architecture, Presentation never references Infrastructure. Here it does, so it can wire up dependency injection without a fourth project whose only job is to call two methods.
-To keep that from turning into a free-for-all, every class in Infrastructure is `internal`. Presentation can call `AddInfrastructure` and nothing else. If you try to new up a repository from the handler, the compiler will politely decline.
+To keep that from turning into a free-for-all, every class in Infrastructure except `DependencyInjection` is `internal`. Presentation can call `AddInfrastructure` and nothing else. If you try to new up a repository from the handler, the compiler will politely decline.
## What happens on an invocation
diff --git a/src/content/docs/lambda/explanation/decisions.mdx b/src/content/docs/lambda/explanation/decisions.mdx
index 0ca81a8..12ba1e7 100644
--- a/src/content/docs/lambda/explanation/decisions.mdx
+++ b/src/content/docs/lambda/explanation/decisions.mdx
@@ -1,6 +1,6 @@
---
-title: Why it's built this way
-description: The choices behind the template, and what we'd tell you if you asked over a coffee.
+title: "Why it's built this way"
+description: "The choices behind the template, and what we'd tell you if you asked over a coffee."
sidebar:
order: 2
---
@@ -39,4 +39,4 @@ The one rule we bend is letting Presentation reference Infrastructure for depend
## Tests that don't share
-Every test class gets its own in-memory SQLite database, built from the real migrations and the same fixed seed. Tests can run in any order, in parallel, forever, and still agree with each other.
+Every test gets its own in-memory SQLite database, built from the real migrations and the same fixed seed. Tests can run in any order, in parallel, forever, and still agree with each other.
diff --git a/src/content/docs/lambda/explanation/how-its-built.mdx b/src/content/docs/lambda/explanation/how-its-built.mdx
index 3c469c8..e4834a9 100644
--- a/src/content/docs/lambda/explanation/how-its-built.mdx
+++ b/src/content/docs/lambda/explanation/how-its-built.mdx
@@ -1,36 +1,17 @@
---
-title: How it's built
-description: The pipeline, the gates and the scanners that keep every pull request honest.
+title: "How it's built"
+description: "How the template repository itself is run: the rules on main, the monthly release and where these docs come from."
sidebar:
order: 3
---
-import ReportCard from '../../../../components/docs/ReportCard.astro';
+This page is about the template repository, not your solution. Your solution starts with a simpler pipeline, and [Set up CI](/lambda/guides/set-up-ci/) covers what it runs, along with every check the template runs on itself and what each one needs if you want to borrow it.
-This page is about how we build the template itself. Your solution starts with a simpler pipeline, covered in [Set up CI](/lambda/guides/set-up-ci/), and you can borrow any of the checks below when you're ready.
-
-These are templates we build for fun, but the pipeline doesn't know that. Every pull request goes through the same checks, and some of them are allowed to say no.
-
-**Gatekeepers** fail the build and block the merge. **Tattletales** report what they find and leave the decision to a human.
-
-## The checks
-
-
+These are templates we build for fun, but the repository doesn't know that. Every pull request goes through the same checks, and some of them are allowed to say no.
## The rules on main
-- Every change goes through a pull request, and every check above must pass.
+- Every change goes through a pull request, and every check must pass.
- Only the maintainer can merge. Pull requests from anyone are welcome, but the green button has one owner.
- Commits must be signed, and merges are squashed so the history stays readable.
- Release tags can't be moved or deleted once they exist, so a version always means the same code.
diff --git a/src/content/docs/lambda/getting-started.mdx b/src/content/docs/lambda/getting-started.mdx
index e1c8ebb..dede5e6 100644
--- a/src/content/docs/lambda/getting-started.mdx
+++ b/src/content/docs/lambda/getting-started.mdx
@@ -1,11 +1,11 @@
---
-title: Get started
-description: Install the template, create a solution and invoke your first Lambda function locally.
+title: "Get started"
+description: "Install the template, create a solution and invoke your first Lambda function locally."
sidebar:
order: 1
---
-import { Steps, FileTree, Tabs, TabItem, LinkCard, CardGrid } from '@astrojs/starlight/components';
+import { Steps, FileTree, LinkCard, CardGrid } from '@astrojs/starlight/components';
By the end of this page you'll have your own solution, a container running it, and a function that answers back. It takes about five minutes, or ten if Docker decides to download the internet first.
@@ -13,7 +13,7 @@ By the end of this page you'll have your own solution, a container running it, a
You need the [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) and [Docker](https://www.docker.com/products/docker-desktop). You don't need an AWS account yet. The container image ships with the Lambda runtime emulator, so your laptop can pretend to be AWS for a while.
-## Make it yours
+## Create and run your solution
@@ -26,8 +26,8 @@ You need the [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) an
2. Create a solution. The name becomes your namespace, project names and folder, so pick something you're happy to see a lot of.
```bash
- dotnet new cleanlambdafunction --name WorksOnMyMachine.Reviews --authorName "Your Name"
- cd WorksOnMyMachine.Reviews
+ dotnet new cleanlambdafunction --name WorksOnMyMachine.Movies --authorName "Your Name"
+ cd WorksOnMyMachine.Movies
```
Rather click? The template shows up in the new project dialog in Visual Studio and Rider with the same options. Fill them in properly. You never know who's reading and handing out .NET Rocks mugs... Hi Richard!
@@ -37,7 +37,7 @@ You need the [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) an
3. Have a look around. Everything you need is already here.
- - WorksOnMyMachine.Reviews.slnx
+ - WorksOnMyMachine.Movies.slnx
- Dockerfile
- docker-compose.yml
- src/
@@ -98,7 +98,7 @@ Hit a snag? Check [Troubleshooting](/lambda/reference/troubleshooting/).
## Where to next
-
+
diff --git a/src/content/docs/lambda/guides/add-a-function.mdx b/src/content/docs/lambda/guides/add-a-function.mdx
index ac04c51..217bd3e 100644
--- a/src/content/docs/lambda/guides/add-a-function.mdx
+++ b/src/content/docs/lambda/guides/add-a-function.mdx
@@ -1,6 +1,6 @@
---
-title: Add a function
-description: Expose a query from the Application layer as a new Lambda function, with its own request, response and test.
+title: "Add a function"
+description: "Expose a query from the Application layer as a new Lambda function, with its own request, response and test."
sidebar:
order: 3
---
@@ -10,7 +10,7 @@ import { Steps, Aside } from '@astrojs/starlight/components';
This guide adds a `GetMovie` function that returns one movie with all of its reviews. The Application layer already has a `GetMovieByIdQuery`, so all the work happens in Presentation. That's the point of the layers: new ways in, same rules underneath.
- If the query you want doesn't exist yet, add it to `src/Application` first: a query class, a handler and a FluentValidation validator, next to the others. Mediator and the validation behaviour pick them up automatically.
+ If the query you want doesn't exist yet, add it to `src/Application` first: a query class, a handler and a FluentValidation validator, next to the others. Mediator and the validation behaviour pick them up automatically. [Make it yours](/lambda/guides/make-it-yours/) walks through a whole new entity, from the Application layer to the tests.
@@ -18,7 +18,7 @@ This guide adds a `GetMovie` function that returns one movie with all of its rev
1. Add a request model in `src/Presentation/LambdaFunction/Models/GetMovieRequest.cs`. This is the JSON your callers send.
```csharp
- namespace WorksOnMyMachine.Reviews.Presentation.LambdaFunction.Models;
+ namespace WorksOnMyMachine.Movies.Presentation.LambdaFunction.Models;
///
/// Gets one movie and its reviews.
@@ -33,8 +33,14 @@ This guide adds a `GetMovie` function that returns one movie with all of its rev
2. Add a response model in `src/Presentation/LambdaFunction/Models/MovieDetail.cs`. It reuses the existing `Review` model.
```csharp
- namespace WorksOnMyMachine.Reviews.Presentation.LambdaFunction.Models;
+ namespace WorksOnMyMachine.Movies.Presentation.LambdaFunction.Models;
+ ///
+ /// A movie and every review of it.
+ ///
+ /// The unique identifier of the movie.
+ /// The title of the movie.
+ /// The reviews written about the movie.
public record MovieDetail(Guid Id, string Title, List Reviews);
```
@@ -61,7 +67,7 @@ This guide adds a `GetMovie` function that returns one movie with all of its rev
public partial class LambdaFunctionJsonSerializerContext : JsonSerializerContext
```
-5. Add the function to `LambdaFunction.cs`. `[LambdaFunction]` tells Lambda Annotations to generate the class Lambda calls and to add the function to `serverless.template`. `Send` opens a scope, runs the query through validation and stops it if the invocation runs out of time.
+5. Add the function to `LambdaFunction.cs`. `[LambdaFunction]` tells Lambda Annotations to generate the class Lambda calls and to add the function to `serverless.template`. `Invoke` wraps the whole invocation in one trace span and flushes the metrics when it ends, and `Send` opens a scope, runs the query through validation and stops it if the invocation runs out of time. Every function uses both.
```csharp
///
@@ -73,9 +79,12 @@ This guide adds a `GetMovie` function that returns one movie with all of its rev
[LambdaFunction(ResourceName = "GetMovie", PackageType = LambdaPackageType.Image, MemorySize = 256, Timeout = 30)]
public async Task GetMovie(GetMovieRequest request, ILambdaContext context)
{
- var movie = await this.Send(new GetMovieByIdQuery { Id = request.Id }, context);
+ return await this.Invoke(request, context, async () =>
+ {
+ var movie = await this.Send(new GetMovieByIdQuery { Id = request.Id }, context);
- return this.mapper.ToPresentation(movie);
+ return this.mapper.ToPresentation(movie);
+ });
}
```
@@ -102,9 +111,9 @@ This guide adds a `GetMovie` function that returns one movie with all of its rev
7. Run it.
```bash
- docker build . --target run --tag acme-reviews:local
- docker run --rm -p 7350:8080 -e Database__SeedSampleData=true acme-reviews:local \
- "Presentation::WorksOnMyMachine.Reviews.Presentation.LambdaFunction.Handler_GetMovie_Generated::GetMovie"
+ docker build . --target run --tag worksonmymachine-movies:local
+ docker run --rm -p 7350:8080 -e Database__SeedSampleData=true worksonmymachine-movies:local \
+ "Presentation::WorksOnMyMachine.Movies.Presentation.LambdaFunction.Handler_GetMovie_Generated::GetMovie"
curl -X POST "http://localhost:7350/2015-03-31/functions/function/invocations" \
-d '{"id":"cb188ef6-14e7-f85f-2f78-924c2ab26b28"}'
diff --git a/src/content/docs/lambda/guides/database.mdx b/src/content/docs/lambda/guides/database.mdx
index 9aef504..e85edc6 100644
--- a/src/content/docs/lambda/guides/database.mdx
+++ b/src/content/docs/lambda/guides/database.mdx
@@ -1,6 +1,6 @@
---
-title: Work with the database
-description: Where the SQLite database lives, how sample data works, and how to add a migration or swap the database out.
+title: "Work with the database"
+description: "Where the SQLite database lives, how sample data works, and how to add a migration or swap the database out."
sidebar:
order: 4
---
@@ -25,7 +25,7 @@ Set `ConnectionStrings__MovieReviews`. To keep everything in memory:
docker run --rm -p 7350:8080 \
-e ConnectionStrings__MovieReviews="Data Source=MovieReviews;Mode=Memory;Cache=Shared" \
-e Database__SeedSampleData=true \
- acme-reviews:local
+ worksonmymachine-movies:local
```
## Add a migration
@@ -34,11 +34,17 @@ After you change the data model in `src/Infrastructure`:
```bash
dotnet tool install --global dotnet-ef
-dotnet ef migrations add AddDirectors --project ./src/Infrastructure --output-dir Databases/MovieReviews/Migrations
+dotnet ef migrations add AddDirectors \
+ --project ./src/Infrastructure \
+ --output-dir Databases/MovieReviews/Migrations \
+ --namespace WorksOnMyMachine.Movies.Infrastructure.Databases.MovieReviews.Migrations
+sed -i '1s/^\xEF\xBB\xBF//' src/Infrastructure/Databases/MovieReviews/Migrations/*.cs
```
-The next container start applies it.
+Without `--namespace` the migration lands in a namespace of its own, and the `sed` strips the byte order mark the tool adds. On macOS, use `sed -i ''`. Then make `// ` the first line of the new `_AddDirectors.cs`, as it is in the other migrations, so the analysers and `dotnet format` leave it alone. Swap `WorksOnMyMachine.Movies` for your solution's name.
+
+The next container start applies it, and [Make it yours](/lambda/guides/make-it-yours/) adds a whole entity this way.
## Swap SQLite for something real
-Everything database-shaped lives in Infrastructure, and every class there is `internal`. To move to DynamoDB, Aurora or anything else, implement the repository interfaces from Application (`IMoviesRepository`, `IAuthorsRepository` and `IReviewsRepository`) against the new store and register them in `Infrastructure/DependencyInjection.cs`. Application and Presentation don't need to know it happened. That's the whole sales pitch of Clean Architecture in one paragraph.
+Everything database-shaped lives in Infrastructure, and every class there but `DependencyInjection` is `internal`. To move to DynamoDB, Aurora or anything else, implement the repository interfaces from Application (`IMoviesRepository`, `IAuthorsRepository` and `IReviewsRepository`) against the new store and register them in `Infrastructure/DependencyInjection.cs`. Application and Presentation don't need to know it happened. That's the whole sales pitch of Clean Architecture in one paragraph.
diff --git a/src/content/docs/lambda/guides/deploy-to-aws.mdx b/src/content/docs/lambda/guides/deploy-to-aws.mdx
index 4b9a18a..0812353 100644
--- a/src/content/docs/lambda/guides/deploy-to-aws.mdx
+++ b/src/content/docs/lambda/guides/deploy-to-aws.mdx
@@ -1,6 +1,6 @@
---
-title: Deploy to AWS
-description: Deploy every function to AWS Lambda as one CloudFormation stack with the Amazon.Lambda.Tools CLI.
+title: "Deploy to AWS"
+description: "Deploy every function to AWS Lambda as one CloudFormation stack with the Amazon.Lambda.Tools CLI."
sidebar:
order: 5
---
@@ -25,7 +25,7 @@ The functions deploy as one container image and one CloudFormation stack. `src/P
"region": "ap-southeast-2",
"configuration": "Release",
"template": "serverless.template",
- "stack-name": "acme-reviews"
+ "stack-name": "worksonmymachine-movies"
}
```
@@ -40,7 +40,7 @@ The functions deploy as one container image and one CloudFormation stack. `src/P
4. Invoke it for real. CloudFormation names each function after the stack and the resource, so look the name up first.
```bash
- FUNCTION=$(aws cloudformation describe-stack-resource --stack-name acme-reviews \
+ FUNCTION=$(aws cloudformation describe-stack-resource --stack-name worksonmymachine-movies \
--logical-resource-id GetMovies --query StackResourceDetail.PhysicalResourceId --output text)
aws lambda invoke --function-name "$FUNCTION" \
@@ -50,7 +50,7 @@ The functions deploy as one container image and one CloudFormation stack. `src/P
Or call it over HTTP. The stack's `ApiUrl` output is the API Gateway address.
```bash
- API_URL=$(aws cloudformation describe-stacks --stack-name acme-reviews \
+ API_URL=$(aws cloudformation describe-stacks --stack-name worksonmymachine-movies \
--query "Stacks[0].Outputs[?OutputKey=='ApiUrl'].OutputValue" --output text)
curl "${API_URL}movies?limit=5"
diff --git a/src/content/docs/lambda/guides/make-it-yours.mdx b/src/content/docs/lambda/guides/make-it-yours.mdx
index b923c8b..978a462 100644
--- a/src/content/docs/lambda/guides/make-it-yours.mdx
+++ b/src/content/docs/lambda/guides/make-it-yours.mdx
@@ -17,6 +17,8 @@ Go the other way round. Build your first slice beside the sample domain, prove i
The code below assumes you named the solution `WorksOnMyMachine.Movies`. Swap in your own name wherever you see it.
+## Build the new slice
+
1. **Add the entity and use cases to `src/Application`** (`add-a-use-case`).
@@ -297,6 +299,12 @@ The code below assumes you named the solution `WorksOnMyMachine.Movies`. Swap in
Without `--namespace` the migration lands in a namespace of its own. The `sed` strips the byte order mark the tool adds. On macOS, use `sed -i ''`. Then make `// ` the first line of the new `_AddCinemas.cs`, as it is in the other migrations, so the analysers leave generated code alone.
+
+
+## Expose and test it
+
+
+
4. **Put it on functions in `src/Presentation`** (`expose-a-use-case`, `api-conventions` and `json-serialization`).
Every use case goes out twice, like the samples: a function you invoke directly with a request record, and an HTTP API function that API Gateway calls with the request in the path, query or body. Both share one private method, so the use case is only sent from one place.
@@ -440,15 +448,13 @@ The code below assumes you named the solution `WorksOnMyMachine.Movies`. Swap in
Each new test has a sample test that already does the same job for movies or reviews. Copy it and rename it, and you have the shape, the names and the house style without having to think about them.
- | New test | Copy from |
- | --- | --- |
- | `tests/Application.Tests.Unit/Cinemas/Queries/GetCinemas/GetCinemasHandlerTests.cs` | `Movies/Queries/GetMovies/GetMoviesHandlerTests.cs` |
- | `tests/Application.Tests.Unit/Cinemas/Queries/GetCinemas/GetCinemasValidatorTests.cs` | `Movies/Queries/GetMovies/GetMoviesValidatorTests.cs` |
- | `tests/Application.Tests.Unit/Cinemas/Commands/CreateCinema/CreateCinemaHandlerTests.cs` | `Reviews/Commands/CreateReview/CreateReviewHandlerTests.cs` |
- | `tests/Application.Tests.Unit/Cinemas/Commands/CreateCinema/CreateCinemaValidatorTests.cs` | `Reviews/Commands/CreateReview/CreateReviewValidatorTests.cs` |
- | A `Cinemas` region in `tests/Infrastructure.Tests.Integration/Databases/MovieReviews/EntityFrameworkMovieReviewsRepositoryTests.cs` | The `Movies` region: search, paging and create |
- | `GetCinemas_ShouldReturn_FirstPage`, `CreateCinema_ShouldReturn_Cinema` and `CreateCinema_ShouldThrow_WhenNameIsEmpty` in `tests/Presentation.Tests.Integration/HandlerTests.cs` | The `GetMovies` tests in the same file |
- | `HttpGetCinemas_ShouldReturn_Ok`, `HttpCreateCinema_ShouldReturn_Created` and `HttpCreateCinema_ShouldReturn_BadRequest_WhenNameIsEmpty` in `tests/Presentation.Tests.Integration/HttpApiTests.cs` | The `HttpGetMovies` tests in the same file |
+ - `tests/Application.Tests.Unit/Cinemas/Queries/GetCinemas/GetCinemasHandlerTests.cs`, copied from `Movies/Queries/GetMovies/GetMoviesHandlerTests.cs`.
+ - `tests/Application.Tests.Unit/Cinemas/Queries/GetCinemas/GetCinemasValidatorTests.cs`, copied from `Movies/Queries/GetMovies/GetMoviesValidatorTests.cs`.
+ - `tests/Application.Tests.Unit/Cinemas/Commands/CreateCinema/CreateCinemaHandlerTests.cs`, copied from `Reviews/Commands/CreateReview/CreateReviewHandlerTests.cs`.
+ - `tests/Application.Tests.Unit/Cinemas/Commands/CreateCinema/CreateCinemaValidatorTests.cs`, copied from `Reviews/Commands/CreateReview/CreateReviewValidatorTests.cs`.
+ - A `Cinemas` region in `tests/Infrastructure.Tests.Integration/Databases/MovieReviews/EntityFrameworkMovieReviewsRepositoryTests.cs`, copied from the `Movies` region: search, paging and create.
+ - `GetCinemas_ShouldReturn_FirstPage`, `CreateCinema_ShouldReturn_Cinema` and `CreateCinema_ShouldThrow_WhenNameIsEmpty` in `tests/Presentation.Tests.Integration/HandlerTests.cs`, copied from the `GetMovies` tests in the same file.
+ - `HttpGetCinemas_ShouldReturn_Ok`, `HttpCreateCinema_ShouldReturn_Created` and `HttpCreateCinema_ShouldReturn_BadRequest_WhenNameIsEmpty` in `tests/Presentation.Tests.Integration/HttpApiTests.cs`, copied from the `HttpGetMovies` tests in the same file.
The repository tests need some cinemas. Add three to `AddTestData` in `tests/Infrastructure.Tests.Integration/Databases/MovieReviews/Extensions/MovieReviewsCollectionData.cs`, named the same way as the sample rows:
@@ -471,6 +477,12 @@ The code below assumes you named the solution `WorksOnMyMachine.Movies`. Swap in
Both domains now run side by side. Stop here if you're keeping the sample for reference, which is a perfectly respectable choice.
+
+
+## Take out the sample
+
+
+
6. **Take out the sample movies, authors and reviews** (`sample-data` and `database-migrations`).
This step is mostly deleting, but the template's own tests use the sample as their test subject, so a few of them change instead of going. Work through it layer by layer and build as you go. The compiler finds what's left faster than any list.
@@ -508,17 +520,14 @@ The code below assumes you named the solution `WorksOnMyMachine.Movies`. Swap in
- `EntityFrameworkMovieReviewsRepositoryTests.cs`: the `Authors`, `Movies` and `Reviews` regions.
- `MovieReviewsCollectionData.cs`: the author, movie and review rows.
- **Tests to change:** these test the template's plumbing, and they used the sample as their subject. Point them at cinemas.
-
- | Test | Change |
- | --- | --- |
- | `Common/Behaviours/LoggingBehaviourTests.cs` and `ValidationBehaviourTests.cs` | Use `CreateCinemaCommand`, `CreateCinemaValidator` and `Cinema` in place of `GetAuthorByIdQuery`, its validator and `Author`. The invalid case is an empty `Name`, so the expected property is `Name`. The logged message names `CreateCinemaCommand`. |
- | `Common/Exceptions/NotFoundExceptionTests.cs` | Use `EntityType.Cinema` and a `Cinema`. The message becomes "The Cinema with the supplied id was not found." |
- | `MovieReviewsConfigurationTests.cs` | Check a cinema loads, ignoring case: look up `"one"` and expect `"One"`. |
- | `DependencyInjectionTests.cs` | Expect `ICinemasRepository`, 5 seeded cinemas and none when seeding is off. Use `GetCinemas` for the concurrency and same-sample-data checks. |
- | `HandlerTests.cs` | Run the generated handlers and the validation checks against `GetCinemas`, which returns 5 cinemas. |
- | `HttpApiTests.cs` | Keep the cinema tests, and point the generated handler test at `HttpGetCinemas`. |
- | `TelemetryTests.cs` | Use `GetCinemas` for the function name, the ARN and the call, and expect a `GetCinemasQuery` span. The HTTP test calls `HttpGetCinemas` and expects one use case span, because a list has no rejection before the use case runs. |
- | `Serialization/LambdaFunctionJsonSerializerTests.cs` | Drop the review test, and write a page of cinemas: `{"items":[{"id":"…","name":"Lorem Ipsum"}],"totalCount":1}`. |
+ - **`Common/Behaviours/LoggingBehaviourTests.cs` and `ValidationBehaviourTests.cs`:** Use `CreateCinemaCommand`, `CreateCinemaValidator` and `Cinema` in place of `GetAuthorByIdQuery`, its validator and `Author`. The invalid case is an empty `Name`, so the expected property is `Name`. The logged message names `CreateCinemaCommand`.
+ - **`Common/Exceptions/NotFoundExceptionTests.cs`:** Use `EntityType.Cinema` and a `Cinema`. The message becomes "The Cinema with the supplied id was not found."
+ - **`MovieReviewsConfigurationTests.cs`:** Check a cinema loads, ignoring case: look up `"one"` and expect `"One"`.
+ - **`DependencyInjectionTests.cs`:** Expect `ICinemasRepository`, 5 seeded cinemas and none when seeding is off. Use `GetCinemas` for the concurrency and same-sample-data checks.
+ - **`HandlerTests.cs`:** Run the generated handlers and the validation checks against `GetCinemas`, which returns 5 cinemas.
+ - **`HttpApiTests.cs`:** Keep the cinema tests, and point the generated handler test at `HttpGetCinemas`.
+ - **`TelemetryTests.cs`:** Use `GetCinemas` for the function name, the ARN and the call, and expect a `GetCinemasQuery` span. The HTTP test calls `HttpGetCinemas` and expects one use case span, because a list has no rejection before the use case runs.
+ - **`Serialization/LambdaFunctionJsonSerializerTests.cs`:** Drop the review test, and write a page of cinemas: `{"items":[{"id":"…","name":"Lorem Ipsum"}],"totalCount":1}`.
7. **Check it the way CI will** (`coverage`).
@@ -538,4 +547,4 @@ The code below assumes you named the solution `WorksOnMyMachine.Movies`. Swap in
## What's still called "movie reviews"
-The database plumbing keeps its sample name: the `Databases/MovieReviews` folder, `MovieReviewsDbContext`, `EntityFrameworkMovieReviewsRepository`, the `MovieReviews` connection string and the `movie-reviews.db` file. They all work as they are. Everything in `src/Infrastructure` is `internal`, so renaming them later is a refactor that never reaches Application or Presentation. Do it when the name starts to bother you, and change the connection string's name and `DefaultConnectionString` in `LambdaFunction/Startup.cs` at the same time.
+The database plumbing keeps its sample name: the `Databases/MovieReviews` folder, `MovieReviewsDbContext`, `EntityFrameworkMovieReviewsRepository`, the `MovieReviews` connection string and the `movie-reviews.db` file. They all work as they are. Everything in `src/Infrastructure` apart from `DependencyInjection` is `internal`, so renaming them later is a refactor that never reaches Application or Presentation. Do it when the name starts to bother you, and change the connection string's name and `DefaultConnectionString` in `LambdaFunction/Startup.cs` at the same time.
diff --git a/src/content/docs/lambda/guides/run-locally.mdx b/src/content/docs/lambda/guides/run-locally.mdx
index 8ada016..49bce91 100644
--- a/src/content/docs/lambda/guides/run-locally.mdx
+++ b/src/content/docs/lambda/guides/run-locally.mdx
@@ -1,6 +1,6 @@
---
-title: Run it locally
-description: Build the container image, invoke each function, run the tests and stamp a version.
+title: "Run it locally"
+description: "Build the container image, invoke each function, run the tests and stamp a version."
sidebar:
order: 2
---
@@ -21,8 +21,8 @@ The image is built on the [AWS Lambda .NET base image](https://gallery.ecr.aws/l
```bash
- docker build . --target run --tag acme-reviews:local
- docker run --rm -p 7350:8080 -e Database__SeedSampleData=true acme-reviews:local
+ docker build . --target run --tag worksonmymachine-movies:local
+ docker run --rm -p 7350:8080 -e Database__SeedSampleData=true worksonmymachine-movies:local
```
@@ -41,11 +41,11 @@ Run the image on its own and nothing is exported, because `OTEL_EXPORTER_OTLP_EN
## Try the other functions
-The image runs `GetMovies` by default. The same image also has `GetAuthors` and `GetReviews`. Pass the function's full handler name to `docker run` to start a different one.
+The image runs `GetMovies` by default. The same image also has `GetAuthors` and `GetReviews`, and the `HttpGetMovies`, `HttpGetAuthors` and `HttpGetReviews` functions that take an API Gateway event. Pass the function's full handler name to `docker run` to start a different one.
```bash
-docker run --rm -p 7350:8080 -e Database__SeedSampleData=true acme-reviews:local \
- "Presentation::WorksOnMyMachine.Reviews.Presentation.LambdaFunction.Handler_GetReviews_Generated::GetReviews"
+docker run --rm -p 7350:8080 -e Database__SeedSampleData=true worksonmymachine-movies:local \
+ "Presentation::WorksOnMyMachine.Movies.Presentation.LambdaFunction.Handler_GetReviews_Generated::GetReviews"
curl -X POST "http://localhost:7350/2015-03-31/functions/function/invocations" \
-d '{"movieId":"cb188ef6-14e7-f85f-2f78-924c2ab26b28","limit":2}'
@@ -59,7 +59,7 @@ The sample data uses a fixed seed, so that movie id is the same on every machine
dotnet test
```
-Each test builds its own in-memory SQLite database from the migrations and the same fixed seed. No test can change what another one sees, and nothing touches the database file in `/tmp`.
+The tests build their own in-memory SQLite databases from the same migrations and seed, so nothing touches the database file in `/tmp`. [Test your changes](/lambda/guides/test-your-changes/) covers where a test goes, how the tests are written and how to check coverage before CI does.
## Run just one part of the Dockerfile
@@ -82,7 +82,7 @@ A failing test doesn't fail the build, so the results still come out. Check `out
## Stamp a version
```bash
-docker build . --target run --tag acme-reviews:latest \
+docker build . --target run --tag worksonmymachine-movies:latest \
--build-arg VERSION_PREFIX=1.2.3 --build-arg VERSION_SUFFIX=beta
```
diff --git a/src/content/docs/lambda/guides/secure-the-function.mdx b/src/content/docs/lambda/guides/secure-the-function.mdx
index bc20186..1dfb879 100644
--- a/src/content/docs/lambda/guides/secure-the-function.mdx
+++ b/src/content/docs/lambda/guides/secure-the-function.mdx
@@ -1,6 +1,6 @@
---
-title: Secure the function
-description: Who can invoke the function, and how to put bearer tokens on its HTTP routes.
+title: "Secure the function"
+description: "Who can invoke the function, and how to put bearer tokens on its HTTP routes."
sidebar:
order: 6
---
@@ -13,6 +13,6 @@ Only principals allowed `lambda:InvokeFunction` on the function can invoke it. G
## Called over HTTP with tokens
-The `Http*` functions already sit behind an API Gateway HTTP API. Add a [JWT authorizer](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-jwt-authorizer.html) to it in `serverless.template` and API Gateway validates the token before the function runs, so bad requests never cost you an invocation, and the function itself stays the same. The routes only read, but a list of everyone's reviews is still worth a lock if it isn't meant to be public.
+The `Http*` functions already sit behind an API Gateway HTTP API. Add a [JWT authorizer](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-jwt-authorizer.html) to it in `serverless.template` and API Gateway validates the token before the function runs, so bad requests never cost you an invocation, and the function itself stays the same. The sample routes only read, but a list of everyone's reviews is still worth a lock if it isn't meant to be public.
The sibling templates for HTTP, [Clean Minimal API](https://github.com/LeanMeanCleanApps/CleanMinimalApi) and [Clean Web API](https://github.com/LeanMeanCleanApps/CleanWebApi), have optional bearer token checking built in if you'd rather run a web API than a function.
diff --git a/src/content/docs/lambda/guides/set-up-ci.mdx b/src/content/docs/lambda/guides/set-up-ci.mdx
index 4842ca5..a3ee8c8 100644
--- a/src/content/docs/lambda/guides/set-up-ci.mdx
+++ b/src/content/docs/lambda/guides/set-up-ci.mdx
@@ -1,25 +1,47 @@
---
title: "Set up CI"
-description: "Your solution starts with a simple pipeline. Here's what it runs and how to add more."
+description: "Your solution starts with a simple pipeline on purpose. Here's what it runs, and the checks you can add when your process calls for them."
sidebar:
order: 7
---
import { Steps } from '@astrojs/starlight/components';
+import ReportCard from '../../../../components/docs/ReportCard.astro';
-Whichever way you made your solution, it starts with a simple pipeline: format, build, and test with a 95% coverage bar. Those are the same commands as in `AGENTS.md`, so the pipeline and your laptop agree on what "green" means. It needs no secrets, no accounts and no paid features. Everything else is yours to add when you want it.
+Your solution starts with a simple pipeline: format, build, and test with a 95% coverage bar. Those are the same commands as in `AGENTS.md`, so the pipeline and your laptop agree on what "green" means. It needs no secrets, no accounts and no paid features, and it's green on the first push.
-## Made with `dotnet new`
+It's minimal on purpose. We don't know your review process, your security team or your deployment target, so the pipeline stops where guessing would start. Everything else on this page is a check you can add when your process calls for it.
+
+## What runs
You get `.github/workflows/ci.yml`, one GitHub Actions job that restores, checks formatting, builds, runs every test and checks the coverage total. Push to GitHub and it runs.
Not on GitHub Actions? Ask your coding agent to move it. It works as Jacques, so it uses the `ci-for-your-solution` skill in `.agents/skills` to port the same commands to Azure Pipelines, GitLab, or the Jenkins box under someone's desk, then breaks something on purpose to prove the new pipeline notices.
-## Made with "Use this template"
+## Adding more
+
+The `ci-for-your-solution` skill lists the checks you can add, what each one catches and what it needs, from a vulnerable package check that needs nothing to CodeQL, which needs Advanced Security on a private repository. Ask your agent to add one and it will agree it with you first.
+
+For a worked example of each, look at the template's own [Build Pipeline](https://github.com/LeanMeanCleanApps/CleanLambdaFunction/blob/main/.github/workflows/build-pipeline.yml). Every pull request to the template goes through these checks, and each one is a job you can lift into `ci.yml`. **Gatekeepers** fail the build and block the merge. **Tattletales** report what they find and leave the decision to a human.
+
+
-Your copy has our Build Pipeline. Outside the LeanMeanCleanApps organisation it runs the format check, build and tests, and the Summary shows the rest as not run. That covers CodeQL, Dependency Review, the container scan, and the steps that pack, verify and publish the template. So a private copy goes green without GitHub Advanced Security, and it never tries to publish to NuGet on your behalf.
+Borrow freely, but leave out the jobs that pack, verify and publish the template. You're shipping the function, not a template. Whatever you add, run the same commands locally that the pipeline runs, and make the checks you rely on required on `main`.
-### After you copy
+## If you used "Use this template"
+
+A copy made with GitHub's **Use this template** button has the whole Build Pipeline rather than `ci.yml`. Outside the LeanMeanCleanApps organisation it runs the format check, build and tests, and the Summary shows the rest as not run: CodeQL, Dependency Review, the container scan, and the steps that pack, verify and publish the template. So a private copy goes green without GitHub Advanced Security, and it never tries to publish to NuGet on your behalf. Three things to change after you copy:
@@ -30,9 +52,3 @@ Your copy has our Build Pipeline. Outside the LeanMeanCleanApps organisation it
3. Fill in **Where we track work** in `AGENTS.md`, so agents put their plans and handoffs where your team will see them.
-
-## Adding more
-
-The `ci-for-your-solution` skill lists the checks you can add, what each one catches and what it needs, from a vulnerable package check that needs nothing to CodeQL, which needs Advanced Security on a private repository. Ask your agent to add one and it will agree it with you first.
-
-For a worked example of every check, read our [Build Pipeline](https://github.com/LeanMeanCleanApps/CleanLambdaFunction/blob/main/.github/workflows/build-pipeline.yml). Borrow freely, but leave out the jobs that pack and publish the template. You're shipping an app, not a template.
diff --git a/src/content/docs/lambda/guides/test-your-changes.mdx b/src/content/docs/lambda/guides/test-your-changes.mdx
index bcd0cfb..0cd3ea8 100644
--- a/src/content/docs/lambda/guides/test-your-changes.mdx
+++ b/src/content/docs/lambda/guides/test-your-changes.mdx
@@ -14,7 +14,7 @@ Your solution starts with four test projects and a pipeline that fails under 95%
| Project | What it tests | How |
| --- | --- | --- |
| `tests/Application.Tests.Unit` | Handlers, validators and pipeline behaviours | xUnit, Shouldly and NSubstitute, with the repositories substituted |
-| `tests/Infrastructure.Tests.Integration` | The repositories against a real database | A private in-memory SQLite database per test class, built from the migrations and the fixed seed |
+| `tests/Infrastructure.Tests.Integration` | The repositories against a real database | A private in-memory SQLite database per test, built from the migrations and the fixed seed |
| `tests/Presentation.Tests.Unit` | The source-generated JSON serializer | Plain unit tests, no host |
| `tests/Presentation.Tests.Integration` | Real calls to every function, through the handler and the classes Lambda Annotations generates | A `TestLambdaContext` and a private database |
@@ -71,4 +71,5 @@ The last table it prints is the merged total. `Program.cs`, the EF Core migratio
## Where to next
- [Add a function](/lambda/guides/add-a-function/) ends with a test for the new function.
+- [Make it yours](/lambda/guides/make-it-yours/) adds a whole entity, then clears the sample out without dropping under the coverage gate.
- [Work with a coding agent](/lambda/guides/work-with-a-coding-agent/): the `test-design`, `unit-tests`, `integration-tests` and `coverage` skills hold these same rules, so an agent writes tests the same way you would.
diff --git a/src/content/docs/lambda/guides/work-with-a-coding-agent.mdx b/src/content/docs/lambda/guides/work-with-a-coding-agent.mdx
index 61abdc8..06d68e3 100644
--- a/src/content/docs/lambda/guides/work-with-a-coding-agent.mdx
+++ b/src/content/docs/lambda/guides/work-with-a-coding-agent.mdx
@@ -27,7 +27,7 @@ The skills are written for this solution, not for .NET in general. `add-a-use-ca
3. For anything bigger than a few lines, Jacques agrees a plan first: the open decisions one at a time, each with a recommendation, then the scope in a paragraph. Answer those and the work starts.
4. Before it says it's done, the agent runs `dotnet build`, `dotnet test` and `dotnet format --verify-no-changes`, the same checks as CI.
-## Make it yours
+## Tune it for your team
- **Where we track work.** `AGENTS.md` has a line that says "Not set". Replace it with your GitHub project, Jira board or Azure Boards, and agents put their plans and handoffs there instead of asking each time.
- **Change a rule, change the skill.** When your team decides to do something differently, update the matching `SKILL.md` in the same pull request. A skill that disagrees with the code is worse than no skill at all.
diff --git a/src/content/docs/lambda/index.mdx b/src/content/docs/lambda/index.mdx
index a9a3079..df9d838 100644
--- a/src/content/docs/lambda/index.mdx
+++ b/src/content/docs/lambda/index.mdx
@@ -1,12 +1,14 @@
---
-title: Clean Lambda Function
-description: A .NET 10 dotnet new template for AWS Lambda, built with Clean Architecture minus the ceremony.
+title: "Clean Lambda Function"
+description: "A .NET 10 dotnet new template for AWS Lambda, built with Clean Architecture minus the ceremony."
template: splash
+editUrl: false
+lastUpdated: false
prev: false
next: false
hero:
title: Clean Architecture for AWS Lambda, minus the ceremony
- tagline: A .NET 10 template that gives you a tested, layered Lambda function in two commands. Fewer layers than an onion, more than a sandwich.
+ tagline: A .NET 10 template that gives you a tested, layered Lambda function in two commands. Your laptop does an impression of AWS until you're ready for the real thing.
actions:
- text: Get started
link: /lambda/getting-started/
@@ -24,19 +26,16 @@ import Family from '../../../components/docs/Family.astro';
- Presentation, Application and Infrastructure, with one job each. Infrastructure is `internal`, so it can't sneak
- into places it doesn't belong.
+ Presentation, Application and Infrastructure, with one job each. Infrastructure is `internal`, so it can't sneak into places it doesn't belong.
Source-generated JSON, Mediator and Mapperly mean no reflection when Lambda wakes your function up.
- xUnit, Shouldly and a 95% coverage gate. Every test gets its own throwaway SQLite database, so nobody steps on
- anybody's toes.
+ xUnit, Shouldly and a 95% coverage gate. Every test gets its own throwaway SQLite database, so nobody steps on anybody's toes.
- The container image includes the Lambda runtime emulator. Build it, run it, `curl` it. No AWS account needed until
- you're ready.
+ The container image includes the Lambda runtime emulator. Build it, run it, `curl` it. No AWS account needed until you're ready.
diff --git a/src/content/docs/lambda/reference/configuration.mdx b/src/content/docs/lambda/reference/configuration.mdx
index 84dace0..3cbbd82 100644
--- a/src/content/docs/lambda/reference/configuration.mdx
+++ b/src/content/docs/lambda/reference/configuration.mdx
@@ -1,6 +1,6 @@
---
-title: Configuration
-description: Environment variables, Docker build arguments and Lambda deployment defaults.
+title: "Configuration"
+description: "Environment variables, Docker build arguments and Lambda deployment defaults."
sidebar:
order: 3
---
@@ -15,7 +15,7 @@ sidebar:
| `OTEL_EXPORTER_OTLP_ENDPOINT` | none | Where to send [OpenTelemetry](https://opentelemetry.io) traces, metrics and logs. Nothing is exported until it's set. Docker Compose sets it to the `otel-collector` service |
| `OTEL_EXPORTER_OTLP_PROTOCOL` | `grpc` | `grpc` or `http/protobuf` |
| `OTEL_EXPORTER_OTLP_HEADERS` | none | Headers for every export, such as an API key for a hosted backend |
-| `OTEL_SERVICE_NAME` | `CleanLambdaFunction` | The `service.name` on everything sent. Every span, metric and log also carries `service.version`, the version stamped into the build |
+| `OTEL_SERVICE_NAME` | Your solution's name | The `service.name` on everything sent. Every span, metric and log also carries `service.version`, the version stamped into the build |
## Docker Compose services
diff --git a/src/content/docs/lambda/reference/functions.mdx b/src/content/docs/lambda/reference/functions.mdx
index 4f90e14..621b5b4 100644
--- a/src/content/docs/lambda/reference/functions.mdx
+++ b/src/content/docs/lambda/reference/functions.mdx
@@ -1,6 +1,6 @@
---
-title: Functions
-description: Every function the handler exposes, with its request, response and errors.
+title: "Functions"
+description: "Every function the handler exposes, with its request, response and errors."
sidebar:
order: 1
---
@@ -10,15 +10,15 @@ The functions live on `Handler` in `src/Presentation/LambdaFunction/LambdaFuncti
The image runs `GetMovies` unless you pass a different handler name. [Lambda Annotations](https://github.com/aws/aws-lambda-dotnet/tree/master/Libraries/src/Amazon.Lambda.Annotations) generates the class each one names, and `src/Presentation/serverless.template` lists them all:
```text
-Presentation::CleanLambdaFunction.Presentation.LambdaFunction.Handler_GetMovies_Generated::GetMovies
-Presentation::CleanLambdaFunction.Presentation.LambdaFunction.Handler_GetAuthors_Generated::GetAuthors
-Presentation::CleanLambdaFunction.Presentation.LambdaFunction.Handler_GetReviews_Generated::GetReviews
-Presentation::CleanLambdaFunction.Presentation.LambdaFunction.Handler_HttpGetMovies_Generated::HttpGetMovies
-Presentation::CleanLambdaFunction.Presentation.LambdaFunction.Handler_HttpGetAuthors_Generated::HttpGetAuthors
-Presentation::CleanLambdaFunction.Presentation.LambdaFunction.Handler_HttpGetReviews_Generated::HttpGetReviews
+Presentation::WorksOnMyMachine.Movies.Presentation.LambdaFunction.Handler_GetMovies_Generated::GetMovies
+Presentation::WorksOnMyMachine.Movies.Presentation.LambdaFunction.Handler_GetAuthors_Generated::GetAuthors
+Presentation::WorksOnMyMachine.Movies.Presentation.LambdaFunction.Handler_GetReviews_Generated::GetReviews
+Presentation::WorksOnMyMachine.Movies.Presentation.LambdaFunction.Handler_HttpGetMovies_Generated::HttpGetMovies
+Presentation::WorksOnMyMachine.Movies.Presentation.LambdaFunction.Handler_HttpGetAuthors_Generated::HttpGetAuthors
+Presentation::WorksOnMyMachine.Movies.Presentation.LambdaFunction.Handler_HttpGetReviews_Generated::HttpGetReviews
```
-In your own solution, `CleanLambdaFunction` is replaced by the name you chose.
+`WorksOnMyMachine.Movies` stands in for the name you gave your solution.
## GetMovies
diff --git a/src/content/docs/lambda/reference/project-structure.mdx b/src/content/docs/lambda/reference/project-structure.mdx
index 09e65fe..24ab71c 100644
--- a/src/content/docs/lambda/reference/project-structure.mdx
+++ b/src/content/docs/lambda/reference/project-structure.mdx
@@ -1,6 +1,6 @@
---
-title: Project structure
-description: What every folder in a new solution is for.
+title: "Project structure"
+description: "What every folder in a new solution is for."
sidebar:
order: 4
---
@@ -45,7 +45,7 @@ import { FileTree } from '@astrojs/starlight/components';
- README.md
- AGENTS.md the map for coding agents
- CLAUDE.md points Claude Code at AGENTS.md
-- WorksOnMyMachine.Reviews.slnx
+- WorksOnMyMachine.Movies.slnx
- Directory.Build.props build rules and analyzers for every project
- Directory.Packages.props every package version, in one place
- Dockerfile
@@ -59,4 +59,4 @@ import { FileTree } from '@astrojs/starlight/components';
| --- | --- | --- |
| Presentation | Application, Infrastructure | References Infrastructure only to wire up dependency injection |
| Application | none | Owns the entities, the repository interfaces and the rules |
-| Infrastructure | Application | Every class is `internal` |
+| Infrastructure | Application | Every class but `DependencyInjection` is `internal` |
diff --git a/src/content/docs/lambda/reference/template-options.mdx b/src/content/docs/lambda/reference/template-options.mdx
index 9d9f463..b400f10 100644
--- a/src/content/docs/lambda/reference/template-options.mdx
+++ b/src/content/docs/lambda/reference/template-options.mdx
@@ -1,6 +1,6 @@
---
-title: Template options
-description: The options dotnet new accepts for this template, and what they change.
+title: "Template options"
+description: "The options dotnet new accepts for this template, and what they change."
sidebar:
order: 2
---
diff --git a/src/content/docs/mcp/explanation/architecture.mdx b/src/content/docs/mcp/explanation/architecture.mdx
index 3c4b0bb..a700115 100644
--- a/src/content/docs/mcp/explanation/architecture.mdx
+++ b/src/content/docs/mcp/explanation/architecture.mdx
@@ -45,7 +45,7 @@ There's a little CQRS in the style of the commands and queries, but reads and wr
In textbook Clean Architecture, Presentation never references Infrastructure. Here it does, so it can wire up dependency injection without a fourth project whose only job is to call two methods.
-To keep that from turning into a free-for-all, every class in Infrastructure is `internal`. Presentation can call `AddInfrastructure` and nothing else. If you try to new up a repository from a tool, the compiler will politely decline.
+To keep that from turning into a free-for-all, every class in Infrastructure except `DependencyInjection` is `internal`. Presentation can call `AddInfrastructure` and nothing else. If you try to new up a repository from a tool, the compiler will politely decline.
## What happens on a tool call
diff --git a/src/content/docs/mcp/explanation/decisions.mdx b/src/content/docs/mcp/explanation/decisions.mdx
index f0e9244..af96b85 100644
--- a/src/content/docs/mcp/explanation/decisions.mdx
+++ b/src/content/docs/mcp/explanation/decisions.mdx
@@ -64,4 +64,4 @@ The one rule we bend is letting Presentation reference Infrastructure for depend
## Tests that don't share
-Every test class gets its own in-memory SQLite database, built from the real migrations and the same fixed seed. Tests can run in any order, in parallel, forever, and still agree with each other.
+Every test gets its own in-memory SQLite database, built from the real migrations and the same fixed seed. Tests can run in any order, in parallel, forever, and still agree with each other.
diff --git a/src/content/docs/mcp/explanation/how-its-built.mdx b/src/content/docs/mcp/explanation/how-its-built.mdx
index 42705e3..c827905 100644
--- a/src/content/docs/mcp/explanation/how-its-built.mdx
+++ b/src/content/docs/mcp/explanation/how-its-built.mdx
@@ -1,36 +1,17 @@
---
title: "How it's built"
-description: "The pipeline, the gates and the scanners that keep every pull request honest."
+description: "How the template repository itself is run: the rules on main, the monthly release and where these docs come from."
sidebar:
order: 3
---
-import ReportCard from '../../../../components/docs/ReportCard.astro';
+This page is about the template repository, not your solution. Your solution starts with a simpler pipeline, and [Set up CI](/mcp/guides/set-up-ci/) covers what it runs, along with every check the template runs on itself and what each one needs if you want to borrow it.
-This page is about how we build the template itself. Your solution starts with a simpler pipeline, covered in [Set up CI](/mcp/guides/set-up-ci/), and you can borrow any of the checks below when you're ready.
-
-These are templates we build for fun, but the pipeline doesn't know that. Every pull request goes through the same checks, and some of them are allowed to say no.
-
-**Gatekeepers** fail the build and block the merge. **Tattletales** report what they find and leave the decision to a human.
-
-## The checks
-
-
+These are templates we build for fun, but the repository doesn't know that. Every pull request goes through the same checks, and some of them are allowed to say no.
## The rules on main
-- Every change goes through a pull request, and every check above must pass.
+- Every change goes through a pull request, and every check must pass.
- Only the maintainer can merge. Pull requests from anyone are welcome, but the green button has one owner.
- Commits must be signed, and merges are squashed so the history stays readable.
- Release tags can't be moved or deleted once they exist, so a version always means the same code.
diff --git a/src/content/docs/mcp/getting-started.mdx b/src/content/docs/mcp/getting-started.mdx
index b0d67f7..e9ba3f9 100644
--- a/src/content/docs/mcp/getting-started.mdx
+++ b/src/content/docs/mcp/getting-started.mdx
@@ -13,7 +13,7 @@ By the end of this page you'll have your own solution, an MCP server running on
You need the [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0). [Docker](https://www.docker.com/products/docker-desktop) is optional, but it gets you the MCP Inspector and the Aspire dashboard without installing anything else, so it's worth having around.
-## Make it yours
+## Create and run your solution
@@ -103,9 +103,9 @@ Hit a snag? Check [Troubleshooting](/mcp/reference/troubleshooting/).
## Where to next
-
+
-
+
diff --git a/src/content/docs/mcp/guides/add-a-tool.mdx b/src/content/docs/mcp/guides/add-a-tool.mdx
index 89a89a7..3e64454 100644
--- a/src/content/docs/mcp/guides/add-a-tool.mdx
+++ b/src/content/docs/mcp/guides/add-a-tool.mdx
@@ -10,19 +10,20 @@ import { Steps, Aside } from '@astrojs/starlight/components';
This guide adds `authors_get`, which returns one author with every review they've written, the mirror of the existing `movies_get`. The Application layer already has a `GetAuthorByIdQuery`, so all the work happens in Presentation. That's the point of the layers: new ways in, same rules underneath.
- If the query you want doesn't exist yet, add it to `src/Application` first: a query class, a handler and a FluentValidation validator, next to the others. Mediator and the validation behaviour pick them up automatically.
+ If the query you want doesn't exist yet, add it to `src/Application` first: a query class, a handler and a FluentValidation validator, next to the others. Mediator and the validation behaviour pick them up automatically. [Make it yours](/mcp/guides/make-it-yours/) walks through a whole new entity, from the Application layer to the tests.
-1. Add the tool to `src/Presentation/Tools/AuthorTools.cs`, after `authors_search`. The class already has `[McpServerToolType]` and aliases the author queries as `Queries`. The `[Description]` attributes aren't decoration: they're the tool description and the argument descriptions the model reads to decide when and how to call it, and `UseStructuredContent` gives the result an output schema too.
+1. Add the tool to `src/Presentation/Tools/AuthorTools.cs`, after `authors_search`. The class already has `[McpServerToolType]` and aliases the author queries as `Queries`. The XML comments aren't decoration: the SDK's source generator turns the summary into the tool description and each ` ` into an argument description, which is what the model reads to decide when and how to call it. `UseStructuredContent` gives the result an output schema too, and `IconSource` gives the tool its icon, which a test insists on.
```csharp
- [McpServerTool(Name = "authors_get", Title = "Get author", ReadOnly = true, Idempotent = true, OpenWorld = false, UseStructuredContent = true)]
- [Description("Get one author with their review count, average stars and every review they have written, including each movie's title.")]
- public static async Task GetAuthor(
+ /// Get one author with their review count, average stars and every review they have written, including each movie's title.
+ /// The author's id (a GUID). Use authors_search to find it.
+ [McpServerTool(Name = "authors_get", Title = "Get author", ReadOnly = true, Idempotent = true, OpenWorld = false, UseStructuredContent = true, IconSource = ItemIcons.Author)]
+ public static async partial Task GetAuthor(
ISender sender,
- [Description("The author's id (a GUID). Use authors_search to find it.")] Guid authorId,
+ Guid authorId,
CancellationToken cancellationToken)
{
var author = await sender.Send(new Queries.GetAuthorById.GetAuthorByIdQuery
@@ -94,6 +95,8 @@ This guide adds `authors_get`, which returns one author with every review they'v
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"authors_get","arguments":{"authorId":"01fa8b58-05d2-abd4-e55c-2633222dd0a5"}}}'
```
+ The reply is a server-sent event, `event: message` followed by a `data:` line holding the JSON-RPC result. Its `structuredContent` is the author:
+
```json
{
"id": "01fa8b58-05d2-abd4-e55c-2633222dd0a5",
diff --git a/src/content/docs/mcp/guides/connect-a-client.mdx b/src/content/docs/mcp/guides/connect-a-client.mdx
index ec2c3c2..1eb5646 100644
--- a/src/content/docs/mcp/guides/connect-a-client.mdx
+++ b/src/content/docs/mcp/guides/connect-a-client.mdx
@@ -14,7 +14,7 @@ No client to hand? Docker Compose can start one for you: see [Chat with it](/mcp
```bash
- claude mcp add --transport http acme-movies http://localhost:7320/mcp
+ claude mcp add --transport http worksonmymachine-movies http://localhost:7320/mcp
```
Then ask it something like "which movie has the best reviews?" and watch it call `movies_search` and `movies_get` on its own.
@@ -28,7 +28,7 @@ No client to hand? Docker Compose can start one for you: see [Chat with it](/mcp
```json
{
"servers": {
- "acme-movies": { "type": "http", "url": "http://localhost:7320/mcp" }
+ "worksonmymachine-movies": { "type": "http", "url": "http://localhost:7320/mcp" }
}
}
```
@@ -39,7 +39,7 @@ No client to hand? Docker Compose can start one for you: see [Chat with it](/mcp
```json
{
"mcpServers": {
- "acme-movies": { "url": "http://localhost:7320/mcp" }
+ "worksonmymachine-movies": { "url": "http://localhost:7320/mcp" }
}
}
```
diff --git a/src/content/docs/mcp/guides/database.mdx b/src/content/docs/mcp/guides/database.mdx
index 445f2e2..a9d2fee 100644
--- a/src/content/docs/mcp/guides/database.mdx
+++ b/src/content/docs/mcp/guides/database.mdx
@@ -32,7 +32,7 @@ Set `ConnectionStrings__MovieReviews`. To keep everything in memory, which is wh
```bash
docker run --rm -p 7320:7320 -e DOTNET_ENVIRONMENT=docker \
-e ConnectionStrings__MovieReviews="Data Source=MovieReviews;Mode=Memory;Cache=Shared" \
- acme-movies:local
+ worksonmymachine-movies:local
```
## Add a migration
@@ -41,15 +41,21 @@ After you change the data model in `src/Infrastructure`:
```bash
dotnet tool install --global dotnet-ef
-dotnet ef migrations add AddDirectors --project ./src/Infrastructure --output-dir Databases/MovieReviews/Migrations
+dotnet ef migrations add AddDirectors \
+ --project ./src/Infrastructure \
+ --output-dir Databases/MovieReviews/Migrations \
+ --namespace WorksOnMyMachine.Movies.Infrastructure.Databases.MovieReviews.Migrations
+sed -i '1s/^\xEF\xBB\xBF//' src/Infrastructure/Databases/MovieReviews/Migrations/*.cs
```
-The next start applies it.
+Without `--namespace` the migration lands in a namespace of its own, and the `sed` strips the byte order mark the tool adds. On macOS, use `sed -i ''`. Then make `// ` the first line of the new `_AddDirectors.cs`, as it is in the other migrations, so the analysers and `dotnet format` leave it alone. Swap `WorksOnMyMachine.Movies` for your solution's name.
+
+The next start applies it, and [Make it yours](/mcp/guides/make-it-yours/) adds a whole entity this way.
- Every test class builds its own in-memory SQLite database from the same migrations and the same seed, so `dotnet test` never touches `movie-reviews.db`. You can run the tests while the server is running and neither will notice.
+ Every test builds its own in-memory SQLite database from the same migrations and the same seed, so `dotnet test` never touches `movie-reviews.db`. You can run the tests while the server is running and neither will notice.
## Swap SQLite for something real
-Everything database-shaped lives in Infrastructure, and every class there is `internal`. To move to PostgreSQL, SQL Server or anything else, implement the repository interfaces from Application (`IMoviesRepository`, `IAuthorsRepository` and `IReviewsRepository`) against the new store and register them in `Infrastructure/DependencyInjection.cs`. Application and Presentation don't need to know it happened. That's the whole sales pitch of Clean Architecture in one paragraph.
+Everything database-shaped lives in Infrastructure, and every class there but `DependencyInjection` is `internal`. To move to PostgreSQL, SQL Server or anything else, implement the repository interfaces from Application (`IMoviesRepository`, `IAuthorsRepository` and `IReviewsRepository`) against the new store and register them in `Infrastructure/DependencyInjection.cs`. Application and Presentation don't need to know it happened. That's the whole sales pitch of Clean Architecture in one paragraph.
diff --git a/src/content/docs/mcp/guides/make-it-yours.mdx b/src/content/docs/mcp/guides/make-it-yours.mdx
index 9f1169b..50100cb 100644
--- a/src/content/docs/mcp/guides/make-it-yours.mdx
+++ b/src/content/docs/mcp/guides/make-it-yours.mdx
@@ -17,6 +17,8 @@ Go the other way round. Build your first slice beside the sample domain, prove i
The code below assumes you named the solution `WorksOnMyMachine.Movies`. Swap in your own name wherever you see it.
+## Build the new slice
+
1. **Add the entity and use cases to `src/Application`** (`add-a-use-case`).
@@ -360,6 +362,12 @@ The code below assumes you named the solution `WorksOnMyMachine.Movies`. Swap in
Without `--namespace` the migration lands in a namespace of its own. The `sed` strips the byte order mark the tool adds. On macOS, use `sed -i ''`. Then make `// ` the first line of the new `_AddCinemas.cs`, as it is in the other migrations, so the analysers leave generated code alone.
+
+
+## Expose and test it
+
+
+
4. **Put it on tools, a resource and a prompt in `src/Presentation`** (`expose-a-use-case`, `api-conventions` and `json-serialization`).
This is where an MCP server earns its keep. A model reads the tool names, titles, annotations and descriptions to decide what to call, so they're written for a model to read, the same way the sample's are. Tools return a result model rather than the Application entity, so the server decides exactly what a client sees.
@@ -559,16 +567,14 @@ The code below assumes you named the solution `WorksOnMyMachine.Movies`. Swap in
Each new test has a sample test that already does the same job for movies or reviews. Copy it and rename it, and you have the shape, the names and the house style without having to think about them.
- | New test | Copy from |
- | --- | --- |
- | `tests/Application.Tests.Unit/Cinemas/Queries/GetCinemas/GetCinemasHandlerTests.cs` | `Movies/Queries/GetMovies/GetMoviesHandlerTests.cs` |
- | `tests/Application.Tests.Unit/Cinemas/Queries/GetCinemas/GetCinemasValidatorTests.cs` | `Movies/Queries/GetMovies/GetMoviesValidatorTests.cs` |
- | `tests/Application.Tests.Unit/Cinemas/Queries/GetCinemaById/GetCinemaByIdHandlerTests.cs` and `GetCinemaByIdValidatorTests.cs` | The tests in `Movies/Queries/GetMovieById/` |
- | `tests/Application.Tests.Unit/Cinemas/Commands/CreateCinema/CreateCinemaHandlerTests.cs` | `Reviews/Commands/CreateReview/CreateReviewHandlerTests.cs` |
- | `tests/Application.Tests.Unit/Cinemas/Commands/CreateCinema/CreateCinemaValidatorTests.cs` | `Reviews/Commands/CreateReview/CreateReviewValidatorTests.cs` |
- | A `Cinemas` region in `tests/Infrastructure.Tests.Integration/Databases/MovieReviews/EntityFrameworkMovieReviewsRepositoryTests.cs` | The `Movies` region: search, paging, get by id and create |
- | `tests/Presentation.Tests.Unit/Tools/CinemaToolTests.cs` | `MovieToolTests.cs` and `ReviewToolTests.cs` |
- | `tests/Presentation.Tests.Integration/Tools/CinemaToolTests.cs` | `MovieToolTests.cs` and `ReviewToolTests.cs` |
+ - `tests/Application.Tests.Unit/Cinemas/Queries/GetCinemas/GetCinemasHandlerTests.cs`, copied from `Movies/Queries/GetMovies/GetMoviesHandlerTests.cs`.
+ - `tests/Application.Tests.Unit/Cinemas/Queries/GetCinemas/GetCinemasValidatorTests.cs`, copied from `Movies/Queries/GetMovies/GetMoviesValidatorTests.cs`.
+ - `tests/Application.Tests.Unit/Cinemas/Queries/GetCinemaById/GetCinemaByIdHandlerTests.cs` and `GetCinemaByIdValidatorTests.cs`, copied from the tests in `Movies/Queries/GetMovieById/`.
+ - `tests/Application.Tests.Unit/Cinemas/Commands/CreateCinema/CreateCinemaHandlerTests.cs`, copied from `Reviews/Commands/CreateReview/CreateReviewHandlerTests.cs`.
+ - `tests/Application.Tests.Unit/Cinemas/Commands/CreateCinema/CreateCinemaValidatorTests.cs`, copied from `Reviews/Commands/CreateReview/CreateReviewValidatorTests.cs`.
+ - A `Cinemas` region in `tests/Infrastructure.Tests.Integration/Databases/MovieReviews/EntityFrameworkMovieReviewsRepositoryTests.cs`, copied from the `Movies` region: search, paging, get by id and create.
+ - `tests/Presentation.Tests.Unit/Tools/CinemaToolTests.cs`, copied from `MovieToolTests.cs` and `ReviewToolTests.cs`.
+ - `tests/Presentation.Tests.Integration/Tools/CinemaToolTests.cs`, copied from `MovieToolTests.cs` and `ReviewToolTests.cs`.
The repository tests need some cinemas. Add three to `AddTestData` in `tests/Infrastructure.Tests.Integration/Databases/MovieReviews/Extensions/MovieReviewsCollectionData.cs`, named the same way as the sample rows:
@@ -598,6 +604,12 @@ The code below assumes you named the solution `WorksOnMyMachine.Movies`. Swap in
Both domains now run side by side. Stop here if you're keeping the sample for reference, which is a perfectly respectable choice.
+
+
+## Take out the sample
+
+
+
6. **Take out the sample movies, authors and reviews** (`sample-data` and `database-migrations`).
This step is mostly deleting, but the template's own tests use the sample as their test subject, so a few of them change instead of going. Work through it layer by layer and build as you go. The compiler finds what's left faster than any list.
@@ -653,20 +665,17 @@ The code below assumes you named the solution `WorksOnMyMachine.Movies`. Swap in
- `EntityFrameworkMovieReviewsRepositoryTests.cs`: the `Authors`, `Movies` and `Reviews` regions.
- `MovieReviewsCollectionData.cs`: the author, movie and review rows.
- **Tests to change:** these test the template's plumbing, and they used the sample as their subject. Point them at cinemas.
-
- | Test | Change |
- | --- | --- |
- | `Common/Behaviours/LoggingBehaviourTests.cs` and `ValidationBehaviourTests.cs` | Use `CreateCinemaCommand`, `CreateCinemaValidator` and `Cinema` in place of `GetAuthorByIdQuery`, its validator and `Author`. The invalid case is an empty `Name`, so the expected property is `Name`. The logged message names `CreateCinemaCommand`. |
- | `Common/Exceptions/NotFoundExceptionTests.cs` | Use `EntityType.Cinema` and a `Cinema`. The message becomes "The Cinema with the supplied id was not found." |
- | `MovieReviewsConfigurationTests.cs` | Check a cinema loads, ignoring case: look up `"one"` and expect `"One"`. |
- | `DependencyInjectionTests.cs` | Expect `ICinemasRepository`, 5 seeded cinemas and none when seeding is off. Use `GetCinemas` for the concurrency and same-sample-data checks. |
- | `Completions/CompletionHandlerTests.cs` | Use a `cinemas://{cinemaId}` reference, and check a value too long to search returns nothing without sending a query. |
- | `Errors/ToolErrorFilterTests.cs` and `ProtocolErrorFilterTests.cs` | Expect the new not found hint. |
- | `Models/ResultMapperTests.cs` | Map a cinema, and keep the paging test, using cinemas. |
- | `Tools/ToolListTests.cs` and `Prompts/PromptTests.cs` | Expect only the cinema tools and `describe_cinema`. |
- | `Resources/ResourceTests.cs` | Expect only `cinemas://{cinemaId}` and the version resource. |
- | `Tools/ResourceLinkTests.cs` and `Completions/CompletionTests.cs` | Use cinemas for every case: links from `cinemas_search` and `cinemas_add`, no link for a failed call, and completion by name, by id prefix, with an empty value and with no match. |
- | `Telemetry/TelemetryTests.cs` | Call `cinemas_search` and expect a `GetCinemasQuery` span. |
+ - **`Common/Behaviours/LoggingBehaviourTests.cs` and `ValidationBehaviourTests.cs`:** Use `CreateCinemaCommand`, `CreateCinemaValidator` and `Cinema` in place of `GetAuthorByIdQuery`, its validator and `Author`. The invalid case is an empty `Name`, so the expected property is `Name`. The logged message names `CreateCinemaCommand`.
+ - **`Common/Exceptions/NotFoundExceptionTests.cs`:** Use `EntityType.Cinema` and a `Cinema`. The message becomes "The Cinema with the supplied id was not found."
+ - **`MovieReviewsConfigurationTests.cs`:** Check a cinema loads, ignoring case: look up `"one"` and expect `"One"`.
+ - **`DependencyInjectionTests.cs`:** Expect `ICinemasRepository`, 5 seeded cinemas and none when seeding is off. Use `GetCinemas` for the concurrency and same-sample-data checks.
+ - **`Completions/CompletionHandlerTests.cs`:** Use a `cinemas://{cinemaId}` reference, and check a value too long to search returns nothing without sending a query.
+ - **`Errors/ToolErrorFilterTests.cs` and `ProtocolErrorFilterTests.cs`:** Expect the new not found hint.
+ - **`Models/ResultMapperTests.cs`:** Map a cinema, and keep the paging test, using cinemas.
+ - **`Tools/ToolListTests.cs` and `Prompts/PromptTests.cs`:** Expect only the cinema tools and `describe_cinema`.
+ - **`Resources/ResourceTests.cs`:** Expect only `cinemas://{cinemaId}` and the version resource.
+ - **`Tools/ResourceLinkTests.cs` and `Completions/CompletionTests.cs`:** Use cinemas for every case: links from `cinemas_search` and `cinemas_add`, no link for a failed call, and completion by name, by id prefix, with an empty value and with no match.
+ - **`Telemetry/TelemetryTests.cs`:** Call `cinemas_search` and expect a `GetCinemasQuery` span.
7. **Check it the way CI will** (`coverage`).
@@ -686,4 +695,4 @@ The code below assumes you named the solution `WorksOnMyMachine.Movies`. Swap in
## What's still called "movie reviews"
-The database plumbing keeps its sample name: the `Databases/MovieReviews` folder, `MovieReviewsDbContext`, `EntityFrameworkMovieReviewsRepository`, the `MovieReviews` connection string and the `movie-reviews.db` file. They all work as they are. Everything in `src/Infrastructure` is `internal`, so renaming them later is a refactor that never reaches Application or Presentation. Do it when the name starts to bother you, and change the connection string in every `appsettings.*.json` and in `docker-compose.yml` at the same time.
+The database plumbing keeps its sample name: the `Databases/MovieReviews` folder, `MovieReviewsDbContext`, `EntityFrameworkMovieReviewsRepository`, the `MovieReviews` connection string and the `movie-reviews.db` file. They all work as they are. Everything in `src/Infrastructure` apart from `DependencyInjection` is `internal`, so renaming them later is a refactor that never reaches Application or Presentation. Do it when the name starts to bother you, and change the connection string in every `appsettings.*.json` and in `docker-compose.yml` at the same time.
diff --git a/src/content/docs/mcp/guides/run-locally.mdx b/src/content/docs/mcp/guides/run-locally.mdx
index f416d26..cd77e1d 100644
--- a/src/content/docs/mcp/guides/run-locally.mdx
+++ b/src/content/docs/mcp/guides/run-locally.mdx
@@ -28,8 +28,8 @@ There are two ready-made environments. `local` is for `dotnet run`: sample data
```bash
- docker build . --target run --tag acme-movies:local
- docker run --rm -p 7320:7320 -e DOTNET_ENVIRONMENT=docker acme-movies:local
+ docker build . --target run --tag worksonmymachine-movies:local
+ docker run --rm -p 7320:7320 -e DOTNET_ENVIRONMENT=docker worksonmymachine-movies:local
```
Without Compose there's no collector, so the logs stay on the console and nothing else is exported. Set `OTEL_EXPORTER_OTLP_ENDPOINT` to send them to a collector of your own.
@@ -108,7 +108,7 @@ curl -X POST "https://localhost:7321/mcp" \
dotnet test
```
-Each test class builds its own in-memory SQLite database from the migrations and the same fixed seed, and the Presentation tests talk to the server through a real `McpClient` from the C# SDK, so they exercise the same protocol path as any other client. No test can change what another one sees, and nothing touches `movie-reviews.db`.
+The tests build their own in-memory SQLite databases from the same migrations and seed, so nothing touches `movie-reviews.db`. [Test your changes](/mcp/guides/test-your-changes/) covers where a test goes, how the tests are written and how to check coverage before CI does.
## Run just one part of the Dockerfile
@@ -131,7 +131,7 @@ A failing test doesn't fail the build, so the results still come out. Check `out
## Stamp a version
```bash
-docker build . --target run --tag acme-movies:latest \
+docker build . --target run --tag worksonmymachine-movies:latest \
--build-arg VERSION_PREFIX=1.2.3 --build-arg VERSION_SUFFIX=beta
```
diff --git a/src/content/docs/mcp/guides/set-up-ci.mdx b/src/content/docs/mcp/guides/set-up-ci.mdx
index f702d3e..961a0d4 100644
--- a/src/content/docs/mcp/guides/set-up-ci.mdx
+++ b/src/content/docs/mcp/guides/set-up-ci.mdx
@@ -1,25 +1,47 @@
---
title: "Set up CI"
-description: "Your solution starts with a simple pipeline. Here's what it runs and how to add more."
+description: "Your solution starts with a simple pipeline on purpose. Here's what it runs, and the checks you can add when your process calls for them."
sidebar:
order: 8
---
import { Steps } from '@astrojs/starlight/components';
+import ReportCard from '../../../../components/docs/ReportCard.astro';
-Whichever way you made your solution, it starts with a simple pipeline: format, build, and test with a 95% coverage bar. Those are the same commands as in `AGENTS.md`, so the pipeline and your laptop agree on what "green" means. It needs no secrets, no accounts and no paid features. Everything else is yours to add when you want it.
+Your solution starts with a simple pipeline: format, build, and test with a 95% coverage bar. Those are the same commands as in `AGENTS.md`, so the pipeline and your laptop agree on what "green" means. It needs no secrets, no accounts and no paid features, and it's green on the first push.
-## Made with `dotnet new`
+It's minimal on purpose. We don't know your review process, your security team or your deployment target, so the pipeline stops where guessing would start. Everything else on this page is a check you can add when your process calls for it.
+
+## What runs
You get `.github/workflows/ci.yml`, one GitHub Actions job that restores, checks formatting, builds, runs every test and checks the coverage total. Push to GitHub and it runs.
Not on GitHub Actions? Ask your coding agent to move it. It works as Jacques, so it uses the `ci-for-your-solution` skill in `.agents/skills` to port the same commands to Azure Pipelines, GitLab, or the Jenkins box under someone's desk, then breaks something on purpose to prove the new pipeline notices.
-## Made with "Use this template"
+## Adding more
+
+The `ci-for-your-solution` skill lists the checks you can add, what each one catches and what it needs, from a vulnerable package check that needs nothing to CodeQL, which needs Advanced Security on a private repository. Ask your agent to add one and it will agree it with you first.
+
+For a worked example of each, look at the template's own [Build Pipeline](https://github.com/LeanMeanCleanApps/CleanModelContextProtocol/blob/main/.github/workflows/build-pipeline.yml). Every pull request to the template goes through these checks, and each one is a job you can lift into `ci.yml`. **Gatekeepers** fail the build and block the merge. **Tattletales** report what they find and leave the decision to a human.
+
+
-Your copy has our Build Pipeline. Outside the LeanMeanCleanApps organisation it runs the format check, build and tests, and the Summary shows the rest as not run. That covers CodeQL, Dependency Review, the container scan, and the steps that pack, verify and publish the template. So a private copy goes green without GitHub Advanced Security, and it never tries to publish to NuGet on your behalf.
+Borrow freely, but leave out the jobs that pack, verify and publish the template. You're shipping the server, not a template. Whatever you add, run the same commands locally that the pipeline runs, and make the checks you rely on required on `main`.
-### After you copy
+## If you used "Use this template"
+
+A copy made with GitHub's **Use this template** button has the whole Build Pipeline rather than `ci.yml`. Outside the LeanMeanCleanApps organisation it runs the format check, build and tests, and the Summary shows the rest as not run: CodeQL, Dependency Review, the container scan, and the steps that pack, verify and publish the template. So a private copy goes green without GitHub Advanced Security, and it never tries to publish to NuGet on your behalf. Three things to change after you copy:
@@ -30,9 +52,3 @@ Your copy has our Build Pipeline. Outside the LeanMeanCleanApps organisation it
3. Fill in **Where we track work** in `AGENTS.md`, so agents put their plans and handoffs where your team will see them.
-
-## Adding more
-
-The `ci-for-your-solution` skill lists the checks you can add, what each one catches and what it needs, from a vulnerable package check that needs nothing to CodeQL, which needs Advanced Security on a private repository. Ask your agent to add one and it will agree it with you first.
-
-For a worked example of every check, read our [Build Pipeline](https://github.com/LeanMeanCleanApps/CleanModelContextProtocol/blob/main/.github/workflows/build-pipeline.yml). Borrow freely, but leave out the jobs that pack and publish the template. You're shipping an app, not a template.
diff --git a/src/content/docs/mcp/guides/ship-the-container.mdx b/src/content/docs/mcp/guides/ship-the-container.mdx
index 3ac40d6..a9142d4 100644
--- a/src/content/docs/mcp/guides/ship-the-container.mdx
+++ b/src/content/docs/mcp/guides/ship-the-container.mdx
@@ -12,7 +12,7 @@ The `run` stage of the `Dockerfile` is the whole server: one image, built on `mc
## Build it
```bash
-docker build . --target run --tag acme-movies:1.2.3 \
+docker build . --target run --tag worksonmymachine-movies:1.2.3 \
--build-arg VERSION_PREFIX=1.2.3
```
@@ -32,13 +32,13 @@ For production, pick a name like `production`, add an `appsettings.production.js
## Put it behind HTTPS
-The container only listens on HTTP. Outside the `local` and `Development` environments it sends an HSTS header and redirects HTTP to HTTPS, which means it expects something in front of it to end TLS: a load balancer, a reverse proxy or your platform's ingress. Tell it so:
+The container only listens on HTTP, and it expects something in front of it to end TLS: a load balancer, a reverse proxy or your platform's ingress. Outside the `local` and `Development` environments it sends an HSTS header, and it redirects HTTP to HTTPS once it knows an HTTPS port to send people to, such as `ASPNETCORE_HTTPS_PORT=443`. Tell it about the proxy too:
```bash
ASPNETCORE_FORWARDEDHEADERS_ENABLED=true
```
-Without that, the server thinks every request arrived over plain HTTP and redirects it, and the proxy sends it back, and the two of them go round and round until someone's pager goes off.
+Without that, the server thinks every request arrived over plain HTTP. Give it an HTTPS port in that state and it redirects the proxy's requests, the proxy sends them back, and the two of them go round and round until someone's pager goes off.
## Wire up the health checks
diff --git a/src/content/docs/mcp/guides/test-your-changes.mdx b/src/content/docs/mcp/guides/test-your-changes.mdx
index 32f9b1c..39e9922 100644
--- a/src/content/docs/mcp/guides/test-your-changes.mdx
+++ b/src/content/docs/mcp/guides/test-your-changes.mdx
@@ -14,7 +14,7 @@ Your solution starts with four test projects and a pipeline that fails under 95%
| Project | What it tests | How |
| --- | --- | --- |
| `tests/Application.Tests.Unit` | Handlers, validators and pipeline behaviours | xUnit, Shouldly and NSubstitute, with the repositories substituted |
-| `tests/Infrastructure.Tests.Integration` | The repositories against a real database | A private in-memory SQLite database per test class, built from the migrations and the fixed seed |
+| `tests/Infrastructure.Tests.Integration` | The repositories against a real database | A private in-memory SQLite database per test, built from the migrations and the fixed seed |
| `tests/Presentation.Tests.Unit` | Tools, the result mapper and the error filters | Plain unit tests, no host |
| `tests/Presentation.Tests.Integration` | A real MCP client calling tools, resources, prompts and completions, plus OAuth and health checks | `WebApplicationFactory` and the MCP C# SDK client |
@@ -71,4 +71,5 @@ The last table it prints is the merged total. `Program.cs`, the EF Core migratio
## Where to next
- [Add a tool](/mcp/guides/add-a-tool/) ends with a test for the new tool.
+- [Make it yours](/mcp/guides/make-it-yours/) adds a whole entity, then clears the sample out without dropping under the coverage gate.
- [Work with a coding agent](/mcp/guides/work-with-a-coding-agent/): the `test-design`, `unit-tests`, `integration-tests` and `coverage` skills hold these same rules, so an agent writes tests the same way you would.
diff --git a/src/content/docs/mcp/guides/work-with-a-coding-agent.mdx b/src/content/docs/mcp/guides/work-with-a-coding-agent.mdx
index 9a1866a..fcabb88 100644
--- a/src/content/docs/mcp/guides/work-with-a-coding-agent.mdx
+++ b/src/content/docs/mcp/guides/work-with-a-coding-agent.mdx
@@ -27,7 +27,7 @@ The skills are written for this solution, not for .NET in general. `add-a-use-ca
3. For anything bigger than a few lines, Jacques agrees a plan first: the open decisions one at a time, each with a recommendation, then the scope in a paragraph. Answer those and the work starts.
4. Before it says it's done, the agent runs `dotnet build`, `dotnet test` and `dotnet format --verify-no-changes`, the same checks as CI.
-## Make it yours
+## Tune it for your team
- **Where we track work.** `AGENTS.md` has a line that says "Not set". Replace it with your GitHub project, Jira board or Azure Boards, and agents put their plans and handoffs there instead of asking each time.
- **Change a rule, change the skill.** When your team decides to do something differently, update the matching `SKILL.md` in the same pull request. A skill that disagrees with the code is worse than no skill at all.
diff --git a/src/content/docs/mcp/index.mdx b/src/content/docs/mcp/index.mdx
index a05c8b6..e5c2593 100644
--- a/src/content/docs/mcp/index.mdx
+++ b/src/content/docs/mcp/index.mdx
@@ -2,6 +2,8 @@
title: Clean Model Context Protocol
description: A .NET 10 dotnet new template for a Model Context Protocol server, built with Clean Architecture minus the ceremony.
template: splash
+editUrl: false
+lastUpdated: false
prev: false
next: false
hero:
diff --git a/src/content/docs/mcp/reference/configuration.mdx b/src/content/docs/mcp/reference/configuration.mdx
index 42861af..6f0a99d 100644
--- a/src/content/docs/mcp/reference/configuration.mdx
+++ b/src/content/docs/mcp/reference/configuration.mdx
@@ -53,7 +53,7 @@ The app always collects [OpenTelemetry](https://opentelemetry.io) traces and met
| `OTEL_EXPORTER_OTLP_ENDPOINT` | none | Where to send traces, metrics and logs. Nothing is exported until it's set. Docker Compose sets it to the `otel-collector` service |
| `OTEL_EXPORTER_OTLP_PROTOCOL` | `grpc` | `grpc` or `http/protobuf` |
| `OTEL_EXPORTER_OTLP_HEADERS` | none | Headers for every export, such as an API key for a hosted backend |
-| `OTEL_SERVICE_NAME` | `CleanModelContextProtocol` | The `service.name` on everything sent |
+| `OTEL_SERVICE_NAME` | Your solution's name | The `service.name` on everything sent |
Every span, metric and log also carries `service.version`, the version stamped into the build, and `deployment.environment.name`.
diff --git a/src/content/docs/mcp/reference/project-structure.mdx b/src/content/docs/mcp/reference/project-structure.mdx
index df63d50..31e6531 100644
--- a/src/content/docs/mcp/reference/project-structure.mdx
+++ b/src/content/docs/mcp/reference/project-structure.mdx
@@ -62,4 +62,4 @@ import { FileTree } from '@astrojs/starlight/components';
| --- | --- | --- |
| Presentation | Application, Infrastructure | References Infrastructure only to wire up dependency injection |
| Application | none | Owns the entities, the repository interfaces and the rules |
-| Infrastructure | Application | Every class is `internal` |
+| Infrastructure | Application | Every class but `DependencyInjection` is `internal` |
diff --git a/src/content/docs/mcp/reference/tools.mdx b/src/content/docs/mcp/reference/tools.mdx
index 1e66a17..93629c7 100644
--- a/src/content/docs/mcp/reference/tools.mdx
+++ b/src/content/docs/mcp/reference/tools.mdx
@@ -67,13 +67,15 @@ Returns a [page](#pages) of [authors](#author).
## reviews_list
-Lists reviews with their star rating and the title and author of each, in movie-title order. Filter by movie, by author, or both, or pass a `reviewId` to get one review.
+Lists reviews with their star rating and the title and author of each, in movie-title order. Filter by movie, by author, by words in the review or by a minimum star rating, or pass a `reviewId` to get one review.
| Argument | Type | Default | Rules |
| --- | --- | --- | --- |
| `movieId` | guid | none | Only reviews of this movie |
| `authorId` | guid | none | Only reviews by this author |
| `reviewId` | guid | none | Get this one review instead. The other filters are ignored |
+| `query` | string | none | Only reviews whose text contains it, ignoring case. Up to 100 characters |
+| `minStars` | integer | none | Only reviews with at least this many stars, from 1 to 5 |
| `offset` | integer | `0` | 0 or more |
| `limit` | integer | `10` | 1 to 50 |
diff --git a/src/content/docs/mcp/reference/troubleshooting.mdx b/src/content/docs/mcp/reference/troubleshooting.mdx
index 017ebd3..720dd17 100644
--- a/src/content/docs/mcp/reference/troubleshooting.mdx
+++ b/src/content/docs/mcp/reference/troubleshooting.mdx
@@ -45,7 +45,7 @@ Each Lean Mean Clean template has its own block of ten ports so different templa
```
- To move the `dotnet run` ports, edit `applicationUrl` in `src/Presentation/Properties/launchSettings.json`.
-## Untrusted certificate or empty `curl` reply on `dotnet run`
+## Browser certificate warning or empty `curl` reply on `dotnet run`
**Symptom:** `curl` or an MCP client fails against `https://localhost:7321/mcp` with a TLS certificate error, or calling `http://localhost:7320/mcp` under `dotnet run` returns `307 Temporary Redirect`.
diff --git a/src/content/docs/minimalapi/explanation/architecture.mdx b/src/content/docs/minimalapi/explanation/architecture.mdx
index dfe2eac..8f7361d 100644
--- a/src/content/docs/minimalapi/explanation/architecture.mdx
+++ b/src/content/docs/minimalapi/explanation/architecture.mdx
@@ -45,7 +45,7 @@ There's a little CQRS in the style of the commands and queries, but reads and wr
In textbook Clean Architecture, Presentation never references Infrastructure. Here it does, so it can wire up dependency injection without a fourth project whose only job is to call two methods.
-To keep that from turning into a free-for-all, every class in Infrastructure is `internal`. Presentation can call `AddInfrastructure` and nothing else. If you try to new up a repository from an endpoint, the compiler will politely decline.
+To keep that from turning into a free-for-all, every class in Infrastructure except `DependencyInjection` is `internal`. Presentation can call `AddInfrastructure` and nothing else. If you try to new up a repository from an endpoint, the compiler will politely decline.
## What happens on a request
diff --git a/src/content/docs/minimalapi/explanation/decisions.mdx b/src/content/docs/minimalapi/explanation/decisions.mdx
index c5e96ce..a6bca3e 100644
--- a/src/content/docs/minimalapi/explanation/decisions.mdx
+++ b/src/content/docs/minimalapi/explanation/decisions.mdx
@@ -52,4 +52,4 @@ The one rule we bend is letting Presentation reference Infrastructure for depend
## Tests that don't share
-Every test class gets its own in-memory SQLite database, built from the real migrations and the same fixed seed. Tests can run in any order, in parallel, forever, and still agree with each other.
+Every test gets its own in-memory SQLite database, built from the real migrations and the same fixed seed. Tests can run in any order, in parallel, forever, and still agree with each other.
diff --git a/src/content/docs/minimalapi/explanation/how-its-built.mdx b/src/content/docs/minimalapi/explanation/how-its-built.mdx
index ffc444e..ea0b756 100644
--- a/src/content/docs/minimalapi/explanation/how-its-built.mdx
+++ b/src/content/docs/minimalapi/explanation/how-its-built.mdx
@@ -1,36 +1,17 @@
---
title: "How it's built"
-description: "The pipeline, the gates and the scanners that keep every pull request honest."
+description: "How the template repository itself is run: the rules on main, the monthly release and where these docs come from."
sidebar:
order: 3
---
-import ReportCard from '../../../../components/docs/ReportCard.astro';
+This page is about the template repository, not your solution. Your solution starts with a simpler pipeline, and [Set up CI](/minimalapi/guides/set-up-ci/) covers what it runs, along with every check the template runs on itself and what each one needs if you want to borrow it.
-This page is about how we build the template itself. Your solution starts with a simpler pipeline, covered in [Set up CI](/minimalapi/guides/set-up-ci/), and you can borrow any of the checks below when you're ready.
-
-These are templates we build for fun, but the pipeline doesn't know that. Every pull request goes through the same checks, and some of them are allowed to say no.
-
-**Gatekeepers** fail the build and block the merge. **Tattletales** report what they find and leave the decision to a human.
-
-## The checks
-
-
+These are templates we build for fun, but the repository doesn't know that. Every pull request goes through the same checks, and some of them are allowed to say no.
## The rules on main
-- Every change goes through a pull request, and every check above must pass.
+- Every change goes through a pull request, and every check must pass.
- Only the maintainer can merge. Pull requests from anyone are welcome, but the green button has one owner.
- Commits must be signed, and merges are squashed so the history stays readable.
- Release tags can't be moved or deleted once they exist, so a version always means the same code.
diff --git a/src/content/docs/minimalapi/getting-started.mdx b/src/content/docs/minimalapi/getting-started.mdx
index cbc833a..cceaeda 100644
--- a/src/content/docs/minimalapi/getting-started.mdx
+++ b/src/content/docs/minimalapi/getting-started.mdx
@@ -13,7 +13,7 @@ By the end of this page you'll have your own solution, an API running on your ma
You need the [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0). [Docker](https://www.docker.com/products/docker-desktop) is optional, but it gets you the Aspire dashboard for the logs, traces and metrics, and a container you can ship, so it's worth having around.
-## Make it yours
+## Create and run your solution
diff --git a/src/content/docs/minimalapi/guides/add-an-endpoint.mdx b/src/content/docs/minimalapi/guides/add-an-endpoint.mdx
index 3cb7986..758f6c6 100644
--- a/src/content/docs/minimalapi/guides/add-an-endpoint.mdx
+++ b/src/content/docs/minimalapi/guides/add-an-endpoint.mdx
@@ -10,7 +10,7 @@ import { Steps, Aside } from '@astrojs/starlight/components';
This guide adds `GET /api/movies/{id}/reviews`, one page of the reviews written about a movie. The Application layer already has a `GetReviewsQuery` that filters by movie, so all the work happens in Presentation. That's the point of the layers: new ways in, same rules underneath.
- If the query you want doesn't exist yet, add it to `src/Application` first: a query class, a handler and a FluentValidation validator, next to the others. Mediator and the validation behaviour pick them up automatically.
+ If the query you want doesn't exist yet, add it to `src/Application` first: a query class, a handler and a FluentValidation validator, next to the others. Mediator and the validation behaviour pick them up automatically. [Make it yours](/minimalapi/guides/make-it-yours/) walks through a whole new entity, from the Application layer to the tests.
@@ -24,7 +24,7 @@ This guide adds `GET /api/movies/{id}/reviews`, one page of the reviews written
using ReviewQueries = Application.Reviews.Queries;
```
-2. Map the route in `MapMovieEndpoints`, after the existing two. The group already has the `/api/movies` prefix, the **Movies** tag and the authorization requirement, so the new route inherits all three.
+2. Map the route in `MapMovieEndpoints`, after the existing two. The group already has the `/api/movies` prefix and the **Movies** tag, so the new route inherits both, along with the authorization requirement if you created the solution with `--include-auth`.
```csharp
_ = root.MapGet("/{id:guid}/reviews", GetMovieReviews)
@@ -96,7 +96,9 @@ This guide adds `GET /api/movies/{id}/reviews`, one page of the reviews written
}
```
-5. Run it.
+5. Change one number. `GetOpenApiDocument_ShouldDescribe_EveryOperation` in `tests/Presentation.Tests.Integration/OpenApi/OpenApiDocumentTests.cs` counts the operations the API publishes, and there's one more now, so make it 11. The same test checks that every operation has a summary, a sample request and a description for each parameter, which is why step 3's comments aren't optional either.
+
+6. Run it.
```bash
dotnet test
diff --git a/src/content/docs/minimalapi/guides/database.mdx b/src/content/docs/minimalapi/guides/database.mdx
index 0edef7b..1a90647 100644
--- a/src/content/docs/minimalapi/guides/database.mdx
+++ b/src/content/docs/minimalapi/guides/database.mdx
@@ -32,7 +32,7 @@ Set `ConnectionStrings__MovieReviews`. To keep everything in memory, which is wh
```bash
docker run --rm -p 7310:7310 -e DOTNET_ENVIRONMENT=docker \
-e ConnectionStrings__MovieReviews="Data Source=MovieReviews;Mode=Memory;Cache=Shared" \
- acme-movies:local
+ worksonmymachine-movies:local
```
## Add a migration
@@ -41,15 +41,21 @@ After you change the data model in `src/Infrastructure`:
```bash
dotnet tool install --global dotnet-ef
-dotnet ef migrations add AddDirectors --project ./src/Infrastructure --output-dir Databases/MovieReviews/Migrations
+dotnet ef migrations add AddDirectors \
+ --project ./src/Infrastructure \
+ --output-dir Databases/MovieReviews/Migrations \
+ --namespace WorksOnMyMachine.Movies.Infrastructure.Databases.MovieReviews.Migrations
+sed -i '1s/^\xEF\xBB\xBF//' src/Infrastructure/Databases/MovieReviews/Migrations/*.cs
```
-The next start applies it.
+Without `--namespace` the migration lands in a namespace of its own, and the `sed` strips the byte order mark the tool adds. On macOS, use `sed -i ''`. Then make `// ` the first line of the new `_AddDirectors.cs`, as it is in the other migrations, so the analysers and `dotnet format` leave it alone. Swap `WorksOnMyMachine.Movies` for your solution's name.
+
+The next start applies it, and [Make it yours](/minimalapi/guides/make-it-yours/) adds a whole entity this way.
- Every test class builds its own in-memory SQLite database from the same migrations and the same seed, so `dotnet test` never touches `movie-reviews.db`. You can run the tests while the API is running and neither will notice.
+ Every test builds its own in-memory SQLite database from the same migrations and the same seed, so `dotnet test` never touches `movie-reviews.db`. You can run the tests while the API is running and neither will notice.
## Swap SQLite for something real
-Everything database-shaped lives in Infrastructure, and every class there is `internal`. To move to PostgreSQL, SQL Server or anything else, implement the repository interfaces from Application (`IMoviesRepository`, `IAuthorsRepository` and `IReviewsRepository`) against the new store and register them in `Infrastructure/DependencyInjection.cs`. Application and Presentation don't need to know it happened. That's the whole sales pitch of Clean Architecture in one paragraph.
+Everything database-shaped lives in Infrastructure, and every class there but `DependencyInjection` is `internal`. To move to PostgreSQL, SQL Server or anything else, implement the repository interfaces from Application (`IMoviesRepository`, `IAuthorsRepository` and `IReviewsRepository`) against the new store and register them in `Infrastructure/DependencyInjection.cs`. Application and Presentation don't need to know it happened. That's the whole sales pitch of Clean Architecture in one paragraph.
diff --git a/src/content/docs/minimalapi/guides/make-it-yours.mdx b/src/content/docs/minimalapi/guides/make-it-yours.mdx
index 0ab9967..4051265 100644
--- a/src/content/docs/minimalapi/guides/make-it-yours.mdx
+++ b/src/content/docs/minimalapi/guides/make-it-yours.mdx
@@ -17,6 +17,8 @@ Go the other way round. Build your first slice beside the sample domain, prove i
The code below assumes you named the solution `WorksOnMyMachine.Movies`. Swap in your own name wherever you see it.
+## Build the new slice
+
1. **Add the entity and use cases to `src/Application`** (`add-a-use-case`).
@@ -297,6 +299,12 @@ The code below assumes you named the solution `WorksOnMyMachine.Movies`. Swap in
Without `--namespace` the migration lands in a namespace of its own. The `sed` strips the byte order mark the tool adds. On macOS, use `sed -i ''`. Then make `// ` the first line of the new `_AddCinemas.cs`, as it is in the other migrations, so the analysers leave generated code alone.
+
+
+## Expose and test it
+
+
+
4. **Put it on endpoints in `src/Presentation`** (`expose-a-use-case` and `xml-docs-and-openapi`).
The XML comments aren't decoration. They become the OpenAPI description, and a test fails if an operation or schema has none.
@@ -431,15 +439,13 @@ The code below assumes you named the solution `WorksOnMyMachine.Movies`. Swap in
Each new test has a sample test that already does the same job for movies or reviews. Copy it and rename it, and you have the shape, the names and the house style without having to think about them.
- | New test | Copy from |
- | --- | --- |
- | `tests/Application.Tests.Unit/Cinemas/Queries/GetCinemas/GetCinemasHandlerTests.cs` | `Movies/Queries/GetMovies/GetMoviesHandlerTests.cs` |
- | `tests/Application.Tests.Unit/Cinemas/Queries/GetCinemas/GetCinemasValidatorTests.cs` | `Movies/Queries/GetMovies/GetMoviesValidatorTests.cs` |
- | `tests/Application.Tests.Unit/Cinemas/Commands/CreateCinema/CreateCinemaHandlerTests.cs` | `Reviews/Commands/CreateReview/CreateReviewHandlerTests.cs` |
- | `tests/Application.Tests.Unit/Cinemas/Commands/CreateCinema/CreateCinemaValidatorTests.cs` | `Reviews/Commands/CreateReview/CreateReviewValidatorTests.cs` |
- | A `Cinemas` region in `tests/Infrastructure.Tests.Integration/Databases/MovieReviews/EntityFrameworkMovieReviewsRepositoryTests.cs` | The `Movies` region: search, paging and create |
- | `tests/Presentation.Tests.Unit/Endpoints/CinemaEndpointTests.cs` | `MovieEndpointTests.cs` and `ReviewEndpointTests.cs` |
- | `tests/Presentation.Tests.Integration/Endpoints/CinemaEndpointTests.cs` | `MovieEndpointTests.cs` and `ReviewEndpointTests.cs` |
+ - `tests/Application.Tests.Unit/Cinemas/Queries/GetCinemas/GetCinemasHandlerTests.cs`, copied from `Movies/Queries/GetMovies/GetMoviesHandlerTests.cs`.
+ - `tests/Application.Tests.Unit/Cinemas/Queries/GetCinemas/GetCinemasValidatorTests.cs`, copied from `Movies/Queries/GetMovies/GetMoviesValidatorTests.cs`.
+ - `tests/Application.Tests.Unit/Cinemas/Commands/CreateCinema/CreateCinemaHandlerTests.cs`, copied from `Reviews/Commands/CreateReview/CreateReviewHandlerTests.cs`.
+ - `tests/Application.Tests.Unit/Cinemas/Commands/CreateCinema/CreateCinemaValidatorTests.cs`, copied from `Reviews/Commands/CreateReview/CreateReviewValidatorTests.cs`.
+ - A `Cinemas` region in `tests/Infrastructure.Tests.Integration/Databases/MovieReviews/EntityFrameworkMovieReviewsRepositoryTests.cs`, copied from the `Movies` region: search, paging and create.
+ - `tests/Presentation.Tests.Unit/Endpoints/CinemaEndpointTests.cs`, copied from `MovieEndpointTests.cs` and `ReviewEndpointTests.cs`.
+ - `tests/Presentation.Tests.Integration/Endpoints/CinemaEndpointTests.cs`, copied from `MovieEndpointTests.cs` and `ReviewEndpointTests.cs`.
The repository tests need some cinemas. Add three to `AddTestData` in `tests/Infrastructure.Tests.Integration/Databases/MovieReviews/Extensions/MovieReviewsCollectionData.cs`, named the same way as the sample rows:
@@ -454,7 +460,7 @@ The code below assumes you named the solution `WorksOnMyMachine.Movies`. Swap in
context.Cinemas.AddRange(cinemas);
```
- Two existing tests in `tests/Presentation.Tests.Integration/OpenApi/OpenApiDocumentTests.cs` fail as soon as the endpoints exist. They're doing their job: they pin down exactly what the API publishes.
+ Three existing tests in `tests/Presentation.Tests.Integration/OpenApi/OpenApiDocumentTests.cs` fail as soon as the endpoints exist. They're doing their job: they pin down exactly what the API publishes.
- **`GetOpenApiDocument_ShouldDescribe_Tags`:** add `"Cinemas"` to the expected tags.
- **`GetOpenApiDocument_ShouldDescribe_EveryOperation`:** add 2 to the operation count, one for each new endpoint.
@@ -468,6 +474,12 @@ The code below assumes you named the solution `WorksOnMyMachine.Movies`. Swap in
Both domains now run side by side. Stop here if you're keeping the sample for reference, which is a perfectly respectable choice.
+
+
+## Take out the sample
+
+
+
6. **Take out the sample movies, authors and reviews** (`sample-data` and `database-migrations`).
This step is mostly deleting, but the template's own tests use the sample as their test subject, so a few of them change instead of going. Work through it layer by layer and build as you go. The compiler finds what's left faster than any list.
@@ -506,16 +518,13 @@ The code below assumes you named the solution `WorksOnMyMachine.Movies`. Swap in
- `EntityFrameworkMovieReviewsRepositoryTests.cs`: the `Authors`, `Movies` and `Reviews` regions.
- `MovieReviewsCollectionData.cs`: the author, movie and review rows.
- **Tests to change:** these test the template's plumbing, and they used the sample as their subject. Point them at cinemas.
-
- | Test | Change |
- | --- | --- |
- | `Common/Behaviours/LoggingBehaviourTests.cs` and `ValidationBehaviourTests.cs` | Use `CreateCinemaCommand`, `CreateCinemaValidator` and `Cinema` in place of `GetAuthorByIdQuery`, its validator and `Author`. The invalid case is an empty `Name`, so the expected property is `Name`. The logged message names `CreateCinemaCommand`. |
- | `Common/Exceptions/NotFoundExceptionTests.cs` | Use `EntityType.Cinema` and a `Cinema`. The message becomes "The Cinema with the supplied id was not found." |
- | `MovieReviewsConfigurationTests.cs` | Check a cinema loads, ignoring case: look up `"one"` and expect `"One"`. |
- | `DependencyInjectionTests.cs` | Expect `ICinemasRepository`, 5 seeded cinemas and none when seeding is off. Use `GetCinemas` for the concurrency and same-sample-data checks. |
- | `Telemetry/TelemetryTests.cs` | Call `/api/cinemas?limit=5` and expect a `GetCinemasQuery` span. |
- | `OpenApi/OpenApiDocumentTests.cs` | Expect the tags `Cinemas` and `Version`, 3 operations, and the schemas `Cinema`, `PageOfCinema`, `CreateCinemaRequest` and `VersionInfo`. |
- | `AuthenticationTests.cs`, only with `--include-auth` | Point its requests at `/api/cinemas`. |
+ - **`Common/Behaviours/LoggingBehaviourTests.cs` and `ValidationBehaviourTests.cs`:** Use `CreateCinemaCommand`, `CreateCinemaValidator` and `Cinema` in place of `GetAuthorByIdQuery`, its validator and `Author`. The invalid case is an empty `Name`, so the expected property is `Name`. The logged message names `CreateCinemaCommand`.
+ - **`Common/Exceptions/NotFoundExceptionTests.cs`:** Use `EntityType.Cinema` and a `Cinema`. The message becomes "The Cinema with the supplied id was not found."
+ - **`MovieReviewsConfigurationTests.cs`:** Check a cinema loads, ignoring case: look up `"one"` and expect `"One"`.
+ - **`DependencyInjectionTests.cs`:** Expect `ICinemasRepository`, 5 seeded cinemas and none when seeding is off. Use `GetCinemas` for the concurrency and same-sample-data checks.
+ - **`Telemetry/TelemetryTests.cs`:** Call `/api/cinemas?limit=5` and expect a `GetCinemasQuery` span.
+ - **`OpenApi/OpenApiDocumentTests.cs`:** Expect the tags `Cinemas` and `Version`, 3 operations, and the schemas `Cinema`, `PageOfCinema`, `CreateCinemaRequest` and `VersionInfo`.
+ - **`AuthenticationTests.cs`, only with `--include-auth`:** Point its requests at `/api/cinemas`.
7. **Check it the way CI will** (`coverage`).
@@ -535,4 +544,4 @@ The code below assumes you named the solution `WorksOnMyMachine.Movies`. Swap in
## What's still called "movie reviews"
-The database plumbing keeps its sample name: the `Databases/MovieReviews` folder, `MovieReviewsDbContext`, `EntityFrameworkMovieReviewsRepository`, the `MovieReviews` connection string and the `movie-reviews.db` file. They all work as they are. Everything in `src/Infrastructure` is `internal`, so renaming them later is a refactor that never reaches Application or Presentation. Do it when the name starts to bother you, and change the connection string in every `appsettings.*.json` and in `docker-compose.yml` at the same time.
+The database plumbing keeps its sample name: the `Databases/MovieReviews` folder, `MovieReviewsDbContext`, `EntityFrameworkMovieReviewsRepository`, the `MovieReviews` connection string and the `movie-reviews.db` file. They all work as they are. Everything in `src/Infrastructure` apart from `DependencyInjection` is `internal`, so renaming them later is a refactor that never reaches Application or Presentation. Do it when the name starts to bother you, and change the connection string in every `appsettings.*.json` and in `docker-compose.yml` at the same time.
diff --git a/src/content/docs/minimalapi/guides/run-locally.mdx b/src/content/docs/minimalapi/guides/run-locally.mdx
index 8f34a90..f0e0a89 100644
--- a/src/content/docs/minimalapi/guides/run-locally.mdx
+++ b/src/content/docs/minimalapi/guides/run-locally.mdx
@@ -28,8 +28,8 @@ There are two ready-made environments. `local` is for `dotnet run`: sample data
```bash
- docker build . --target run --tag acme-movies:local
- docker run --rm -p 7310:7310 -e DOTNET_ENVIRONMENT=docker acme-movies:local
+ docker build . --target run --tag worksonmymachine-movies:local
+ docker run --rm -p 7310:7310 -e DOTNET_ENVIRONMENT=docker worksonmymachine-movies:local
```
Without Compose there's no collector, so the logs stay on the console and nothing else is exported. Set `OTEL_EXPORTER_OTLP_ENDPOINT` to send them to a collector of your own.
@@ -82,7 +82,7 @@ curl -X POST "https://localhost:7311/api/reviews" -H "Content-Type: application/
dotnet test
```
-Each test class builds its own in-memory SQLite database from the migrations and the same fixed seed. No test can change what another one sees, and nothing touches `movie-reviews.db`.
+The tests build their own in-memory SQLite databases from the same migrations and seed, so nothing touches `movie-reviews.db`. [Test your changes](/minimalapi/guides/test-your-changes/) covers where a test goes, how the tests are written and how to check coverage before CI does.
## Run just one part of the Dockerfile
@@ -105,7 +105,7 @@ A failing test doesn't fail the build, so the results still come out. Check `out
## Stamp a version
```bash
-docker build . --target run --tag acme-movies:latest \
+docker build . --target run --tag worksonmymachine-movies:latest \
--build-arg VERSION_PREFIX=1.2.3 --build-arg VERSION_SUFFIX=beta
```
diff --git a/src/content/docs/minimalapi/guides/secure-the-api.mdx b/src/content/docs/minimalapi/guides/secure-the-api.mdx
index 6c56f2d..f45fc5e 100644
--- a/src/content/docs/minimalapi/guides/secure-the-api.mdx
+++ b/src/content/docs/minimalapi/guides/secure-the-api.mdx
@@ -53,7 +53,7 @@ Copy across what's new: the `Microsoft.AspNetCore.Authentication.JwtBearer` pack
## What changes
- The movie, author and review endpoints need a valid token. `GET /api/version` needs one too. `/health/live` and `/health/ready` stay open, so load balancers and curious humans can still check the lights are on.
-- The OpenAPI document gains a `Bearer` security scheme, and Scalar shows an **Auth** box where you can paste a token and keep clicking **Send**.
+- The OpenAPI document gains a `Bearer` security scheme, and Scalar gains an **Authentication** box beside the introduction. Pick **Bearer** under **Select Auth Type**, paste a token, and **Send** carries it on every request.
- Tokens are validated against the authority's published keys, so there's no secret to store and nothing to rotate on your side.
## How it works
diff --git a/src/content/docs/minimalapi/guides/set-up-ci.mdx b/src/content/docs/minimalapi/guides/set-up-ci.mdx
index ad4a623..5f48680 100644
--- a/src/content/docs/minimalapi/guides/set-up-ci.mdx
+++ b/src/content/docs/minimalapi/guides/set-up-ci.mdx
@@ -1,25 +1,47 @@
---
title: "Set up CI"
-description: "Your solution starts with a simple pipeline. Here's what it runs and how to add more."
+description: "Your solution starts with a simple pipeline on purpose. Here's what it runs, and the checks you can add when your process calls for them."
sidebar:
order: 7
---
import { Steps } from '@astrojs/starlight/components';
+import ReportCard from '../../../../components/docs/ReportCard.astro';
-Whichever way you made your solution, it starts with a simple pipeline: format, build, and test with a 95% coverage bar. Those are the same commands as in `AGENTS.md`, so the pipeline and your laptop agree on what "green" means. It needs no secrets, no accounts and no paid features. Everything else is yours to add when you want it.
+Your solution starts with a simple pipeline: format, build, and test with a 95% coverage bar. Those are the same commands as in `AGENTS.md`, so the pipeline and your laptop agree on what "green" means. It needs no secrets, no accounts and no paid features, and it's green on the first push.
-## Made with `dotnet new`
+It's minimal on purpose. We don't know your review process, your security team or your deployment target, so the pipeline stops where guessing would start. Everything else on this page is a check you can add when your process calls for it.
+
+## What runs
You get `.github/workflows/ci.yml`, one GitHub Actions job that restores, checks formatting, builds, runs every test and checks the coverage total. Push to GitHub and it runs.
Not on GitHub Actions? Ask your coding agent to move it. It works as Jacques, so it uses the `ci-for-your-solution` skill in `.agents/skills` to port the same commands to Azure Pipelines, GitLab, or the Jenkins box under someone's desk, then breaks something on purpose to prove the new pipeline notices.
-## Made with "Use this template"
+## Adding more
+
+The `ci-for-your-solution` skill lists the checks you can add, what each one catches and what it needs, from a vulnerable package check that needs nothing to CodeQL, which needs Advanced Security on a private repository. Ask your agent to add one and it will agree it with you first.
+
+For a worked example of each, look at the template's own [Build Pipeline](https://github.com/LeanMeanCleanApps/CleanMinimalApi/blob/main/.github/workflows/build-pipeline.yml). Every pull request to the template goes through these checks, and each one is a job you can lift into `ci.yml`. **Gatekeepers** fail the build and block the merge. **Tattletales** report what they find and leave the decision to a human.
+
+
-Your copy has our Build Pipeline. Outside the LeanMeanCleanApps organisation it runs the format check, build and tests, and the Summary shows the rest as not run. That covers CodeQL, Dependency Review, the container scan, and the steps that pack, verify and publish the template. So a private copy goes green without GitHub Advanced Security, and it never tries to publish to NuGet on your behalf.
+Borrow freely, but leave out the jobs that pack, verify and publish the template. You're shipping the API, not a template. Whatever you add, run the same commands locally that the pipeline runs, and make the checks you rely on required on `main`.
-### After you copy
+## If you used "Use this template"
+
+A copy made with GitHub's **Use this template** button has the whole Build Pipeline rather than `ci.yml`. Outside the LeanMeanCleanApps organisation it runs the format check, build and tests, and the Summary shows the rest as not run: CodeQL, Dependency Review, the container scan, and the steps that pack, verify and publish the template. So a private copy goes green without GitHub Advanced Security, and it never tries to publish to NuGet on your behalf. Three things to change after you copy:
@@ -30,9 +52,3 @@ Your copy has our Build Pipeline. Outside the LeanMeanCleanApps organisation it
3. Fill in **Where we track work** in `AGENTS.md`, so agents put their plans and handoffs where your team will see them.
-
-## Adding more
-
-The `ci-for-your-solution` skill lists the checks you can add, what each one catches and what it needs, from a vulnerable package check that needs nothing to CodeQL, which needs Advanced Security on a private repository. Ask your agent to add one and it will agree it with you first.
-
-For a worked example of every check, read our [Build Pipeline](https://github.com/LeanMeanCleanApps/CleanMinimalApi/blob/main/.github/workflows/build-pipeline.yml). Borrow freely, but leave out the jobs that pack and publish the template. You're shipping an app, not a template.
diff --git a/src/content/docs/minimalapi/guides/ship-the-container.mdx b/src/content/docs/minimalapi/guides/ship-the-container.mdx
index 35e7127..3c32bf6 100644
--- a/src/content/docs/minimalapi/guides/ship-the-container.mdx
+++ b/src/content/docs/minimalapi/guides/ship-the-container.mdx
@@ -12,7 +12,7 @@ The `run` stage of the `Dockerfile` is the whole API: one image, built on `mcr.m
## Build it
```bash
-docker build . --target run --tag acme-movies:1.2.3 \
+docker build . --target run --tag worksonmymachine-movies:1.2.3 \
--build-arg VERSION_PREFIX=1.2.3
```
@@ -32,13 +32,13 @@ For production, pick a name like `production`, add an `appsettings.production.js
## Put it behind HTTPS
-The container only listens on HTTP. Outside the `local` and `Development` environments it sends an HSTS header and redirects HTTP to HTTPS, which means it expects something in front of it to end TLS: a load balancer, a reverse proxy or your platform's ingress. Tell it so:
+The container only listens on HTTP, and it expects something in front of it to end TLS: a load balancer, a reverse proxy or your platform's ingress. Outside the `local` and `Development` environments it sends an HSTS header, and it redirects HTTP to HTTPS once it knows an HTTPS port to send people to, such as `ASPNETCORE_HTTPS_PORT=443`. Tell it about the proxy too:
```bash
ASPNETCORE_FORWARDEDHEADERS_ENABLED=true
```
-Without that, the API thinks every request arrived over plain HTTP and redirects it, and the proxy sends it back, and the two of them go round and round until someone's pager goes off.
+Without that, the API thinks every request arrived over plain HTTP. Give it an HTTPS port in that state and it redirects the proxy's requests, the proxy sends them back, and the two of them go round and round until someone's pager goes off.
## Wire up the health checks
diff --git a/src/content/docs/minimalapi/guides/test-your-changes.mdx b/src/content/docs/minimalapi/guides/test-your-changes.mdx
index 78cbc68..8c92b84 100644
--- a/src/content/docs/minimalapi/guides/test-your-changes.mdx
+++ b/src/content/docs/minimalapi/guides/test-your-changes.mdx
@@ -14,7 +14,7 @@ Your solution starts with four test projects and a pipeline that fails under 95%
| Project | What it tests | How |
| --- | --- | --- |
| `tests/Application.Tests.Unit` | Handlers, validators and pipeline behaviours | xUnit, Shouldly and NSubstitute, with the repositories substituted |
-| `tests/Infrastructure.Tests.Integration` | The repositories against a real database | A private in-memory SQLite database per test class, built from the migrations and the fixed seed |
+| `tests/Infrastructure.Tests.Integration` | The repositories against a real database | A private in-memory SQLite database per test, built from the migrations and the fixed seed |
| `tests/Presentation.Tests.Unit` | Endpoints and the error handler, called directly | Plain unit tests, no host |
| `tests/Presentation.Tests.Integration` | Real HTTP requests to every endpoint, authentication, health checks, OpenAPI and telemetry | `WebApplicationFactory` with a private database |
@@ -71,4 +71,5 @@ The last table it prints is the merged total. `Program.cs`, the EF Core migratio
## Where to next
- [Add an endpoint](/minimalapi/guides/add-an-endpoint/) ends with a test for the new endpoint.
+- [Make it yours](/minimalapi/guides/make-it-yours/) adds a whole entity, then clears the sample out without dropping under the coverage gate.
- [Work with a coding agent](/minimalapi/guides/work-with-a-coding-agent/): the `test-design`, `unit-tests`, `integration-tests` and `coverage` skills hold these same rules, so an agent writes tests the same way you would.
diff --git a/src/content/docs/minimalapi/guides/work-with-a-coding-agent.mdx b/src/content/docs/minimalapi/guides/work-with-a-coding-agent.mdx
index 708011c..a73d267 100644
--- a/src/content/docs/minimalapi/guides/work-with-a-coding-agent.mdx
+++ b/src/content/docs/minimalapi/guides/work-with-a-coding-agent.mdx
@@ -27,7 +27,7 @@ The skills are written for this solution, not for .NET in general. `add-a-use-ca
3. For anything bigger than a few lines, Jacques agrees a plan first: the open decisions one at a time, each with a recommendation, then the scope in a paragraph. Answer those and the work starts.
4. Before it says it's done, the agent runs `dotnet build`, `dotnet test` and `dotnet format --verify-no-changes`, the same checks as CI.
-## Make it yours
+## Tune it for your team
- **Where we track work.** `AGENTS.md` has a line that says "Not set". Replace it with your GitHub project, Jira board or Azure Boards, and agents put their plans and handoffs there instead of asking each time.
- **Change a rule, change the skill.** When your team decides to do something differently, update the matching `SKILL.md` in the same pull request. A skill that disagrees with the code is worse than no skill at all.
diff --git a/src/content/docs/minimalapi/index.mdx b/src/content/docs/minimalapi/index.mdx
index 54e826c..eb11144 100644
--- a/src/content/docs/minimalapi/index.mdx
+++ b/src/content/docs/minimalapi/index.mdx
@@ -2,6 +2,8 @@
title: Clean Minimal API
description: A .NET 10 dotnet new template for an ASP.NET Core Minimal API, built with Clean Architecture minus the ceremony.
template: splash
+editUrl: false
+lastUpdated: false
prev: false
next: false
hero:
diff --git a/src/content/docs/minimalapi/reference/configuration.mdx b/src/content/docs/minimalapi/reference/configuration.mdx
index 87d85d0..31446f9 100644
--- a/src/content/docs/minimalapi/reference/configuration.mdx
+++ b/src/content/docs/minimalapi/reference/configuration.mdx
@@ -51,7 +51,7 @@ The app always collects [OpenTelemetry](https://opentelemetry.io) traces and met
| `OTEL_EXPORTER_OTLP_ENDPOINT` | none | Where to send traces, metrics and logs. Nothing is exported until it's set. Docker Compose sets it to the `otel-collector` service |
| `OTEL_EXPORTER_OTLP_PROTOCOL` | `grpc` | `grpc` or `http/protobuf` |
| `OTEL_EXPORTER_OTLP_HEADERS` | none | Headers for every export, such as an API key for a hosted backend |
-| `OTEL_SERVICE_NAME` | `CleanMinimalApi` | The `service.name` on everything sent |
+| `OTEL_SERVICE_NAME` | Your solution's name | The `service.name` on everything sent |
Every span, metric and log also carries `service.version`, the version stamped into the build, and `deployment.environment.name`.
diff --git a/src/content/docs/minimalapi/reference/project-structure.mdx b/src/content/docs/minimalapi/reference/project-structure.mdx
index bcbdc51..2e97561 100644
--- a/src/content/docs/minimalapi/reference/project-structure.mdx
+++ b/src/content/docs/minimalapi/reference/project-structure.mdx
@@ -60,4 +60,4 @@ import { FileTree } from '@astrojs/starlight/components';
| --- | --- | --- |
| Presentation | Application, Infrastructure | References Infrastructure only to wire up dependency injection |
| Application | none | Owns the entities, the repository interfaces and the rules |
-| Infrastructure | Application | Every class is `internal` |
+| Infrastructure | Application | Every class but `DependencyInjection` is `internal` |
diff --git a/src/content/docs/minimalapi/reference/troubleshooting.mdx b/src/content/docs/minimalapi/reference/troubleshooting.mdx
index 4975dd4..8ef8395 100644
--- a/src/content/docs/minimalapi/reference/troubleshooting.mdx
+++ b/src/content/docs/minimalapi/reference/troubleshooting.mdx
@@ -87,5 +87,5 @@ Docker doesn't start itself, however much we'd like it to. Start Docker Desktop
That's authentication doing its job. Every `/api/*` route requires a valid JWT bearer token when authentication is included.
-- In Scalar (`/scalar`), click **Authorize** at the top of the page and paste a bearer token issued by your `--authority`.
+- In Scalar (`/scalar`), find the **Authentication** box beside the introduction, pick **Bearer** under **Select Auth Type** and paste a token issued by your `--authority`. Until you pick it, the box says "No authentication selected" and every request goes without one.
- In `src/Presentation/Presentation.http`, uncomment the `@token` line at the top of the file, paste your token after it, and uncomment the `Authorization` header on each request.
diff --git a/src/content/docs/webapi/explanation/architecture.mdx b/src/content/docs/webapi/explanation/architecture.mdx
index 34873bb..e140d70 100644
--- a/src/content/docs/webapi/explanation/architecture.mdx
+++ b/src/content/docs/webapi/explanation/architecture.mdx
@@ -45,7 +45,7 @@ There's a little CQRS in the style of the commands and queries, but reads and wr
In textbook Clean Architecture, Presentation never references Infrastructure. Here it does, so it can wire up dependency injection without a fourth project whose only job is to call two methods.
-To keep that from turning into a free-for-all, every class in Infrastructure is `internal`. Presentation can call `AddInfrastructure` and nothing else. If you try to new up a repository from an endpoint, the compiler will politely decline.
+To keep that from turning into a free-for-all, every class in Infrastructure except `DependencyInjection` is `internal`. Presentation can call `AddInfrastructure` and nothing else. If you try to new up a repository from an endpoint, the compiler will politely decline.
## What happens on a request
diff --git a/src/content/docs/webapi/explanation/decisions.mdx b/src/content/docs/webapi/explanation/decisions.mdx
index 4c1918b..160be4a 100644
--- a/src/content/docs/webapi/explanation/decisions.mdx
+++ b/src/content/docs/webapi/explanation/decisions.mdx
@@ -52,4 +52,4 @@ The one rule we bend is letting Presentation reference Infrastructure for depend
## Tests that don't share
-Every test class gets its own in-memory SQLite database, built from the real migrations and the same fixed seed. Tests can run in any order, in parallel, forever, and still agree with each other.
+Every test gets its own in-memory SQLite database, built from the real migrations and the same fixed seed. Tests can run in any order, in parallel, forever, and still agree with each other.
diff --git a/src/content/docs/webapi/explanation/how-its-built.mdx b/src/content/docs/webapi/explanation/how-its-built.mdx
index 9bddff6..1d2c8d2 100644
--- a/src/content/docs/webapi/explanation/how-its-built.mdx
+++ b/src/content/docs/webapi/explanation/how-its-built.mdx
@@ -1,36 +1,17 @@
---
title: "How it's built"
-description: "The pipeline, the gates and the scanners that keep every pull request honest."
+description: "How the template repository itself is run: the rules on main, the monthly release and where these docs come from."
sidebar:
order: 3
---
-import ReportCard from '../../../../components/docs/ReportCard.astro';
+This page is about the template repository, not your solution. Your solution starts with a simpler pipeline, and [Set up CI](/webapi/guides/set-up-ci/) covers what it runs, along with every check the template runs on itself and what each one needs if you want to borrow it.
-This page is about how we build the template itself. Your solution starts with a simpler pipeline, covered in [Set up CI](/webapi/guides/set-up-ci/), and you can borrow any of the checks below when you're ready.
-
-These are templates we build for fun, but the pipeline doesn't know that. Every pull request goes through the same checks, and some of them are allowed to say no.
-
-**Gatekeepers** fail the build and block the merge. **Tattletales** report what they find and leave the decision to a human.
-
-## The checks
-
-
+These are templates we build for fun, but the repository doesn't know that. Every pull request goes through the same checks, and some of them are allowed to say no.
## The rules on main
-- Every change goes through a pull request, and every check above must pass.
+- Every change goes through a pull request, and every check must pass.
- Only the maintainer can merge. Pull requests from anyone are welcome, but the green button has one owner.
- Commits must be signed, and merges are squashed so the history stays readable.
- Release tags can't be moved or deleted once they exist, so a version always means the same code.
diff --git a/src/content/docs/webapi/getting-started.mdx b/src/content/docs/webapi/getting-started.mdx
index ec51e6f..dd1c01d 100644
--- a/src/content/docs/webapi/getting-started.mdx
+++ b/src/content/docs/webapi/getting-started.mdx
@@ -13,7 +13,7 @@ By the end of this page you'll have your own solution, an API running on your ma
You need the [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0). [Docker](https://www.docker.com/products/docker-desktop) is optional, but it gets you the Aspire dashboard for the logs, traces and metrics, and a container you can ship, so it's worth having around.
-## Make it yours
+## Create and run your solution
diff --git a/src/content/docs/webapi/guides/add-an-endpoint.mdx b/src/content/docs/webapi/guides/add-an-endpoint.mdx
index 0c23e89..2db4847 100644
--- a/src/content/docs/webapi/guides/add-an-endpoint.mdx
+++ b/src/content/docs/webapi/guides/add-an-endpoint.mdx
@@ -10,7 +10,7 @@ import { Steps, Aside } from '@astrojs/starlight/components';
This guide adds `GET /api/movies/{id}/reviews`, one page of the reviews written about a movie. The Application layer already has a `GetReviewsQuery` that filters by movie, so all the work happens in Presentation. That's the point of the layers: new ways in, same rules underneath.
- If the query you want doesn't exist yet, add it to `src/Application` first: a query class, a handler and a FluentValidation validator, next to the others. Mediator and the validation behaviour pick them up automatically.
+ If the query you want doesn't exist yet, add it to `src/Application` first: a query class, a handler and a FluentValidation validator, next to the others. Mediator and the validation behaviour pick them up automatically. [Make it yours](/webapi/guides/make-it-yours/) walks through a whole new entity, from the Application layer to the tests.
@@ -24,7 +24,7 @@ This guide adds `GET /api/movies/{id}/reviews`, one page of the reviews written
using ReviewQueries = Application.Reviews.Queries;
```
-2. Add the action to the controller. `ApiControllerBase` already gives it `[ApiController]`, `[Authorize]`, `[Consumes("application/json")]` and the `Sender`, and the `[Route("api/movies")]` on the class gives the route its prefix. The XML comments aren't decoration: they become the summary, description, parameter docs and response descriptions in the OpenAPI document, and Scalar shows every one of them.
+2. Add the action to the controller. `ApiControllerBase` already gives it `[ApiController]`, `[Consumes("application/json")]` and the `Sender`, plus `[Authorize]` if you created the solution with `--include-auth`, and the `[Route("api/movies")]` on the class gives the route its prefix. The XML comments aren't decoration: they become the summary, description, parameter docs and response descriptions in the OpenAPI document, and Scalar shows every one of them.
```csharp
///
@@ -90,7 +90,9 @@ This guide adds `GET /api/movies/{id}/reviews`, one page of the reviews written
}
```
-4. Run it.
+4. Change one number. `GetOpenApiDocument_ShouldDescribe_EveryOperation` in `tests/Presentation.Tests.Integration/OpenApi/OpenApiDocumentTests.cs` counts the operations the API publishes, and there's one more now, so make it 11. The same test checks that every operation has a summary, a sample request and a description for each parameter, which is why step 2's comments aren't optional either.
+
+5. Run it.
```bash
dotnet test
diff --git a/src/content/docs/webapi/guides/database.mdx b/src/content/docs/webapi/guides/database.mdx
index dc4aed6..596a9b4 100644
--- a/src/content/docs/webapi/guides/database.mdx
+++ b/src/content/docs/webapi/guides/database.mdx
@@ -32,7 +32,7 @@ Set `ConnectionStrings__MovieReviews`. To keep everything in memory, which is wh
```bash
docker run --rm -p 7340:7340 -e DOTNET_ENVIRONMENT=docker \
-e ConnectionStrings__MovieReviews="Data Source=MovieReviews;Mode=Memory;Cache=Shared" \
- acme-movies:local
+ worksonmymachine-movies:local
```
## Add a migration
@@ -41,15 +41,21 @@ After you change the data model in `src/Infrastructure`:
```bash
dotnet tool install --global dotnet-ef
-dotnet ef migrations add AddDirectors --project ./src/Infrastructure --output-dir Databases/MovieReviews/Migrations
+dotnet ef migrations add AddDirectors \
+ --project ./src/Infrastructure \
+ --output-dir Databases/MovieReviews/Migrations \
+ --namespace WorksOnMyMachine.Movies.Infrastructure.Databases.MovieReviews.Migrations
+sed -i '1s/^\xEF\xBB\xBF//' src/Infrastructure/Databases/MovieReviews/Migrations/*.cs
```
-The next start applies it.
+Without `--namespace` the migration lands in a namespace of its own, and the `sed` strips the byte order mark the tool adds. On macOS, use `sed -i ''`. Then make `// ` the first line of the new `_AddDirectors.cs`, as it is in the other migrations, so the analysers and `dotnet format` leave it alone. Swap `WorksOnMyMachine.Movies` for your solution's name.
+
+The next start applies it, and [Make it yours](/webapi/guides/make-it-yours/) adds a whole entity this way.
- Every test class builds its own in-memory SQLite database from the same migrations and the same seed, so `dotnet test` never touches `movie-reviews.db`. You can run the tests while the API is running and neither will notice.
+ Every test builds its own in-memory SQLite database from the same migrations and the same seed, so `dotnet test` never touches `movie-reviews.db`. You can run the tests while the API is running and neither will notice.
## Swap SQLite for something real
-Everything database-shaped lives in Infrastructure, and every class there is `internal`. To move to PostgreSQL, SQL Server or anything else, implement the repository interfaces from Application (`IMoviesRepository`, `IAuthorsRepository` and `IReviewsRepository`) against the new store and register them in `Infrastructure/DependencyInjection.cs`. Application and Presentation don't need to know it happened. That's the whole sales pitch of Clean Architecture in one paragraph.
+Everything database-shaped lives in Infrastructure, and every class there but `DependencyInjection` is `internal`. To move to PostgreSQL, SQL Server or anything else, implement the repository interfaces from Application (`IMoviesRepository`, `IAuthorsRepository` and `IReviewsRepository`) against the new store and register them in `Infrastructure/DependencyInjection.cs`. Application and Presentation don't need to know it happened. That's the whole sales pitch of Clean Architecture in one paragraph.
diff --git a/src/content/docs/webapi/guides/make-it-yours.mdx b/src/content/docs/webapi/guides/make-it-yours.mdx
index 91c47c0..7086926 100644
--- a/src/content/docs/webapi/guides/make-it-yours.mdx
+++ b/src/content/docs/webapi/guides/make-it-yours.mdx
@@ -17,6 +17,8 @@ Go the other way round. Build your first slice beside the sample domain, prove i
The code below assumes you named the solution `WorksOnMyMachine.Movies`. Swap in your own name wherever you see it.
+## Build the new slice
+
1. **Add the entity and use cases to `src/Application`** (`add-a-use-case`).
@@ -297,6 +299,12 @@ The code below assumes you named the solution `WorksOnMyMachine.Movies`. Swap in
Without `--namespace` the migration lands in a namespace of its own. The `sed` strips the byte order mark the tool adds. On macOS, use `sed -i ''`. Then make `// ` the first line of the new `_AddCinemas.cs`, as it is in the other migrations, so the analysers leave generated code alone.
+
+
+## Expose and test it
+
+
+
4. **Put it on a controller in `src/Presentation`** (`expose-a-use-case` and `xml-docs-and-openapi`).
The XML comments aren't decoration. They become the OpenAPI description, and a test fails if an operation or schema has none. Each controller gets its own folder under `Controllers/`, with its request models beside it.
@@ -415,14 +423,12 @@ The code below assumes you named the solution `WorksOnMyMachine.Movies`. Swap in
Each new test has a sample test that already does the same job for movies or reviews. Copy it and rename it, and you have the shape, the names and the house style without having to think about them.
- | New test | Copy from |
- | --- | --- |
- | `tests/Application.Tests.Unit/Cinemas/Queries/GetCinemas/GetCinemasHandlerTests.cs` | `Movies/Queries/GetMovies/GetMoviesHandlerTests.cs` |
- | `tests/Application.Tests.Unit/Cinemas/Queries/GetCinemas/GetCinemasValidatorTests.cs` | `Movies/Queries/GetMovies/GetMoviesValidatorTests.cs` |
- | `tests/Application.Tests.Unit/Cinemas/Commands/CreateCinema/CreateCinemaHandlerTests.cs` | `Reviews/Commands/CreateReview/CreateReviewHandlerTests.cs` |
- | `tests/Application.Tests.Unit/Cinemas/Commands/CreateCinema/CreateCinemaValidatorTests.cs` | `Reviews/Commands/CreateReview/CreateReviewValidatorTests.cs` |
- | A `Cinemas` region in `tests/Infrastructure.Tests.Integration/Databases/MovieReviews/EntityFrameworkMovieReviewsRepositoryTests.cs` | The `Movies` region: search, paging and create |
- | `tests/Presentation.Tests.Integration/Controllers/CinemaEndpointTests.cs` | `MovieEndpointTests.cs` and `ReviewEndpointTests.cs` |
+ - `tests/Application.Tests.Unit/Cinemas/Queries/GetCinemas/GetCinemasHandlerTests.cs`, copied from `Movies/Queries/GetMovies/GetMoviesHandlerTests.cs`.
+ - `tests/Application.Tests.Unit/Cinemas/Queries/GetCinemas/GetCinemasValidatorTests.cs`, copied from `Movies/Queries/GetMovies/GetMoviesValidatorTests.cs`.
+ - `tests/Application.Tests.Unit/Cinemas/Commands/CreateCinema/CreateCinemaHandlerTests.cs`, copied from `Reviews/Commands/CreateReview/CreateReviewHandlerTests.cs`.
+ - `tests/Application.Tests.Unit/Cinemas/Commands/CreateCinema/CreateCinemaValidatorTests.cs`, copied from `Reviews/Commands/CreateReview/CreateReviewValidatorTests.cs`.
+ - A `Cinemas` region in `tests/Infrastructure.Tests.Integration/Databases/MovieReviews/EntityFrameworkMovieReviewsRepositoryTests.cs`, copied from the `Movies` region: search, paging and create.
+ - `tests/Presentation.Tests.Integration/Controllers/CinemaEndpointTests.cs`, copied from `MovieEndpointTests.cs` and `ReviewEndpointTests.cs`.
The repository tests need some cinemas. Add three to `AddTestData` in `tests/Infrastructure.Tests.Integration/Databases/MovieReviews/Extensions/MovieReviewsCollectionData.cs`, named the same way as the sample rows:
@@ -437,7 +443,7 @@ The code below assumes you named the solution `WorksOnMyMachine.Movies`. Swap in
context.Cinemas.AddRange(cinemas);
```
- Two existing tests in `tests/Presentation.Tests.Integration/OpenApi/OpenApiDocumentTests.cs` fail as soon as the endpoints exist. They're doing their job: they pin down exactly what the API publishes.
+ Three existing tests in `tests/Presentation.Tests.Integration/OpenApi/OpenApiDocumentTests.cs` fail as soon as the endpoints exist. They're doing their job: they pin down exactly what the API publishes.
- **`GetOpenApiDocument_ShouldDescribe_Tags`:** add `"Cinemas"` to the expected tags.
- **`GetOpenApiDocument_ShouldDescribe_EveryOperation`:** add 2 to the operation count, one for each new endpoint.
@@ -451,6 +457,12 @@ The code below assumes you named the solution `WorksOnMyMachine.Movies`. Swap in
Both domains now run side by side. Stop here if you're keeping the sample for reference, which is a perfectly respectable choice.
+
+
+## Take out the sample
+
+
+
6. **Take out the sample movies, authors and reviews** (`sample-data` and `database-migrations`).
This step is mostly deleting, but the template's own tests use the sample as their test subject, so a few of them change instead of going. Work through it layer by layer and build as you go. The compiler finds what's left faster than any list.
@@ -487,16 +499,13 @@ The code below assumes you named the solution `WorksOnMyMachine.Movies`. Swap in
- `EntityFrameworkMovieReviewsRepositoryTests.cs`: the `Authors`, `Movies` and `Reviews` regions.
- `MovieReviewsCollectionData.cs`: the author, movie and review rows.
- **Tests to change:** these test the template's plumbing, and they used the sample as their subject. Point them at cinemas.
-
- | Test | Change |
- | --- | --- |
- | `Common/Behaviours/LoggingBehaviourTests.cs` and `ValidationBehaviourTests.cs` | Use `CreateCinemaCommand`, `CreateCinemaValidator` and `Cinema` in place of `GetAuthorByIdQuery`, its validator and `Author`. The invalid case is an empty `Name`, so the expected property is `Name`. The logged message names `CreateCinemaCommand`. |
- | `Common/Exceptions/NotFoundExceptionTests.cs` | Use `EntityType.Cinema` and a `Cinema`. The message becomes "The Cinema with the supplied id was not found." |
- | `MovieReviewsConfigurationTests.cs` | Check a cinema loads, ignoring case: look up `"one"` and expect `"One"`. |
- | `DependencyInjectionTests.cs` | Expect `ICinemasRepository`, 5 seeded cinemas and none when seeding is off. Use `GetCinemas` for the concurrency and same-sample-data checks. |
- | `Telemetry/TelemetryTests.cs` | Call `/api/cinemas?limit=5` and expect a `GetCinemasQuery` span. |
- | `OpenApi/OpenApiDocumentTests.cs` | Expect the tags `Cinemas` and `Version`, 3 operations, and the schemas `Cinema`, `PageOfCinema`, `CreateCinemaRequest` and `VersionInfo`. |
- | `Authentication/AuthenticationTests.cs`, only with `--include-auth` | Replace the movie, author and review routes with `/api/cinemas`. |
+ - **`Common/Behaviours/LoggingBehaviourTests.cs` and `ValidationBehaviourTests.cs`:** Use `CreateCinemaCommand`, `CreateCinemaValidator` and `Cinema` in place of `GetAuthorByIdQuery`, its validator and `Author`. The invalid case is an empty `Name`, so the expected property is `Name`. The logged message names `CreateCinemaCommand`.
+ - **`Common/Exceptions/NotFoundExceptionTests.cs`:** Use `EntityType.Cinema` and a `Cinema`. The message becomes "The Cinema with the supplied id was not found."
+ - **`MovieReviewsConfigurationTests.cs`:** Check a cinema loads, ignoring case: look up `"one"` and expect `"One"`.
+ - **`DependencyInjectionTests.cs`:** Expect `ICinemasRepository`, 5 seeded cinemas and none when seeding is off. Use `GetCinemas` for the concurrency and same-sample-data checks.
+ - **`Telemetry/TelemetryTests.cs`:** Call `/api/cinemas?limit=5` and expect a `GetCinemasQuery` span.
+ - **`OpenApi/OpenApiDocumentTests.cs`:** Expect the tags `Cinemas` and `Version`, 3 operations, and the schemas `Cinema`, `PageOfCinema`, `CreateCinemaRequest` and `VersionInfo`.
+ - **`Authentication/AuthenticationTests.cs`, only with `--include-auth`:** Replace the movie, author and review routes with `/api/cinemas`.
7. **Check it the way CI will** (`coverage`).
@@ -516,4 +525,4 @@ The code below assumes you named the solution `WorksOnMyMachine.Movies`. Swap in
## What's still called "movie reviews"
-The database plumbing keeps its sample name: the `Databases/MovieReviews` folder, `MovieReviewsDbContext`, `EntityFrameworkMovieReviewsRepository`, the `MovieReviews` connection string and the `movie-reviews.db` file. They all work as they are. Everything in `src/Infrastructure` is `internal`, so renaming them later is a refactor that never reaches Application or Presentation. Do it when the name starts to bother you, and change the connection string in every `appsettings.*.json` and in `docker-compose.yml` at the same time.
+The database plumbing keeps its sample name: the `Databases/MovieReviews` folder, `MovieReviewsDbContext`, `EntityFrameworkMovieReviewsRepository`, the `MovieReviews` connection string and the `movie-reviews.db` file. They all work as they are. Everything in `src/Infrastructure` apart from `DependencyInjection` is `internal`, so renaming them later is a refactor that never reaches Application or Presentation. Do it when the name starts to bother you, and change the connection string in every `appsettings.*.json` and in `docker-compose.yml` at the same time.
diff --git a/src/content/docs/webapi/guides/run-locally.mdx b/src/content/docs/webapi/guides/run-locally.mdx
index 29295bd..40a9a1a 100644
--- a/src/content/docs/webapi/guides/run-locally.mdx
+++ b/src/content/docs/webapi/guides/run-locally.mdx
@@ -28,8 +28,8 @@ There are two ready-made environments. `local` is for `dotnet run`: sample data
```bash
- docker build . --target run --tag acme-movies:local
- docker run --rm -p 7340:7340 -e DOTNET_ENVIRONMENT=docker acme-movies:local
+ docker build . --target run --tag worksonmymachine-movies:local
+ docker run --rm -p 7340:7340 -e DOTNET_ENVIRONMENT=docker worksonmymachine-movies:local
```
Without Compose there's no collector, so the logs stay on the console and nothing else is exported. Set `OTEL_EXPORTER_OTLP_ENDPOINT` to send them to a collector of your own.
@@ -82,7 +82,7 @@ curl -X POST "https://localhost:7341/api/reviews" -H "Content-Type: application/
dotnet test
```
-Each test class builds its own in-memory SQLite database from the migrations and the same fixed seed. No test can change what another one sees, and nothing touches `movie-reviews.db`.
+The tests build their own in-memory SQLite databases from the same migrations and seed, so nothing touches `movie-reviews.db`. [Test your changes](/webapi/guides/test-your-changes/) covers where a test goes, how the tests are written and how to check coverage before CI does.
## Run just one part of the Dockerfile
@@ -105,7 +105,7 @@ A failing test doesn't fail the build, so the results still come out. Check `out
## Stamp a version
```bash
-docker build . --target run --tag acme-movies:latest \
+docker build . --target run --tag worksonmymachine-movies:latest \
--build-arg VERSION_PREFIX=1.2.3 --build-arg VERSION_SUFFIX=beta
```
diff --git a/src/content/docs/webapi/guides/secure-the-api.mdx b/src/content/docs/webapi/guides/secure-the-api.mdx
index 34d4f3c..2e3053a 100644
--- a/src/content/docs/webapi/guides/secure-the-api.mdx
+++ b/src/content/docs/webapi/guides/secure-the-api.mdx
@@ -53,7 +53,7 @@ Copy across what's new: the `Microsoft.AspNetCore.Authentication.JwtBearer` pack
## What changes
- The movie, author and review endpoints need a valid token. `GET /api/version` needs one too. `/health/live` and `/health/ready` stay open, so load balancers and curious humans can still check the lights are on.
-- The OpenAPI document gains a `Bearer` security scheme, and Scalar shows an **Auth** box where you can paste a token and keep clicking **Send**.
+- The OpenAPI document gains a `Bearer` security scheme, and Scalar gains an **Authentication** box beside the introduction. Pick **Bearer** under **Select Auth Type**, paste a token, and **Send** carries it on every request.
- Tokens are validated against the authority's published keys, so there's no secret to store and nothing to rotate on your side.
## How it works
diff --git a/src/content/docs/webapi/guides/set-up-ci.mdx b/src/content/docs/webapi/guides/set-up-ci.mdx
index aca3f41..32bf926 100644
--- a/src/content/docs/webapi/guides/set-up-ci.mdx
+++ b/src/content/docs/webapi/guides/set-up-ci.mdx
@@ -1,25 +1,47 @@
---
title: "Set up CI"
-description: "Your solution starts with a simple pipeline. Here's what it runs and how to add more."
+description: "Your solution starts with a simple pipeline on purpose. Here's what it runs, and the checks you can add when your process calls for them."
sidebar:
order: 7
---
import { Steps } from '@astrojs/starlight/components';
+import ReportCard from '../../../../components/docs/ReportCard.astro';
-Whichever way you made your solution, it starts with a simple pipeline: format, build, and test with a 95% coverage bar. Those are the same commands as in `AGENTS.md`, so the pipeline and your laptop agree on what "green" means. It needs no secrets, no accounts and no paid features. Everything else is yours to add when you want it.
+Your solution starts with a simple pipeline: format, build, and test with a 95% coverage bar. Those are the same commands as in `AGENTS.md`, so the pipeline and your laptop agree on what "green" means. It needs no secrets, no accounts and no paid features, and it's green on the first push.
-## Made with `dotnet new`
+It's minimal on purpose. We don't know your review process, your security team or your deployment target, so the pipeline stops where guessing would start. Everything else on this page is a check you can add when your process calls for it.
+
+## What runs
You get `.github/workflows/ci.yml`, one GitHub Actions job that restores, checks formatting, builds, runs every test and checks the coverage total. Push to GitHub and it runs.
Not on GitHub Actions? Ask your coding agent to move it. It works as Jacques, so it uses the `ci-for-your-solution` skill in `.agents/skills` to port the same commands to Azure Pipelines, GitLab, or the Jenkins box under someone's desk, then breaks something on purpose to prove the new pipeline notices.
-## Made with "Use this template"
+## Adding more
+
+The `ci-for-your-solution` skill lists the checks you can add, what each one catches and what it needs, from a vulnerable package check that needs nothing to CodeQL, which needs Advanced Security on a private repository. Ask your agent to add one and it will agree it with you first.
+
+For a worked example of each, look at the template's own [Build Pipeline](https://github.com/LeanMeanCleanApps/CleanWebApi/blob/main/.github/workflows/build-pipeline.yml). Every pull request to the template goes through these checks, and each one is a job you can lift into `ci.yml`. **Gatekeepers** fail the build and block the merge. **Tattletales** report what they find and leave the decision to a human.
+
+
-Your copy has our Build Pipeline. Outside the LeanMeanCleanApps organisation it runs the format check, build and tests, and the Summary shows the rest as not run. That covers CodeQL, Dependency Review, the container scan, and the steps that pack, verify and publish the template. So a private copy goes green without GitHub Advanced Security, and it never tries to publish to NuGet on your behalf.
+Borrow freely, but leave out the jobs that pack, verify and publish the template. You're shipping the API, not a template. Whatever you add, run the same commands locally that the pipeline runs, and make the checks you rely on required on `main`.
-### After you copy
+## If you used "Use this template"
+
+A copy made with GitHub's **Use this template** button has the whole Build Pipeline rather than `ci.yml`. Outside the LeanMeanCleanApps organisation it runs the format check, build and tests, and the Summary shows the rest as not run: CodeQL, Dependency Review, the container scan, and the steps that pack, verify and publish the template. So a private copy goes green without GitHub Advanced Security, and it never tries to publish to NuGet on your behalf. Three things to change after you copy:
@@ -30,9 +52,3 @@ Your copy has our Build Pipeline. Outside the LeanMeanCleanApps organisation it
3. Fill in **Where we track work** in `AGENTS.md`, so agents put their plans and handoffs where your team will see them.
-
-## Adding more
-
-The `ci-for-your-solution` skill lists the checks you can add, what each one catches and what it needs, from a vulnerable package check that needs nothing to CodeQL, which needs Advanced Security on a private repository. Ask your agent to add one and it will agree it with you first.
-
-For a worked example of every check, read our [Build Pipeline](https://github.com/LeanMeanCleanApps/CleanWebApi/blob/main/.github/workflows/build-pipeline.yml). Borrow freely, but leave out the jobs that pack and publish the template. You're shipping an app, not a template.
diff --git a/src/content/docs/webapi/guides/ship-the-container.mdx b/src/content/docs/webapi/guides/ship-the-container.mdx
index 1e4df90..fc63a13 100644
--- a/src/content/docs/webapi/guides/ship-the-container.mdx
+++ b/src/content/docs/webapi/guides/ship-the-container.mdx
@@ -12,7 +12,7 @@ The `run` stage of the `Dockerfile` is the whole API: one image, built on `mcr.m
## Build it
```bash
-docker build . --target run --tag acme-movies:1.2.3 \
+docker build . --target run --tag worksonmymachine-movies:1.2.3 \
--build-arg VERSION_PREFIX=1.2.3
```
@@ -32,13 +32,13 @@ For production, pick a name like `production`, add an `appsettings.production.js
## Put it behind HTTPS
-The container only listens on HTTP. Outside the `local` and `Development` environments it sends an HSTS header and redirects HTTP to HTTPS, which means it expects something in front of it to end TLS: a load balancer, a reverse proxy or your platform's ingress. Tell it so:
+The container only listens on HTTP, and it expects something in front of it to end TLS: a load balancer, a reverse proxy or your platform's ingress. Outside the `local` and `Development` environments it sends an HSTS header, and it redirects HTTP to HTTPS once it knows an HTTPS port to send people to, such as `ASPNETCORE_HTTPS_PORT=443`. Tell it about the proxy too:
```bash
ASPNETCORE_FORWARDEDHEADERS_ENABLED=true
```
-Without that, the API thinks every request arrived over plain HTTP and redirects it, and the proxy sends it back, and the two of them go round and round until someone's pager goes off.
+Without that, the API thinks every request arrived over plain HTTP. Give it an HTTPS port in that state and it redirects the proxy's requests, the proxy sends them back, and the two of them go round and round until someone's pager goes off.
## Wire up the health checks
diff --git a/src/content/docs/webapi/guides/test-your-changes.mdx b/src/content/docs/webapi/guides/test-your-changes.mdx
index b605bec..8060452 100644
--- a/src/content/docs/webapi/guides/test-your-changes.mdx
+++ b/src/content/docs/webapi/guides/test-your-changes.mdx
@@ -14,7 +14,7 @@ Your solution starts with four test projects and a pipeline that fails under 95%
| Project | What it tests | How |
| --- | --- | --- |
| `tests/Application.Tests.Unit` | Handlers, validators and pipeline behaviours | xUnit, Shouldly and NSubstitute, with the repositories substituted |
-| `tests/Infrastructure.Tests.Integration` | The repositories against a real database | A private in-memory SQLite database per test class, built from the migrations and the fixed seed |
+| `tests/Infrastructure.Tests.Integration` | The repositories against a real database | A private in-memory SQLite database per test, built from the migrations and the fixed seed |
| `tests/Presentation.Tests.Unit` | The error handler, settings, the OpenAPI transformer and the JSON options | Plain unit tests, no host |
| `tests/Presentation.Tests.Integration` | Real HTTP requests to every controller action, authentication, health checks, OpenAPI and telemetry | `WebApplicationFactory` with a private database |
@@ -71,4 +71,5 @@ The last table it prints is the merged total. `Program.cs`, the EF Core migratio
## Where to next
- [Add an endpoint](/webapi/guides/add-an-endpoint/) ends with a test for the new action.
+- [Make it yours](/webapi/guides/make-it-yours/) adds a whole entity, then clears the sample out without dropping under the coverage gate.
- [Work with a coding agent](/webapi/guides/work-with-a-coding-agent/): the `test-design`, `unit-tests`, `integration-tests` and `coverage` skills hold these same rules, so an agent writes tests the same way you would.
diff --git a/src/content/docs/webapi/guides/work-with-a-coding-agent.mdx b/src/content/docs/webapi/guides/work-with-a-coding-agent.mdx
index ef86627..76016f6 100644
--- a/src/content/docs/webapi/guides/work-with-a-coding-agent.mdx
+++ b/src/content/docs/webapi/guides/work-with-a-coding-agent.mdx
@@ -27,7 +27,7 @@ The skills are written for this solution, not for .NET in general. `add-a-use-ca
3. For anything bigger than a few lines, Jacques agrees a plan first: the open decisions one at a time, each with a recommendation, then the scope in a paragraph. Answer those and the work starts.
4. Before it says it's done, the agent runs `dotnet build`, `dotnet test` and `dotnet format --verify-no-changes`, the same checks as CI.
-## Make it yours
+## Tune it for your team
- **Where we track work.** `AGENTS.md` has a line that says "Not set". Replace it with your GitHub project, Jira board or Azure Boards, and agents put their plans and handoffs there instead of asking each time.
- **Change a rule, change the skill.** When your team decides to do something differently, update the matching `SKILL.md` in the same pull request. A skill that disagrees with the code is worse than no skill at all.
diff --git a/src/content/docs/webapi/index.mdx b/src/content/docs/webapi/index.mdx
index f09279d..c76c8c5 100644
--- a/src/content/docs/webapi/index.mdx
+++ b/src/content/docs/webapi/index.mdx
@@ -2,6 +2,8 @@
title: Clean Web API
description: A .NET 10 dotnet new template for an ASP.NET Core Web API with controllers, built with Clean Architecture minus the ceremony.
template: splash
+editUrl: false
+lastUpdated: false
prev: false
next: false
hero:
diff --git a/src/content/docs/webapi/reference/configuration.mdx b/src/content/docs/webapi/reference/configuration.mdx
index 4867295..350a2e0 100644
--- a/src/content/docs/webapi/reference/configuration.mdx
+++ b/src/content/docs/webapi/reference/configuration.mdx
@@ -51,7 +51,7 @@ The app always collects [OpenTelemetry](https://opentelemetry.io) traces and met
| `OTEL_EXPORTER_OTLP_ENDPOINT` | none | Where to send traces, metrics and logs. Nothing is exported until it's set. Docker Compose sets it to the `otel-collector` service |
| `OTEL_EXPORTER_OTLP_PROTOCOL` | `grpc` | `grpc` or `http/protobuf` |
| `OTEL_EXPORTER_OTLP_HEADERS` | none | Headers for every export, such as an API key for a hosted backend |
-| `OTEL_SERVICE_NAME` | `CleanWebApi` | The `service.name` on everything sent |
+| `OTEL_SERVICE_NAME` | Your solution's name | The `service.name` on everything sent |
Every span, metric and log also carries `service.version`, the version stamped into the build, and `deployment.environment.name`.
diff --git a/src/content/docs/webapi/reference/project-structure.mdx b/src/content/docs/webapi/reference/project-structure.mdx
index 6af2fa7..8e18bd5 100644
--- a/src/content/docs/webapi/reference/project-structure.mdx
+++ b/src/content/docs/webapi/reference/project-structure.mdx
@@ -60,4 +60,4 @@ import { FileTree } from '@astrojs/starlight/components';
| --- | --- | --- |
| Presentation | Application, Infrastructure | References Infrastructure only to wire up dependency injection |
| Application | none | Owns the entities, the repository interfaces and the rules |
-| Infrastructure | Application | Every class is `internal` |
+| Infrastructure | Application | Every class but `DependencyInjection` is `internal` |
diff --git a/src/content/docs/webapi/reference/troubleshooting.mdx b/src/content/docs/webapi/reference/troubleshooting.mdx
index a76bc90..0becaba 100644
--- a/src/content/docs/webapi/reference/troubleshooting.mdx
+++ b/src/content/docs/webapi/reference/troubleshooting.mdx
@@ -87,5 +87,5 @@ Docker doesn't start itself, however much we'd like it to. Start Docker Desktop
That's authentication doing its job. Every `/api/*` controller action requires a valid JWT bearer token when authentication is included.
-- In Scalar (`/scalar`), click **Authorize** at the top of the page and paste a bearer token issued by your `--authority`.
+- In Scalar (`/scalar`), find the **Authentication** box beside the introduction, pick **Bearer** under **Select Auth Type** and paste a token issued by your `--authority`. Until you pick it, the box says "No authentication selected" and every request goes without one.
- In `src/Presentation/Presentation.http`, uncomment the `@token` line at the top of the file, paste your token after it, and uncomment the `Authorization` header on each request.
diff --git a/src/styles/docs/brand.css b/src/styles/docs/brand.css
index 9cd9e5d..e9a9ca0 100644
--- a/src/styles/docs/brand.css
+++ b/src/styles/docs/brand.css
@@ -40,6 +40,7 @@
.sl-markdown-content .cl-marks a {
display: grid;
+ align-content: start;
gap: 0.6rem;
text-decoration: none;
color: var(--sl-color-white);
diff --git a/src/styles/docs/theme.css b/src/styles/docs/theme.css
index 2d5215b..44a9747 100644
--- a/src/styles/docs/theme.css
+++ b/src/styles/docs/theme.css
@@ -27,8 +27,14 @@
}
/* The header bar runs edge to edge like the front page's, with its contents inside the box. */
-header.header {
+header.header,
+mobile-starlight-toc nav {
background-color: color-mix(in srgb, var(--sl-color-black) 85%, transparent);
+ -webkit-backdrop-filter: blur(12px);
+ backdrop-filter: blur(12px);
+}
+
+header.header {
border-bottom-color: color-mix(in srgb, var(--sl-color-white) 11%, transparent);
}
@@ -121,8 +127,7 @@ h1#_top {
}
-/* Concentric rings behind the splash pages, echoing the logo. */
-[data-has-hero] .main-frame,
+/* Concentric rings echoing the logo, for anything that asks for them. The splash pages don't: they match the front page. */
.cl-splash-rings {
background-image: radial-gradient(
circle at 85% 12%,
@@ -180,21 +185,32 @@ pre {
font-variant-ligatures: none;
}
-/* Inline code reads as a chip. */
+/* Inline code reads as a chip. A chip moves to the next line whole, so an option like `--authorName` never breaks
+ at its hyphen, and only a chip wider than the column, usually a file path, breaks inside itself. The negative
+ margin gives the padding back to the line, so the line spacing stays even. */
.sl-markdown-content :not(pre) > code {
- border-radius: 0.4rem;
+ display: inline-block;
+ max-width: 100%;
+ margin-block: -0.1em;
padding: 0.1em 0.35em;
+ border-radius: 0.4rem;
+ overflow-wrap: anywhere;
+ vertical-align: baseline;
}
-/* Beside the sidebars a chip never breaks in half, so an option like `--authorName` stays on one line. Phones let
- it wrap, so a reference table still fits the screen. */
-@media (min-width: 50rem) {
- .sl-markdown-content :not(pre) > code {
- white-space: nowrap;
- }
+/* A long chip, usually a file path, is marked by the build and flows like text, so the words after it stay on
+ the same line as its last piece. In a table every chip stays inline so the columns can share the width, and a
+ table still too wide for the column scrolls. */
+.sl-markdown-content :not(pre) > code:is(.cl-long, :is(td, th) > code) {
+ display: inline;
+ margin-block: 0;
}
-/* Tables used for reference pages. A table too wide for the column scrolls rather than being cut off. */
+.sl-markdown-content :is(td, th) > code {
+ overflow-wrap: normal;
+}
+
+/* Tables used for reference pages. A table still too wide for the column scrolls rather than being cut off. */
.sl-markdown-content table {
border-radius: 0.75rem;
overflow-x: auto;
@@ -218,6 +234,7 @@ pre {
.cl-family a {
display: grid;
justify-items: center;
+ align-content: start;
gap: 0.6rem;
padding: 1rem 0.5rem;
border: 1px solid var(--sl-color-gray-5);
@@ -252,6 +269,17 @@ pre {
color: var(--sl-color-gray-3);
}
+/* Five cards in a narrower box keep the names and drop the subtitles, so the row stays one line of cards. */
+@media (max-width: 72rem) {
+ .cl-family {
+ grid-template-columns: repeat(auto-fit, minmax(7.5rem, 1fr));
+ }
+
+ .cl-family small {
+ display: none;
+ }
+}
+
/* The report card on "How it's built". */
.cl-report {
display: grid;