Build web APIs with ASP.NET Core

By Scott Addie

View or download sample code (how to download)

This document explains how to build a web API in ASP.NET Core and when it's most appropriate to use each feature.

Derive class from ControllerBase

Inherit from the ControllerBase class in a controller that's intended to serve as a web API. For example:

[Produces("application/json")]
[Route("api/[controller]")]
public class PetsController : ControllerBase
{
    private readonly PetsRepository _repository;

    public PetsController(PetsRepository repository)
    {
        _repository = repository;
    }

    [HttpGet]
    public async Task<ActionResult<List<Pet>>> GetAllAsync()
    {
        return await _repository.GetPetsAsync();
    }

    [HttpGet("{id}")]
    [ProducesResponseType(404)]
    public async Task<ActionResult<Pet>> GetByIdAsync(int id)
    {
        var pet = await _repository.GetPetAsync(id);

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

        return pet;
    }

    [HttpPost]
    [ProducesResponseType(400)]
    public async Task<ActionResult<Pet>> CreateAsync(Pet pet)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        await _repository.AddPetAsync(pet);

        return CreatedAtAction(nameof(GetByIdAsync),
            new { id = pet.Id }, pet);
    }
}
[Produces("application/json")]
[Route("api/[controller]")]
public class PetsController : ControllerBase
{
    private readonly PetsRepository _repository;

    public PetsController(PetsRepository repository)
    {
        _repository = repository;
    }

    [HttpGet]
    [ProducesResponseType(typeof(IEnumerable<Pet>), 200)]
    public async Task<IActionResult> GetAllAsync()
    {
        var pets = await _repository.GetPetsAsync();

        return Ok(pets);
    }

    [HttpGet("{id}")]
    [ProducesResponseType(typeof(Pet), 200)]
    [ProducesResponseType(404)]
    public async Task<IActionResult> GetByIdAsync(int id)
    {
        var pet = await _repository.GetPetAsync(id);

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

        return Ok(pet);
    }

    [HttpPost]
    [ProducesResponseType(typeof(Pet), 201)]
    [ProducesResponseType(400)]
    public async Task<IActionResult> CreateAsync([FromBody] Pet pet)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        await _repository.AddPetAsync(pet);

        return CreatedAtAction(nameof(GetByIdAsync),
            new { id = pet.Id }, pet);
    }
}

The ControllerBase class provides access to several properties and methods. In the preceding code, examples include BadRequest(ModelStateDictionary) and CreatedAtAction(String, Object, Object). These methods are called within action methods to return HTTP 400 and 201 status codes, respectively. The ModelState property, also provided by ControllerBase, is accessed to handle request model validation.

Annotation with ApiController attribute

ASP.NET Core 2.1 introduces the [ApiController] attribute to denote a web API controller class. For example:

[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase

A compatibility version of 2.1 or later, set via SetCompatibilityVersion, is required to use this attribute at the controller level. For example, the highlighted code in Startup.ConfigureServices sets the 2.1 compatibility flag:

services.AddMvc()
        .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

For more information, see Compatibility version for ASP.NET Core MVC.

In ASP.NET Core 2.2 or later, the [ApiController] attribute can be applied to an assembly. Annotation in this manner applies web API behavior to all controllers in the assembly. Beware that there's no way to opt out for individual controllers. As a recommendation, assembly-level attributes should be applied to the Startup class:

[assembly: ApiController]
namespace WebApiSample.Api._22
{
    public class Startup
    {

A compatibility version of 2.2 or later, set via SetCompatibilityVersion, is required to use this attribute at the assembly level.

The [ApiController] attribute is commonly coupled with ControllerBase to enable REST-specific behavior for controllers. ControllerBase provides access to methods such as NotFound and File.

Another approach is to create a custom base controller class annotated with the [ApiController] attribute:

[ApiController]
public class MyBaseController
{
}

The following sections describe convenience features added by the attribute.

Automatic HTTP 400 responses

Model validation errors automatically trigger an HTTP 400 response. Consequently, the following code becomes unnecessary in your actions:

if (!ModelState.IsValid)
{
    return BadRequest(ModelState);
}

Use InvalidModelStateResponseFactory to customize the output of the resulting response.

Disabling the default behavior is useful when your action can recover from a model validation error. The default behavior is disabled when the SuppressModelStateInvalidFilter property is set to true. Add the following code in Startup.ConfigureServices after services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_<version_number>);:

services.AddMvc()
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressConsumesConstraintForFormFileParameters = true;
        options.SuppressInferBindingSourcesForParameters = true;
        options.SuppressModelStateInvalidFilter = true;
        options.SuppressMapClientErrors = true;

        options.ClientErrorMapping[404].Link = 
            "https://httpstatuses.com/404";
    });
services.Configure<ApiBehaviorOptions>(options =>
{
    options.SuppressConsumesConstraintForFormFileParameters = true;
    options.SuppressInferBindingSourcesForParameters = true;
    options.SuppressModelStateInvalidFilter = true;
});

With a compatibility flag of 2.2 or later, the default response type for HTTP 400 responses is ValidationProblemDetails. The ValidationProblemDetails type complies with the RFC 7807 specification. Set the SuppressUseValidationProblemDetailsForInvalidModelStateResponses property to true to instead return the ASP.NET Core 2.1 error format of SerializableError. Add the following code in Startup.ConfigureServices:

services.AddMvc()
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
    .ConfigureApiBehaviorOptions(options =>
    {
        options
          .SuppressUseValidationProblemDetailsForInvalidModelStateResponses = true;
    });

Binding source parameter inference

A binding source attribute defines the location at which an action parameter's value is found. The following binding source attributes exist:

Attribute Binding source
[FromBody] Request body
[FromForm] Form data in the request body
[FromHeader] Request header
[FromQuery] Request query string parameter
[FromRoute] Route data from the current request
[FromServices] The request service injected as an action parameter

Warning

Don't use [FromRoute] when values might contain %2f (that is /). %2f won't be unescaped to /. Use [FromQuery] if the value might contain %2f.

Without the [ApiController] attribute, binding source attributes are explicitly defined. In the following example, the [FromQuery] attribute indicates that the discontinuedOnly parameter value is provided in the request URL's query string:

[HttpGet]
public async Task<ActionResult<List<Product>>> GetAsync(
    [FromQuery] bool discontinuedOnly = false)
{
    List<Product> products = null;

    if (discontinuedOnly)
    {
        products = await _repository.GetDiscontinuedProductsAsync();
    }
    else
    {
        products = await _repository.GetProductsAsync();
    }

    return products;
}

Inference rules are applied for the default data sources of action parameters. These rules configure the binding sources you're otherwise likely to manually apply to the action parameters. The binding source attributes behave as follows:

  • [FromBody] is inferred for complex type parameters. An exception to this rule is any complex, built-in type with a special meaning, such as IFormCollection and CancellationToken. The binding source inference code ignores those special types. [FromBody] isn't inferred for simple types such as string or int. Therefore, the [FromBody] attribute should be used for simple types when that functionality is needed. When an action has more than one parameter explicitly specified (via [FromBody]) or inferred as bound from the request body, an exception is thrown. For example, the following action signatures cause an exception:
// Don't do this. All of the following actions result in an exception.
[HttpPost]
public IActionResult Action1(Product product, 
                             Order order) => null;

[HttpPost]
public IActionResult Action2(Product product, 
                             [FromBody] Order order) => null;

[HttpPost]
public IActionResult Action3([FromBody] Product product, 
                             [FromBody] Order order) => null;
  • [FromForm] is inferred for action parameters of type IFormFile and IFormFileCollection. It's not inferred for any simple or user-defined types.
  • [FromRoute] is inferred for any action parameter name matching a parameter in the route template. When more than one route matches an action parameter, any route value is considered [FromRoute].
  • [FromQuery] is inferred for any other action parameters.

The default inference rules are disabled when the SuppressInferBindingSourcesForParameters property is set to true. Add the following code in Startup.ConfigureServices after services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_<version_number>);:

services.AddMvc()
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressConsumesConstraintForFormFileParameters = true;
        options.SuppressInferBindingSourcesForParameters = true;
        options.SuppressModelStateInvalidFilter = true;
        options.SuppressMapClientErrors = true;

        options.ClientErrorMapping[404].Link = 
            "https://httpstatuses.com/404";
    });
services.Configure<ApiBehaviorOptions>(options =>
{
    options.SuppressConsumesConstraintForFormFileParameters = true;
    options.SuppressInferBindingSourcesForParameters = true;
    options.SuppressModelStateInvalidFilter = true;
});

Multipart/form-data request inference

When an action parameter is annotated with the [FromForm] attribute, the multipart/form-data request content type is inferred.

The default behavior is disabled when the SuppressConsumesConstraintForFormFileParameters property is set to true.

Add the following code in Startup.ConfigureServices:

services.AddMvc()
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressConsumesConstraintForFormFileParameters = true;
        options.SuppressInferBindingSourcesForParameters = true;
        options.SuppressModelStateInvalidFilter = true;
        options.SuppressMapClientErrors = true;

        options.ClientErrorMapping[404].Link = 
            "https://httpstatuses.com/404";
    });

Add the following code in Startup.ConfigureServices after services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);:

services.Configure<ApiBehaviorOptions>(options =>
{
    options.SuppressConsumesConstraintForFormFileParameters = true;
    options.SuppressInferBindingSourcesForParameters = true;
    options.SuppressModelStateInvalidFilter = true;
});

Attribute routing requirement

Attribute routing becomes a requirement. For example:

[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase

Actions are inaccessible via conventional routes defined in UseMvc or by UseMvcWithDefaultRoute in Startup.Configure.

Problem details responses for error status codes

In ASP.NET Core 2.2 or later, MVC transforms an error result (a result with status code 400 or higher) to a result with ProblemDetails. ProblemDetails is:

  • A type based on the RFC 7807 specification.
  • A standardized format for specifying machine-readable error details in an HTTP response.

Consider the following code in a controller action:

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

The HTTP response for NotFound has a 404 status code with a ProblemDetails body. For example:

{
    type: "https://tools.ietf.org/html/rfc7231#section-6.5.4",
    title: "Not Found",
    status: 404,
    traceId: "0HLHLV31KRN83:00000001"
}

The problem details feature requires a compatibility flag of 2.2 or later. The default behavior is disabled when the SuppressMapClientErrors property is set to true. Add the following code in Startup.ConfigureServices:

services.AddMvc()
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressConsumesConstraintForFormFileParameters = true;
        options.SuppressInferBindingSourcesForParameters = true;
        options.SuppressModelStateInvalidFilter = true;
        options.SuppressMapClientErrors = true;

        options.ClientErrorMapping[404].Link = 
            "https://httpstatuses.com/404";
    });

Use the ClientErrorMapping property to configure the contents of the ProblemDetails response. For example, the following code updates the type property for 404 responses:

services.AddMvc()
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressConsumesConstraintForFormFileParameters = true;
        options.SuppressInferBindingSourcesForParameters = true;
        options.SuppressModelStateInvalidFilter = true;
        options.SuppressMapClientErrors = true;

        options.ClientErrorMapping[404].Link = 
            "https://httpstatuses.com/404";
    });

Additional resources