This is the final part of a 4-part series. In Part 3, we completed the write operations POST, PUT, and DELETE. Now we take a step back, look at the big picture, and talk about where to go from here.
What We Built
Over the course of this series, we went from zero to a fully functional RESTful API. Let's take stock of everything that's now in place.
Project Structure
ContosoPizza/
├── Controllers/
│ └── PizzaController.cs ← handles all HTTP requests
├── Models/
│ └── Pizza.cs ← our data shape
├── Services/
│ └── PizzaService.cs ← in-memory data layer
├── Program.cs ← app startup & pipeline
├── ContosoPizza.http ← request testing file
└── ContosoPizza.csproj ← project config
The Full API Surface
| Method | Route | What It Does | Response |
|---|---|---|---|
GET |
/pizza |
Returns all pizzas | 200 OK |
GET |
/pizza/{id} |
Returns one pizza by ID |
200 / 404
|
POST |
/pizza |
Adds a new pizza | 201 Created |
PUT |
/pizza/{id} |
Replaces an existing pizza |
204 / 400 / 404
|
DELETE |
/pizza/{id} |
Removes a pizza |
204 / 404
|
Five endpoints. Full CRUD coverage. Clean status codes on every path including the unhappy ones.
How the Pieces Fit Together
It's worth zooming out and seeing how data flows through the app from a single request.
Take a POST /pizza request as an example:
- The request arrives at the ASP.NET Core HTTP pipeline
- The router matches
/pizzatoPizzaController, and thePOSTverb to theCreatemethod -
[ApiController]deserializes the JSON body into aPizzaobject automatically -
Createpasses that object toPizzaService.Add() - The service assigns an ID and appends it to the in-memory list
- The controller returns
CreatedAtAction(...), which ASP.NET Core converts into a201 Createdresponse with aLocationheader
Every request follows this same arc: route → controller action → service → response. The controller stays thin, the service handles the logic, and the model defines the shape. That separation is what makes the code easy to extend later.
The Limitations of In-Memory Storage
Our PizzaService uses a static List<Pizza> which was perfect for learning, but has real constraints:
- Data resets on every restart. Stop the server and all your pizzas are gone, back to the two defaults.
-
No concurrency safety. If two requests hit
Add()at exactly the same time, you could get race conditions. - No persistence. Nothing is written to disk, a file, or a database.
In a real application, you'd swap the in-memory list for a proper database. The good news is that ASP.NET Core is designed for exactly this transition.
Moving to a Real Database
When you're ready to add persistence, Entity Framework Core (EF Core) is the natural next step. It's Microsoft's official ORM for .NET and integrates tightly with ASP.NET Core.
The migration path is straightforward:
1. Install EF Core:
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
# or for SQLite (great for local dev):
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
2. Create a DbContext:
using Microsoft.EntityFrameworkCore;
using ContosoPizza.Models;
public class PizzaContext : DbContext
{
public PizzaContext(DbContextOptions<PizzaContext> options)
: base(options) { }
public DbSet<Pizza> Pizzas => Set<Pizza>();
}
3. Register it in Program.cs:
builder.Services.AddDbContext<PizzaContext>(options =>
options.UseSqlite("Data Source=contosopizza.db"));
4. Inject it into your controller instead of calling the static service:
public class PizzaController : ControllerBase
{
private readonly PizzaContext _context;
public PizzaController(PizzaContext context)
{
_context = context;
}
[HttpGet]
public ActionResult<List<Pizza>> GetAll() =>
_context.Pizzas.ToList();
}
Your routes, attributes, and HTTP behavior stay exactly the same only the data layer changes. That's the benefit of keeping the controller and the service layer separate from the start.
What Else Should You Add?
A working CRUD API is a solid foundation. Here are the most common things you'd layer on next in a real project:
Input Validation
Right now, someone could POST a pizza with no name at all and it would go straight into the list. Data annotations let you enforce rules at the model level:
using System.ComponentModel.DataAnnotations;
public class Pizza
{
public int Id { get; set; }
[Required]
[StringLength(100, MinimumLength = 1)]
public string? Name { get; set; }
public bool IsGlutenFree { get; set; }
}
Because [ApiController] is on your controller, invalid requests are automatically rejected with a 400 Bad Request no extra code needed.
Swagger / OpenAPI
ASP.NET Core projects come with Swashbuckle pre-installed. It generates an interactive API documentation page automatically from your controller code. Just navigate to /swagger while your app is running and you'll see every endpoint, its expected inputs, and possible responses all explorable in the browser.
Authentication & Authorization
When you're ready to lock down your endpoints, ASP.NET Core has built-in support for JWT Bearer tokens. Add the NuGet package, register the middleware, and protect specific endpoints with [Authorize]:
[Authorize]
[HttpDelete("{id}")]
public IActionResult Delete(int id) { ... }
Async All the Way
Our current service methods are synchronous. In production, especially with a real database, you'd want everything to be async/await to avoid blocking threads under load:
[HttpGet]
public async Task<ActionResult<List<Pizza>>> GetAll() =>
await _context.Pizzas.ToListAsync();
Series Recap
Here's everything we covered across all four parts:
Part 1 What REST is, why ASP.NET Core, scaffolding the project, running it for the first time, and testing with .http files and HTTP REPL.
Part 2 How controllers, ControllerBase, [ApiController], and [Route] work; building the Pizza model and PizzaService; implementing GET endpoints.
Part 3 Implementing POST, PUT, and DELETE; understanding IActionResult vs ActionResult<T>; correct status codes on every path; end-to-end testing of all five operations.
Part 4 (this one) The full picture, in-memory storage limitations, migrating to EF Core, and the natural next steps for a production-ready API.
Final Thoughts
Building a Web API with ASP.NET Core is remarkably approachable once you understand the underlying structure. Controllers handle routing and HTTP concerns. Services handle logic. Models define the data. Each layer has a clear job.
The patterns you've learned here attribute routing, ActionResult types, proper HTTP status codes, thin controllers, separated service layers are the same ones used in large-scale production APIs. You're not learning toy concepts; you're learning the real thing.
From here, the best thing you can do is keep building. Add a second resource. Connect a real database. Explore authentication. Every layer you add will reinforce what you already know.
Good luck and thanks for following along through the whole series. 🍕
Found this series helpful? Drop a reaction or share it with someone learning .NET. Questions are always welcome in the comments!
Tags: dotnet csharp webapi aspnetcore beginners
Top comments (0)