Featured resource
2026 Tech Forecast
2026 Tech Forecast

1,500+ tech insiders, business leaders, and Pluralsight Authors share their predictions on what’s shifting fastest and how to stay ahead.

Download the forecast
  • Lab
    • Libraries: If you want this lab, consider one of these libraries.
    • Core Tech
Labs

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 platform
Lab Info
Level
Beginner
Last updated
Oct 21, 2026
Duration
45m

Contact sales

By clicking submit, you agree to our Privacy Policy and Terms of Use, and consent to receive marketing emails from Pluralsight.
Table of Contents
  1. 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 of appsettings.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) and POST /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 a Console.WriteLine here 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 curl works. ### 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.json is 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:

    1. Step 2 — give every request a correlation-scoped log entry and replace raw exceptions with ProblemDetails.
    2. Step 3 — require a valid JWT and a dispatcher claim before anyone can write a congestion override.
    3. Step 4 — split the API into /v1 and /v2 routes and publish an authenticated OpenAPI document through Scalar.
    4. 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.

  2. 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.BeginScope fixes 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/catch blocks 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's IExceptionHandler interface lets you centralize this: register one implementation, and every unhandled exception from every endpoint flows through TryHandleAsync, 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 500

    A 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 — ArgumentException to 400, a not-found lookup to 404 — turns your exception handler into something callers can actually build retry logic against.

  3. 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, through TokenValidationParameters, 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.

  4. 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 v2

    With v1 preserved untouched, v2 is free to add fields that v1 callers never asked for. Here you'll add a SensorConfidence value 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 authenticate

    An 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 OpenApiDocument and can add to it before it's served. Adding a Bearer security scheme here is what lets Scalar UI render a working "Authorize" button so a partner can try an authenticated request straight from the browser.

  5. 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-For and X-Forwarded-Proto headers 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 checks HttpContext.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 fallback

    In 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 fast

    When 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 curl against the running app.

  6. 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.csproj
    

    Wait 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 API

    In 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-NORTHBOUND and I-80-WESTBOUND.

    Read a v1 congestion reading. The -i flag also prints the response headers, so look for the X-Correlation-Id header from Task 2.1.

    curl -i localhost:5000/v1/corridors/I-90-EASTBOUND/congestion
    

    Read the same corridor from v2. The response adds the sensorConfidence field from Task 4.2.

    curl localhost:5000/v2/corridors/I-90-EASTBOUND
    

    Try 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/override
    

    Fetch the OpenAPI document. Near the end of the output, securitySchemes should contain the Bearer scheme from Task 4.3.

    curl localhost:5000/openapi/v1.json
    

    Check which secrets loaded. The response from Task 5.3 shows connectionStringLoaded as false here. 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 from appsettings.json in the workspace root.

    curl localhost:5000/health/secrets
    

    When you are finished, switch back to the first Terminal tab and press Ctrl+C to stop the API. ### Run the checks yourself

    The 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 quiet
    

    Or one task at a time:

    bash runTest.sh Task_2_1
    

    Each 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

Pluralsight’s AI authoring technology is designed to accelerate the creation of hands-on, technical learning experiences. Serving as a first-pass content generator, it produces structured lab drafts aligned to learning objectives defined by Pluralsight’s Curriculum team. Each lab is then enhanced by our Content team, who configure the environments, refine instructions, and conduct rigorous technical and quality reviews. The result is a collaboration between artificial intelligence and human expertise, where AI supports scale and efficiency, and Pluralsight experts ensure accuracy, relevance, and instructional quality, helping learners build practical skills with confidence.

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.

Get started with Pluralsight