This browser is no longer supported.

Upgrade to Microsoft Edge to take advantage of the latest features, security updates, and technical support.

Download Microsoft Edge More info about Internet Explorer and Microsoft Edge

By Rick Anderson and Kirk Larkin

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:

Description Request body Response body
  • Visual Studio 2022 for Mac (latest version)

  • In Visual Studio 2022 for Mac, select Tools > Preferences... > Preview Features and enable Use the .NET 8 SDK if installed (requires restart) .
  • Important

    Microsoft has announced the retirement of Visual Studio for Mac. Visual Studio for Mac will no longer be supported starting August 31, 2024. Alternatives include:

  • Visual Studio Code with the C# Dev Kit and related extensions, such as .NET MAUI and Unity .
  • Visual Studio IDE running on Windows in a VM on Mac.
  • Visual Studio IDE running on Windows in a VM in the Cloud .
  • For more information, see Visual Studio for Mac retirement announcement .

  • 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 8.0 (Long Term Support) .
  • Confirm the checkbox for Use controllers(uncheck to use minimal APIs) is checked.
  • Confirm the checkbox for Enable OpenAPI support is checked.
  • Select Create .
  • Add a NuGet package

    A NuGet package must be added to support the database used in this tutorial.

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

  • Change directories ( cd ) to the folder that will contain the project folder.

  • Run the following commands:

    dotnet new webapi --use-controllers -o TodoApi
    cd TodoApi
    dotnet add package Microsoft.EntityFrameworkCore.InMemory
    code -r ../TodoApi
    

    These commands:

  • Create a new web API project and open it in Visual Studio Code.
  • Add a NuGet package that is needed for the next section.
  • Open the TodoApi folder in the current instance of Visual Studio Code.
  • Visual Studio Code might display a dialog box that asks: Do you trust the authors of the files in this folder?

  • If you trust all files in the parent folder, select Trust the authors of all files in the parent folder.
  • Select Yes, I trust the authors since the project folder has files generated by .NET.
  • When Visual Studio Code requests that you add assets to build and debug the project, select Yes. If Visual Studio Code doesn't offer to add build and debug assets, select View > Command Palette and type ".NET" into the search box. From the list of commands, select the .NET: Generate Assets for Build and Debug command.
  • Visual Studio Code adds a .vscode folder with generated launch.json and tasks.json files.

  • In Visual Studio for Mac 2022, select File > New Project....

  • In the Choose a template for your new project dialog:

  • Select Web and Console > App > API.
  • Select Continue.
  • In the Configure your new API dialog, make the following selections:

  • Confirm the Target framework is .NET 8.0.
  • Confirm the checkbox for Enable OpenAPI support is checked.
  • Confirm the checkbox for Use controllers is checked.
  • Select Create.
  • Enter the following:

  • Project name: TodoApi
  • Solution name: TodoApi
  • Select Create.
  • Add a NuGet package

  • In the Visual Studio for Mac 2022 toolbar, select Project > Manage NuGet Packages....
  • In the search box, enter Microsoft.EntityFrameworkCore.InMemory.
  • In the results window, check Microsoft.EntityFrameworkCore.InMemory.
  • Select Add Package
  • In the Select Projects window, select Ok.
  • In the License Agreement window, select Agree.
  • 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.

    Visual Studio Visual Studio Code Visual Studio for Mac

    Press Ctrl+F5 to run without the debugger.

    Visual Studio displays the following dialog when a project is not yet configured to use SSL:

    Select Yes if you trust the IIS Express SSL certificate.

    The following dialog is displayed:

    Select Yes if you agree to trust the development certificate.

    For information on trusting the Firefox browser, see Firefox SEC_ERROR_INADEQUATE_KEY_USAGE certificate error.

    Visual Studio launches the default browser and navigates to https://localhost:<port>/swagger/index.html, where <port> is a randomly chosen port number set at the project creation.

  • Trust the HTTPS development certificate by running the following command:

    dotnet dev-certs https --trust
    

    The preceding command doesn't work on Linux. See your Linux distribution's documentation for trusting a certificate.

    The preceding command displays the following dialog, provided the certificate was not previously trusted:

  • Select Yes if you agree to trust the development certificate.

    See Trust the ASP.NET Core HTTPS development certificate for more information.

    For information on trusting the Firefox browser, see Firefox SEC_ERROR_INADEQUATE_KEY_USAGE certificate error.

    Run the app:

  • Run the following command to start the app on the https profile:

    dotnet run --launch-profile https
    

    The output shows messages similar to the following, indicating that the app is running and awaiting requests:

    info: Microsoft.Hosting.Lifetime[14] Now listening on: https://localhost:{port}
  • Ctrl+click the HTTPS URL in the output to test the web app in a browser.

  • The default browser is launched to https://localhost:<port>/swagger/index.html, where <port> is the randomly chosen port number displayed in the output. There is no endpoint at https://localhost:<port>, so the browser returns HTTP 404 Not Found. Append /swagger to the URL, https://localhost:<port>/swagger.

    After testing the web app in the following instruction, press Ctrl+C in the integrated terminal to shut it down.

    Select Debug > Start Debugging to launch the app. Visual Studio for Mac launches a browser and navigates to https://localhost:<port>/swagger/index.html, where <port> is a randomly chosen port number set at the project creation.

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

  • 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 uses Swagger to test the app. For more information on Swagger, see ASP.NET Core web API documentation with Swagger / OpenAPI.

    Copy and paste the Request URL in the browser: https://localhost:<port>/weatherforecast

    JSON similar to the following example is returned:

    "date": "2019-07-16T19:04:05.7257911-06:00", "temperatureC": 52, "temperatureF": 125, "summary": "Mild" "date": "2019-07-17T19:04:05.7258461-06:00", "temperatureC": 36, "temperatureF": 96, "summary": "Warm" "date": "2019-07-18T19:04:05.7258467-06:00", "temperatureC": 39, "temperatureF": 102, "summary": "Cool" "date": "2019-07-19T19:04:05.7258471-06:00", "temperatureC": 10, "temperatureF": 49, "summary": "Bracing" "date": "2019-07-20T19:04:05.7258474-06:00", "temperatureC": -1, "temperatureF": 31, "summary": "Chilly"

    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.

    Visual Studio Visual Studio Code Visual Studio for Mac
  • 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:
  • Control-click the TodoAPI project and select Add > New Folder. Name the folder Models.

  • Control-click the Models folder, and select Add > New Class... > General > Empty Class.

  • Name the class TodoItem, and then select Create.

  • Replace the template code with the following:

    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.

    Visual Studio Visual Studio Code / Visual Studio for Mac 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:

    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

    Visual Studio Visual Studio Code / Visual Studio for Mac
  • 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.

    Make sure that all of your changes so far are saved.

  • Control-click the TodoAPI project and select Open in Terminal. The terminal opens at the TodoAPI project folder. Run the following commands:
  • dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
    dotnet add package Microsoft.EntityFrameworkCore.Design
    dotnet add package Microsoft.EntityFrameworkCore.SqlServer
    dotnet add package Microsoft.EntityFrameworkCore.Tools
    dotnet tool uninstall -g dotnet-aspnet-codegenerator
    dotnet tool install -g dotnet-aspnet-codegenerator
    dotnet tool update -g dotnet-aspnet-codegenerator
    

    The preceding commands:

  • Add NuGet packages required for scaffolding.
  • Install the scaffolding engine (dotnet-aspnet-codegenerator) after uninstalling any possible previous version.
  • 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.

    Build the project.

    Run the following command:

    dotnet aspnet-codegenerator controller -name TodoItemsController -async -api -m TodoItem -dc TodoContext -outDir Controllers
    

    The preceding command scaffolds the TodoItemsController.

    The generated code:

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

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

    "name": "walk dog", "isComplete": true
  • Select Execute

    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.

    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:

    [Route("api/[controller]")]
    [ApiController]
    public class TodoItemsController : ControllerBase
    
  • Replace [controller] with the name of the controller, which by convention is the controller class name minus the "Controller" suffix. For this sample, the controller class name is TodoItemsController, so the controller name is "TodoItems". ASP.NET Core routing is case insensitive.

  • If the [HttpGet] attribute has a route template (for example, [HttpGet("products")]), append that to the path. This sample doesn't use a template. For more information, see Attribute routing with Http[Verb] attributes.

    In the following GetTodoItem method, "{id}" is a placeholder variable for the unique identifier of the to-do item. When GetTodoItem is invoked, the value of "{id}" in the URL is provided to the method in its id parameter.

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

    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:

    [HttpPut("{id}")]
    public async Task<IActionResult> PutTodoItem(long id, TodoItem todoItem)
        if (id != todoItem.Id)
            return BadRequest();
        _context.Entry(todoItem).State = EntityState.Modified;
            await _context.SaveChangesAsync();
        catch (DbUpdateConcurrencyException)
            if (!TodoItemExists(id))
                return NotFound();
                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:

    [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 other tools

    There are many other tools that can be used to test web APIs, for example:

  • Visual Studio Endpoints Explorer and .http files
  • http-repl
  • Postman
  • curl. Swagger uses curl and shows the curl commands it submits.
  • Fiddler
  • For more information, see:

  • Minimal API tutorial: test with .http files and Endpoints Explorer
  • Test APIs with Postman
  • Install and test APIs with http-repl
  • Prevent over-posting

    Currently the sample app exposes the entire TodoItem object. Production apps typically limit the data that's input and returned using a subset of the model. There are multiple reasons behind this, and security is a major one. The subset of a model is usually referred to as a Data Transfer Object (DTO), input model, or view model. DTO is used in this tutorial.

    A DTO may be used to:

  • Prevent over-posting.
  • Hide properties that clients are not supposed to view.
  • Omit some properties in order to reduce payload size.
  • Flatten object graphs that contain nested objects. Flattened object graphs can be more convenient for clients.
  • To demonstrate the DTO approach, update the TodoItem class to include a secret field:

    namespace TodoApi.Models
        public class TodoItem
            public long Id { get; set; }
            public string? Name { get; set; }
            public bool IsComplete { get; set; }
            public string? Secret { get; set; }
    

    The secret field needs to be hidden from this app, but an administrative app could choose to expose it.

    Verify you can post and get the secret field.

    Create a DTO model:

    namespace TodoApi.Models;
    public class TodoItemDTO
        public long Id { get; set; }
        public string? Name { get; set; }
        public bool IsComplete { get; set; }
    

    Update the TodoItemsController to use TodoItemDTO:

    using Microsoft.AspNetCore.Mvc;
    using Microsoft.EntityFrameworkCore;
    using TodoApi.Models;
    namespace TodoApi.Controllers;
    [Route("api/[controller]")]
    [ApiController]
    public class TodoItemsController : ControllerBase
        private readonly TodoContext _context;
        public TodoItemsController(TodoContext context)
            _context = context;
        // GET: api/TodoItems
        [HttpGet]
        public async Task<ActionResult<IEnumerable<TodoItemDTO>>> GetTodoItems()
            return await _context.TodoItems
                .Select(x => ItemToDTO(x))
                .ToListAsync();
        // GET: api/TodoItems/5
        // <snippet_GetByID>
        [HttpGet("{id}")]
        public async Task<ActionResult<TodoItemDTO>> GetTodoItem(long id)
            var todoItem = await _context.TodoItems.FindAsync(id);
            if (todoItem == null)
                return NotFound();
            return ItemToDTO(todoItem);
        // </snippet_GetByID>
        // PUT: api/TodoItems/5
        // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
        // <snippet_Update>
        [HttpPut("{id}")]
        public async Task<IActionResult> PutTodoItem(long id, TodoItemDTO todoDTO)
            if (id != todoDTO.Id)
                return BadRequest();
            var todoItem = await _context.TodoItems.FindAsync(id);
            if (todoItem == null)
                return NotFound();
            todoItem.Name = todoDTO.Name;
            todoItem.IsComplete = todoDTO.IsComplete;
                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:

  • Microsoft Entra ID
  • Azure Active Directory B2C (Azure AD B2C)
  • Duende Identity Server
  • 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:

  • Create web APIs with ASP.NET Core
  • Tutorial: Create a minimal API with ASP.NET Core
  • ASP.NET Core web API documentation with Swagger / OpenAPI
  • Razor Pages with Entity Framework Core in ASP.NET Core - Tutorial 1 of 8
  • Routing to controller actions in ASP.NET Core
  • Controller action return types in ASP.NET Core web API
  • Deploy ASP.NET Core apps to Azure App Service
  • Host and deploy ASP.NET Core
  • Create a web API with ASP.NET Core
  • 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:

    Description Request body Response body

    Important

    Microsoft has announced the retirement of Visual Studio for Mac. Visual Studio for Mac will no longer be supported starting August 31, 2024. Alternatives include:

  • Visual Studio Code with the C# Dev Kit and related extensions, such as .NET MAUI and Unity.
  • Visual Studio IDE running on Windows in a VM on Mac.
  • Visual Studio IDE running on Windows in a VM in the Cloud.
  • For more information, see Visual Studio for Mac retirement announcement.

  • 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.
  • Open the integrated terminal.

  • Change directories (cd) to the folder that will contain the project folder.

  • Run the following commands:

    dotnet new webapi -o TodoApi -f net7.0
    cd TodoApi
    dotnet add package Microsoft.EntityFrameworkCore.InMemory -v 7.0.0
    code -r ../TodoApi
    

    These commands:

  • Create a new web API project that targets .NET 7.0 and open it in Visual Studio Code.
  • Add a NuGet package that is needed for the next section.
  • Open the TodoApi folder in the current instance of Visual Studio Code.
  • Visual Studio Code might display a dialog box that asks: Do you trust the authors of the files in this folder?

  • If you trust all files in the parent folder, select Trust the authors of all files in the parent folder.
  • Select Yes, I trust the authors since the project folder has files generated by .NET.
  • When Visual Studio Code requests that you add assets to build and debug the project, select Yes. If Visual Studio Code doesn't offer to add build and debug assets, select View > Command Palette and type ".NET" into the search box. From the list of commands, select the .NET: Generate Assets for Build and Debug command.
  • Visual Studio Code adds a .vscode folder with generated launch.json and tasks.json files.

  • In Visual Studio for Mac 2022, select File > New Project....

  • In the Choose a template for your new project dialog:

  • Select Web and Console > App > API.
  • Select Continue.
  • In the Configure your new API dialog, make the following selections:

  • Confirm the Target framework is .NET 7.0 (or later).
  • Confirm the checkbox for Use controllers (uncheck to use minimal APIs) is checked.
  • Confirm the checkbox for Enable OpenAPI support is checked.
  • Select Continue.
  • Enter the following:

  • Project name: TodoApi
  • Solution name: TodoApi
  • Select Create.
  • Add a NuGet package

  • In the Visual Studio for Mac 2022 toolbar, select Project > Manage NuGet Packages...
  • In the search box, enter Microsoft.EntityFrameworkCore.InMemory.
  • In the results window, check Microsoft.EntityFrameworkCore.InMemory.
  • Select Add Package
  • In the Select Projects window, select Ok
  • In the License Agreement window, select Agree
  • 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.

    Visual Studio Visual Studio Code Visual Studio for Mac

    Press Ctrl+F5 to run without the debugger.

    Visual Studio displays the following dialog when a project is not yet configured to use SSL:

    Select Yes if you trust the IIS Express SSL certificate.

    The following dialog is displayed:

    Select Yes if you agree to trust the development certificate.

    For information on trusting the Firefox browser, see Firefox SEC_ERROR_INADEQUATE_KEY_USAGE certificate error.

    Visual Studio launches the default browser and navigates to https://localhost:<port>/swagger/index.html, where <port> is a randomly chosen port number.

  • Trust the HTTPS development certificate by running the following command:

    dotnet dev-certs https --trust
    

    The preceding command doesn't work on Linux. See your Linux distribution's documentation for trusting a certificate.

    The preceding command displays the following dialog, provided the certificate was not previously trusted:

  • Select Yes if you agree to trust the development certificate.

    See Trust the ASP.NET Core HTTPS development certificate for more information.

    For information on trusting the Firefox browser, see Firefox SEC_ERROR_INADEQUATE_KEY_USAGE certificate error.

    Run the app:

  • Run the following command to start the app on the https profile:

    dotnet run --launch-profile https
    

    The output shows messages similar to the following, indicating that the app is running and awaiting requests:

    info: Microsoft.Hosting.Lifetime[14] Now listening on: https://localhost:{port}
  • Ctrl+click the HTTPS URL in the output to test the web app in a browser.

  • The default browser is launched to https://localhost:<port>, where <port> is the randomly chosen port number displayed in the output. There is no endpoint at https://localhost:<port>, so the browser returns HTTP 404 Not Found. Append /swagger to the URL, https://localhost:<port>/swagger.

    After testing the web app in the following instruction, press Ctrl+C in the integrated terminal to shut it down.

    Select Debug > Start Debugging to launch the app. Visual Studio for Mac launches a browser and navigates to https://localhost:<port>, where <port> is a randomly chosen port number. There is no endpoint at https://localhost:<port> so the browser returns HTTP 404 Not Found. Append /swagger to the URL, https://localhost:<port>/swagger.

    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:

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

    Visual Studio Visual Studio Code Visual Studio for Mac
  • 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:
  • Control-click the TodoAPI project and select Add > New Folder. Name the folder Models.

  • Control-click the Models folder, and select Add > New Class... > General > Empty Class.

  • Name the class TodoItem, and then select Create.

  • Replace the template code with the following:

    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.

    Visual Studio Visual Studio Code / Visual Studio for Mac

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

    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

    Visual Studio Visual Studio Code / Visual Studio for Mac
  • 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.

    Make sure that all of your changes so far are saved.

  • Control-click the TodoAPI project and select Open in Terminal. The terminal opens at the TodoAPI project folder. Run the following commands:
  • dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design -v 7.0.0
    dotnet add package Microsoft.EntityFrameworkCore.Design -v 7.0.0
    dotnet add package Microsoft.EntityFrameworkCore.SqlServer -v 7.0.0
    dotnet add package Microsoft.EntityFrameworkCore.Tools -v 7.0.0
    dotnet tool uninstall -g dotnet-aspnet-codegenerator
    dotnet tool install -g dotnet-aspnet-codegenerator
    dotnet tool update -g dotnet-aspnet-codegenerator
    

    The preceding commands:

  • Add NuGet packages required for scaffolding.
  • Install the scaffolding engine (dotnet-aspnet-codegenerator) after uninstalling any possible previous version.
  • 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.

    Build the project.

    Run the following command:

    dotnet aspnet-codegenerator controller -name TodoItemsController -async -api -m TodoItem -dc TodoContext -outDir Controllers
    

    The preceding command scaffolds the TodoItemsController.

    The generated code:

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

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

    "name": "walk dog", "isComplete": true
  • Select Execute

    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.

    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:

    [Route("api/[controller]")]
    [ApiController]
    public class TodoItemsController : ControllerBase
    
  • Replace [controller] with the name of the controller, which by convention is the controller class name minus the "Controller" suffix. For this sample, the controller class name is TodoItemsController, so the controller name is "TodoItems". ASP.NET Core routing is case insensitive.

  • If the [HttpGet] attribute has a route template (for example, [HttpGet("products")]), append that to the path. This sample doesn't use a template. For more information, see Attribute routing with Http[Verb] attributes.

    In the following GetTodoItem method, "{id}" is a placeholder variable for the unique identifier of the to-do item. When GetTodoItem is invoked, the value of "{id}" in the URL is provided to the method in its id parameter.

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

    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:

    [HttpPut("{id}")]
    public async Task<IActionResult> PutTodoItem(long id, TodoItem todoItem)
        if (id != todoItem.Id)
            return BadRequest();
        _context.Entry(todoItem).State = EntityState.Modified;
            await _context.SaveChangesAsync();
        catch (DbUpdateConcurrencyException)
            if (!TodoItemExists(id))
                return NotFound();
                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:

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

  • Test APIs with Postman
  • Install and test APIs with http-repl
  • 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:

    namespace TodoApi.Models
        public class TodoItem
            public long Id { get; set; }
            public string? Name { get; set; }
            public bool IsComplete { get; set; }
            public string? Secret { get; set; }
    

    The secret field needs to be hidden from this app, but an administrative app could choose to expose it.

    Verify you can post and get the secret field.

    Create a DTO model:

    namespace TodoApi.Models;
    public class TodoItemDTO
        public long Id { get; set; }
        public string? Name { get; set; }
        public bool IsComplete { get; set; }
    

    Update the TodoItemsController to use TodoItemDTO:

    using Microsoft.AspNetCore.Mvc;
    using Microsoft.EntityFrameworkCore;
    using TodoApi.Models;
    namespace TodoApi.Controllers;
    [Route("api/[controller]")]
    [ApiController]
    public class TodoItemsController : ControllerBase
        private readonly TodoContext _context;
        public TodoItemsController(TodoContext context)
            _context = context;
        // GET: api/TodoItems
        [HttpGet]
        public async Task<ActionResult<IEnumerable<TodoItemDTO>>> GetTodoItems()
            return await _context.TodoItems
                .Select(x => ItemToDTO(x))
                .ToListAsync();
        // GET: api/TodoItems/5
        // <snippet_GetByID>
        [HttpGet("{id}")]
        public async Task<ActionResult<TodoItemDTO>> GetTodoItem(long id)
            var todoItem = await _context.TodoItems.FindAsync(id);
            if (todoItem == null)
                return NotFound();
            return ItemToDTO(todoItem);
        // </snippet_GetByID>
        // PUT: api/TodoItems/5
        // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
        // <snippet_Update>
        [HttpPut("{id}")]
        public async Task<IActionResult> PutTodoItem(long id, TodoItemDTO todoDTO)
            if (id != todoDTO.Id)
                return BadRequest();
            var todoItem = await _context.TodoItems.FindAsync(id);
            if (todoItem == null)
                return NotFound();
            todoItem.Name = todoDTO.Name;
            todoItem.IsComplete = todoDTO.IsComplete;
                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:

  • Microsoft Entra ID
  • Azure Active Directory B2C (Azure AD B2C)
  • Duende Identity Server
  • 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:

  • Create web APIs with ASP.NET Core
  • Tutorial: Create a minimal API with ASP.NET Core
  • ASP.NET Core web API documentation with Swagger / OpenAPI
  • Razor Pages with Entity Framework Core in ASP.NET Core - Tutorial 1 of 8
  • Routing to controller actions in ASP.NET Core
  • Controller action return types in ASP.NET Core web API
  • Deploy ASP.NET Core apps to Azure App Service
  • Host and deploy ASP.NET Core
  • Create a web API with ASP.NET Core
  • 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:

    Description Request body Response body

    Important

    Microsoft has announced the retirement of Visual Studio for Mac. Visual Studio for Mac will no longer be supported starting August 31, 2024. Alternatives include:

  • Visual Studio Code with the C# Dev Kit and related extensions, such as .NET MAUI and Unity.
  • Visual Studio IDE running on Windows in a VM on Mac.
  • Visual Studio IDE running on Windows in a VM in the Cloud.
  • For more information, see Visual Studio for Mac retirement announcement.

  • 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.
  • Open the integrated terminal.

  • Change directories (cd) to the folder that will contain the project folder.

  • Run the following commands:

    dotnet new webapi -o TodoApi
    cd TodoApi
    dotnet add package Microsoft.EntityFrameworkCore.InMemory
    code -r ../TodoApi
    

    These commands:

  • Create a new web API project and open it in Visual Studio Code.
  • Add a NuGet package that is needed for the next section.
  • Open the TodoApi folder in the current instance of Visual Studio Code.
  • Visual Studio Code might display a dialog box that asks: Do you trust the authors of the files in this folder?

  • If you trust all files in the parent folder, select Trust the authors of all files in the parent folder.
  • Select Yes, I trust the authors since the project folder has files generated by .NET.
  • When Visual Studio Code requests that you add assets to build and debug the project, select Yes. If Visual Studio Code doesn't offer to add build and debug assets, select View > Command Palette and type ".NET" into the search box. From the list of commands, select the .NET: Generate Assets for Build and Debug command.
  • Visual Studio Code adds a .vscode folder with generated launch.json and tasks.json files.

  • In Visual Studio for Mac earlier than version 8.6, select .NET Core > App > API > Next. In version 8.6 or later, select Web and Console > App > API > Next.

  • In the Configure the new ASP.NET Core Web API dialog, select the latest .NET Core 5.x Target Framework. Select Next.

  • Enter TodoApi for the Project Name and then select Create.

    Accessing a Command Terminal on Mac

    Accessing a command terminal on Mac for the first time requires the following setting configurations:

  • Navigate to System Preferences > Keyboard > Shortcuts > Services.
  • Under Files and Folders, verify New Terminal at Folder is selected.
  • The preceding instructions enable accessing a command terminal two ways: from inside Visual Studio or from Finder.

    To access a command terminal from Visual Studio for Mac:

  • Right click the project name.
  • Navigate to Tools > Open in Terminal.
  • To access a command terminal from Finder:

  • Right click the desired folder.
  • At the bottom of the list, select New Terminal at Folder.
  • Open a command terminal in the project folder and run the following command:

    dotnet add package Microsoft.EntityFrameworkCore.InMemory
    

    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.

    Visual Studio Visual Studio Code Visual Studio for Mac

    Press Ctrl+F5 to run without the debugger.

    Visual Studio displays the following dialog when a project is not yet configured to use SSL:

    Select Yes if you trust the IIS Express SSL certificate.

    The following dialog is displayed:

    Select Yes if you agree to trust the development certificate.

    For information on trusting the Firefox browser, see Firefox SEC_ERROR_INADEQUATE_KEY_USAGE certificate error.

    Visual Studio launches the default browser and navigates to https://localhost:<port>/swagger/index.html, where <port> is a randomly chosen port number.

  • Trust the HTTPS development certificate by running the following command:

    dotnet dev-certs https --trust
    

    The preceding command doesn't work on Linux. See your Linux distribution's documentation for trusting a certificate.

    The preceding command displays the following dialog, provided the certificate was not previously trusted:

  • Select Yes if you agree to trust the development certificate.

    See Trust the ASP.NET Core HTTPS development certificate for more information.

    For information on trusting the Firefox browser, see Firefox SEC_ERROR_INADEQUATE_KEY_USAGE certificate error.

    Run the app:

  • Press Ctrl+F5.
  • At the Select environment prompt, choose .NET Core.
  • Select Add Configuration > .NET: Launch a local .NET Core Console App.
  • In the configuration JSON:
  • Replace <target-framework> with net6.0.
  • Replace <project-name.dll> with TodoApi.dll.
  • Press Ctrl+F5.
  • In the Could not find the task 'build' dialog, select Configure Task.
  • Select Create tasks.json file from template.
  • Select the .NET Core task template.
  • Press Ctrl+F5.
  • In a browser, navigate to https://localhost:<port>/swagger, where <port> is the randomly chosen port number displayed in the output.

    For Visual Studio for Mac, see the .NET 5 version of this tutorial.

    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:

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

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

    Visual Studio Visual Studio Code Visual Studio for Mac
  • 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:

    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.

    Visual Studio Visual Studio Code Visual Studio for Mac

    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:

    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:

    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

    Visual Studio Visual Studio Code Visual Studio for Mac
  • 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.

    Make sure that all of your changes so far are saved.

    Run the following commands from the project folder, that is, the TodoApi folder:

    dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
    dotnet add package Microsoft.EntityFrameworkCore.Design
    dotnet add package Microsoft.EntityFrameworkCore.SqlServer
    dotnet tool install -g dotnet-aspnet-codegenerator
    dotnet aspnet-codegenerator controller -name TodoItemsController -async -api -m TodoItem -dc TodoContext -outDir Controllers
    

    The preceding commands:

  • Add NuGet packages required for scaffolding.
  • Install the scaffolding engine (dotnet-aspnet-codegenerator).
  • Scaffold the TodoItemsController.
  • 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.

    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:

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

    dotnet tool install -g Microsoft.dotnet-httprepl
    

    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.

    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:

    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:

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

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

    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:

    connect https://localhost:5001/api/todoitems
    

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

    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:

    [Route("api/[controller]")]
    [ApiController]
    public class TodoItemsController : ControllerBase
    
  • Replace [controller] with the name of the controller, which by convention is the controller class name minus the "Controller" suffix. For this sample, the controller class name is TodoItemsController, so the controller name is "TodoItems". ASP.NET Core routing is case insensitive.

  • If the [HttpGet] attribute has a route template (for example, [HttpGet("products")]), append that to the path. This sample doesn't use a template. For more information, see Attribute routing with Http[Verb] attributes.

    In the following GetTodoItem method, "{id}" is a placeholder variable for the unique identifier of the to-do item. When GetTodoItem is invoked, the value of "{id}" in the URL is provided to the method in its id parameter.

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

    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:

    [HttpPut("{id}")]
    public async Task<IActionResult> PutTodoItem(long id, TodoItem todoItem)
        if (id != todoItem.Id)
            return BadRequest();
        _context.Entry(todoItem).State = EntityState.Modified;
            await _context.SaveChangesAsync();
        catch (DbUpdateConcurrencyException)
            if (!TodoItemExists(id))
                return NotFound();
                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":

    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:

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

    The DeleteTodoItem method

    Examine the DeleteTodoItem method:

    [HttpDelete("{id}")]
    public async Task<IActionResult> DeleteTodoItem(long id)
        var todoItem = await _context.TodoItems.FindAsync(id);
        if (todoItem == null)
            return NotFound();
        _context.TodoItems.Remove(todoItem);
        await _context.SaveChangesAsync();
        return NoContent();
    

    Test the DeleteTodoItem method

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

    connect https://localhost:5001/api/todoitems/1
    delete
    

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

    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:

    namespace TodoApi.Models
        public class TodoItem
            public long Id { get; set; }
            public string? Name { get; set; }
            public bool IsComplete { get; set; }
            public string? Secret { get; set; }
    

    The secret field needs to be hidden from this app, but an administrative app could choose to expose it.

    Verify you can post and get the secret field.

    Create a DTO model:

    namespace TodoApi.Models
        public class TodoItemDTO
            public long Id { get; set; }
            public string? Name { get; set; }
            public bool IsComplete { get; set; }
    

    Update the TodoItemsController to use TodoItemDTO:

    using Microsoft.AspNetCore.Mvc;
    using Microsoft.EntityFrameworkCore;
    using TodoApi.Models;
    namespace TodoApi.Controllers
        [Route("api/[controller]")]
        [ApiController]
        public class TodoItemsController : ControllerBase
            private readonly TodoContext _context;
            public TodoItemsController(TodoContext context)
                _context = context;
            // GET: api/TodoItems
            [HttpGet]
            public async Task<ActionResult<IEnumerable<TodoItemDTO>>> GetTodoItems()
                return await _context.TodoItems
                    .Select(x => ItemToDTO(x))
                    .ToListAsync();
            // GET: api/TodoItems/5
            [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;
                    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:

  • Microsoft Entra ID
  • Azure Active Directory B2C (Azure AD B2C)
  • Duende Identity Server
  • 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:

  • Create web APIs with ASP.NET Core
  • Tutorial: Create a minimal API with ASP.NET Core
  • ASP.NET Core web API documentation with Swagger / OpenAPI
  • Razor Pages with Entity Framework Core in ASP.NET Core - Tutorial 1 of 8
  • Routing to controller actions in ASP.NET Core
  • Controller action return types in ASP.NET Core web API
  • Deploy ASP.NET Core apps to Azure App Service
  • Host and deploy ASP.NET Core
  • Create a web API with ASP.NET Core
  • 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:

    Description Request body Response body
  • 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.
  • Open the integrated terminal.

  • Change directories (cd) to the folder that will contain the project folder.

  • Run the following commands:

    dotnet new webapi -o TodoApi
    cd TodoApi
    dotnet add package Microsoft.EntityFrameworkCore.InMemory
    code -r ../TodoApi
    

    These commands:

  • Create a new web API project and open it in Visual Studio Code.
  • Add a NuGet package that is needed for the next section.
  • Open the TodoApi folder in the current instance of Visual Studio Code.
  • Visual Studio Code might display a dialog box that asks: Do you trust the authors of the files in this folder?

  • If you trust all files in the parent folder, select Trust the authors of all files in the parent folder.
  • Select Yes, I trust the authors since the project folder has files generated by .NET.
  • When Visual Studio Code requests that you add assets to build and debug the project, select Yes. If Visual Studio Code doesn't offer to add build and debug assets, select View > Command Palette and type ".NET" into the search box. From the list of commands, select the .NET: Generate Assets for Build and Debug command.
  • Visual Studio Code adds a .vscode folder with generated launch.json and tasks.json files.

  • In Visual Studio for Mac earlier than version 8.6, select .NET Core > App > API > Next. In version 8.6 or later, select Web and Console > App > API > Next.

  • In the Configure the new ASP.NET Core Web API dialog, select the latest .NET Core 5.x Target Framework. Select Next.

  • Enter TodoApi for the Project Name and then select Create.

    Accessing a Command Terminal on Mac

    Accessing a command terminal on Mac for the first time requires the following setting configurations:

  • Navigate to System Preferences > Keyboard > Shortcuts > Services.
  • Under Files and Folders, verify New Terminal at Folder is selected.
  • The preceding instructions enable accessing a command terminal two ways: from inside Visual Studio or from Finder.

    To access a command terminal from Visual Studio for Mac:

  • Right click the project name.
  • Navigate to Tools > Open in Terminal.
  • To access a command terminal from Finder:

  • Right click the desired folder.
  • At the bottom of the list, select New Terminal at Folder.
  • Open a command terminal in the project folder and run the following command:

    dotnet add package Microsoft.EntityFrameworkCore.InMemory
    

    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.

    Visual Studio Visual Studio Code Visual Studio for Mac

    Press Ctrl+F5 to run without the debugger.

    Visual Studio displays the following dialog when a project is not yet configured to use SSL:

    Select Yes if you trust the IIS Express SSL certificate.

    The following dialog is displayed:

    Select Yes if you agree to trust the development certificate.

    For information on trusting the Firefox browser, see Firefox SEC_ERROR_INADEQUATE_KEY_USAGE certificate error.

    Visual Studio launches:

  • The IIS Express web server.
  • The default browser and navigates to https://localhost:<port>/swagger/index.html, where <port> is a randomly chosen port number.
  • Trust the HTTPS development certificate by running the following command:

    dotnet dev-certs https --trust
    

    The preceding command doesn't work on Linux. See your Linux distribution's documentation for trusting a certificate.

    The preceding command displays the following dialog, provided the certificate was not previously trusted:

  • Select Yes if you agree to trust the development certificate.

    See Trust the ASP.NET Core HTTPS development certificate for more information.

    For information on trusting the Firefox browser, see Firefox SEC_ERROR_INADEQUATE_KEY_USAGE certificate error.

    Press Ctrl+F5 to run the app. In a browser, go to following URL: https://localhost:5001/swagger

    Select Run > Start Debugging to launch the app. Visual Studio for Mac launches a browser and navigates to https://localhost:<port>, where <port> is a randomly chosen port number. An HTTP 404 (Not Found) error is returned. Append /swagger to the URL (change the URL to https://localhost:<port>/swagger).

    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:

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

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

    Visual Studio Visual Studio Code Visual Studio for Mac
  • 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:

  • Right-click the Models folder, and select Add > New File > General > Empty Class.

  • Name the class TodoItem, and then click New.

  • Replace the template code with the following:

    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.

    Visual Studio Visual Studio Code / Visual Studio for Mac

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

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

    Visual Studio Visual Studio Code / Visual Studio for Mac
  • 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.
  • Run the following commands from the project folder, TodoApi/TodoApi:

    dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
    dotnet add package Microsoft.EntityFrameworkCore.Design
    dotnet add package Microsoft.EntityFrameworkCore.SqlServer
    dotnet tool install -g dotnet-aspnet-codegenerator
    dotnet aspnet-codegenerator controller -name TodoItemsController -async -api -m TodoItem -dc TodoContext -outDir Controllers
    

    The preceding commands:

  • Add NuGet packages required for scaffolding.
  • Installs the scaffolding engine (dotnet-aspnet-codegenerator).
  • Scaffolds the TodoItemsController.
  • 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.

    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:

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

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

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

    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:

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

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

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

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

    // 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;
            await _context.SaveChangesAsync();
        catch (DbUpdateConcurrencyException)
            if (!TodoItemExists(id))
                return NotFound();
                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":

    "Id":1, "name":"feed fish", "isComplete":true

    The following image shows the Postman update:

    The DeleteTodoItem method

    Examine the DeleteTodoItem method:

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

    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:

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

    Update the TodoItemsController to use TodoItemDTO:

    // 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;
            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:

  • Microsoft Entra ID
  • Azure Active Directory B2C (Azure AD B2C)
  • Duende Identity Server
  • 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:

  • Create web APIs with ASP.NET Core
  • Tutorial: Create a minimal API with ASP.NET Core
  • ASP.NET Core web API documentation with Swagger / OpenAPI
  • Razor Pages with Entity Framework Core in ASP.NET Core - Tutorial 1 of 8
  • Routing to controller actions in ASP.NET Core
  • Controller action return types in ASP.NET Core web API
  • Deploy ASP.NET Core apps to Azure App Service
  • Host and deploy ASP.NET Core
  • Create a web API with ASP.NET Core
  • 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:

    Description Request body Response body
  • .NET Core 3.1 SDK
  • The Visual Studio Code instructions use the .NET Core CLI for ASP.NET Core development functions such as project creation. You can follow these instructions on any platform (macOS, Linux, or Windows) and with any code editor. Minor changes may be required if you use something other than Visual Studio Code. For more information on installing Visual Studio Code on macOS, see Visual Studio Code on macOS.

  • 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.
  • Open the integrated terminal.

  • Change directories (cd) to the folder that will contain the project folder.

  • Run the following commands:

    dotnet new webapi -o TodoApi
    cd TodoApi
    dotnet add package Microsoft.EntityFrameworkCore.InMemory
    code -r ../TodoApi
    
  • When a dialog box asks if you want to add required assets to the project, select Yes.

    The preceding commands:

  • Creates a new web API project and opens it in Visual Studio Code.
  • Adds the NuGet packages which are required in the next section.
  • In Visual Studio for Mac earlier than version 8.6, select .NET Core > App > API > Next. In version 8.6 or later, select Web and Console > App > API > Next.

  • In the Configure the new ASP.NET Core Web API dialog, select the latest .NET Core 3.x Target Framework. Select Next.

  • Enter TodoApi for the Project Name and then select Create.

    Accessing a Command Terminal on Mac

    Accessing a command terminal on Mac for the first time requires the following setting configurations:

  • Navigate to System Preferences > Keyboard > Shortcuts > Services.
  • Under Files and Folders, verify New Terminal at Folder is selected.
  • The preceding instructions enable accessing a command terminal two ways: from inside Visual Studio or from Finder.

    To access a command terminal from Visual Studio for Mac:

  • Right click the project name.
  • Navigate to Tools > Open in Terminal.
  • To access a command terminal from Finder:

  • Right click the desired folder.
  • At the bottom of the list, select New Terminal at Folder.
  • Open a command terminal in the project folder and run the following command:

    dotnet add package Microsoft.EntityFrameworkCore.InMemory
    

    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.

    Visual Studio Visual Studio Code Visual Studio for Mac

    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.

    Press Ctrl+F5 to run the app. In a browser, go to following URL: https://localhost:5001/weatherforecast.

    Select Run > Start Debugging to launch the app. Visual Studio for Mac launches a browser and navigates to https://localhost:<port>, where <port> is a randomly chosen port number. An HTTP 404 (Not Found) error is returned. Append /weatherforecast to the URL (change the URL to https://localhost:<port>/weatherforecast).

    JSON similar to the following is returned:

    "date": "2019-07-16T19:04:05.7257911-06:00", "temperatureC": 52, "temperatureF": 125, "summary": "Mild" "date": "2019-07-17T19:04:05.7258461-06:00", "temperatureC": 36, "temperatureF": 96, "summary": "Warm" "date": "2019-07-18T19:04:05.7258467-06:00", "temperatureC": 39, "temperatureF": 102, "summary": "Cool" "date": "2019-07-19T19:04:05.7258471-06:00", "temperatureC": 10, "temperatureF": 49, "summary": "Bracing" "date": "2019-07-20T19:04:05.7258474-06:00", "temperatureC": -1, "temperatureF": 31, "summary": "Chilly"

    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.

    Visual Studio Visual Studio Code Visual Studio for Mac
  • 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:

  • Right-click the Models folder, and select Add > New File > General > Empty Class.

  • Name the class TodoItem, and then click New.

  • Replace the template code with the following code:

    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.

    Visual Studio Visual Studio Code / Visual Studio for Mac

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

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

    Visual Studio Visual Studio Code / Visual Studio for Mac
  • 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.
  • Run the following commands:

    dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
    dotnet add package Microsoft.EntityFrameworkCore.Design
    dotnet tool install --global dotnet-aspnet-codegenerator
    dotnet tool update -g Dotnet-aspnet-codegenerator
    dotnet aspnet-codegenerator controller -name TodoItemsController -async -api -m TodoItem -dc TodoContext -outDir Controllers
    

    The preceding commands:

  • Add NuGet packages required for scaffolding.
  • Installs the scaffolding engine (dotnet-aspnet-codegenerator).
  • Scaffolds the TodoItemsController.
  • 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.

    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:

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

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

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

    Test the location header URI with Postman

  • Select the Headers tab in the Response pane.

  • Copy the Location header value:

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

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

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

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

    // 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;
            await _context.SaveChangesAsync();
        catch (DbUpdateConcurrencyException)
            if (!TodoItemExists(id))
                return NotFound();
                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":

    "id":1, "name":"feed fish", "isComplete":true

    The following image shows the Postman update:

    The DeleteTodoItem method

    Examine the DeleteTodoItem method:

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

    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:

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

    Update the TodoItemsController to use TodoItemDTO:

        [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;
                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:

  • Microsoft Entra ID
  • Azure Active Directory B2C (Azure AD B2C)
  • Duende Identity Server
  • 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:

  • Create web APIs with ASP.NET Core
  • Tutorial: Create a minimal API with ASP.NET Core
  • ASP.NET Core web API documentation with Swagger / OpenAPI
  • Razor Pages with Entity Framework Core in ASP.NET Core - Tutorial 1 of 8
  • Routing to controller actions in ASP.NET Core
  • Controller action return types in ASP.NET Core web API
  • Deploy ASP.NET Core apps to Azure App Service
  • Host and deploy ASP.NET Core
  • Create a web API with ASP.NET Core
  • Coming soon: Throughout 2024 we will be phasing out GitHub Issues as the feedback mechanism for content and replacing it with a new feedback system. For more information see: https://aka.ms/ContentUserFeedback.

    Submit and view feedback for

    This product
  •