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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions .agents/skills/docker-and-compose/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,26 @@ One multi-stage `Dockerfile` does everything the pipeline needs:
| `publish` | Publishes the Presentation project |
| `run` | The runtime image: non-root `$APP_UID`, `WORKDIR /app`, data in `/app/data`, a `HEALTHCHECK` on `/health/live` |

`docker-compose.yml` runs the app with `DOTNET_ENVIRONMENT=docker`, the `movie-reviews-data` volume a pinned OpenTelemetry Collector that receives the app's OTLP logs, traces and metrics, configured by `otel-collector.yaml`, and a pinned Aspire dashboard the collector forwards them to (`telemetry`). The dashboard UI is at <http://localhost:18888>, and the collector's OTLP ports are published on 4317 and 4318 so a `dotnet run` can send to it too. The dashboard runs with `ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS` and the collector's OTLP ports take anything sent to them, on purpose: the compose file is for a developer's machine, with nothing to sign in to. Keep it that way rather than adding sign-in, and don't offer the compose file for a shared or deployed environment. Images are built by CI but not pushed anywhere; the template ships the Dockerfile so users can.
`docker-compose.yml` runs the app with `DOTNET_ENVIRONMENT=docker`, the `movie-reviews-data` volume a pinned OpenTelemetry Collector that receives the app's OTLP logs, traces and metrics, configured by `otel-collector.yaml`, and a pinned Aspire dashboard the collector forwards them to (`telemetry`). The dashboard UI and the collector's OTLP ports are published on the template's own ports (below), so a `dotnet run` can send to the collector too. The dashboard runs with `ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS` and the collector's OTLP ports take anything sent to them, on purpose: the compose file is for a developer's machine, with nothing to sign in to. Keep it that way rather than adding sign-in, and don't offer the compose file for a shared or deployed environment. Images are built by CI but not pushed anywhere; the template ships the Dockerfile so users can.

The Lambda template's `run` stage is the AWS Lambda .NET base image instead. It runs as user `65534` from `/var/task`, keeps its database in `/tmp`, and has no `HEALTHCHECK`. Its compose file runs the collector and the Aspire dashboard as well. Compose maps the runtime interface emulator to port 9000, so the function can be invoked with a `POST` to `/2015-03-31/functions/function/invocations`.
The Lambda template's `run` stage is the AWS Lambda .NET base image instead. It runs as user `65534` from `/var/task`, keeps its database in `/tmp`, and has no `HEALTHCHECK`. Its compose file runs the collector and the Aspire dashboard as well. Compose maps the runtime interface emulator to 7350, so the function can be invoked with a `POST` to `/2015-03-31/functions/function/invocations`.

### Ports

Every port published on the developer's machine, by the compose file or by `launchSettings.json`, sits in the template's own block of ten, so any two Lean Mean Clean solutions run side by side. The blocks follow the template order: Minimal API 7310, MCP 7320, GraphQL 7330, Web API 7340, Lambda Function 7350. Each port has the same place in every block:

| Offset | Port |
| --- | --- |
| +0 | The app over HTTP, from Docker Compose or `dotnet run` |
| +1 | The app over HTTPS, `dotnet run` only |
| +2 | The Aspire dashboard |
| +3 | The collector's OTLP over gRPC |
| +4 | The collector's OTLP over HTTP |
| +5 | The MCP Inspector |
| +6 | The MCP chat demo |
| +7 to +9 | Spare |

`docs/RUN.md` lists the ports this solution uses. The app listens on its own port everywhere: under `dotnet run`, in the image (the `Dockerfile` sets `ASPNETCORE_HTTP_PORTS`, `EXPOSE` and the `HEALTHCHECK` to it) and in the compose file, so `docker run -p` maps it to the same number. Change all four together. The Lambda template is the one exception: Lambda's runtime interface emulator always listens on 8080 inside its container, so only the host side of its mapping is ours. The dashboard, the collector and other third-party images keep their usual ports inside (18888, 4317 and 4318), and only the host side of their mappings moves. A new service takes a spare port from the block, never a product's well-known one such as 3000, 5432 or 9000. `docker compose port <service> <container port>` prints where a port ended up.

## Steps

Expand All @@ -42,7 +59,7 @@ The Lambda template's `run` stage is the AWS Lambda .NET base image instead. It

``` bash
docker build . --target run --tag local-check
docker compose up -d --build && curl -fsS http://localhost:8080/health/ready
# Lambda: curl -fsS -X POST http://localhost:9000/2015-03-31/functions/function/invocations -d '{}'
docker compose up -d --build --wait # waits for the image's HEALTHCHECK
# Lambda: curl -fsS -X POST "http://$(docker compose port function 8080)/2015-03-31/functions/function/invocations" -d '{}'
docker compose down
```
2 changes: 1 addition & 1 deletion .agents/skills/expose-a-use-case/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,5 +100,5 @@ public static async Task<Review> UpdateReview(
``` bash
dotnet build
dotnet test tests/Presentation.Tests.Integration
dotnet run --project ./src/Presentation # then try it in Nitro at http://localhost:8080/graphql
dotnet run --project ./src/Presentation # then try it in Nitro at http://localhost:7330/graphql
```
8 changes: 4 additions & 4 deletions .agents/skills/nitro/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ metadata:

## The rule

`MapGraphQL()` serves Nitro, Hot Chocolate's GraphQL IDE, at <http://localhost:8080/graphql> when a browser asks for it. It's served embedded (`ServeMode.Embedded`) so it works offline and never loads from a CDN. The schema and Nitro stay open when authentication is on; every field, `version` included, asks for a token.
`MapGraphQL()` serves Nitro, Hot Chocolate's GraphQL IDE, at <http://localhost:7330/graphql> when a browser asks for it. It's served embedded (`ServeMode.Embedded`) so it works offline and never loads from a CDN. The schema and Nitro stay open when authentication is on; every field, `version` included, asks for a token.

| What | Address |
| --- | --- |
| Nitro | <http://localhost:8080/graphql> |
| Schema as SDL | <http://localhost:8080/graphql?sdl> |
| Aspire dashboard (logs, traces and metrics, in Docker) | <http://localhost:18888> |
| Nitro | <http://localhost:7330/graphql> |
| Schema as SDL | <http://localhost:7330/graphql?sdl> |
| Aspire dashboard (logs, traces and metrics, in Docker) | <http://localhost:7332> |

## Steps

Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/schema-snapshot/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The test ignores lines that start with `#`, which GraphQL treats as comments. In

1. Make the schema change.
2. Run the tests. The snapshot test fails and prints the new schema.
3. Copy the new SDL into `schema.graphql`. Either take it from the test output, or run the app and open <http://localhost:8080/graphql?sdl>.
3. Copy the new SDL into `schema.graphql`. Either take it from the test output, or run the app and open <http://localhost:7330/graphql?sdl>.
4. Read the diff. A removed field, a renamed one, or a field that became non-null breaks clients, so say so in the pull request.
5. Run the tests again.

Expand Down
4 changes: 2 additions & 2 deletions .agents/skills/serilog/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Serilog is the logger. Application and Infrastructure code log through `Microsof
| Environment | Sink | Where to look |
| --- | --- | --- |
| `local` | Console, with the output template in `appsettings.local.json` | The terminal |
| `docker` | Console, and OTLP through the collector to the Aspire dashboard | <http://localhost:18888>, or `docker compose logs` |
| `docker` | Console, and OTLP through the collector to the Aspire dashboard | The Aspire dashboard, on the port `docs/RUN.md` lists, or `docker compose logs` |
| Lambda, anywhere | Console, as compact JSON, and OTLP when the endpoint is set | CloudWatch Logs, or `docker compose logs` for the emulator, and the Aspire dashboard in Docker Compose |
| Any, when `OTEL_EXPORTER_OTLP_ENDPOINT` is set | Also OTLP, through `Serilog.Sinks.OpenTelemetry` | Whatever receives the OTLP (`telemetry`) |

Expand Down Expand Up @@ -108,6 +108,6 @@ _ = builder.Services.AddSerilog((services, loggerConfiguration) =>

``` bash
dotnet run --project ./src/Presentation # watch the console, then make a request
docker compose up -d --build # then open http://localhost:18888
docker compose up -d --build # then open the Aspire dashboard on the port docs/RUN.md lists
dotnet test tests/Application.Tests.Unit # LoggingBehaviourTests check the messages with FakeLogger
```
6 changes: 3 additions & 3 deletions .agents/skills/telemetry/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ The Lambda template builds its tracer and meter providers once in `Startup` with

The app sends to an OpenTelemetry Collector, never straight to a backend, because that's how it should run in production: the collector owns batching, retries, sampling, scrubbing and the choice of backend, so none of that lives in the app. `otel-collector.yaml` holds its configuration: an OTLP receiver on 4317 (gRPC) and 4318 (HTTP), the `memory_limiter` and `batch` processors, and one exporter per backend.

Locally, Docker Compose runs the collector (`otel/opentelemetry-collector`) and the Aspire dashboard (`mcr.microsoft.com/dotnet/aspire-dashboard`), and the collector forwards everything to the dashboard. The dashboard is only a container that receives OTLP and shows logs, traces and metrics at <http://localhost:18888>; it keeps everything in memory, so it's empty after a restart. In production, run a collector beside the app or as a gateway and swap the dashboard exporter for your backend's.
Locally, Docker Compose runs the collector (`otel/opentelemetry-collector`) and the Aspire dashboard (`mcr.microsoft.com/dotnet/aspire-dashboard`), and the collector forwards everything to the dashboard. The dashboard is only a container that receives OTLP and shows logs, traces and metrics on the template's dashboard port (`docker-and-compose`); it keeps everything in memory, so it's empty after a restart. In production, run a collector beside the app or as a gateway and swap the dashboard exporter for your backend's.

## Steps

Expand All @@ -51,7 +51,7 @@ Locally, Docker Compose runs the collector (`otel/opentelemetry-collector`) and
5. Use stable packages only. When the only instrumentation is a prerelease package, go without that signal and add it to the watch list below.
6. Test spans with an `ActivityListener` that keeps only the test's own trace (`unit-tests`). The integration tests send a `traceparent` header and check the request and use case spans join up.
7. To send telemetry somewhere new, add an exporter to `otel-collector.yaml` and list it in the pipelines. The app's settings don't change.
8. To see it locally, run `docker compose up -d --build` and open <http://localhost:18888>. For a `dotnet run`, start the collector with `docker compose up -d otel-collector` and set `OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317`.
8. To see it locally, run `docker compose up -d --build` and open the dashboard on the port `docs/RUN.md` lists. For a `dotnet run`, start the collector with `docker compose up -d otel-collector` and set `OTEL_EXPORTER_OTLP_ENDPOINT` to its gRPC port, `http://$(docker compose port otel-collector 4317)`.

## Example

Expand Down Expand Up @@ -110,5 +110,5 @@ These signals and packages aren't stable for .NET yet. Check them when dependenc

``` bash
dotnet test --filter "FullyQualifiedName~Telemetry|FullyQualifiedName~LoggingBehaviour" # use case span and end-to-end trace tests
docker compose up -d --build # then open http://localhost:18888 and make a request
docker compose up -d --build # then open the dashboard on the port docs/RUN.md lists and make a request
```
6 changes: 3 additions & 3 deletions .github/workflows/build-pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -357,16 +357,16 @@ jobs:
echo "smoke=failed" >> "$GITHUB_OUTPUT"
# The database goes in /app/data, the only folder the non-root user can write to, as it does
# in docker-compose.yml. Logs stay on the console so a failure shows up below.
docker run --detach --name api --publish 8080:8080 \
docker run --detach --name api --publish 7330:7330 \
--env ConnectionStrings__MovieReviews="Data Source=/app/data/movie-reviews.db" \
--env Database__SeedSampleData=true "$IMAGE"
for attempt in $(seq 1 30); do
if curl --silent --fail --max-time 5 http://localhost:8080/health/ready > /dev/null; then
if curl --silent --fail --max-time 5 http://localhost:7330/health/ready > /dev/null; then
break
fi
sleep 1
done
RESPONSE=$(curl --silent --fail --max-time 30 http://localhost:8080/graphql \
RESPONSE=$(curl --silent --fail --max-time 30 http://localhost:7330/graphql \
--header 'Content-Type: application/json' \
--data '{ "query": "{ movies(first: 2) { nodes { id title } } }" }' || true)
echo "$RESPONSE"
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Run these before you say a change is done. They're what CI runs.
dotnet build # warnings are errors
dotnet test # every test project
dotnet format --verify-no-changes # style, whitespace and usings
dotnet run --project ./src/Presentation # Nitro at http://localhost:8080/graphql
dotnet run --project ./src/Presentation # Nitro at http://localhost:7330/graphql
```

The `coverage` skill has the command that checks the 95% line coverage gate locally.
Expand Down
5 changes: 3 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,15 @@ RUN dotnet publish ./src/**/Presentation.csproj --no-restore -c Release -v quiet
# Runtime Image
FROM ${BASE_IMAGE_REPO}/${BASE_IMAGE_RUNTIME}:${BASE_IMAGE_RUNTIME_TAG} AS run
WORKDIR /app
EXPOSE 8080
ENV ASPNETCORE_HTTP_PORTS=7330
EXPOSE 7330
COPY --from=publish /sln/app .

# The database lives in /app/data, the only folder the non-root app user can write to.
RUN mkdir -p /app/data && chown $APP_UID /app/data
USER $APP_UID

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget -qO- http://localhost:8080/health/live || exit 1
CMD wget -qO- http://localhost:7330/health/live || exit 1

ENTRYPOINT ["dotnet", "Presentation.dll"]
2 changes: 1 addition & 1 deletion NUGET.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ docker compose up -d --build
dotnet test
```

Then open [Nitro](https://chillicream.com/docs/nitro), the GraphQL IDE, at <http://localhost:8080/graphql> and ask it for some movies.
Then open [Nitro](https://chillicream.com/docs/nitro), the GraphQL IDE, at <http://localhost:7330/graphql> and ask it for some movies.

The API is open unless you create it with `--include-auth --authority` (and usually `--audience`), and then the queries and mutations ask for a JWT bearer token. Leave `--include-auth` out and there's no auth code at all.

Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ That's three layers, four test projects, a Dockerfile and a CI pipeline. Go and
docker compose up -d --build

# Ask it for a page of movies
curl http://localhost:8080/graphql \
curl http://localhost:7330/graphql \
-H 'Content-Type: application/json' \
-d '{ "query": "{ movies(query: \"space\", first: 2) { totalCount nodes { title reviewCount averageStars } } }" }'
```
Expand Down Expand Up @@ -75,9 +75,9 @@ curl http://localhost:8080/graphql \

The sample movies are all made up, and the critics are harsh.

Typing GraphQL into curl is a character-building exercise. Open [Nitro](https://chillicream.com/docs/nitro), the GraphQL IDE, at <http://localhost:8080/graphql> instead, where the schema is documented from the same XML comments as the code.
Typing GraphQL into curl is a character-building exercise. Open [Nitro](https://chillicream.com/docs/nitro), the GraphQL IDE, at <http://localhost:7330/graphql> instead, where the schema is documented from the same XML comments as the code.

The `movies`, `authors` and `reviews` queries use Relay-style cursor paging (`first`, `after`, `pageInfo`, `totalCount`). Movies and authors come with their review count and average stars, worked out by the database rather than in a loop somewhere. Ask for their reviews and a [DataLoader](https://chillicream.com/docs/hotchocolate/fetching-data/dataloader) fetches them for the whole page at once, so 50 movies take three queries, not 52. Its logs, traces and metrics land in the [Aspire dashboard](https://learn.microsoft.com/dotnet/aspire/fundamentals/dashboard/standalone) at <http://localhost:18888>.
The `movies`, `authors` and `reviews` queries use Relay-style cursor paging (`first`, `after`, `pageInfo`, `totalCount`). Movies and authors come with their review count and average stars, worked out by the database rather than in a loop somewhere. Ask for their reviews and a [DataLoader](https://chillicream.com/docs/hotchocolate/fetching-data/dataloader) fetches them for the whole page at once, so 50 movies take three queries, not 52. Its logs, traces and metrics land in the [Aspire dashboard](https://learn.microsoft.com/dotnet/aspire/fundamentals/dashboard/standalone) at <http://localhost:7332>.

No Docker? `dotnet run --project src/Presentation` works too. Every other way to run it, from plain `docker run` to stamping a version into the image, is in [docs/RUN.md](docs/RUN.md).

Expand Down
8 changes: 4 additions & 4 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ services:
VERSION_PREFIX: 1.0.0.0
VERSION_SUFFIX: docker
ports:
- "8080:8080"
- "7330:7330"
environment:
DOTNET_ENVIRONMENT: docker
OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4317
Expand All @@ -23,16 +23,16 @@ services:
volumes:
- ./otel-collector.yaml:/etc/otelcol/config.yaml:ro
ports:
- "4317:4317"
- "4318:4318"
- "7333:4317"
- "7334:4318"
networks:
- cleangraphqlapi_network
depends_on:
- aspire-dashboard
aspire-dashboard:
image: mcr.microsoft.com/dotnet/aspire-dashboard:13.6.2
ports:
- "18888:18888"
- "7332:18888"
environment:
ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS: "true"
networks:
Expand Down
Loading
Loading