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 Services

A neighborhood tool-lending library wants to replace its spreadsheet with a real API. Members browse categories of tools (ladders, power drills, garden equipment), each category nests the tools that belong to it, and each tool can carry an uploaded manual. Staff need to add tools, replace them, and patch individual fields without breaking clients that expect predictable status codes and error bodies, and the whole thing has to run correctly whether it's on a developer's laptop or in front of real members, with data that survives a restart.

Lab platform
Lab Info
Level
Beginner
Last updated
Oct 23, 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: Meet the ToolShare catalog service

    Welcome to ToolShare, the API behind a neighborhood tool-lending library. Members browse categories — ladders, power drills, garden equipment — and each category nests the tools that belong to it. Each tool can carry an uploaded manual, and staff need to add tools, replace them, and patch individual fields without breaking clients that expect predictable status codes and error bodies. The starter project is already split the way a production ASP.NET Core API is: src/Controllers holds CategoriesController and ToolsController, which is where every HTTP verb you write lands. src/Services holds ICatalogService and its implementation, CatalogService, which every controller action calls into rather than touching data directly. src/Data holds ToolShareDbContext and ToolRepository, which is where SQLite persistence lives once you get there. src/Options holds ToolShareOptions, the strongly typed settings the upload flow reads from configuration. src/Program.cs is where all of it gets wired together — middleware pipeline, DI registrations, and route mapping. Right now the project builds but barely responds: the pipeline doesn't distinguish development from production, and no controller route is actually mapped. Step 2 gets the skeleton answering requests at all. Step 3 makes the catalog's nested resources negotiate formats and fail with real Problem Details. Step 4 takes tools through validated creates, full replacements, partial patches, and file uploads, with upload limits read from centralized configuration. Step 5 finishes the data layer with a migrated, seeded SQLite database and async, paged repository queries.

    If you get stuck, you can refer to the provided solution code for each task, available in the solution folder.

  2. Challenge

    Step 2: Stand up the environment-aware API skeleton

    ASP.NET Core's request pipeline is a chain of middleware you register in order, and the order matters: whatever you register first runs first on the way in and last on the way out. A production API almost never wants the same pipeline in every environment — a developer wants a raw stack trace when something throws, but a member of the public should only ever see a sanitized error body. IHostEnvironment.IsDevelopment() is the standard way to branch that pipeline, and UseExceptionHandler is the middleware that turns an unhandled exception into a controlled response instead of letting it crash the connection. With the pipeline branching by environment, the next gap is routing itself: attribute-routed controllers like CategoriesController need app.MapControllers() to be reachable at all, and UseAuthorization() needs to run first so any [Authorize] attribute you add later is honored before a controller action executes.

  3. Challenge

    Step 3: Serve the nested tool catalog with negotiated responses

    A category and its tools are naturally nested — /api/categories/{id}/tools reads as 'the tools that belong to this category,' and that's the shape REST clients expect. The action itself already exists on CategoriesController; it just needs to return the tools that match the category in the route instead of the empty list it returns now. Not every client wants JSON. Content negotiation is the pattern of inspecting the Accept header a client sends and choosing the response format accordingly, and ASP.NET Core leaves you free to do that inspection yourself inside an action when you want fine control over exactly what gets serialized. RFC 7807 defines a standard JSON shape for error responses — type, title, status, and detail — so a client can handle a 404 or a 400 the same way no matter which API it's talking to. ASP.NET Core's ProblemDetails type and the controller's Problem(...)/ValidationProblem(...) helpers build that shape for you; right now GetCategoryById has no guard clauses at all and answers every id with a bare NotFound().

  4. Challenge

    Step 4: Carry tools through validated writes, patches, and uploads

    Every write to the catalog should be checked before it's trusted. [ApiController] already answers a request whose model fails validation — a missing [Required] Name, say — with an automatic 400, but that filter only runs inside the MVC pipeline; checking ModelState.IsValid in the action keeps the rule in the code itself, so the action still refuses bad input when it's called directly, as the tests do. ToolsController never touches the database itself: it calls into the injected ICatalogService, whose CatalogService implementation owns persistence — it builds the Tool entity, saves it through ToolShareDbContext, and reports back whether a tool existed. The controller's job is to validate the request, call the right service method, and turn the result into the right status code. PUT means replace: the request body is the resource's new, complete representation, and any field the caller leaves out should also disappear from the stored tool if it isn't in the DTO. That's different from PATCH, which only touches the fields the caller actually sent — the distinction is why ToolsController has two separate actions instead of one. A ToolPatchDto models an update where every property is nullable by design: null means 'the caller didn't mention this field,' not 'set it to empty.' The safe way to apply one is to load the existing tool first and copy over only the properties that aren't null, which is exactly what's missing from PatchTool right now. A file upload is one of the easier ways to accidentally let a caller write outside the directory you intended, if the code trusts the caller's own file name or skips a size check. ToolShareOptions, bound from configuration via the options pattern, centralizes the manuals directory (ManualsDirectory) and the maximum upload size (MaxManualSizeBytes) in one place so every action that touches uploads reads the same limits instead of hardcoding them.

  5. Challenge

    Step 5: Persist the catalog through a migrated SQLite database

    EF Core migrations are the versioned steps that turn a DbContext's model into actual SQLite tables; without applying them, every query against ToolShareDbContext is running against a database file that has no schema at all. Database.Migrate() applies whatever migrations haven't run yet, and it's safe to call every time the API starts because it's a no-op once the schema is current. Every database call in ASP.NET Core should be awaited, not blocked on, because blocking a request thread on I/O is exactly the kind of thing that makes a busy API run out of threads under load. EF Core's FirstOrDefaultAsync, ToListAsync, and friends exist so a repository method can await the database instead of freezing the thread that's handling the HTTP request. Returning an entire table in one response works fine with the four tools the seed adds and falls over the moment the catalog has thousands. Paging pushes Skip and Take into the SQL that SQLite runs, so only the rows a client actually asked for ever cross the wire — as long as those calls happen before the query is materialized with ToListAsync.

  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

    Open the Terminal tab and run:

    dotnet run --project src/Lab.csproj
    

    The program runs in the terminal and prints its output there. Every behaviour it shows is code you wrote in the previous steps, so read the output against what each task asked for — that is the whole lab, working as one program. ### 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