Training
Module
Create a web API with ASP.NET Core controllers - Training
Create a RESTful service with ASP.NET Core controllers that supports create, read, update, and delete (CRUD) operations.
This browser is no longer supported.
Upgrade to Microsoft Edge to take advantage of the latest features, security updates, and technical support.
Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Note
This isn't the latest version of this article. For the current release, see the .NET 9 version of this article.
Warning
This version of ASP.NET Core is no longer supported. For more information, see the .NET and .NET Core Support Policy. For the current release, see the .NET 9 version of this article.
Important
This information relates to a pre-release product that may be substantially modified before it's commercially released. Microsoft makes no warranties, express or implied, with respect to the information provided here.
For the current release, see the .NET 9 version of this article.
By Tim Deschryver and Rick Anderson
This tutorial teaches the basics of building a controller-based web API that uses a database. Another approach to creating APIs in ASP.NET Core is to create minimal APIs. For help with choosing between minimal APIs and controller-based APIs, see APIs overview. For a tutorial on creating a minimal API, see Tutorial: Create a minimal API with ASP.NET Core.
This tutorial creates 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.
Visual Studio 2022 with the ASP.NET and web development workload.
A NuGet package must be added to support the database used in this tutorial.
Microsoft.EntityFrameworkCore.InMemory
.Note
For guidance on adding packages to .NET apps, see the articles under Install and manage packages at Package consumption workflow (NuGet documentation). Confirm correct package versions at NuGet.org.
The project template creates a WeatherForecast
API with support for OpenAPI.
Press Ctrl+F5 to run without the debugger.
Visual Studio displays the following dialog when a project is not yet configured to use SSL:
Select Yes if you trust the IIS Express SSL certificate.
The following dialog is displayed:
Select Yes if you agree to trust the development certificate.
For information on trusting the Firefox browser, see Firefox SEC_ERROR_INADEQUATE_KEY_USAGE certificate error.
Visual Studio launches a terminal window and displays the URL of the running app. The API is hosted at https://localhost:<port>
, where <port>
is a randomly chosen port number set at the project creation.
...
info: Microsoft.Hosting.Lifetime[14]
Now listening on: https://localhost:7260
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:7261
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
...
Ctrl+click the HTTPS URL in the output to test the web app in a browser. There's no endpoint at https://localhost:<port>
, so the browser returns HTTP 404 Not Found.
Append /weatherforecast
to the URL to test the WeatherForecast API.
The browser displays JSON similar to the following example:
[
{
"date": "2025-07-16",
"temperatureC": 52,
"temperatureF": 125,
"summary": "Mild"
},
{
"date": "2025-07-17",
"temperatureC": 36,
"temperatureF": 96,
"summary": "Warm"
},
{
"date": "2025-07-18",
"temperatureC": 39,
"temperatureF": 102,
"summary": "Cool"
},
{
"date": "2025-07-19",
"temperatureC": 10,
"temperatureF": 49,
"summary": "Bracing"
},
{
"date": "2025-07-20",
"temperatureC": -1,
"temperatureF": 31,
"summary": "Chilly"
}
]
This tutorial uses Endpoints Explorer and .http files to test the API.
A model is a set of classes that represent the data that the app manages. The model for this app is the TodoItem
class.
Models
.Models
folder and select Add > Class. Name the class TodoItem and select Add.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.
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.
Right-click the Models
folder and select Add > Class. Name the class TodoContext and click Add.
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!;
}
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 highlighted 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:
using
directives.Right-click the Controllers
folder.
Select Add > New Scaffolded Item.
Select API Controller with actions, using Entity Framework, and then select Add.
In the Add API Controller with actions, using Entity Framework dialog:
If the scaffolding operation fails, select Add to try scaffolding a second time.
This step adds the Microsoft.VisualStudio.Web.CodeGeneration.Design
and Microsoft.EntityFrameworkCore.Tools
NuGet packages to the project.
These packages are required for scaffolding.
The generated code:
[ApiController]
attribute. This attribute indicates that the controller responds to web API requests. For information about specific behaviors that the attribute enables, see Create web APIs with ASP.NET Core.TodoContext
) into the controller. The database context is used in each of the CRUD methods in the controller.The ASP.NET Core templates for:
[action]
in the route template.[action]
in the route template.When the [action]
token isn't in the route template, the action name (method name) isn't included in the endpoint. That is, the action's associated method name isn't used in the matching route.
Update the return statement in the PostTodoItem
to use the nameof operator:
[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 preceding code is an HTTP POST
method, as indicated by the [HttpPost]
attribute. The method gets the value of the TodoItem
from the body of the HTTP request.
For more information, see Attribute routing with Http[Verb] attributes.
The CreatedAtAction method:
HTTP 201
is the standard response for an HTTP POST
method that creates a new resource on the server.Location
header specifies the URI of the newly created to-do item. For more information, see 10.2.2 201 Created.GetTodoItem
action to create the Location
header's URI. The C# nameof
keyword is used to avoid hard-coding the action name in the CreatedAtAction
call.Select View > Other Windows > Endpoints Explorer.
Right-click the POST endpoint and select Generate request.
A new file is created in the project folder named TodoApi.http
, with contents similar to the following example:
@TodoApi_HostAddress = https://localhost:49738
POST {{TodoApi_HostAddress}}/api/todoitems
Content-Type: application/json
{
//TodoItem
}
###
###
) line is a request delimiter: what comes after it is for a different request.The POST request expects a TodoItem
. To define the todo, replace the //TodoItem
comment with the following JSON:
{
"name": "walk dog",
"isComplete": true
}
The TodoApi.http file should now look like the following example, but with your port number:
@TodoApi_HostAddress = https://localhost:7260
Post {{TodoApi_HostAddress}}/api/todoitems
Content-Type: application/json
{
"name": "walk dog",
"isComplete": true
}
###
Run the app.
Select the Send request link that is above the POST
request line.
The POST request is sent to the app and the response is displayed in the Response pane.
Test the app by calling the GET
endpoints from a browser or by using Endpoints Explorer. The following steps are for Endpoints Explorer.
In Endpoints Explorer, right-click the first GET endpoint, and select Generate request.
The following content is added to the TodoApi.http
file:
GET {{TodoApi_HostAddress}}/api/todoitems
###
Select the Send request link that is above the new GET
request line.
The GET request is sent to the app and the response is displayed in the Response pane.
The response body is similar to the following JSON:
[
{
"id": 1,
"name": "walk dog",
"isComplete": true
}
]
In Endpoints Explorer, right-click the /api/todoitems/{id}
GET endpoint and select Generate request.
The following content is added to the TodoApi.http
file:
@id=0
GET {{TodoApi_HostAddress}}/api/todoitems/{{id}}
###
Assign {@id}
to 1
(instead of 0
).
Select the Send request link that is above the new GET request line.
The GET request is sent to the app and the response is displayed in the Response pane.
The response body is similar to the following JSON:
{
"id": 1,
"name": "walk dog",
"isComplete": true
}
Two GET endpoints are implemented:
GET /api/todoitems
GET /api/todoitems/{id}
The previous section showed an example of the /api/todoitems/{id}
route.
Follow the POST instructions to add another todo item, and then test the /api/todoitems
route using Swagger.
This app uses an in-memory database. If the app is stopped and started, the preceding GET request doesn't return any data. If no data is returned, POST data to the app.
The [HttpGet]
attribute denotes a method that responds to an HTTP GET
request. The URL path for each method is constructed as follows:
Start with the template string in the controller's Route
attribute:
[Route("api/[controller]")]
[ApiController]
public class TodoItemsController : ControllerBase
Replace [controller]
with the name of the controller, which by convention is the controller class name minus the "Controller" suffix. For this sample, the controller class name is TodoItemsController, so the controller name is "TodoItems". ASP.NET Core routing is case insensitive.
If the [HttpGet]
attribute has a route template (for example, [HttpGet("products")]
), append that to the path. This sample doesn't use a template. For more information, see Attribute routing with Http[Verb] attributes.
In the following GetTodoItem
method, "{id}"
is a placeholder variable for the unique identifier of the to-do item. When GetTodoItem
is invoked, the value of "{id}"
in the URL is provided to the method in its id
parameter.
[HttpGet("{id}")]
public async Task<ActionResult<TodoItem>> GetTodoItem(long id)
{
var todoItem = await _context.TodoItems.FindAsync(id);
if (todoItem == null)
{
return NotFound();
}
return todoItem;
}
The return type of the GetTodoItems
and GetTodoItem
methods is ActionResult<T> type. ASP.NET Core automatically serializes the object to JSON and writes the JSON into the body of the response message. The response code for this return type is 200 OK, assuming there are no unhandled exceptions. Unhandled exceptions are translated into 5xx errors.
ActionResult
return types can represent a wide range of HTTP status codes. For example, GetTodoItem
can return two different status values:
item
results in an HTTP 200
response.Examine the PutTodoItem
method:
[HttpPut("{id}")]
public async Task<IActionResult> PutTodoItem(long id, TodoItem todoItem)
{
if (id != todoItem.Id)
{
return BadRequest();
}
_context.Entry(todoItem).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!TodoItemExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
PutTodoItem
is similar to PostTodoItem
, except it uses HTTP PUT
. The response is 204 (No Content). According to the HTTP specification, a PUT
request requires the client to send the entire updated entity, not just the changes. To support partial updates, use HTTP PATCH.
This sample uses an in-memory database that must be initialized each time the app is started. There must be an item in the database before you make a PUT call. Call GET to ensure there's an item in the database before making a PUT call.
Use the PUT
method to update the TodoItem
that has Id = 1 and set its name to "feed fish"
. Note the response is HTTP 204 No Content
.
In Endpoints Explorer, right-click the PUT endpoint, and select Generate request.
The following content is added to the TodoApi.http
file:
PUT {{TodoApi_HostAddress}}/api/todoitems/{{id}}
Content-Type: application/json
{
//TodoItem
}
###
In the PUT request line, replace {{id}}
with 1
.
Replace the //TodoItem
placeholder with the following lines:
PUT {{TodoApi_HostAddress}}/api/todoitems/1
Content-Type: application/json
{
"id": 1,
"name": "feed fish",
"isComplete": false
}
Select the Send request link that is above the new PUT request line.
The PUT request is sent to the app and the response is displayed in the Response pane. The response body is empty, and the status code is 204.
Examine the DeleteTodoItem
method:
[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();
}
Use the DELETE
method to delete the TodoItem
that has Id = 1. Note the response is HTTP 204 No Content
.
In Endpoints Explorer, right-click the DELETE endpoint and select Generate request.
A DELETE request is added to TodoApi.http
.
Replace {{id}}
in the DELETE request line with 1
. The DELETE request should look like the following example:
DELETE {{TodoApi_HostAddress}}/api/todoitems/{{id}}
###
Select the Send request link for the DELETE request.
The DELETE request is sent to the app and the response is displayed in the Response pane. The response body is empty, and the status code is 204.
There are many other tools that can be used to test web APIs, for example:
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:
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
};
}
Verify you can't post or get the secret field.
See Tutorial: Call an ASP.NET Core web API with JavaScript.
See Video: Beginner's Series to: Web APIs.
For guidance on creating a reliable, secure, performant, testable, and scalable ASP.NET Core app, see Enterprise web app patterns. A complete production-quality sample web app that implements the patterns is available.
ASP.NET Core Identity adds user interface (UI) login functionality to ASP.NET Core web apps. To secure web APIs and SPAs, use one of the following:
Duende Identity Server is an OpenID Connect and OAuth 2.0 framework for ASP.NET Core. Duende Identity Server enables the following security features:
Important
Duende Software might require you to pay a license fee for production use of Duende Identity Server. For more information, see Migrate from ASP.NET Core 5.0 to 6.0.
For more information, see the Duende Identity Server documentation (Duende Software website).
For information on deploying to Azure, see Quickstart: Deploy an ASP.NET web app.
View or download sample code for this tutorial. See how to download.
For more information, see the following resources:
This tutorial teaches the basics of building a controller-based web API that uses a database. Another approach to creating APIs in ASP.NET Core is to create minimal APIs. For help with choosing between minimal APIs and controller-based APIs, see APIs overview. For a tutorial on creating a minimal API, see Tutorial: Create a minimal API with ASP.NET Core.
This tutorial creates 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.
Visual Studio 2022 with the ASP.NET and web development workload.
A NuGet package must be added to support the database used in this tutorial.
Microsoft.EntityFrameworkCore.InMemory
.Note
For guidance on adding packages to .NET apps, see the articles under Install and manage packages at Package consumption workflow (NuGet documentation). Confirm correct package versions at NuGet.org.
The project template creates a WeatherForecast
API with support for Swagger.
Press Ctrl+F5 to run without the debugger.
Visual Studio displays the following dialog when a project is not yet configured to use SSL:
Select Yes if you trust the IIS Express SSL certificate.
The following dialog is displayed:
Select Yes if you agree to trust the development certificate.
For information on trusting the Firefox browser, see Firefox SEC_ERROR_INADEQUATE_KEY_USAGE certificate error.
Visual Studio launches the default browser and navigates to https://localhost:<port>/swagger/index.html
, where <port>
is a randomly chosen port number set at the project creation.
The Swagger page /swagger/index.html
is displayed. Select GET > Try it out > Execute. The page displays:
If the Swagger page doesn't appear, see this GitHub issue.
Swagger is used to generate useful documentation and help pages for web APIs. This tutorial uses Swagger to test the app. For more information on Swagger, see ASP.NET Core web API documentation with Swagger / OpenAPI.
Copy and paste the Request URL in the browser: https://localhost:<port>/weatherforecast
JSON similar to the following example is returned:
[
{
"date": "2019-07-16T19:04:05.7257911-06:00",
"temperatureC": 52,
"temperatureF": 125,
"summary": "Mild"
},
{
"date": "2019-07-17T19:04:05.7258461-06:00",
"temperatureC": 36,
"temperatureF": 96,
"summary": "Warm"
},
{
"date": "2019-07-18T19:04:05.7258467-06:00",
"temperatureC": 39,
"temperatureF": 102,
"summary": "Cool"
},
{
"date": "2019-07-19T19:04:05.7258471-06:00",
"temperatureC": 10,
"temperatureF": 49,
"summary": "Bracing"
},
{
"date": "2019-07-20T19:04:05.7258474-06:00",
"temperatureC": -1,
"temperatureF": 31,
"summary": "Chilly"
}
]
A model is a set of classes that represent the data that the app manages. The model for this app is the TodoItem
class.
Models
.Models
folder and select Add > Class. Name the class TodoItem and select Add.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.
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.
Models
folder and select Add > Class. Name the class TodoContext and click Add.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!;
}
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 highlighted code:
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
The preceding code:
using
directives.Right-click the Controllers
folder.
Select Add > New Scaffolded Item.
Select API Controller with actions, using Entity Framework, and then select Add.
In the Add API Controller with actions, using Entity Framework dialog:
If the scaffolding operation fails, select Add to try scaffolding a second time.
The generated code:
[ApiController]
attribute. This attribute indicates that the controller responds to web API requests. For information about specific behaviors that the attribute enables, see Create web APIs with ASP.NET Core.TodoContext
) into the controller. The database context is used in each of the CRUD methods in the controller.The ASP.NET Core templates for:
[action]
in the route template.[action]
in the route template.When the [action]
token isn't in the route template, the action name (method name) isn't included in the endpoint. That is, the action's associated method name isn't used in the matching route.
Update the return statement in the PostTodoItem
to use the nameof operator:
[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 preceding code is an HTTP POST
method, as indicated by the [HttpPost]
attribute. The method gets the value of the TodoItem
from the body of the HTTP request.
For more information, see Attribute routing with Http[Verb] attributes.
The CreatedAtAction method:
HTTP 201
is the standard response for an HTTP POST
method that creates a new resource on the server.Location
header specifies the URI of the newly created to-do item. For more information, see 10.2.2 201 Created.GetTodoItem
action to create the Location
header's URI. The C# nameof
keyword is used to avoid hard-coding the action name in the CreatedAtAction
call.Press Ctrl+F5 to run the app.
In the Swagger browser window, select POST /api/TodoItems, and then select Try it out.
In the Request body input window, update the JSON. For example,
{
"name": "walk dog",
"isComplete": true
}
Select Execute
In the preceding POST, the Swagger UI shows the location header under Response headers. For example, location: https://localhost:7260/api/TodoItems/1
. The location header shows the URI to the created resource.
To test the location header:
In the Swagger browser window, select GET /api/TodoItems/{id}, and then select Try it out.
Enter 1
in the id
input box, and then select Execute.
Two GET endpoints are implemented:
GET /api/todoitems
GET /api/todoitems/{id}
The previous section showed an example of the /api/todoitems/{id}
route.
Follow the POST instructions to add another todo item, and then test the /api/todoitems
route using Swagger.
This app uses an in-memory database. If the app is stopped and started, the preceding GET request doesn't return any data. If no data is returned, POST data to the app.
The [HttpGet]
attribute denotes a method that responds to an HTTP GET
request. The URL path for each method is constructed as follows:
Start with the template string in the controller's Route
attribute:
[Route("api/[controller]")]
[ApiController]
public class TodoItemsController : ControllerBase
Replace [controller]
with the name of the controller, which by convention is the controller class name minus the "Controller" suffix. For this sample, the controller class name is TodoItemsController, so the controller name is "TodoItems". ASP.NET Core routing is case insensitive.
If the [HttpGet]
attribute has a route template (for example, [HttpGet("products")]
), append that to the path. This sample doesn't use a template. For more information, see Attribute routing with Http[Verb] attributes.
In the following GetTodoItem
method, "{id}"
is a placeholder variable for the unique identifier of the to-do item. When GetTodoItem
is invoked, the value of "{id}"
in the URL is provided to the method in its id
parameter.
[HttpGet("{id}")]
public async Task<ActionResult<TodoItem>> GetTodoItem(long id)
{
var todoItem = await _context.TodoItems.FindAsync(id);
if (todoItem == null)
{
return NotFound();
}
return todoItem;
}
The return type of the GetTodoItems
and GetTodoItem
methods is ActionResult<T> type. ASP.NET Core automatically serializes the object to JSON and writes the JSON into the body of the response message. The response code for this return type is 200 OK, assuming there are no unhandled exceptions. Unhandled exceptions are translated into 5xx errors.
ActionResult
return types can represent a wide range of HTTP status codes. For example, GetTodoItem
can return two different status values:
item
results in an HTTP 200
response.Examine the PutTodoItem
method:
[HttpPut("{id}")]
public async Task<IActionResult> PutTodoItem(long id, TodoItem todoItem)
{
if (id != todoItem.Id)
{
return BadRequest();
}
_context.Entry(todoItem).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!TodoItemExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
PutTodoItem
is similar to PostTodoItem
, except it uses HTTP PUT
. The response is 204 (No Content). According to the HTTP specification, a PUT
request requires the client to send the entire updated entity, not just the changes. To support partial updates, use HTTP PATCH.
This sample uses an in-memory database that must be initialized each time the app is started. There must be an item in the database before you make a PUT call. Call GET to ensure there's an item in the database before making a PUT call.
Using the Swagger UI, use the PUT button to update the TodoItem
that has Id = 1 and set its name to "feed fish"
. Note the response is HTTP 204 No Content
.
Examine the DeleteTodoItem
method:
[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();
}
Use the Swagger UI to delete the TodoItem
that has Id = 1. Note the response is HTTP 204 No Content
.
There are many other tools that can be used to test web APIs, for example:
curl
and shows the curl
commands it submits.For more information, see:
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:
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:
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
};
}
Verify you can't post or get the secret field.
See Tutorial: Call an ASP.NET Core web API with JavaScript.
See Video: Beginner's Series to: Web APIs.
For guidance on creating a reliable, secure, performant, testable, and scalable ASP.NET Core app, see Enterprise web app patterns. A complete production-quality sample web app that implements the patterns is available.
ASP.NET Core Identity adds user interface (UI) login functionality to ASP.NET Core web apps. To secure web APIs and SPAs, use one of the following:
Duende Identity Server is an OpenID Connect and OAuth 2.0 framework for ASP.NET Core. Duende Identity Server enables the following security features:
Important
Duende Software might require you to pay a license fee for production use of Duende Identity Server. For more information, see Migrate from ASP.NET Core 5.0 to 6.0.
For more information, see the Duende Identity Server documentation (Duende Software website).
For information on deploying to Azure, see Quickstart: Deploy an ASP.NET web app.
View or download sample code for this tutorial. See how to download.
For more information, see the following resources:
This tutorial teaches the basics of building a controller-based web API that uses a database. Another approach to creating APIs in ASP.NET Core is to create minimal APIs. For help with choosing between minimal APIs and controller-based APIs, see APIs overview. For a tutorial on creating a minimal API, see Tutorial: Create a minimal API with ASP.NET Core.
This tutorial creates 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.
Visual Studio 2022 with the ASP.NET and web development workload.
A NuGet package must be added to support the database used in this tutorial.
Microsoft.EntityFrameworkCore.InMemory
.Note
For guidance on adding packages to .NET apps, see the articles under Install and manage packages at Package consumption workflow (NuGet documentation). Confirm correct package versions at NuGet.org.
The project template creates a WeatherForecast
API with support for Swagger.
Press Ctrl+F5 to run without the debugger.
Visual Studio displays the following dialog when a project is not yet configured to use SSL:
Select Yes if you trust the IIS Express SSL certificate.
The following dialog is displayed:
Select Yes if you agree to trust the development certificate.
For information on trusting the Firefox browser, see Firefox SEC_ERROR_INADEQUATE_KEY_USAGE certificate error.
Visual Studio launches the default browser and navigates to https://localhost:<port>/swagger/index.html
, where <port>
is a randomly chosen port number set at the project creation.
The Swagger page /swagger/index.html
is displayed. Select GET > Try it out > Execute. The page displays:
If the Swagger page doesn't appear, see this GitHub issue.
Swagger is used to generate useful documentation and help pages for web APIs. This tutorial uses Swagger to test the app. For more information on Swagger, see ASP.NET Core web API documentation with Swagger / OpenAPI.
Copy and paste the Request URL in the browser: https://localhost:<port>/weatherforecast
JSON similar to the following example is returned:
[
{
"date": "2019-07-16T19:04:05.7257911-06:00",
"temperatureC": 52,
"temperatureF": 125,
"summary": "Mild"
},
{
"date": "2019-07-17T19:04:05.7258461-06:00",
"temperatureC": 36,
"temperatureF": 96,
"summary": "Warm"
},
{
"date": "2019-07-18T19:04:05.7258467-06:00",
"temperatureC": 39,
"temperatureF": 102,
"summary": "Cool"
},
{
"date": "2019-07-19T19:04:05.7258471-06:00",
"temperatureC": 10,
"temperatureF": 49,
"summary": "Bracing"
},
{
"date": "2019-07-20T19:04:05.7258474-06:00",
"temperatureC": -1,
"temperatureF": 31,
"summary": "Chilly"
}
]
A model is a set of classes that represent the data that the app manages. The model for this app is the TodoItem
class.
Models
.Models
folder and select Add > Class. Name the class TodoItem and select Add.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.
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.
Models
folder and select Add > Class. Name the class TodoContext and click Add.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!;
}
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 highlighted code:
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
The preceding code:
using
directives.Right-click the Controllers
folder.
Select Add > New Scaffolded Item.
Select API Controller with actions, using Entity Framework, and then select Add.
In the Add API Controller with actions, using Entity Framework dialog:
If the scaffolding operation fails, select Add to try scaffolding a second time.
The generated code:
[ApiController]
attribute. This attribute indicates that the controller responds to web API requests. For information about specific behaviors that the attribute enables, see Create web APIs with ASP.NET Core.TodoContext
) into the controller. The database context is used in each of the CRUD methods in the controller.The ASP.NET Core templates for:
[action]
in the route template.[action]
in the route template.When the [action]
token isn't in the route template, the action name (method name) isn't included in the endpoint. That is, the action's associated method name isn't used in the matching route.
Update the return statement in the PostTodoItem
to use the nameof operator:
[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 preceding code is an HTTP POST
method, as indicated by the [HttpPost]
attribute. The method gets the value of the TodoItem
from the body of the HTTP request.
For more information, see Attribute routing with Http[Verb] attributes.
The CreatedAtAction method:
HTTP 201
is the standard response for an HTTP POST
method that creates a new resource on the server.Location
header specifies the URI of the newly created to-do item. For more information, see 10.2.2 201 Created.GetTodoItem
action to create the Location
header's URI. The C# nameof
keyword is used to avoid hard-coding the action name in the CreatedAtAction
call.Press Ctrl+F5 to run the app.
In the Swagger browser window, select POST /api/TodoItems, and then select Try it out.
In the Request body input window, update the JSON. For example,
{
"name": "walk dog",
"isComplete": true
}
Select Execute
In the preceding POST, the Swagger UI shows the location header under Response headers. For example, location: https://localhost:7260/api/TodoItems/1
. The location header shows the URI to the created resource.
To test the location header:
In the Swagger browser window, select GET /api/TodoItems/{id}, and then select Try it out.
Enter 1
in the id
input box, and then select Execute.
Two GET endpoints are implemented:
GET /api/todoitems
GET /api/todoitems/{id}
The previous section showed an example of the /api/todoitems/{id}
route.
Follow the POST instructions to add another todo item, and then test the /api/todoitems
route using Swagger.
This app uses an in-memory database. If the app is stopped and started, the preceding GET request doesn't return any data. If no data is returned, POST data to the app.
The [HttpGet]
attribute denotes a method that responds to an HTTP GET
request. The URL path for each method is constructed as follows:
Start with the template string in the controller's Route
attribute:
[Route("api/[controller]")]
[ApiController]
public class TodoItemsController : ControllerBase
Replace [controller]
with the name of the controller, which by convention is the controller class name minus the "Controller" suffix. For this sample, the controller class name is TodoItemsController, so the controller name is "TodoItems". ASP.NET Core routing is case insensitive.
If the [HttpGet]
attribute has a route template (for example, [HttpGet("products")]
), append that to the path. This sample doesn't use a template. For more information, see Attribute routing with Http[Verb] attributes.
In the following GetTodoItem
method, "{id}"
is a placeholder variable for the unique identifier of the to-do item. When GetTodoItem
is invoked, the value of "{id}"
in the URL is provided to the method in its id
parameter.
[HttpGet("{id}")]
public async Task<ActionResult<TodoItem>> GetTodoItem(long id)
{
var todoItem = await _context.TodoItems.FindAsync(id);
if (todoItem == null)
{
return NotFound();
}
return todoItem;
}
The return type of the GetTodoItems
and GetTodoItem
methods is ActionResult<T> type. ASP.NET Core automatically serializes the object to JSON and writes the JSON into the body of the response message. The response code for this return type is 200 OK, assuming there are no unhandled exceptions. Unhandled exceptions are translated into 5xx errors.
ActionResult
return types can represent a wide range of HTTP status codes. For example, GetTodoItem
can return two different status values:
item
results in an HTTP 200
response.Examine the PutTodoItem
method:
[HttpPut("{id}")]
public async Task<IActionResult> PutTodoItem(long id, TodoItem todoItem)
{
if (id != todoItem.Id)
{
return BadRequest();
}
_context.Entry(todoItem).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!TodoItemExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
PutTodoItem
is similar to PostTodoItem
, except it uses HTTP PUT
. The response is 204 (No Content). According to the HTTP specification, a PUT
request requires the client to send the entire updated entity, not just the changes. To support partial updates, use HTTP PATCH.
This sample uses an in-memory database that must be initialized each time the app is started. There must be an item in the database before you make a PUT call. Call GET to ensure there's an item in the database before making a PUT call.
Using the Swagger UI, use the PUT button to update the TodoItem
that has Id = 1 and set its name to "feed fish"
. Note the response is HTTP 204 No Content
.
Examine the DeleteTodoItem
method:
[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();
}
Use the Swagger UI to delete the TodoItem
that has Id = 1. Note the response is HTTP 204 No Content
.
There are many other tools that can be used to test web APIs, for example:
curl
and shows the curl
commands it submits.For more information, see:
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:
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:
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
};
}
Verify you can't post or get the secret field.
See Tutorial: Call an ASP.NET Core web API with JavaScript.
See Video: Beginner's Series to: Web APIs.
For guidance on creating a reliable, secure, performant, testable, and scalable ASP.NET Core app, see Enterprise web app patterns. A complete production-quality sample web app that implements the patterns is available.
ASP.NET Core Identity adds user interface (UI) login functionality to ASP.NET Core web apps. To secure web APIs and SPAs, use one of the following:
Duende Identity Server is an OpenID Connect and OAuth 2.0 framework for ASP.NET Core. Duende Identity Server enables the following security features:
Important
Duende Software might require you to pay a license fee for production use of Duende Identity Server. For more information, see Migrate from ASP.NET Core 5.0 to 6.0.
For more information, see the Duende Identity Server documentation (Duende Software website).
For information on deploying to Azure, see Quickstart: Deploy an ASP.NET web app.
View or download sample code for this tutorial. See how to download.
For more information, see the following resources:
ASP.NET Core feedback
ASP.NET Core is an open source project. Select a link to provide feedback:
Training
Module
Create a web API with ASP.NET Core controllers - Training
Create a RESTful service with ASP.NET Core controllers that supports create, read, update, and delete (CRUD) operations.