Create a Web API with ASP.NET Core and Visual Studio for Mac

By Rick Anderson and Mike Wasson

In this tutorial, build a web API for managing a list of "to-do" items. The UI isn't constructed.

There are three versions of this tutorial:

Overview

This tutorial creates the following API:

API Description Request body Response body
GET /api/todo Get all to-do items None Array of to-do items
GET /api/todo/{id} Get an item by ID None To-do item
POST /api/todo Add a new item To-do item To-do item
PUT /api/todo/{id} Update an existing item   To-do item None
DELETE /api/todo/{id}     Delete an item     None None

The following diagram shows the basic design of the app.

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

  • The client is whatever consumes the web API (mobile app, browser, etc.). This tutorial doesn't create a client. Postman or curl is used as the client to test the app.

  • A model is an object that represents the data in the app. In this case, the only model is a to-do item. Models are represented as C# classes, also known as Plain Old CLR Object (POCOs).

  • A controller is an object that handles HTTP requests and creates the HTTP response. This app has a single controller.

  • To keep the tutorial simple, the app doesn't use a persistent database. The sample app stores to-do items in an in-memory database.

See Introduction to ASP.NET Core MVC on macOS or Linux for an example that uses a persistent database.

Prerequisites

Visual Studio for Mac

Create the project

From Visual Studio, select File > New Solution.

macOS New solution

Select .NET Core App > ASP.NET Core Web API > Next.

macOS New project dialog

Enter TodoApi for the Project Name, and then click Create.

config dialog

Launch the app

In Visual Studio, select Run > Start With Debugging to launch the app. Visual Studio launches a browser and navigates to http://localhost:5000. You get an HTTP 404 (Not Found) error. Change the URL to http://localhost:<port>/api/values. The ValuesController data is displayed:

["value1","value2"]

Add support for Entity Framework Core

Install the Entity Framework Core InMemory database provider. This database provider allows Entity Framework Core to be used with an in-memory database.

  • From the Project menu, select Add NuGet Packages.

    • Alternatively, you can right-click Dependencies, and then select Add Packages.
  • Enter EntityFrameworkCore.InMemory in the search box.

  • Select Microsoft.EntityFrameworkCore.InMemory, and then select Add Package.

Add a model class

A model is an object representing the data in your app. In this case, the only model is a to-do item.

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

new folder

Note

You can put model classes anywhere in your project, but the Models folder is used by convention.

Right-click the Models folder, and select Add > New File > General > Empty Class. Name the class TodoItem, and then click New.

Replace the generated code with:

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

The database generates the Id when a TodoItem is created.

Create the database context

The database context is the main class that coordinates Entity Framework functionality for a given data model. You create this class by deriving from the Microsoft.EntityFrameworkCore.DbContext class.

Add a TodoContext class to the Models folder.

using Microsoft.EntityFrameworkCore;

namespace TodoApi.Models
{
    public class TodoContext : DbContext
    {
        public TodoContext(DbContextOptions<TodoContext> options)
            : base(options)
        {
        }

        public DbSet<TodoItem> TodoItems { get; set; }
    }
}

Register the database context

In this step, the database context is registered with the dependency injection container. Services (such as the DB context) that are registered with the dependency injection (DI) container are available to the controllers.

Register the DB context with the service container using the built-in support for dependency injection. Replace the contents of the Startup.cs file with the following code:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using TodoApi.Models;

namespace TodoApi
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<TodoContext>(opt => 
                opt.UseInMemoryDatabase("TodoList"));
            services.AddMvc()
                    .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
        }

        public void Configure(IApplicationBuilder app)
        {
            app.UseMvc();
        }
    }
}
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using TodoApi.Models;

namespace TodoApi
{
    public class Startup
    {       
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<TodoContext>(opt => 
                opt.UseInMemoryDatabase("TodoList"));
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app)
        {
            app.UseMvc();
        }
    }
}

The preceding code:

  • Removes the unused code.
  • Specifies an in-memory database is injected into the service container.

Add a controller

In Solution Explorer, in the Controllers folder, add the class TodoController.

Replace the generated code with the following:

using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
using TodoApi.Models;

namespace TodoApi.Controllers
{
    [Route("api/[controller]")]
    public class TodoController : ControllerBase
    {
        private readonly TodoContext _context;

        public TodoController(TodoContext context)
        {
            _context = context;

            if (_context.TodoItems.Count() == 0)
            {
                _context.TodoItems.Add(new TodoItem { Name = "Item1" });
                _context.SaveChanges();
            }
        }       
    }
}

The preceding code defines an API controller class without methods. In the next sections, methods are added to implement the API.

using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
using TodoApi.Models;

namespace TodoApi.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class TodoController : ControllerBase
    {
        private readonly TodoContext _context;

        public TodoController(TodoContext context)
        {
            _context = context;

            if (_context.TodoItems.Count() == 0)
            {
                _context.TodoItems.Add(new TodoItem { Name = "Item1" });
                _context.SaveChanges();
            }
        }       
    }
}

The preceding code defines an API controller class without methods. In the next sections, methods are added to implement the API. The class is annotated with an [ApiController] attribute to enable some convenient features. For information on features enabled by the attribute, see Annotate class with ApiControllerAttribute.

The controller's constructor uses Dependency Injection to inject the database context (TodoContext) into the controller. The database context is used in each of the CRUD methods in the controller. The constructor adds an item to the in-memory database if one doesn't exist.

Get to-do items

To get to-do items, add the following methods to the TodoController class:

[HttpGet]
public List<TodoItem> GetAll()
{
    return _context.TodoItems.ToList();
}

[HttpGet("{id}", Name = "GetTodo")]
public IActionResult GetById(long id)
{
    var item = _context.TodoItems.Find(id);
    if (item == null)
    {
        return NotFound();
    }
    return Ok(item);
}
[HttpGet]
public ActionResult<List<TodoItem>> GetAll()
{
    return _context.TodoItems.ToList();
}

[HttpGet("{id}", Name = "GetTodo")]
public ActionResult<TodoItem> GetById(long id)
{
    var item = _context.TodoItems.Find(id);
    if (item == null)
    {
        return NotFound();
    }
    return item;
}

These methods implement the two GET methods:

  • GET /api/todo
  • GET /api/todo/{id}

Here's a sample HTTP response for the GetAll method:

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

Later in the tutorial, I'll show how the HTTP response can be viewed with Postman or curl.

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:

  • Take the template string in the controller's Route attribute:
namespace TodoApi.Controllers
{
    [Route("api/[controller]")]
    public class TodoController : ControllerBase
    {
        private readonly TodoContext _context;
namespace TodoApi.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class TodoController : ControllerBase
    {
        private readonly TodoContext _context;
  • Replace [controller] with the name of the controller, which is the controller class name minus the "Controller" suffix. For this sample, the controller class name is TodoController and the root name is "todo". ASP.NET Core routing is case insensitive.
  • If the [HttpGet] attribute has a route template (such as [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 GetById method, "{id}" is a placeholder variable for the unique identifier of the to-do item. When GetById is invoked, it assigns the value of "{id}" in the URL to the method's id parameter.

[HttpGet("{id}", Name = "GetTodo")]
public IActionResult GetById(long id)
{
    var item = _context.TodoItems.Find(id);
    if (item == null)
    {
        return NotFound();
    }
    return Ok(item);
}
[HttpGet("{id}", Name = "GetTodo")]
public ActionResult<TodoItem> GetById(long id)
{
    var item = _context.TodoItems.Find(id);
    if (item == null)
    {
        return NotFound();
    }
    return item;
}

Name = "GetTodo" creates a named route. Named routes:

  • Enable the app to create an HTTP link using the route name.
  • Are explained later in the tutorial.

Return values

The GetAll method returns a collection of TodoItem objects. MVC automatically serializes the object to JSON and writes the JSON into the body of the response message. The response code for this method is 200, assuming there are no unhandled exceptions. Unhandled exceptions are translated into 5xx errors.

In contrast, the GetById method returns the more general IActionResult type, which represents a wide range of return types. GetById has two different return types:

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

In contrast, the GetById method returns the ActionResult<T> type, which represents a wide range of return types. GetById has two different return types:

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

Launch the app

In Visual Studio, select Run > Start With Debugging to launch the app. Visual Studio launches a browser and navigates to http://localhost:<port>, where <port> is a randomly chosen port number. You get an HTTP 404 (Not Found) error. Change the URL to http://localhost:<port>/api/values. The ValuesController data is displayed:

["value1","value2"]

Navigate to the Todo controller at http://localhost:<port>/api/todo. The following JSON is returned:

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

Implement the other CRUD operations

We'll add Create, Update, and Delete methods to the controller. These methods are variations on a theme, so I'll just show the code and highlight the main differences. Build the project after adding or changing code.

Create

[HttpPost]
public IActionResult Create([FromBody] TodoItem item)
{
    if (item == null)
    {
        return BadRequest();
    }

    _context.TodoItems.Add(item);
    _context.SaveChanges();

    return CreatedAtRoute("GetTodo", new { id = item.Id }, item);
}

The preceding method responds to an HTTP POST, as indicated by the [HttpPost] attribute. The [FromBody] attribute tells MVC to get the value of the to-do item from the body of the HTTP request.

[HttpPost]
public IActionResult Create(TodoItem item)
{
    _context.TodoItems.Add(item);
    _context.SaveChanges();

    return CreatedAtRoute("GetTodo", new { id = item.Id }, item);
}

The preceding method responds to an HTTP POST, as indicated by the [HttpPost] attribute. MVC gets the value of the to-do item from the body of the HTTP request.

The CreatedAtRoute method returns a 201 response. It's the standard response for an HTTP POST method that creates a new resource on the server. CreatedAtRoute also adds a Location header to the response. The Location header specifies the URI of the newly created to-do item. See 10.2.2 201 Created.

Use Postman to send a Create request

  • Start the app (Run > Start With Debugging).
  • Open Postman.

Postman console

  • Update the port number in the localhost URL.
  • Set the HTTP method to POST.
  • Click the Body tab.
  • Select the raw radio button.
  • Set the type to JSON (application/json).
  • Enter a request body with a to-do item resembling the following JSON:
{
  "name":"walk dog",
  "isComplete":true
}
  • Click the Send button.

Tip

If no response displays after clicking Send, disable the SSL certification verification option. This is found under File > Settings. Click the Send button again after disabling the setting.

Click the Headers tab in the Response pane and copy the Location header value:

Headers tab of the Postman console

You can use the Location header URI to access the resource you created. The Create method returns CreatedAtRoute. The first parameter passed to CreatedAtRoute represents the named route to use for generating the URL. Recall that the GetById method created the "GetTodo" named route:

[HttpGet("{id}", Name = "GetTodo")]

Update

[HttpPut("{id}")]
public IActionResult Update(long id, [FromBody] TodoItem item)
{
    if (item == null || item.Id != id)
    {
        return BadRequest();
    }

    var todo = _context.TodoItems.Find(id);
    if (todo == null)
    {
        return NotFound();
    }

    todo.IsComplete = item.IsComplete;
    todo.Name = item.Name;

    _context.TodoItems.Update(todo);
    _context.SaveChanges();
    return NoContent();
}
[HttpPut("{id}")]
public IActionResult Update(long id, TodoItem item)
{
    var todo = _context.TodoItems.Find(id);
    if (todo == null)
    {
        return NotFound();
    }

    todo.IsComplete = item.IsComplete;
    todo.Name = item.Name;

    _context.TodoItems.Update(todo);
    _context.SaveChanges();
    return NoContent();
}

Update is similar to Create, but uses HTTP PUT. The response is 204 (No Content). According to the HTTP spec, a PUT request requires the client to send the entire updated entity, not just the deltas. To support partial updates, use HTTP PATCH.

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

Postman console showing 204 (No Content) response

Delete

[HttpDelete("{id}")]
public IActionResult Delete(long id)
{
    var todo = _context.TodoItems.Find(id);
    if (todo == null)
    {
        return NotFound();
    }

    _context.TodoItems.Remove(todo);
    _context.SaveChanges();
    return NoContent();
}

The response is 204 (No Content).

Postman console showing 204 (No Content) response

Call the Web API with jQuery

In this section, an HTML page is added that uses jQuery to call the Web API. jQuery initiates the request and updates the page with the details from the API's response.

Configure the project to serve static files and to enable default file mapping. This is accomplished by invoking the UseStaticFiles and UseDefaultFiles extension methods in Startup.Configure. For more information, see Static files.

public void Configure(IApplicationBuilder app)
{
    app.UseDefaultFiles();
    app.UseStaticFiles();
    app.UseMvc();
}

Add an HTML file, named index.html, to the project's wwwroot directory. Replace its contents with the following markup:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>To-do CRUD</title>
    <style>
        input[type='submit'], button, [aria-label] {
            cursor: pointer;
        }

        #spoiler {
            display: none;
        }

        table {
            font-family: Arial, sans-serif;
            border: 1px solid;
            border-collapse: collapse;
        }

        th {
            background-color: #0066CC;
            color: white;
        }

        td {
            border: 1px solid;
            padding: 5px;
        }
    </style>
</head>
<body>
    <h1>To-do CRUD</h1>
    <h3>Add</h3>
    <form action="javascript:void(0);" method="POST" onsubmit="addItem()">
        <input type="text" id="add-name" placeholder="New to-do">
        <input type="submit" value="Add">
    </form>

    <div id="spoiler">
        <h3>Edit</h3>
        <form class="my-form">
            <input type="hidden" id="edit-id">
            <input type="checkbox" id="edit-isComplete">
            <input type="text" id="edit-name">
            <input type="submit" value="Edit">
            <a onclick="closeInput()" aria-label="Close">&#10006;</a>
        </form>
    </div>

    <p id="counter"></p>

    <table>
        <tr>
            <th>Is Complete</th>
            <th>Name</th>
            <th></th>
            <th></th>
        </tr>
        <tbody id="todos"></tbody>
    </table>

    <script src="https://code.jquery.com/jquery-3.3.1.min.js"
            integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
            crossorigin="anonymous"></script>
    <script src="site.js"></script>
</body>
</html>

Add a JavaScript file, named site.js, to the project's wwwroot directory. Replace its contents with the following code:

const uri = 'api/todo';
let todos = null;
function getCount(data) {
    const el = $('#counter');
    let name = 'to-do';
    if (data) {
        if (data > 1) {
            name = 'to-dos';
        }
        el.text(data + ' ' + name);
    } else {
        el.html('No ' + name);
    }
}

$(document).ready(function () {
    getData();
});

function getData() {
    $.ajax({
        type: 'GET',
        url: uri,
        success: function (data) {
            $('#todos').empty();
            getCount(data.length);
            $.each(data, function (key, item) {
                const checked = item.isComplete ? 'checked' : '';

                $('<tr><td><input disabled="true" type="checkbox" ' + checked + '></td>' +
                    '<td>' + item.name + '</td>' +
                    '<td><button onclick="editItem(' + item.id + ')">Edit</button></td>' +
                    '<td><button onclick="deleteItem(' + item.id + ')">Delete</button></td>' +
                    '</tr>').appendTo($('#todos'));
            });

            todos = data;
        }
    });
}

function addItem() {
    const item = {
        'name': $('#add-name').val(),
        'isComplete': false
    };

    $.ajax({
        type: 'POST',
        accepts: 'application/json',
        url: uri,
        contentType: 'application/json',
        data: JSON.stringify(item),
        error: function (jqXHR, textStatus, errorThrown) {
            alert('here');
        },
        success: function (result) {
            getData();
            $('#add-name').val('');
        }
    });
}

function deleteItem(id) {
    $.ajax({
        url: uri + '/' + id,
        type: 'DELETE',
        success: function (result) {
            getData();
        }
    });
}

function editItem(id) {
    $.each(todos, function (key, item) {
        if (item.id === id) {
            $('#edit-name').val(item.name);
            $('#edit-id').val(item.id);
            $('#edit-isComplete')[0].checked = item.isComplete;
        }
    });
    $('#spoiler').css({ 'display': 'block' });
}

$('.my-form').on('submit', function () {
    const item = {
        'name': $('#edit-name').val(),
        'isComplete': $('#edit-isComplete').is(':checked'),
        'id': $('#edit-id').val()
    };

    $.ajax({
        url: uri + '/' + $('#edit-id').val(),
        type: 'PUT',
        accepts: 'application/json',
        contentType: 'application/json',
        data: JSON.stringify(item),
        success: function (result) {
            getData();
        }
    });

    closeInput();
    return false;
});

function closeInput() {
    $('#spoiler').css({ 'display': 'none' });
}

A change to the ASP.NET Core project's launch settings may be required to test the HTML page locally. Open launchSettings.json in the Properties directory of the project. Remove the launchUrl property to force the app to open at index.html—the project's default file.

There are several ways to get jQuery. In the preceding snippet, the library is loaded from a CDN. This sample is a complete CRUD example of calling the API with jQuery. There are additional features in this sample to make the experience richer. Below are explanations around the calls to the API.

Get a list of to-do items

To get a list of to-do items, send an HTTP GET request to /api/todo.

The jQuery ajax function sends an AJAX request to the API, which returns JSON representing an object or array. This function can handle all forms of HTTP interaction, sending an HTTP request to the specified url. GET is used as the type. The success callback function is invoked if the request succeeds. In the callback, the DOM is updated with the to-do information.

$(document).ready(function () {
    getData();
});

function getData() {
    $.ajax({
        type: 'GET',
        url: uri,
        success: function (data) {
            $('#todos').empty();
            getCount(data.length);
            $.each(data, function (key, item) {
                const checked = item.isComplete ? 'checked' : '';

                $('<tr><td><input disabled="true" type="checkbox" ' + checked + '></td>' +
                    '<td>' + item.name + '</td>' +
                    '<td><button onclick="editItem(' + item.id + ')">Edit</button></td>' +
                    '<td><button onclick="deleteItem(' + item.id + ')">Delete</button></td>' +
                    '</tr>').appendTo($('#todos'));
            });

            todos = data;
        }
    });
}

Add a to-do item

To add a to-do item, send an HTTP POST request to /api/todo/. The request body should contain a to-do object. The ajax function is using POST to call the API. For POST and PUT requests, the request body represents the data sent to the API. The API is expecting a JSON request body. The accepts and contentType options are set to application/json to classify the media type being received and sent, respectively. The data is converted to a JSON object using JSON.stringify. When the API returns a successful status code, the getData function is invoked to update the HTML table.

function addItem() {
    const item = {
        'name': $('#add-name').val(),
        'isComplete': false
    };

    $.ajax({
        type: 'POST',
        accepts: 'application/json',
        url: uri,
        contentType: 'application/json',
        data: JSON.stringify(item),
        error: function (jqXHR, textStatus, errorThrown) {
            alert('here');
        },
        success: function (result) {
            getData();
            $('#add-name').val('');
        }
    });
}

Update a to-do item

Updating a to-do item is very similar to adding one, since both rely on a request body. The only real difference between the two in this case is that the url changes to add the unique identifier of the item, and the type is PUT.

$.ajax({
    url: uri + '/' + $('#edit-id').val(),
    type: 'PUT',
    accepts: 'application/json',
    contentType: 'application/json',
    data: JSON.stringify(item),
    success: function (result) {
        getData();
    }
});

Delete a to-do item

Deleting a to-do item is accomplished by setting the type on the AJAX call to DELETE and specifing the item's unique identifier in the URL.

$.ajax({
    url: uri + '/' + id,
    type: 'DELETE',
    success: function (result) {
        getData();
    }
});

Next steps