A controller-based web API with ASP.NET Core

This tutorial teaches the basics of building a controller-based web API that uses a database.
You will embark on a journey to create a fully functional API for managing "to-do" items, featuring GET, POST, PUT, and DELETE endpoints. This practical guide will lead you through the process of setting up controllers, routing, models, and database connectivity, establishing a strong foundation for further exploration.
overview
This tutorial builds the following API:
API | Description | Request body | Response body |
GET /api/todoitems | Get all to-do items | None | Array of to-do items |
GET /api/todoitems/{id} | Get an item by ID | None | To-do item |
POST /api/todoitems | Add a new item | To-do item | To-do item |
PUT /api/todoitems/{id} | Update an existing item | To-do item | None |
DELETE /api/todoitems/{id} | Delete an item | None | None |
The following diagram shows the design of the app.
Prerequisites
Visual Studio Code
C# Dev Kit for Visual Studio Code
.NET 9 SDK
Create a Web API project
Change directories to the folder that will contain the project folder in the terminal & run the following command.
dotnet new webapi --use-controllers -o TodoApi cd TodoApi dotnet add package Microsoft.EntityFrameworkCore.InMemory
these commands:
◦ Create a new web API project. ◦ Add a NuGet package that is needed.
- If Visual Studio Code doesn't offer to add build and debug assets, do it from Command Palette.
Run the project
The project template creates a WeatherForecast
API
- Trust the HTTPS development certificate by running the following command:
dotnet dev-certs https --trust
- Run the following command to start the app on the
https
profile:
dotnet run --launch-profile https
The default browser is launched to
https://localhost:<port>
, where<port>
is the randomly chosen port number displayed in the output. There's no endpoint athttps://localhost:<port>
, so the browser returns HTTP 404 Not Found.Append
/weatherforecast
to the URL to test the WeatherForecast API.
Test the project
Create API testing UI with Swagger
We will utilizes the .NET package NSwag.AspNetCore, which integrates Swagger tools for generating a testing UI adhering to the OpenAPI specification:
NSwag: A .NET library that integrates Swagger directly into ASP.NET Core applications, providing middleware and configuration.
Swagger: A set of open-source tools such as OpenAPIGenerator and SwaggerUI that generate API testing pages that follow the OpenAPI specification.
OpenAPI specification: A document that describes the capabilities of the API, based on the XML and attribute annotations within the controllers and models.
Install Swagger tooling
- Run the following command:
dotnet add package NSwag.AspNetCore
This command adds the NSwag.AspNetCore package, which contains tools to generate Swagger documents and UI. Because our project is using OpenAPI, we only use the NSwag package to generate the Swagger UI.
Configure Swagger middleware
- In
Program.cs
, add the following highlighted code:
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.UseSwaggerUi(options =>
{
options.DocumentPath = "/openapi/v1.json";
});
}
The code provided activates the Swagger middleware, allowing the generated JSON document to be served via the Swagger UI. It is important to note that Swagger is exclusively enabled in a development environment; enabling it in a production environment may risk exposing sensitive information regarding the API’s structure and implementation.
The application utilizes the OpenAPI document, which is generated by OpenApi and can be found at /openapi/v1.json, to create the user interface. To view the generated OpenAPI specification for the WeatherForecast API while the project is operational, navigate to https://localhost:<port>/openapi/v1.json in your web browser.
The OpenAPI specification is a JSON formatted document that outlines the structure and capabilities of your API, detailing endpoints, request and response formats, parameters, and more. It serves as a comprehensive blueprint for your API, facilitating interaction with various tools.
Add a model class
A model is a set of classes that represent the data that the app manages. The model for this app is the TodoItem
class.
Add a folder named
Models
.Add a
TodoItem.cs
file to theModels
folder with the following code:
namespace TodoApi.Models;
public class TodoItem
{
public long Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
The Id
property functions as the unique key in a relational database.
Model classes can go anywhere in the project, but the Models
folder is used by convention.
Add a database context
The database context is the main class that coordinates Entity Framework functionality for a data model. This class is created by deriving from the Microsoft.EntityFrameworkCore.DbContext class.
Add a
TodoContext.cs
file to theModels
folder.Enter the following code:
using Microsoft.EntityFrameworkCore;
namespace TodoApi.Models;
public class TodoContext : DbContext
{
public TodoContext(DbContextOptions<TodoContext> options)
: base(options)
{
}
public DbSet<TodoItem> TodoItems { get; set; } = null!;
}
Register the database context
In ASP.NET Core, services such as the DB context must be registered with the dependency injection (DI) container. The container provides the service to controllers.Update Program.cs
with the following code:
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddOpenApi();
builder.Services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
The preceding code:
Adds
using
directives.Adds the database context to the DI container.
Specifies that the database context will use an in-memory database.
Scaffold a controller
Make sure that all of your changes so far are saved.
- In terminal opens at the
TodoAPI
project folder. Run the following commands:
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet tool uninstall -g dotnet-aspnet-codegenerator
dotnet tool install -g dotnet-aspnet-codegenerator
dotnet tool update -g dotnet-aspnet-codegenerator
The preceding commands:
Add NuGet packages required for scaffolding.
Install the scaffolding engine (
dotnet-aspnet-codegenerator
) after uninstalling any possible previous version.
Build the project.
Run the following command:
dotnet aspnet-codegenerator controller -name TodoItemsController -async -api -m TodoItem -dc TodoContext -outDir Controllers
The preceding command scaffolds the TodoItemsController
.
The generated code performs several key functions:
1. ApiController Attribute: It marks the class with the [ApiController]
attribute, which signifies that the controller is designed to handle web API requests. For detailed information about the specific behaviors this attribute enables, refer to the guide on creating web APIs with ASP.NET Core.
2. Dependency Injection (DI): The code utilizes dependency injection to incorporate the database context (`TodoContext`) into the controller. This context is essential for performing all CRUD operations within the controller methods.
3. Route Template Differences:
• For Controllers with Views, the route template includes the [action]
token.
• For API Controllers, the route template does not include the [action]
token.
When the [action]
token is absent from the route template, the action name (method name) is excluded from the endpoint. This means that the method name associated with the action is not utilized in route matching.
Update the PostTodoItem create method
Refactor the PostTodoItem
return statement to use the nameof
operator for improved clarity and maintainability.
[HttpPost]
public async Task<ActionResult<TodoItem>> PostTodoItem(TodoItem todoItem)
{
_context.TodoItems.Add(todoItem);
await _context.SaveChangesAsync();
// return CreatedAtAction("GetTodoItem", new { id = todoItem.Id }, todoItem);
return CreatedAtAction(nameof(GetTodoItem), new { id = todoItem.Id }, todoItem);
}
The code shown above represents an HTTP POST method, identified by the [HttpPost]
attribute. This method reads the TodoItem
object from the body of the incoming HTTP request.
(See: Attribute routing with Http[Verb] attributes for details.)
The CreatedAtAction
method plays a key role here:
Returns an HTTP 201 (Created) status code when the operation succeeds — the standard response for creating a new resource.
Adds a Location header to the response, which points to the URI of the newly created to-do item.
Uses the
GetTodoItem
action to generate that URI. By applying the C#nameof
operator, we avoid hard-coding the action name, improving maintainability and reducing errors.
Test PostTodoItem
With the app still running, in the browser, navigate to
https://localhost:<port>/swagger
to display the API testing page generated by Swagger. Click on TodoItems to expand the operations.On the Swagger API testing page, select Post /api/todoitems > Try it out.
Note that the Request body field contains a generated example format reflecting the parameters for the API.
n the request body, provide the JSON for a to-do item, leaving out the optional
id
field.
{
"name": "Sex on the beach",
"isComplete": true
}
Select Execute.
Swagger provides a Responses pane below the Execute button.
A few useful details to note:
cURL: Swagger generates an example cURL command in Unix/Linux syntax. On macOS, you can run this directly in the Terminal (or any shell that supports Unix commands, such as Git Bash).
Request URL: Shows a simplified version of the HTTP request made by Swagger UI. Real requests may also include headers, query parameters, and a request body.
Server Response: Displays both the response body and headers. In this case, the body shows the
id
field set to1
.Response Code: Returns 201 Created, which means the request succeeded and a new resource was created on the server.
Test the location header URI
Test the app by calling the endpoints from a browser or Swagger.
In Swagger select GET /api/todoitems > Try it out > Execute.
Alternatively, call GET /api/todoitems from a browser by entering the URI
https://localhost:<port>/api/todoitems
. For example,https://localhost:7260/api/todoitems
Exploring the GET methods
This API provides two GET endpoints:
GET /api/todoitems
– retrieves all to-do items.GET /api/todoitems/{id}
– retrieves a single to-do item by its ID.
Earlier, we looked at an example of the GET /api/todoitems/{id}
route.
Now, try adding another to-do item by following the POST instructions. Once it’s created, test the GET /api/todoitems
endpoint in Swagger to see the full list of items.
⚠️ Note: This project uses an in-memory database. That means whenever the app is stopped and restarted, the stored data is lost. If your GET
request doesn’t return any items, simply use the POST endpoint again to add new data before testing.
Return values
The GetTodoItems
and GetTodoItem
methods return an ActionResult<T>
type in ASP.NET Core. This means that the framework automatically converts the returned object into JSON and sends it as part of the response body.
When everything goes smoothly and there are no unhandled exceptions, the response will have a status code of 200 OK. However, if there are any unhandled exceptions, the response will instead show a 5xx error code.
The ActionResult
type can represent different HTTP status codes, allowing for flexibility in response handling. For example, the GetTodoItem
method can produce two possible outcomes:
• If there is no item that matches the requested ID, the method responds with a 404 Not Found error.
• If an item is successfully found, it returns a 200 status code along with a JSON response body containing the details of the item.
Prevent over-posting
Currently the sample app exposes the entire TodoItem
object. Production apps typically limit the data that's input and returned using a subset of the model. There are multiple reasons behind this, and security is a major one. The subset of a model is usually referred to as a Data Transfer Object (DTO), input model, or view model. DTO is used in this tutorial.
A DTO may be used to:
Prevent over-posting.
Hide properties that clients are not supposed to view.
Omit some properties in order to reduce payload size.
Flatten object graphs that contain nested objects. Flattened object graphs can be more convenient for clients.
To demonstrate the DTO approach, update the TodoItem
class to include a secret field:
namespace TodoApi.Models;
public class TodoItem
{
public long Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public string? Secret { get; set; }
}
The secret field needs to be hidden from this app, but an administrative app could choose to expose it.
Verify you can post and get the secret field.
Create a DTO model in a Models/TodoItemsDTO.cs file
namespace TodoApi.Models;
public class TodoItemDTO
{
public long Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
Update the TodoItemsController
to use TodoItemDTO
:
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;
namespace TodoApi.Controllers;
[Route("api/[controller]")]
[ApiController]
public class TodoItemsController : ControllerBase
{
private readonly TodoContext _context;
public TodoItemsController(TodoContext context)
{
_context = context;
}
// GET: api/TodoItems
[HttpGet]
public async Task<ActionResult<IEnumerable<TodoItemDTO>>> GetTodoItems()
{
return await _context.TodoItems
.Select(x => ItemToDTO(x))
.ToListAsync();
}
// GET: api/TodoItems/5
// <snippet_GetByID>
[HttpGet("{id}")]
public async Task<ActionResult<TodoItemDTO>> GetTodoItem(long id)
{
var todoItem = await _context.TodoItems.FindAsync(id);
if (todoItem == null)
{
return NotFound();
}
return ItemToDTO(todoItem);
}
// </snippet_GetByID>
// PUT: api/TodoItems/5
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
// <snippet_Update>
[HttpPut("{id}")]
public async Task<IActionResult> PutTodoItem(long id, TodoItemDTO todoDTO)
{
if (id != todoDTO.Id)
{
return BadRequest();
}
var todoItem = await _context.TodoItems.FindAsync(id);
if (todoItem == null)
{
return NotFound();
}
todoItem.Name = todoDTO.Name;
todoItem.IsComplete = todoDTO.IsComplete;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException) when (!TodoItemExists(id))
{
return NotFound();
}
return NoContent();
}
// </snippet_Update>
// POST: api/TodoItems
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
// <snippet_Create>
[HttpPost]
public async Task<ActionResult<TodoItemDTO>> PostTodoItem(TodoItemDTO todoDTO)
{
var todoItem = new TodoItem
{
IsComplete = todoDTO.IsComplete,
Name = todoDTO.Name
};
_context.TodoItems.Add(todoItem);
await _context.SaveChangesAsync();
return CreatedAtAction(
nameof(GetTodoItem),
new { id = todoItem.Id },
ItemToDTO(todoItem));
}
// </snippet_Create>
// DELETE: api/TodoItems/5
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteTodoItem(long id)
{
var todoItem = await _context.TodoItems.FindAsync(id);
if (todoItem == null)
{
return NotFound();
}
_context.TodoItems.Remove(todoItem);
await _context.SaveChangesAsync();
return NoContent();
}
private bool TodoItemExists(long id)
{
return _context.TodoItems.Any(e => e.Id == id);
}
private static TodoItemDTO ItemToDTO(TodoItem todoItem) =>
new TodoItemDTO
{
Id = todoItem.Id,
Name = todoItem.Name,
IsComplete = todoItem.IsComplete
};
}
Add authentication support to a web API
refer https://www.c-sharpcorner.com/article/basic-authentication-in-web-api/
Publish to Azure
refer https://learn.microsoft.com/en-us/azure/?product=popular
Subscribe to my newsletter
Read articles from Shivam Saini directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
