Tutorial: creeu una API web amb ASP.NET Core

Nota

Aquesta no és l'última versió d'aquest article. Per a la versió actual, consulteu la versió .NET 8 d'aquest article .

Aquest tutorial ensenya els conceptes bàsics per crear una API web basada en controladors que utilitza una base de dades. Un altre enfocament per crear API a ASP.NET Core és crear API mínimes . Per obtenir ajuda per triar entre les API mínimes i les API basades en controladors, consulteu Visió general de les API . Per obtenir un tutorial sobre com crear una API mínima, vegeu Tutorial: crear una API mínima amb ASP.NET Core .

Visió general

Aquest tutorial crea la següent API:

API Descripció Cos de la sol·licitud Cos de resposta
GET /api/todoitems Obteniu tots els elements pendents Cap Matriu d'elements pendents
GET /api/todoitems/{id} Obteniu un article per ID Cap Element per fer
POST /api/todoitems Afegeix un element nou Element per fer Element per fer
PUT /api/todoitems/{id} Actualitza un element existent   Element per fer Cap
DELETE /api/todoitems/{id}     Suprimeix un element     Cap Cap

El diagrama següent mostra el disseny de l'aplicació.

El client està representat per una casella a l'esquerra.  Presenta una sol·licitud i rep una resposta de la sol·licitud, una casella dibuixada a la dreta.  Dins del quadre d'aplicació, tres quadres representen el controlador, el model i la capa d'accés a les dades.  La sol·licitud arriba al controlador de l'aplicació i es produeixen operacions de lectura/escriptura entre el controlador i la capa d'accés a les dades.  El model es serialitza i es retorna al client a la resposta.

Requisits previs

Crear un projecte web

  • Al menú Fitxer , seleccioneu Nou > Projecte .
  • Introduïu l'API web al quadre de cerca.
  • Seleccioneu la plantilla de l'API web ASP.NET Core i seleccioneu Següent .
  • Al quadre de diàleg Configura el teu projecte nou , anomena el projecte TodoApi i selecciona Següent .
  • Al diàleg Informació addicional :
    • Confirmeu que el marc és .NET 8.0 (suport a llarg termini) .
    • Confirmeu que la casella de selecció Utilitza controladors (desmarqueu per utilitzar API mínimes) estigui marcada.
    • Confirmeu que la casella de selecció Habilita el suport d'OpenAPI estigui marcada.
    • Seleccioneu Crear .

Afegiu un paquet NuGet

S'ha d'afegir un paquet NuGet per donar suport a la base de dades utilitzada en aquest tutorial.

  • Al menú Eines , seleccioneu Gestor de paquets NuGet > Gestiona paquets NuGet per a la solució .
  • Seleccioneu la pestanya Navega .
  • Seleccioneu la casella de selecció Inclou la versió prèvia .
  • Introduïu Microsoft.EntityFrameworkCore.InMemory al quadre de cerca i, a continuació, seleccioneu Microsoft.EntityFrameworkCore.InMemory.
  • Seleccioneu la casella Projecte al panell dret i, a continuació, seleccioneu Instal·la .

Nota

Per obtenir informació sobre com afegir paquets a les aplicacions .NET, consulteu els articles a Instal·lació i gestió de paquets a Flux de treball de consum de paquets (documentació de NuGet) . Confirmeu les versions correctes dels paquets a NuGet.org .

Prova el projecte

La plantilla del projecte crea una WeatherForecastAPI amb suport per a Swagger .

Premeu Ctrl+F5 per executar-se sense el depurador.

Visual Studio mostra el diàleg següent quan un projecte encara no està configurat per utilitzar SSL:

Aquest projecte està configurat per utilitzar SSL.  Per evitar avisos SSL al navegador, podeu optar per confiar en el certificat autofirmat que ha generat IIS Express.  Voleu confiar en el certificat SSL IIS Express?

Seleccioneu si confieu en el certificat SSL IIS Express.

Es mostra el següent diàleg:

Diàleg d'avís de seguretat

Seleccioneu si accepteu confiar en el certificat de desenvolupament.

Per obtenir informació sobre com confiar en el navegador Firefox, consulteu Error de certificat SEC_ERROR_INADEQUATE_KEY_USAGE de Firefox .

Visual Studio llança el navegador predeterminat i navega a https://localhost:<port>/swagger/index.html, on <port>hi ha un número de port escollit a l'atzar establert en la creació del projecte.

/swagger/index.htmlEs mostra la pàgina Swagger . Seleccioneu OBTENIR > Prova-ho > Executar . La pàgina mostra:

  • L' ordre Curl per provar l'API WeatherForecast.
  • L'URL per provar l'API WeatherForecast.
  • El codi de resposta, el cos i les capçaleres.
  • Un quadre de llista desplegable amb tipus de suports i el valor i l'esquema d'exemple.

Si la pàgina Swagger no apareix, consulteu aquest problema de GitHub .

Swagger s'utilitza per generar documentació útil i pàgines d'ajuda per a les API web. Aquest tutorial utilitza Swagger per provar l'aplicació. Per obtenir més informació sobre Swagger, consulteu la documentació de l'API web ASP.NET Core amb Swagger/OpenAPI .

Copieu i enganxeu l' URL de sol·licitud al navegador: https://localhost:<port>/weatherforecast

Es retorna un JSON semblant a l'exemple següent:

JSON
[
    {
        "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"
    }
]

Afegeix una classe de model

Un model és un conjunt de classes que representen les dades que gestiona l'aplicació. El model d'aquesta aplicació és la TodoItemclasse.

  • A l'Explorador de solucions , feu clic amb el botó dret al projecte. Seleccioneu Afegeix > Carpeta nova . Anomena la carpeta Models.
  • Feu clic amb el botó dret a la Modelscarpeta i seleccioneu Afegeix > Classe . Anomena la classe TodoItem i selecciona Afegeix .
  • Substituïu el codi de la plantilla pel següent:
C#
namespace TodoApi.Models;

public class TodoItem
{
    public long Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

La Idpropietat funciona com a clau única en una base de dades relacional.

Les classes model poden anar a qualsevol part del projecte, però la Modelscarpeta s'utilitza per convenció.

Afegiu un context de base de dades

El context de la base de dades és la classe principal que coordina la funcionalitat de l'Entity Framework per a un model de dades. Aquesta classe es crea derivant de la classe Microsoft.EntityFrameworkCore.DbContext .

  • Feu clic amb el botó dret a la Modelscarpeta i seleccioneu Afegeix > Classe . Anomeneu la classe TodoContext i feu clic a Afegeix .
  • Introduïu el codi següent:

    C#
    using Microsoft.EntityFrameworkCore;
    
    namespace TodoApi.Models;
    
    public class TodoContext : DbContext
    {
        public TodoContext(DbContextOptions<TodoContext> options)
            : base(options)
        {
        }
    
        public DbSet<TodoItem> TodoItems { get; set; } = null!;
    }
    

Registreu el context de la base de dades

A ASP.NET Core, els serveis com el context de la base de dades s'han de registrar amb el contenidor d'injecció de dependències (DI) . El contenidor proporciona el servei als controladors.

Actualitzeu Program.csamb el següent codi destacat:

C#
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();

El codi anterior:

  • Afegeix usingdirectives.
  • Afegeix el context de la base de dades al contenidor DI.
  • Especifica que el context de la base de dades utilitzarà una base de dades en memòria.

Bastida un controlador

  • Feu clic amb el botó dret a la carpeta Controladors .

  • Seleccioneu Afegeix > Nou element de bastida .

  • Seleccioneu Controlador d'API amb accions mitjançant Entity Framework i, a continuació, seleccioneu Afegeix .

  • Al quadre de diàleg Afegeix un controlador d'API amb accions, utilitzant el quadre de diàleg Entity Framework:

    • Seleccioneu TodoItem (TodoApi.Models) a la classe Model .
    • Seleccioneu TodoContext (TodoApi.Models) a la classe de context de dades .
    • Seleccioneu Afegeix .

    Si l'operació de bastida falla, seleccioneu Afegeix per provar la bastida una segona vegada.

El codi generat:

  • Marca la classe amb l' [ApiController]atribut. Aquest atribut indica que el controlador respon a les sol·licituds de l'API web. Per obtenir informació sobre comportaments específics que activa l'atribut, vegeu Crear API web amb ASP.NET Core .
  • Utilitza DI per injectar el context de la base de dades ( TodoContext) al controlador. El context de la base de dades s'utilitza en cadascun dels mètodes CRUD del controlador.

Les plantilles ASP.NET Core per a:

  • Els controladors amb vistes inclouen [action]a la plantilla de ruta.
  • Els controladors de l'API no s'inclouen [action]a la plantilla de ruta.

Quan el [action]testimoni no es troba a la plantilla de ruta, el nom de l'acció (nom del mètode) no s'inclou al punt final. És a dir, el nom del mètode associat a l'acció no s'utilitza a la ruta coincident.

Actualitzeu el mètode de creació de PostTodoItem

Actualitzeu la declaració de retorn a la PostTodoItemper utilitzar l' operador nameof :

C#
[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);
}

El codi anterior és un HTTP POSTmètode, tal com indica l' [HttpPost]atribut. El mètode obté el valor del TodoItemcos de la sol·licitud HTTP.

Per obtenir més informació, vegeu Encaminament d'atributs amb atributs Http[Verb] .

El mètode CreatedAtAction :

  • Retorna un codi d'estat HTTP 201 si té èxit. HTTP 201és la resposta estàndard per a un HTTP POSTmètode que crea un recurs nou al servidor.
  • Afegeix una capçalera Ubicació a la resposta. La Locationcapçalera especifica l' URI de l'element de tasca que s'acaba de crear. Per obtenir més informació, vegeu 10.2.2 201 Creat .
  • Fa referència a l' GetTodoItemacció per crear l' LocationURI de la capçalera. La paraula clau C# nameofs'utilitza per evitar codificar el nom de l'acció a la CreatedAtActiontrucada.

Prova PostTodoItem

  • Premeu Ctrl+F5 per executar l'aplicació.

  • A la finestra del navegador Swagger, seleccioneu POST /api/TodoItems i, a continuació, seleccioneu Prova-ho .

  • A la finestra d'entrada del cos de la sol·licitud , actualitzeu el JSON. Per exemple,

    JSON
    {
      "name": "walk dog",
      "isComplete": true
    }
    
  • Seleccioneu Executar

    Swagger POST

Proveu l'URI de la capçalera de la ubicació

A la publicació POST anterior, la interfície d'usuari de Swagger mostra la capçalera d'ubicació sota Capçaleres de resposta . Per exemple, location: https://localhost:7260/api/TodoItems/1. La capçalera de la ubicació mostra l'URI del recurs creat.

Per provar la capçalera de la ubicació:

  • A la finestra del navegador Swagger, seleccioneu GET /api/TodoItems/{id} i, a continuació, seleccioneu Prova-ho .

  • Introduïu 1al idquadre d'entrada i, a continuació, seleccioneu Executar .

    Swagger GET

Examineu els mètodes GET

S'implementen dos punts finals GET:

  • GET /api/todoitems
  • GET /api/todoitems/{id}

L'apartat anterior mostrava un exemple de la /api/todoitems/{id}ruta.

Seguiu les instruccions POST per afegir un altre ítem i, a continuació, proveu la /api/todoitemsruta amb Swagger.

Aquesta aplicació utilitza una base de dades a la memòria. Si l'aplicació s'atura i s'inicia, la sol·licitud GET anterior no retornarà cap dada. Si no es retornen dades, POST les dades a l'aplicació.

Rutes d'enrutament i URL

L' [HttpGet]atribut denota un mètode que respon a una HTTP GETsol·licitud. El camí de l'URL per a cada mètode es construeix de la següent manera:

  • Comenceu amb la cadena de plantilla a l'atribut del controlador Route:

    C#
    [Route("api/[controller]")]
    [ApiController]
    public class TodoItemsController : ControllerBase
    
  • Substituïu [controller]-lo pel nom del controlador, que per convenció és el nom de la classe del controlador menys el sufix "Controlador". Per a aquesta mostra, el nom de la classe del controlador és Controlador TodoItems , de manera que el nom del controlador és "TodoItems". L'encaminament ASP.NET Core no distingeix entre majúscules i minúscules.

  • Si l' [HttpGet]atribut té una plantilla de ruta (per exemple, [HttpGet("products")]), afegiu-la a la ruta. Aquesta mostra no utilitza cap plantilla. Per obtenir més informació, vegeu Encaminament d'atributs amb atributs Http[Verb] .

En el GetTodoItemmètode següent, "{id}"és una variable de marcador de posició per a l'identificador únic de l'element pendent. Quan GetTodoItems'invoca, el valor de "{id}"a l'URL es proporciona al mètode en el seu idparàmetre.

C#
[HttpGet("{id}")]
public async Task<ActionResult<TodoItem>> GetTodoItem(long id)
{
    var todoItem = await _context.TodoItems.FindAsync(id);

    if (todoItem == null)
    {
        return NotFound();
    }

    return todoItem;
}

Valors de retorn

El tipus de retorn dels mètodes GetTodoItemsi és el tipus ActionResult<T> . ASP.NET Core serialitza automàticament l'objecte a JSON i escriu el JSON al cos del missatge de resposta. El codi de resposta per a aquest tipus de retorn és 200 OK , suposant que no hi ha excepcions no gestionades. Les excepcions no gestionades es tradueixen en errors 5xx.GetTodoItem

ActionResultEls tipus de retorn poden representar una àmplia gamma de codis d'estat HTTP. Per exemple, GetTodoItempot retornar dos valors d'estat diferents:

  • Si cap element coincideix amb l'ID sol·licitat, el mètode retorna un codi d'error d'estat 404 No trobat .
  • En cas contrari, el mètode retorna 200 amb un cos de resposta JSON. El retorn itemresulta en una HTTP 200resposta.

El mètode PutTodoItem

Examineu el PutTodoItemmètode:

C#
[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és similar a PostTodoItem, excepte que utilitza HTTP PUT. La resposta és 204 (Sense contingut) . Segons l'especificació HTTP, una PUTsol·licitud requereix que el client enviï tota l'entitat actualitzada, no només els canvis. Per admetre actualitzacions parcials, utilitzeu HTTP PATCH .

Proveu el mètode PutTodoItem

Aquesta mostra utilitza una base de dades a la memòria que s'ha d'inicialitzar cada vegada que s'inicia l'aplicació. Hi ha d'haver un element a la base de dades abans de fer una trucada PUT. Truqueu a GET per assegurar-vos que hi ha un element a la base de dades abans de fer una trucada PUT.

Amb la interfície d'usuari de Swagger, utilitzeu el botó PUT per actualitzar el TodoItemque té Id = 1 i establir-ne el nom a "feed fish". Tingueu en compte que la resposta és HTTP 204 No Content.

El mètode DeleteTodoItem

Examineu el DeleteTodoItemmètode:

C#
[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();
}

Proveu el mètode DeleteTodoItem

Utilitzeu la interfície d'usuari de Swagger per suprimir el TodoItemque té Id = 1. Tingueu en compte que la resposta és HTTP 204 No Content.

Prova amb altres eines

Hi ha moltes altres eines que es poden utilitzar per provar les API web, per exemple:

Per a més informació, consulteu:

Evita la publicació excessiva

Actualment, l'aplicació de mostra exposa tot l' TodoItemobjecte. Les aplicacions de producció solen limitar les dades que s'introdueixen i es retornen mitjançant un subconjunt del model. Hi ha diverses raons darrere d'això, i la seguretat és una de les principals. El subconjunt d'un model es coneix normalment com a objecte de transferència de dades (DTO), model d'entrada o model de vista. DTO s'utilitza en aquest tutorial.

Un DTO es pot utilitzar per:

  • Evita la publicació excessiva.
  • Amaga les propietats que els clients no han de veure.
  • Omet algunes propietats per reduir la mida de la càrrega útil.
  • Aplaneu els gràfics d'objectes que contenen objectes imbricats. Els gràfics d'objectes aplanats poden ser més convenients per als clients.

Per demostrar l'enfocament DTO, actualitzeu la TodoItemclasse per incloure un camp secret:

C#
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; }
    }
}

El camp secret s'ha d'ocultar d'aquesta aplicació, però una aplicació administrativa podria optar per exposar-lo.

Verifiqueu que podeu publicar i obtenir el camp secret.

Creeu un model DTO:

C#
namespace TodoApi.Models;

public class TodoItemDTO
{
    public long Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

Actualitzeu TodoItemsControllerper utilitzar TodoItemDTO:

C#
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
       };
}

Verifiqueu que no podeu publicar ni obtenir el camp secret.

Truqueu a l'API web amb JavaScript

Vegeu el tutorial: crida a una API web ASP.NET Core amb JavaScript .

Sèrie de vídeos de l'API web

Vegeu Vídeo: Sèrie per a principiants a: API web .

Patrons d'aplicacions web fiables

Consulteu els vídeos i l'article de The Reliable Web App Pattern for.NET de YouTube per obtenir orientació sobre com crear una aplicació ASP.NET Core moderna, fiable, rendible, provable, rendible i escalable, ja sigui des de zero o refactoritzant una aplicació existent.

Afegiu suport d'autenticació a una API web

ASP.NET Core Identity afegeix la funcionalitat d'inici de sessió de la interfície d'usuari (UI) a les aplicacions web ASP.NET Core. Per protegir les API web i els SPA, utilitzeu una de les opcions següents:

Duende Identity Server és un marc OpenID Connect i OAuth 2.0 per a ASP.NET Core. Duende Identity Server habilita les funcions de seguretat següents:

  • Autenticació com a servei (AaaS)
  • Inici/desactivació únic (SSO) en diversos tipus d'aplicacions
  • Control d'accés a les API
  • Portal de la Federació

Important

Duende Software pot requerir que pagueu una tarifa de llicència per a l'ús de producció del Duende Identity Server. Per obtenir més informació, vegeu Migrar d'ASP.NET Core 5.0 a 6.0 .

Per obtenir més informació, consulteu la documentació del Duende Identity Server (lloc web de Duende Software) .

Publicar a Azure

Per obtenir informació sobre la implementació a Azure, vegeu Inici ràpid: implementar una aplicació web ASP.NET .

Recursos addicionals

Consulteu o descarregueu el codi de mostra d'aquest tutorial . Vegeu com descarregar .

Per a més informació, consulteu els recursos següents:

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 in 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.

Overview

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.

The client is represented by a box on the left. It submits a request and receives a response from the application, a box drawn on the right. Within the application box, three boxes represent the controller, the model, and the data access layer. The request comes into the application's controller, and read/write operations occur between the controller and the data access layer. The model is serialized and returned to the client in the response.

Prerequisites

Create a web project

  • From the File menu, select New > Project.
  • Enter Web API in the search box.
  • Select the ASP.NET Core Web API template and select Next.
  • In the Configure your new project dialog, name the project TodoApi and select Next.
  • In the Additional information dialog:
    • Confirm the Framework is .NET 7.0 (or later).
    • Confirm the checkbox for Use controllers(uncheck to use minimal APIs) is checked.
    • Select Create.

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.

Test the project

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:

This project is configured to use SSL. To avoid SSL warnings in the browser you can choose to trust the self-signed certificate that IIS Express has generated. Would you like to trust the IIS Express SSL certificate?

Select Yes if you trust the IIS Express SSL certificate.

The following dialog is displayed:

Security warning dialog

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.

The Swagger page /swagger/index.html is displayed. Select GET > Try it out > Execute. The page displays:

  • The Curl command to test the WeatherForecast API.
  • The URL to test the WeatherForecast API.
  • The response code, body, and headers.
  • A drop-down list box with media types and the example value and schema.

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 focuses on creating a web API. 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:

JSON
[
    {
        "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"
    }
]

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.

  • In Solution Explorer, right-click the project. Select Add > New Folder. Name the folder Models.
  • Right-click the Models folder and select Add > Class. Name the class TodoItem and select Add.
  • Replace the template code with the following:
C#
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 NuGet packages

  • From the Tools menu, select NuGet Package Manager > Manage NuGet Packages for Solution.
  • Select the Browse tab, and then enter Microsoft.EntityFrameworkCore.InMemory in the search box.
  • Select Microsoft.EntityFrameworkCore.InMemory in the left pane.
  • Select the Project checkbox in the right pane and then select Install.

Add the TodoContext database context

  • Right-click the Models folder and select Add > Class. Name the class TodoContext and click Add.
  • Enter the following code:

    C#
    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 highlighted code:

C#
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:

  • 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

  • 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:

    • Select TodoItem (TodoApi.Models) in the Model class.
    • Select TodoContext (TodoApi.Models) in the Data context class.
    • Select Add.

    If the scaffolding operation fails, select Add to try scaffolding a second time.

The generated code:

  • Marks the class with the [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.
  • Uses DI to inject the database context (TodoContext) into the controller. The database context is used in each of the CRUD methods in the controller.

The ASP.NET Core templates for:

  • Controllers with views include [action] in the route template.
  • API controllers don't include [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 PostTodoItem create method

Update the return statement in the PostTodoItem to use the nameof operator:

C#
[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:

  • Returns an HTTP 201 status code if successful. HTTP 201 is the standard response for an HTTP POST method that creates a new resource on the server.
  • Adds a Location header to the response. The Location header specifies the URI of the newly created to-do item. For more information, see 10.2.2 201 Created.
  • References the 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.

Test PostTodoItem

  • 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,

    JSON
    {
      "name": "walk dog",
      "isComplete": true
    }
    
  • Select Execute

    Swagger POST

Test the location header URI

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.

    Swagger GET

Examine the GET methods

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 will not return any data. If no data is returned, POST data to the app.

Routing and URL paths

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:

    C#
    [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.

C#
[HttpGet("{id}")]
public async Task<ActionResult<TodoItem>> GetTodoItem(long id)
{
    var todoItem = await _context.TodoItems.FindAsync(id);

    if (todoItem == null)
    {
        return NotFound();
    }

    return todoItem;
}

Return values

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:

  • If no item matches the requested ID, the method returns a 404 status NotFound error code.
  • Otherwise, the method returns 200 with a JSON response body. Returning item results in an HTTP 200 response.

The PutTodoItem method

Examine the PutTodoItem method:

C#
[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.

Test the PutTodoItem method

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.

The DeleteTodoItem method

Examine the DeleteTodoItem method:

C#
[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();
}

Test the DeleteTodoItem method

Use the Swagger UI to delete the TodoItem that has Id = 1. Note the response is HTTP 204 No Content.

Test with http-repl, Postman, or curl

http-repl, Postman, and curl are often used to test API's. Swagger uses curl and shows the curl command it submitted.

For instructions on these tools, see the following links:

For more information on http-repl, see Test web APIs with the HttpRepl.

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:

C#
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:

C#
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:

C#
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.

Call the web API with JavaScript

See Tutorial: Call an ASP.NET Core web API with JavaScript.

Web API video series

See Video: Beginner's Series to: Web APIs.

Reliable web app patterns

See The Reliable Web App Pattern for.NET YouTube videos and article for guidance on creating a modern, reliable, performant, testable, cost-efficient, and scalable ASP.NET Core app, whether from scratch or refactoring an existing app.

Add authentication support to a web API

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:

  • Authentication as a Service (AaaS)
  • Single sign-on/off (SSO) over multiple application types
  • Access control for APIs
  • Federation Gateway

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).

Publish to Azure

For information on deploying to Azure, see Quickstart: Deploy an ASP.NET web app.

Additional resources

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 in 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.

In this tutorial, you learn how to:

  • Create a web API project.
  • Add a model class and a database context.
  • Scaffold a controller with CRUD methods.
  • Configure routing, URL paths, and return values.
  • Call the web API with http-repl.

At the end, you have a web API that can manage "to-do" items stored in a database.

Overview

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.

The client is represented by a box on the left. It submits a request and receives a response from the application, a box drawn on the right. Within the application box, three boxes represent the controller, the model, and the data access layer. The request comes into the application's controller, and read/write operations occur between the controller and the data access layer. The model is serialized and returned to the client in the response.

Prerequisites

Create a web project

  • From the File menu, select New > Project.
  • Enter Web API in the search box.
  • Select the ASP.NET Core Web API template and select Next.
  • In the Configure your new project dialog, name the project TodoApi and select Next.
  • In the Additional information dialog:
    • Confirm the Framework is .NET 6.0 (Long-term support).
    • Confirm the checkbox for Use controllers(uncheck to use minimal APIs) is checked.
    • Select Create.

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.

Test the project

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:

This project is configured to use SSL. To avoid SSL warnings in the browser you can choose to trust the self-signed certificate that IIS Express has generated. Would you like to trust the IIS Express SSL certificate?

Select Yes if you trust the IIS Express SSL certificate.

The following dialog is displayed:

Security warning dialog

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.

The Swagger page /swagger/index.html is displayed. Select GET > Try it out > Execute. The page displays:

  • The Curl command to test the WeatherForecast API.
  • The URL to test the WeatherForecast API.
  • The response code, body, and headers.
  • A drop-down list box with media types and the example value and schema.

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 focuses on creating a web API. 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:

JSON
[
    {
        "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"
    }
]

Update the launchUrl

In Properties\launchSettings.json, update launchUrl from "swagger" to "api/todoitems":

JSON
"launchUrl": "api/todoitems",

Because Swagger will be removed, the preceding markup changes the URL that is launched to the GET method of the controller added in the following sections.

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 a single TodoItem class.

  • In Solution Explorer, right-click the project. Select Add > New Folder. Name the folder Models.

  • Right-click the Models folder and select Add > Class. Name the class TodoItem and select Add.

  • Replace the template code with the following:

C#
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 NuGet packages

  • From the Tools menu, select NuGet Package Manager > Manage NuGet Packages for Solution.
  • Select the Browse tab, and then enter Microsoft.EntityFrameworkCore.InMemory in the search box.
  • Select Microsoft.EntityFrameworkCore.InMemory in the left pane.
  • Select the Project checkbox in the right pane and then select Install.

Add the TodoContext database context

  • Right-click the Models folder and select Add > Class. Name the class TodoContext and click Add.
  • Enter the following code:

    C#
    using Microsoft.EntityFrameworkCore;
    using System.Diagnostics.CodeAnalysis;
    
    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:

C#
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;


var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
builder.Services.AddDbContext<TodoContext>(opt =>
    opt.UseInMemoryDatabase("TodoList"));
//builder.Services.AddSwaggerGen(c =>
//{
//    c.SwaggerDoc("v1", new() { Title = "TodoApi", Version = "v1" });
//});

var app = builder.Build();

// Configure the HTTP request pipeline.
if (builder.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
    //app.UseSwagger();
    //app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "TodoApi v1"));
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

The preceding code:

  • Removes the Swagger calls.
  • Removes unused using directives.
  • Adds the database context to the DI container.
  • Specifies that the database context will use an in-memory database.

Scaffold a controller

  • 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:

    • Select TodoItem (TodoApi.Models) in the Model class.
    • Select TodoContext (TodoApi.Models) in the Data context class.
    • Select Add.

    If the scaffolding operation fails, select Add to try scaffolding a second time.

The generated code:

  • Marks the class with the [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.
  • Uses DI to inject the database context (TodoContext) into the controller. The database context is used in each of the CRUD methods in the controller.

The ASP.NET Core templates for:

  • Controllers with views include [action] in the route template.
  • API controllers don't include [action] in the route template.

When the [action] token isn't in the route template, the action name is excluded from the route. That is, the action's associated method name isn't used in the matching route.

Update the PostTodoItem create method

Update the return statement in the PostTodoItem to use the nameof operator:

C#
[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 to-do item from the body of the HTTP request.

For more information, see Attribute routing with Http[Verb] attributes.

The CreatedAtAction method:

  • Returns an HTTP 201 status code if successful. HTTP 201 is the standard response for an HTTP POST method that creates a new resource on the server.
  • Adds a Location header to the response. The Location header specifies the URI of the newly created to-do item. For more information, see 10.2.2 201 Created.
  • References the 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.

Install http-repl

This tutorial uses http-repl to test the web API.

  • Run the following command at a command prompt:

    .NET CLI
    dotnet tool install -g Microsoft.dotnet-httprepl
    

    Note

    By default the architecture of the .NET binaries to install represents the currently running OS architecture. To specify a different OS architecture, see dotnet tool install, --arch option. For more information, see GitHub issue dotnet/AspNetCore.Docs #29262.

  • If you don't have the .NET 6.0 SDK or runtime installed, install the .NET 6.0 runtime.

Test PostTodoItem

  • Press Ctrl+F5 to run the app.

  • Open a new terminal window, and run the following commands. If your app uses a different port number, replace 5001 in the httprepl command with your port number.

    .NET CLI
    httprepl https://localhost:5001/api/todoitems
    post -h Content-Type=application/json -c "{"name":"walk dog","isComplete":true}"
    

    Here's an example of the output from the command:

    Output
    HTTP/1.1 201 Created
    Content-Type: application/json; charset=utf-8
    Date: Tue, 07 Sep 2021 20:39:47 GMT
    Location: https://localhost:5001/api/TodoItems/1
    Server: Kestrel
    Transfer-Encoding: chunked
    
    {
      "id": 1,
      "name": "walk dog",
      "isComplete": true
    }
    

Test the location header URI

To test the location header, copy and paste it into an httprepl get command.

The following example assumes that you're still in an httprepl session. If you ended the previous httprepl session, replace connect with httprepl in the following commands:

.NET CLI
connect https://localhost:5001/api/todoitems/1
get

Here's an example of the output from the command:

Output
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 07 Sep 2021 20:48:10 GMT
Server: Kestrel
Transfer-Encoding: chunked

{
  "id": 1,
  "name": "walk dog",
  "isComplete": true
}

Examine the GET methods

Two GET endpoints are implemented:

  • GET /api/todoitems
  • GET /api/todoitems/{id}

You just saw an example of the /api/todoitems/{id} route. Test the /api/todoitems route:

.NET CLI
connect https://localhost:5001/api/todoitems
get

Here's an example of the output from the command:

Output
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 07 Sep 2021 20:59:21 GMT
Server: Kestrel
Transfer-Encoding: chunked

[
  {
    "id": 1,
    "name": "walk dog",
    "isComplete": true
  }
]

This time, the JSON returned is an array of one item.

This app uses an in-memory database. If the app is stopped and started, the preceding GET request will not return any data. If no data is returned, POST data to the app.

Routing and URL paths

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:

    C#
    [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.

C#
[HttpGet("{id}")]
public async Task<ActionResult<TodoItem>> GetTodoItem(long id)
{
    var todoItem = await _context.TodoItems.FindAsync(id);

    if (todoItem == null)
    {
        return NotFound();
    }

    return todoItem;
}

Return values

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:

  • If no item matches the requested ID, the method returns a 404 status NotFound error code.
  • Otherwise, the method returns 200 with a JSON response body. Returning item results in an HTTP 200 response.

The PutTodoItem method

Examine the PutTodoItem method:

C#
[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.

If you get an error calling PutTodoItem in the following section, call GET to ensure there's an item in the database.

Test the PutTodoItem method

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.

Update the to-do item that has Id = 1 and set its name to "feed fish":

.NET CLI
connect https://localhost:5001/api/todoitems/1
put -h Content-Type=application/json -c "{"id":1,"name":"feed fish","isComplete":true}"

Here's an example of the output from the command:

Output
HTTP/1.1 204 No Content
Date: Tue, 07 Sep 2021 21:20:47 GMT
Server: Kestrel

The DeleteTodoItem method

Examine the DeleteTodoItem method:

C#
[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();
}

Test the DeleteTodoItem method

Delete the to-do item that has Id = 1:

.NET CLI
connect https://localhost:5001/api/todoitems/1
delete

Here's an example of the output from the command:

Output
HTTP/1.1 204 No Content
Date: Tue, 07 Sep 2021 21:43:00 GMT
Server: Kestrel

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:

C#
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:

C#
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:

C#
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
        [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);
        }
        // PUT: api/TodoItems/5
        // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
        [HttpPut("{id}")]
        public async Task<IActionResult> UpdateTodoItem(long id, TodoItemDTO todoItemDTO)
        {
            if (id != todoItemDTO.Id)
            {
                return BadRequest();
            }

            var todoItem = await _context.TodoItems.FindAsync(id);
            if (todoItem == null)
            {
                return NotFound();
            }

            todoItem.Name = todoItemDTO.Name;
            todoItem.IsComplete = todoItemDTO.IsComplete;

            try
            {
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException) when (!TodoItemExists(id))
            {
                return NotFound();
            }

            return NoContent();
        }
        // POST: api/TodoItems
        // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
        [HttpPost]
        public async Task<ActionResult<TodoItemDTO>> CreateTodoItem(TodoItemDTO todoItemDTO)
        {
            var todoItem = new TodoItem
            {
                IsComplete = todoItemDTO.IsComplete,
                Name = todoItemDTO.Name
            };

            _context.TodoItems.Add(todoItem);
            await _context.SaveChangesAsync();

            return CreatedAtAction(
                nameof(GetTodoItem),
                new { id = todoItem.Id },
                ItemToDTO(todoItem));
        }

        // 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.

Call the web API with JavaScript

See Tutorial: Call an ASP.NET Core web API with JavaScript.

Web API video series

See Video: Beginner's Series to: Web APIs.

Add authentication support to a web API

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:

  • Authentication as a Service (AaaS)
  • Single sign-on/off (SSO) over multiple application types
  • Access control for APIs
  • Federation Gateway

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).

Publish to Azure

For information on deploying to Azure, see Quickstart: Deploy an ASP.NET web app.

Additional resources

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 in 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.

In this tutorial, you learn how to:

  • Create a web API project.
  • Add a model class and a database context.
  • Scaffold a controller with CRUD methods.
  • Configure routing, URL paths, and return values.
  • Call the web API with Postman.

At the end, you have a web API that can manage "to-do" items stored in a database.

Overview

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.

The client is represented by a box on the left. It submits a request and receives a response from the application, a box drawn on the right. Within the application box, three boxes represent the controller, the model, and the data access layer. The request comes into the application's controller, and read/write operations occur between the controller and the data access layer. The model is serialized and returned to the client in the response.

Prerequisites

Create a web project

  • From the File menu, select New > Project.
  • Select the ASP.NET Core Web API template and click Next.
  • Name the project TodoApi and click Create.
  • In the Create a new ASP.NET Core Web Application dialog, confirm that .NET Core and ASP.NET Core 5.0 are selected. Select the API template and click Create.

VS new project dialog

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.

Test the project

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:

This project is configured to use SSL. To avoid SSL warnings in the browser you can choose to trust the self-signed certificate that IIS Express has generated. Would you like to trust the IIS Express SSL certificate?

Select Yes if you trust the IIS Express SSL certificate.

The following dialog is displayed:

Security warning dialog

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 IIS Express web server.
  • The default browser and navigates to https://localhost:<port>/swagger/index.html, where <port> is a randomly chosen port number.

The Swagger page /swagger/index.html is displayed. Select GET > Try it out > Execute. The page displays:

  • The Curl command to test the WeatherForecast API.
  • The URL to test the WeatherForecast API.
  • The response code, body, and headers.
  • A drop down list box with media types and the example value and schema.

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 focuses on creating a web API. 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 is returned:

JSON
[
    {
        "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"
    }
]

Update the launchUrl

In Properties\launchSettings.json, update launchUrl from "swagger" to "api/todoitems":

JSON
"launchUrl": "api/todoitems",

Because Swagger will be removed, the preceding markup changes the URL that is launched to the GET method of the controller added in the following sections.

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 a single TodoItem class.

  • In Solution Explorer, right-click the project. Select Add > New Folder. Name the folder Models.

  • Right-click the Models folder and select Add > Class. Name the class TodoItem and select Add.

  • Replace the template code with the following:

C#
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 NuGet packages

  • From the Tools menu, select NuGet Package Manager > Manage NuGet Packages for Solution.
  • Select the Browse tab, and then enter Microsoft.EntityFrameworkCore.InMemory in the search box.
  • Select Microsoft.EntityFrameworkCore.InMemory in the left pane.
  • Select the Project checkbox in the right pane and then select Install.

NuGet Package Manager

Add the TodoContext database context

  • Right-click the Models folder and select Add > Class. Name the class TodoContext and click Add.
  • Enter the following code:

    C#
    using Microsoft.EntityFrameworkCore;
    
    namespace TodoApi.Models
    {
        public class TodoContext : DbContext
        {
            public TodoContext(DbContextOptions<TodoContext> options)
                : base(options)
            {
            }
    
            public DbSet<TodoItem> TodoItems { get; set; }
        }
    }
    

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 Startup.cs with the following code:

C#
// Unused usings removed
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

namespace TodoApi
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();

            services.AddDbContext<TodoContext>(opt =>
                                               opt.UseInMemoryDatabase("TodoList"));
            //services.AddSwaggerGen(c =>
            //{
            //    c.SwaggerDoc("v1", new OpenApiInfo { Title = "TodoApi", Version = "v1" });
            //});
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                //app.UseSwagger();
                //app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "TodoApi v1"));
            }

            app.UseHttpsRedirection();
            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}

The preceding code:

  • Removes the Swagger calls.
  • Removes unused using declarations.
  • Adds the database context to the DI container.
  • Specifies that the database context will use an in-memory database.

Scaffold a controller

  • 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:

    • Select TodoItem (TodoApi.Models) in the Model class.
    • Select TodoContext (TodoApi.Models) in the Data context class.
    • Select Add.

The generated code:

  • Marks the class with the [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.
  • Uses DI to inject the database context (TodoContext) into the controller. The database context is used in each of the CRUD methods in the controller.

The ASP.NET Core templates for:

  • Controllers with views include [action] in the route template.
  • API controllers don't include [action] in the route template.

When the [action] token isn't in the route template, the action name is excluded from the route. That is, the action's associated method name isn't used in the matching route.

Update the PostTodoItem create method

Update the return statement in the PostTodoItem to use the nameof operator:

C#
// POST: api/TodoItems
[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 to-do item from the body of the HTTP request.

For more information, see Attribute routing with Http[Verb] attributes.

The CreatedAtAction method:

  • Returns an HTTP 201 status code if successful. HTTP 201 is the standard response for an HTTP POST method that creates a new resource on the server.
  • Adds a Location header to the response. The Location header specifies the URI of the newly created to-do item. For more information, see 201 Created.
  • References the 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.

Install Postman

This tutorial uses Postman to test the web API.

  • Install Postman
  • Start the web app.
  • Start Postman.
  • Disable SSL certificate verification:
    • Postman for Windows: Select File > Settings (General tab), disable SSL certificate verification.
    • Postman for macOS: Select Postman > Settings (General tab), disable SSL certificate verification.

      Warning

      Re-enable SSL certificate verification after testing the controller.

Test PostTodoItem with Postman

  • Create a new request.

  • Set the HTTP method to POST.

  • Set the URI to https://localhost:<port>/api/todoitems. For example, https://localhost:5001/api/todoitems.

  • Select the Body tab.

  • Select the raw radio button.

  • Set the type to JSON (application/json).

  • In the request body enter JSON for a to-do item:

    JSON
    {
      "name":"walk dog",
      "isComplete":true
    }
    
  • Select Send.

    Postman with create request

Test the location header URI

The location header URI can be tested in the browser. Copy and paste the location header URI into the browser.

To test in Postman:

  • Select the Headers tab in the Response pane.

  • Copy the Location header value:

    Headers tab of the Postman console

  • Set the HTTP method to GET.

  • Set the URI to https://localhost:<port>/api/todoitems/1. For example, https://localhost:5001/api/todoitems/1.

  • Select Send.

Examine the GET methods

Two GET endpoints are implemented:

  • GET /api/todoitems
  • GET /api/todoitems/{id}

Test the app by calling the two endpoints from a browser or Postman. For example:

  • https://localhost:5001/api/todoitems
  • https://localhost:5001/api/todoitems/1

A response similar to the following is produced by the call to GetTodoItems:

JSON
[
  {
    "id": 1,
    "name": "Item1",
    "isComplete": false
  }
]

Test Get with Postman

  • Create a new request.
  • Set the HTTP method to GET.
  • Set the request URI to https://localhost:<port>/api/todoitems. For example, https://localhost:5001/api/todoitems.
  • Set Two pane view in Postman.
  • Select Send.

This app uses an in-memory database. If the app is stopped and started, the preceding GET request will not return any data. If no data is returned, POST data to the app.

Routing and URL paths

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:

    C#
    [Route("api/[controller]")]
    [ApiController]
    public class TodoItemsController : ControllerBase
    {
        private readonly TodoContext _context;
    
        public TodoItemsController(TodoContext context)
        {
            _context = context;
        }
    
  • 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.

C#
// GET: api/TodoItems/5
[HttpGet("{id}")]
public async Task<ActionResult<TodoItem>> GetTodoItem(long id)
{
    var todoItem = await _context.TodoItems.FindAsync(id);

    if (todoItem == null)
    {
        return NotFound();
    }

    return todoItem;
}

Return values

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:

  • If no item matches the requested ID, the method returns a 404 status NotFound error code.
  • Otherwise, the method returns 200 with a JSON response body. Returning item results in an HTTP 200 response.

The PutTodoItem method

Examine the PutTodoItem method:

C#
// PUT: api/TodoItems/5
[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.

If you get an error calling PutTodoItem, call GET to ensure there's an item in the database.

Test the PutTodoItem method

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.

Update the to-do item that has Id = 1 and set its name to "feed fish":

JSON
  {
    "Id":1,
    "name":"feed fish",
    "isComplete":true
  }

The following image shows the Postman update:

Postman console showing 204 (No Content) response

The DeleteTodoItem method

Examine the DeleteTodoItem method:

C#
// 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();
}

Test the DeleteTodoItem method

Use Postman to delete a to-do item:

  • Set the method to DELETE.
  • Set the URI of the object to delete (for example https://localhost:5001/api/todoitems/1).
  • Select Send.

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 article.

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:

C#
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:

C#
public class TodoItemDTO
{
    public long Id { get; set; }
    public string Name { get; set; }
    public bool IsComplete { get; set; }
}

Update the TodoItemsController to use TodoItemDTO:

C#
// GET: api/TodoItems
[HttpGet]
public async Task<ActionResult<IEnumerable<TodoItemDTO>>> GetTodoItems()
{
    return await _context.TodoItems
        .Select(x => ItemToDTO(x))
        .ToListAsync();
}

[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);
}

[HttpPut("{id}")]
public async Task<IActionResult> UpdateTodoItem(long id, TodoItemDTO todoItemDTO)
{
    if (id != todoItemDTO.Id)
    {
        return BadRequest();
    }

    var todoItem = await _context.TodoItems.FindAsync(id);
    if (todoItem == null)
    {
        return NotFound();
    }

    todoItem.Name = todoItemDTO.Name;
    todoItem.IsComplete = todoItemDTO.IsComplete;

    try
    {
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException) when (!TodoItemExists(id))
    {
        return NotFound();
    }

    return NoContent();
}

[HttpPost]
public async Task<ActionResult<TodoItemDTO>> CreateTodoItem(TodoItemDTO todoItemDTO)
{
    var todoItem = new TodoItem
    {
        IsComplete = todoItemDTO.IsComplete,
        Name = todoItemDTO.Name
    };

    _context.TodoItems.Add(todoItem);
    await _context.SaveChangesAsync();

    return CreatedAtAction(
        nameof(GetTodoItem),
        new { id = todoItem.Id },
        ItemToDTO(todoItem));
}

[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) =>
     _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.

Call the web API with JavaScript

See Tutorial: Call an ASP.NET Core web API with JavaScript.

Add authentication support to a web API

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:

  • Authentication as a Service (AaaS)
  • Single sign-on/off (SSO) over multiple application types
  • Access control for APIs
  • Federation Gateway

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).

Publish to Azure

For information on deploying to Azure, see Quickstart: Deploy an ASP.NET web app.

Additional resources

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 in 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.

In this tutorial, you learn how to:

  • Create a web API project.
  • Add a model class and a database context.
  • Scaffold a controller with CRUD methods.
  • Configure routing, URL paths, and return values.
  • Call the web API with Postman.

At the end, you have a web API that can manage "to-do" items stored in a database.

Overview

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.

The client is represented by a box on the left. It submits a request and receives a response from the application, a box drawn on the right. Within the application box, three boxes represent the controller, the model, and the data access layer. The request comes into the application's controller, and read/write operations occur between the controller and the data access layer. The model is serialized and returned to the client in the response.

Prerequisites

Create a web project

  • From the File menu, select New > Project.
  • Select the ASP.NET Core Web Application template and click Next.
  • Name the project TodoApi and click Create.
  • In the Create a new ASP.NET Core Web Application dialog, confirm that .NET Core and ASP.NET Core 3.1 are selected. Select the API template and click Create.

VS new project dialog

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.

Test the API

The project template creates a WeatherForecast API. Call the Get method from a browser to test the app.

Press Ctrl+F5 to run the app. Visual Studio launches a browser and navigates to https://localhost:<port>/weatherforecast, where <port> is a randomly chosen port number.

If you get a dialog box that asks if you should trust the IIS Express certificate, select Yes. In the Security Warning dialog that appears next, select Yes.

JSON similar to the following is returned:

JSON
[
    {
        "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"
    }
]

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 a single TodoItem class.

  • In Solution Explorer, right-click the project. Select Add > New Folder. Name the folder Models.

  • Right-click the Models folder and select Add > Class. Name the class TodoItem and select Add.

  • Replace the template code with the following code:

C#
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 NuGet packages

  • From the Tools menu, select NuGet Package Manager > Manage NuGet Packages for Solution.
  • Select the Browse tab, and then enter Microsoft.EntityFrameworkCore.InMemory in the search box.
  • Select Microsoft.EntityFrameworkCore.InMemory in the left pane.
  • Select the Project checkbox in the right pane and then select Install.

NuGet Package Manager

Add the TodoContext database context

  • Right-click the Models folder and select Add > Class. Name the class TodoContext and click Add.
  • Enter the following code:

    C#
    using Microsoft.EntityFrameworkCore;
    
    namespace TodoApi.Models
    {
        public class TodoContext : DbContext
        {
            public TodoContext(DbContextOptions<TodoContext> options)
                : base(options)
            {
            }
    
            public DbSet<TodoItem> TodoItems { get; set; }
        }
    }
    

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 Startup.cs with the following highlighted code:

C#
// Unused usings removed
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

namespace TodoApi
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<TodoContext>(opt =>
               opt.UseInMemoryDatabase("TodoList"));
            services.AddControllers();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}

The preceding code:

  • Removes unused using declarations.
  • Adds the database context to the DI container.
  • Specifies that the database context will use an in-memory database.

Scaffold a controller

  • 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:

    • Select TodoItem (TodoApi.Models) in the Model class.
    • Select TodoContext (TodoApi.Models) in the Data context class.
    • Select Add.

The generated code:

  • Marks the class with the [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.
  • Uses DI to inject the database context (TodoContext) into the controller. The database context is used in each of the CRUD methods in the controller.

The ASP.NET Core templates for:

  • Controllers with views include [action] in the route template.
  • API controllers don't include [action] in the route template.

When the [action] token isn't in the route template, the action name is excluded from the route. That is, the action's associated method name isn't used in the matching route.

Examine the PostTodoItem create method

Replace the return statement in the PostTodoItem to use the nameof operator:

C#
// POST: api/TodoItems
[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 to-do item from the body of the HTTP request.

For more information, see Attribute routing with Http[Verb] attributes.

The CreatedAtAction method:

  • Returns an HTTP 201 status code if successful. HTTP 201 is the standard response for an HTTP POST method that creates a new resource on the server.
  • Adds a Location header to the response. The Location header specifies the URI of the newly created to-do item. For more information, see 201 Created.
  • References the 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.

Install Postman

This tutorial uses Postman to test the web API.

  • Install Postman
  • Start the web app.
  • Start Postman.
  • Disable SSL certificate verification:
    • Postman for Windows: Postman for Windows File > Settings (General tab), disable SSL certificate verification.
    • Postman for macOS: Postman for Windows Postman > Settings (General tab), disable SSL certificate verification.

      Warning

      Re-enable SSL certificate verification after testing the controller.

Test PostTodoItem with Postman

  • Create a new request.

  • Set the HTTP method to POST.

  • Set the URI to https://localhost:<port>/api/todoitems. For example, https://localhost:5001/api/todoitems.

  • Select the Body tab.

  • Select the raw radio button.

  • Set the type to JSON (application/json).

  • In the request body enter JSON for a to-do item:

    JSON
    {
      "name":"walk dog",
      "isComplete":true
    }
    
  • Select Send.

    Postman with create request

Test the location header URI with Postman

  • Select the Headers tab in the Response pane.

  • Copy the Location header value:

    Headers tab of the Postman console

  • Set the HTTP method to GET.

  • Set the URI to https://localhost:<port>/api/todoitems/1. For example, https://localhost:5001/api/todoitems/1.

  • Select Send.

Examine the GET methods

These methods implement two GET endpoints:

  • GET /api/todoitems
  • GET /api/todoitems/{id}

Test the app by calling the two endpoints from a browser or Postman. For example:

  • https://localhost:5001/api/todoitems
  • https://localhost:5001/api/todoitems/1

A response similar to the following is produced by the call to GetTodoItems:

JSON
[
  {
    "id": 1,
    "name": "Item1",
    "isComplete": false
  }
]

Test Get with Postman

  • Create a new request.
  • Set the HTTP method to GET.
  • Set the request URI to https://localhost:<port>/api/todoitems. For example, https://localhost:5001/api/todoitems.
  • Set Two pane view in Postman.
  • Select Send.

This app uses an in-memory database. If the app is stopped and started, the preceding GET request will not return any data. If no data is returned, POST data to the app.

Routing and URL paths

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:

    C#
    [Route("api/[controller]")]
    [ApiController]
    public class TodoItemsController : ControllerBase
    {
        private readonly TodoContext _context;
    
        public TodoItemsController(TodoContext context)
        {
            _context = context;
        }
    
  • 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.

C#
// GET: api/TodoItems/5
[HttpGet("{id}")]
public async Task<ActionResult<TodoItem>> GetTodoItem(long id)
{
    var todoItem = await _context.TodoItems.FindAsync(id);

    if (todoItem == null)
    {
        return NotFound();
    }

    return todoItem;
}

Return values

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, 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:

  • If no item matches the requested ID, the method returns a 404 NotFound error code.
  • Otherwise, the method returns 200 with a JSON response body. Returning item results in an HTTP 200 response.

The PutTodoItem method

Examine the PutTodoItem method:

C#
// PUT: api/TodoItems/5
[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.

If you get an error calling PutTodoItem, call GET to ensure there's an item in the database.

Test the PutTodoItem method

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.

Update the to-do item that has Id = 1 and set its name to "feed fish":

JSON
  {
    "id":1,
    "name":"feed fish",
    "isComplete":true
  }

The following image shows the Postman update:

Postman console showing 204 (No Content) response

The DeleteTodoItem method

Examine the DeleteTodoItem method:

C#
// DELETE: api/TodoItems/5
[HttpDelete("{id}")]
public async Task<ActionResult<TodoItem>> DeleteTodoItem(long id)
{
    var todoItem = await _context.TodoItems.FindAsync(id);
    if (todoItem == null)
    {
        return NotFound();
    }

    _context.TodoItems.Remove(todoItem);
    await _context.SaveChangesAsync();

    return todoItem;
}

Test the DeleteTodoItem method

Use Postman to delete a to-do item:

  • Set the method to DELETE.
  • Set the URI of the object to delete (for example https://localhost:5001/api/todoitems/1).
  • Select Send.

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 article.

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:

C#
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:

C#
public class TodoItemDTO
{
    public long Id { get; set; }
    public string Name { get; set; }
    public bool IsComplete { get; set; }
}

Update the TodoItemsController to use TodoItemDTO:

C#
    [HttpGet]
    public async Task<ActionResult<IEnumerable<TodoItemDTO>>> GetTodoItems()
    {
        return await _context.TodoItems
            .Select(x => ItemToDTO(x))
            .ToListAsync();
    }

    [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);
    }

    [HttpPut("{id}")]
    public async Task<IActionResult> UpdateTodoItem(long id, TodoItemDTO todoItemDTO)
    {
        if (id != todoItemDTO.Id)
        {
            return BadRequest();
        }

        var todoItem = await _context.TodoItems.FindAsync(id);
        if (todoItem == null)
        {
            return NotFound();
        }

        todoItem.Name = todoItemDTO.Name;
        todoItem.IsComplete = todoItemDTO.IsComplete;

        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException) when (!TodoItemExists(id))
        {
            return NotFound();
        }

        return NoContent();
    }

    [HttpPost]
    public async Task<ActionResult<TodoItemDTO>> CreateTodoItem(TodoItemDTO todoItemDTO)
    {
        var todoItem = new TodoItem
        {
            IsComplete = todoItemDTO.IsComplete,
            Name = todoItemDTO.Name
        };

        _context.TodoItems.Add(todoItem);
        await _context.SaveChangesAsync();

        return CreatedAtAction(
            nameof(GetTodoItem),
            new { id = todoItem.Id },
            ItemToDTO(todoItem));
    }

    [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) =>
         _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.

Call the web API with JavaScript

See Tutorial: Call an ASP.NET Core web API with JavaScript.

Add authentication support to a web API

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:

  • Authentication as a Service (AaaS)
  • Single sign-on/off (SSO) over multiple application types
  • Access control for APIs
  • Federation Gateway

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).

Publish to Azure

For information on deploying to Azure, see Quickstart: Deploy an ASP.NET web app.

Additional resources

View or download sample code for this tutorial. See how to download.

For more information, see the following resources: