Lab 5: Run It Like Production

30 minutes · Starts from checkpoint-4 · Ends at checkpoint-5 (the finished Roastery API)

Steps 1 and 2 need only the SDK. Step 3 needs Docker or Podman. Step 4 is the first to be cut if the day is running long.

Step 1: Add the Aspire projects

From the src folder:

dotnet new aspire-apphost -n Roastery.AppHost
dotnet new aspire-servicedefaults -n Roastery.ServiceDefaults
dotnet sln add Roastery.AppHost Roastery.ServiceDefaults
cd Roastery.AppHost
dotnet add reference ../Roastery.Api ../Roastery.Payments
cd ../Roastery.Api
dotnet add reference ../Roastery.ServiceDefaults
cd ../Roastery.Payments
dotnet add reference ../Roastery.ServiceDefaults

Replace the AppHost's Program.cs with the version on the concepts page.

In both the API and the payments project, add builder.AddServiceDefaults(); immediately after CreateBuilder, and app.MapDefaultEndpoints(); near the other Map calls. Then remove the hand-written OpenTelemetry block and the two MapHealthChecks lines from Lab 4; ServiceDefaults now owns them. Keep the AddDbContextCheck registration; ServiceDefaults adds the endpoints, not your checks.

Change the payment client's base address to https://payments and delete the Payments:BaseUrl setting.

Step 2: Run it and read the trace

Set Roastery.AppHost as the startup project and run it (or dotnet run --project src/Roastery.AppHost). The dashboard opens in the browser.

  1. Confirm both api and payments show Running and both health endpoints are green.
  2. Using module-5.http (the base URL variable now points at the address shown in the dashboard), get a staff token and a customer token, then place an order as the customer.
  3. Open Traces, find POST /api/orders, and identify: the EF Core spans, the outbound call to payments, and any retry spans. Place a few more orders until you catch a retry.
  4. Advance the order to Ready as staff, then open Structured logs and find the OrderReadyNotifier entry. Click its trace id and confirm it links back to the PUT request that caused it.

Step 3: Build and run the container

dotnet publish src/Roastery.Api -c Release --os linux --arch x64 /t:PublishContainer -p:ContainerRepository=roastery-api
docker images roastery-api

Run it with configuration supplied as environment variables. Because the container cannot reach the Aspire-hosted payments service by name, point it at the standalone payments process from Lab 4 (start it in another terminal first):

docker run --rm -p 8080:8080 \
  -e ASPNETCORE_ENVIRONMENT=Development \
  -e ConnectionStrings__Roastery="Data Source=/tmp/roastery.db" \
  -e Jwt__SigningKey="roastery-dev-signing-key-change-me-32-bytes!!" \
  -e Jwt__Issuer=roastery -e Jwt__Audience=roastery \
  -e services__payments__https__0="https://host.docker.internal:7281" \
  roastery-api

Browse to http://localhost:8080/api/products. It is the same code, the same configuration keys, and a different environment.

Step 4: Version the API

cd src/Roastery.Api && dotnet add package Asp.Versioning.Http

Add the AddApiVersioning registration and the version set from the concepts page. Move the MapProductEndpoints() and MapOrderEndpoints() calls under the versioned group so every URL becomes /api/v1/.... Update the base path variable in module-5.http.

Add one v2 endpoint: GET /api/v2/products/{id} that returns the ProductDto plus an Origin string. Map it with .MapToApiVersion(2). Refresh Scalar and confirm both v1 and v2 documents exist and the Api-Supported-Versions header appears on responses.

Stretch goals

  • Replace SQLite with Postgres: builder.AddPostgres("db").AddDatabase("roastery") in the AppHost, .WithReference(db) on the API, the Npgsql EF Core provider, and a fresh migration. Note how little of the API changed.
  • Deploy: azd init in the src folder, choose the AppHost, then azd up. Requires an Azure subscription and about ten minutes.
  • Add the Aspire Redis resource and switch HybridCache to use it as the L2 with AddRedisDistributedCache("cache").

Common pitfalls

  • Health endpoints 404 under Aspire. MapDefaultEndpoints() is missing, or you deleted the AddDbContextCheck registration along with the old MapHealthChecks lines.
  • payments does not resolve. AddServiceDefaults() is not called in the API (it registers service discovery), or WithReference(payments) is missing in the AppHost.
  • Container starts and immediately exits. Read docker logs; it is almost always a missing Jwt__SigningKey.
  • Versioned routes 404. The route template must be /api/v{version:apiVersion} exactly, and the group needs WithApiVersionSet.

Checkpoint

git add -A && git commit -m "Lab 5 complete"
# or: git checkout checkpoint-5

That is the finished Roastery API. Head to the Wrap-up.