- Lab
-
Libraries: If you want this lab, consider one of these libraries.
- Core Tech
ASP.NET Core API Real Traffic
TrafficPulse is a freight dispatch API that reports live highway congestion so trucks can be rerouted around it. It currently runs unauthenticated with unstructured console logging, a single unversioned route set, and no deployment story -- fine for a prototype, unacceptable now that partner carriers and a public status page are about to depend on it. You are the engineer asked to bring it up to the standard required before it goes behind an Azure App Service front door: locked-down dispatch endpoints, a documented and versioned contract, and secrets that come from Key Vault instead of a checked-in appsettings file.
Lab Info
Table of Contents
-
Challenge
Step 1: Explore the TrafficPulse dispatch API
Welcome to TrafficPulse
TrafficPulse is a freight dispatch API that reports live highway congestion, and routing software polls it every few seconds to reroute trucks around delays. It started as a prototype and it shows: the project you're opening now,
TrafficPulse.Api, has no authentication, logs to the console with plain strings, exposes one unversioned set of routes, and reads a placeholder connection string straight out ofappsettings.json. The goal is to put this behind Azure App Service and let partner carriers hit it directly — none of that is acceptable until you fix it.Spend this step reading the code before you touch anything. Every later step assumes you understand what's already here. ### What's already in
src/CongestionStore.cs— an in-memory store of corridor congestion readings, seeded with sample data. You won't need to change this.CongestionEndpoints.cs— minimal-API routes:GET /corridors/{id}/congestion(anyone can read it) andPOST /corridors/{id}/congestion/override(anyone can write it — that's the problem you'll fix in Step 3).Program.cs— wires up the app:builder.Services,app.MapCongestionEndpoints(), and aConsole.WriteLinehere and there instead of real logging.
Run the endpoints locally if you want to see them respond — there's no auth in front of them yet, so a plain
curlworks. ### Why each gap is a blocker- Unstructured logging: when a partner carrier reports a bad congestion reading, there's no correlation id to grep for — you'd be reading raw console output line by line.
- No global error handling: an unhandled exception today returns a raw stack trace to the caller, which is both an information leak and an ugly experience for a partner integration.
- No authentication: the override endpoint lets anyone rewrite congestion data for any corridor. That's fine for a demo, not for live dispatch.
- One unversioned route set: the moment the sensor payload needs a new field, you either break every existing caller or bolt on ad-hoc optional fields forever.
- Hard-coded config: the connection string in
appsettings.jsonis a placeholder. Azure App Service will supply real secrets through Key Vault, but only if the app knows how to ask for them. ### Where you're headed
Over the next four steps you will:
- Step 2 — give every request a correlation-scoped log entry and replace raw exceptions with
ProblemDetails. - Step 3 — require a valid JWT and a dispatcher claim before anyone can write a congestion override.
- Step 4 — split the API into
/v1and/v2routes and publish an authenticated OpenAPI document through Scalar. - Step 5 — make the app behave correctly behind Azure App Service's reverse proxy and load its secrets from Key Vault with a safe local fallback.
Each step builds on the one before it, so keep the project building as you go — a broken build fails every check in the lab, not just the one you're working on. info> Feeling stuck? Check out the matching
solution/stepN/folder for the step you're on to see a working implementation. Give it a try on your own first. The solution folder is your safety net, not your starting point. -
Challenge
Step 2: Harden logging and error handling for live traffic feeds
Structured logging with correlation scopes
Right now a failed request produces a handful of unrelated console lines with no way to tell which ones belong together.
ILogger.BeginScopefixes this: everything logged inside an open scope automatically carries whatever key/value pairs you put in the scope, without you having to pass them into every log call by hand.You'll build a middleware that opens one scope per request, keyed by a correlation id either supplied by the caller or generated fresh, and echoes that id back on the response so a caller can quote it when they report a problem. ## From try/catch soup to one exception handler
Scattered
try/catchblocks mean every endpoint decides its own error shape — or forgets to catch anything and lets a raw exception (and its stack trace) reach the caller. ASP.NET Core'sIExceptionHandlerinterface lets you centralize this: register one implementation, and every unhandled exception from every endpoint flows throughTryHandleAsync, which decides what the caller actually sees.The standard shape for that response is
ProblemDetails— a small JSON object with a title, a status, and enough context to act on without leaking implementation details. ## Not every exception is a 500A blanket 500 for every exception is safe but unhelpful: a caller who sent a malformed corridor id gets the same status as a genuine server bug, so they can't tell whether retrying will ever work. Mapping specific exception types to specific status codes —
ArgumentExceptionto 400, a not-found lookup to 404 — turns your exception handler into something callers can actually build retry logic against. -
Challenge
Step 3: Lock down dispatch endpoints with JWT and claims policies
Authenticating dispatch traffic with JWT bearer tokens
The override endpoint currently trusts every caller. JWT bearer authentication fixes that at the transport layer: the caller presents a signed token in an
Authorization: Bearer <token>header, and ASP.NET Core's JWT bearer handler verifies the signature, issuer, audience, and expiry before your endpoint code ever runs. You configure that verification once, throughTokenValidationParameters, using a signing key read from configuration so it can differ between environments without a code change. ## From "authenticated" to "authorized"A valid token only proves who the caller is — a token belonging to a read-only partner carrier is just as valid as one belonging to an internal dispatcher. Authorization policies add the second check: a named policy that inspects the token's claims and only succeeds if a specific claim is present with a specific value. Defining the policy once, by name, means every endpoint that needs it just references that name instead of repeating the claim check. ## Applying the policy where it matters
Read access to congestion data should stay open — the whole point of TrafficPulse is that dispatch software polls it constantly. Only the endpoint that writes an override needs to be locked down.
RequireAuthorization("PolicyName")attaches a policy to one specific route, so you can protect the write path without touching the read path at all. -
Challenge
Step 4: Version corridor routes and publish the OpenAPI contract
Versioning routes with a URL segment
As soon as a partner carrier depends on the exact shape of your JSON response, changing that shape breaks them. URL-segment versioning —
/v1/...versus/v2/...— lets you publish a new payload shape without touching the old one: both route groups stay live, backed by the same underlying data, and each caller upgrades on their own schedule.app.MapGroup("/v1/corridors")gives you a route-builder scoped to that prefix, just like the unversioned one you already have. ## Evolving the payload in v2With v1 preserved untouched, v2 is free to add fields that v1 callers never asked for. Here you'll add a
SensorConfidencevalue computed from the existing reading data, giving carriers who upgrade a genuinely richer response instead of a copy-pasted one with a placeholder field bolted on. ## Documenting the contract, including how to authenticateAn OpenAPI document that doesn't mention the JWT bearer scheme is only half a contract — a partner reading it has no idea a token is required, let alone how to supply one. .NET's OpenAPI generation supports document transformers: functions that receive the generated
OpenApiDocumentand can add to it before it's served. Adding aBearersecurity scheme here is what lets Scalar UI render a working "Authorize" button so a partner can try an authenticated request straight from the browser. -
Challenge
Step 5: Deploy behind Azure App Service and load Key Vault secrets
Trusting Azure App Service's reverse proxy
App Service terminates TLS at its own edge and forwards your app plain HTTP internally, adding
X-Forwarded-ForandX-Forwarded-Protoheaders so you can tell what the original request actually looked like. Without the forwarded-headers middleware, your app thinks every request arrived over plain HTTP, which breaks anything that checksHttpContext.Request.IsHttps— including redirects and any absolute URLs you generate. Configuring which headers to trust, and from how many hops, is what makes that information reliable instead of spoofable. ## Secrets from Key Vault, with a safe local fallbackIn production, Azure Key Vault will inject the real connection string into configuration before your app starts. Locally, no Key Vault is configured, so that key is either missing entirely or still holds the placeholder value checked into
appsettings.json. The code that reads it needs to treat both of those the same way — as "not loaded" — and fall back to something that lets the app start anyway, rather than crashing or silently using a placeholder as if it were real. ## Diagnosing a bad deployment fastWhen a deployment goes out and something's wrong, the fastest question to answer is "did the secrets actually load?" A small diagnostics endpoint that reports a boolean per secret — reusing the exact same "missing or placeholder" rule the app uses at startup — turns a guessing game into a ten-second
curlagainst the running app. -
Challenge
Step 6: Run the application
Every task is done, and each one was graded the moment you completed it. What you have not done yet is watch the whole application run at once -- which is the only place the pieces you wrote separately become a single working program.
Start the application
TrafficPulse is a web API, so it does not print results the way a console program does. Once it starts, it waits for HTTP requests and only logs a few startup lines. You will see its behaviour in the responses you get back from it.
Open the Terminal tab and run:
dotnet run --project src/Lab.csprojWait for the line
Now listening on: http://0.0.0.0:5000. The API keeps running in this terminal and holds it until you stop it, so leave it running and open a new Terminal tab alongside this one for the requests in the next section. ### Send requests to the APIIn the other Terminal tab, send the requests below. Each one exercises code you wrote in an earlier step. The seeded corridor ids are
I-90-EASTBOUND,I-5-NORTHBOUNDandI-80-WESTBOUND.Read a v1 congestion reading. The
-iflag also prints the response headers, so look for theX-Correlation-Idheader from Task 2.1.curl -i localhost:5000/v1/corridors/I-90-EASTBOUND/congestionRead the same corridor from v2. The response adds the
sensorConfidencefield from Task 4.2.curl localhost:5000/v2/corridors/I-90-EASTBOUNDTry to post an override without a token. The dispatcher policy from Tasks 3.1 to 3.3 should reject it with
401 Unauthorized.curl -i -X POST -H "Content-Type: application/json" -d '{}' localhost:5000/corridors/I-90-EASTBOUND/congestion/overrideFetch the OpenAPI document. Near the end of the output,
securitySchemesshould contain theBearerscheme from Task 4.3.curl localhost:5000/openapi/v1.jsonCheck which secrets loaded. The response from Task 5.3 shows
connectionStringLoadedasfalsehere. That is expected: no Key Vault is configured, so the app is using the local fallback from Task 5.2. The signing key, issuer and audience come fromappsettings.jsonin the workspace root.curl localhost:5000/health/secretsWhen you are finished, switch back to the first Terminal tab and press
Ctrl+Cto stop the API. ### Run the checks yourselfThe lab ran each task's test for you as you went, but they are ordinary tests and you can run them whenever you like. The whole suite:
dotnet test tests/Lab.Tests.csproj --nologo --verbosity quietOr one task at a time:
bash runTest.sh Task_2_1Each failure names the task it belongs to, so the output tells you which step to go back to rather than which line to stare at. Two things worth trying before you finish: break something on purpose and watch the matching check fail, then put it back. Knowing what a failure looks like is worth as much as knowing what a pass looks like, and it costs you nothing here.
When you are done, everything you wrote is still in the editor tabs -- the lab is yours to keep reading.
About the author
Real skill practice before real-world application
Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.
Learn by doing
Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.
Follow your guide
All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.
Turn time into mastery
On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.