- Lab
-
Libraries: If you want this lab, consider one of these libraries.
- Core Tech
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 Info
Table of Contents
-
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/ControllersholdsCategoriesControllerandToolsController, which is where every HTTP verb you write lands.src/ServicesholdsICatalogServiceand its implementation,CatalogService, which every controller action calls into rather than touching data directly.src/DataholdsToolShareDbContextandToolRepository, which is where SQLite persistence lives once you get there.src/OptionsholdsToolShareOptions, the strongly typed settings the upload flow reads from configuration.src/Program.csis 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
solutionfolder. -
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, andUseExceptionHandleris 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 likeCategoriesControllerneedapp.MapControllers()to be reachable at all, andUseAuthorization()needs to run first so any[Authorize]attribute you add later is honored before a controller action executes. -
Challenge
Step 3: Serve the nested tool catalog with negotiated responses
A category and its tools are naturally nested —
/api/categories/{id}/toolsreads as 'the tools that belong to this category,' and that's the shape REST clients expect. The action itself already exists onCategoriesController; 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 theAcceptheader 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, anddetail— so a client can handle a 404 or a 400 the same way no matter which API it's talking to. ASP.NET Core'sProblemDetailstype and the controller'sProblem(...)/ValidationProblem(...)helpers build that shape for you; right nowGetCategoryByIdhas no guard clauses at all and answers every id with a bareNotFound(). -
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; checkingModelState.IsValidin 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.ToolsControllernever touches the database itself: it calls into the injectedICatalogService, whoseCatalogServiceimplementation owns persistence — it builds theToolentity, saves it throughToolShareDbContext, 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.PUTmeans 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 fromPATCH, which only touches the fields the caller actually sent — the distinction is whyToolsControllerhas two separate actions instead of one. AToolPatchDtomodels an update where every property is nullable by design:nullmeans '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 fromPatchToolright 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. -
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 againstToolShareDbContextis 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'sFirstOrDefaultAsync,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 pushesSkipandTakeinto 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 withToListAsync. -
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.csprojThe 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 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.