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.

The `demo` profile adds Open WebUI for a chat over the tools (`mcp-inspector`). Keep it behind the profile so plain `docker compose up` stays the same. Solutions generated with authentication on leave it out.

Expand All @@ -44,7 +61,7 @@ The `demo` profile adds Open WebUI for a chat over the tools (`mcp-inspector`).

``` 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
```
16 changes: 8 additions & 8 deletions .agents/skills/mcp-inspector/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,36 +9,36 @@ metadata:

## The rule

`docker-compose.yml` runs the official [MCP Inspector](https://github.com/modelcontextprotocol/inspector) next to the server, already pointed at `http://api:8080/mcp` over streamable HTTP. It's the quickest way to see exactly what a client gets: every tool with its description, input and output schema and annotations, every resource and template, and every prompt. The image version is pinned in the compose file, and the port is bound to `127.0.0.1` only.
`docker-compose.yml` runs the official [MCP Inspector](https://github.com/modelcontextprotocol/inspector) next to the server, already pointed at `http://api:7320/mcp` over streamable HTTP. It's the quickest way to see exactly what a client gets: every tool with its description, input and output schema and annotations, every resource and template, and every prompt. The image version is pinned in the compose file, and the port is bound to `127.0.0.1` only.

| Service | Address |
| --- | --- |
| MCP server | <http://localhost:8080/mcp> |
| MCP Inspector | <http://localhost:6274> |
| Aspire dashboard (logs, traces, metrics) | <http://localhost:18888> |
| MCP server | <http://localhost:7320/mcp> |
| MCP Inspector | <http://localhost:7325> |
| Aspire dashboard (logs, traces, metrics) | <http://localhost:7322> |

## Steps

1. Start everything: `docker compose up -d --build`.
2. Open <http://localhost:6274>, flip the switch on the server card to connect, then open **Tools**, **Resources** or **Prompts**.
2. Open <http://localhost:7325>, flip the switch on the server card to connect, then open **Tools**, **Resources** or **Prompts**.
3. After changing a tool, check its description and every argument's description read well, the annotations are right, and a call returns the result model you expect.
4. Try the failure paths too: an unknown id, a star rating of 6. The model should get a message it can act on.
5. Find the tool call in the Aspire dashboard if something looks wrong: its trace shows the tool, the use case and each SQL command, and its logs sit beside it (`telemetry`).
6. With authentication on, the Inspector needs a token from your authority. Leave `Authentication:Authority` empty for local checks.

## Chat demo

To try the tools the way a model does, add the optional `demo` profile: `docker compose --profile demo up -d --build`. It starts [Open WebUI](https://github.com/open-webui/open-webui) at <http://localhost:3000>, already connected to the server and using Kilo's free gateway as the model, with no account or key. It's online only, and your prompts and tool results go to Kilo, a third party, so use it with sample data. Plain `docker compose up` doesn't start it, and solutions created with `--include-auth` leave it out, because Open WebUI has no token to send.
To try the tools the way a model does, add the optional `demo` profile: `docker compose --profile demo up -d --build`. It starts [Open WebUI](https://github.com/open-webui/open-webui) at <http://localhost:7326>, already connected to the server and using Kilo's free gateway as the model, with no account or key. It's online only, and your prompts and tool results go to Kilo, a third party, so use it with sample data. Plain `docker compose up` doesn't start it, and solutions created with `--include-auth` leave it out, because Open WebUI has no token to send.

## Don't

- Expose port 6274 beyond `127.0.0.1`. The Inspector can call every tool.
- Expose port 7325 beyond `127.0.0.1`. The Inspector can call every tool.
- Use `latest` for the Inspector image. Change the pinned version on purpose.
- Treat a manual check as a test. Write the integration test as well (`presentation-integration-tests`).

## Check

``` bash
docker compose up -d --build && curl -fsS http://localhost:8080/health/ready
docker compose up -d --build && curl -fsS http://localhost:7320/health/ready
docker compose 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 7320:7320 \
--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:7320/health/ready > /dev/null; then
break
fi
sleep 1
done
RESPONSE=$(curl --silent --fail --max-time 30 http://localhost:8080/mcp \
RESPONSE=$(curl --silent --fail --max-time 30 http://localhost:7320/mcp \
--header 'Content-Type: application/json' \
--header 'Accept: application/json, text/event-stream' \
--data '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "movies_search", "arguments": { "limit": 2 } } }' || true)
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 # the MCP server at http://localhost:8080/mcp
dotnet run --project ./src/Presentation # the MCP server at http://localhost:7320/mcp
```

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=7320
EXPOSE 7320
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:7320/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 the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) at <http://localhost:6274>. It's already pointed at the server at <http://localhost:8080/mcp>, so flip the switch to connect and open **Tools**.
Then open the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) at <http://localhost:7325>. It's already pointed at the server at <http://localhost:7320/mcp>, so flip the switch to connect and open **Tools**.

The server is open unless you create it with `--include-auth --authority` (and usually `--audience`), and then `/mcp` asks for a bearer token and publishes protected resource metadata so clients can sign in. Leave `--include-auth` out and there's no auth code at all.

Expand Down
8 changes: 4 additions & 4 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 the movies_search tool for a page of movies
curl http://localhost:8080/mcp \
curl http://localhost:7320/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{
Expand Down Expand Up @@ -85,11 +85,11 @@ The answer comes back as an event stream, and its result carries the movies as s

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

Curl is a fine way to prove it works and a terrible way to spend an afternoon. Open the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) at <http://localhost:6274> instead. It's already pointed at the server, so flip the switch to connect and open **Tools**.
Curl is a fine way to prove it works and a terrible way to spend an afternoon. Open the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) at <http://localhost:7325> instead. It's already pointed at the server, so flip the switch to connect and open **Tools**.

For a chat over the same tools, `docker compose --profile demo up -d --build` adds [Open WebUI](https://github.com/open-webui/open-webui) at <http://localhost:3000>. It's optional and online only, and your prompts go to Kilo's free gateway, a third party. [Run it](docs/RUN.md#chat-demo) has the details.
For a chat over the same tools, `docker compose --profile demo up -d --build` adds [Open WebUI](https://github.com/open-webui/open-webui) at <http://localhost:7326>. It's optional and online only, and your prompts go to Kilo's free gateway, a third party. [Run it](docs/RUN.md#chat-demo) has the details.

There are six tools shaped around jobs rather than tables: `movies_search`, `movies_get`, `authors_search`, `reviews_list`, `reviews_write` and `reviews_delete`. Each has a description written for agents and hints that say what's read-only and what's destructive. Lists come back ten at a time, with a `totalCount` and a `nextOffset` when there's more to come, so an agent doesn't drown in its own context window. There are resources for every movie, author and review, and prompts for jobs like comparing two movies. Its logs, traces and metrics land in the [Aspire dashboard](https://learn.microsoft.com/dotnet/aspire/fundamentals/dashboard/standalone) at <http://localhost:18888>.
There are six tools shaped around jobs rather than tables: `movies_search`, `movies_get`, `authors_search`, `reviews_list`, `reviews_write` and `reviews_delete`. Each has a description written for agents and hints that say what's read-only and what's destructive. Lists come back ten at a time, with a `totalCount` and a `nextOffset` when there's more to come, so an agent doesn't drown in its own context window. There are resources for every movie, author and review, and prompts for jobs like comparing two movies. Its logs, traces and metrics land in the [Aspire dashboard](https://learn.microsoft.com/dotnet/aspire/fundamentals/dashboard/standalone) at <http://localhost:7322>.

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
Loading
Loading