ASP.NET Core 2.1.0-preview1: Improvements for building Web APIs

ASP.NET Core 2.1 adds a number of features that make it easier and more convenient to build Web APIs. These features include Web API controller specific conventions, more robust input processing and error handling, and JSON patch improvements.

Please note that some of these features require enabling MVC compatibility with 2.1, so be sure to check out the post on MVC compatibility versions as well.

[ApiController] and ActionResult<T>

ASP.NET Core 2.1 introduces new Web API controller specific conventions that make Web API development more convenient. These conventions can be applied to a controller using the new [ApiController] attribute:

  • Automatically respond with a 400 when validation errors occur – no need to check the model state in your action method
  • Infer smarter defaults for action parameters: [FromBody] for complex types, [FromRoute] when possible, otherwise [FromQuery]
  • Require attribute routing – actions are not accessible by convention-based routes

You can also now return ActionResult<T> from your Web API actions, which allows you to return arbitrary action results or a specific return type (thanks to some clever use of implicit cast operators). Most Web API action methods have a specific return type, but also need to be able to return multiple different action results.

Here’s an example Web API controller that uses these new enhancements:

[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
    private readonly ProductsRepository _repository;

    public ProductsController(ProductsRepository repository)
    {
        _repository = repository;
    }

    [HttpGet]
    public IEnumerable<Product> Get()
    {
        return _repository.GetProducts();
    }

    [HttpGet("{id}")]
    public ActionResult<Product> Get(int id)
    {
        if (!_repository.TryGetProduct(id, out var product))
        {
            return NotFound();
        }
        return product;
    }

    [HttpPost]
    [ProducesResponseType(201)]
    public ActionResult<Product> Post(Product product)
    {
        _repository.AddProduct(product);
        return CreatedAtAction(nameof(Get), new { id = product.Id }, product);
    }
}

Because these conventions are more descriptive tools like Swashbuckle or NSwag can do a better job generating an OpenAPI specification for this Web API that includes information like return types, parameter sources, and possible error responses without needing addition attributes.

Better input processing

ASP.NET Core 2.1 does a much better job of providing appropriate error information when the request body fails to deserialize or the JSON is invalid.

For example, in ASP.NET Core 2.0 if your Web API received a request with a JSON property that had the wrong type (like a string instead of an int) you get a generic error message, like this:

{
  "count": [
    "The input was not valid."
  ]
}

In 2.1 we provide more detailed error information about what was wrong with the request including path and line number information:

{
  "count": [
    "Could not convert string to integer: abc. Path 'count', line 1, position 16."
  ]
}

Similarly, if the request is syntactically invalid (ex. missing a curly brace) then 2.1 will let you know:

{
  "": [
    "Unexpected end when reading JSON. Path '', line 1, position 1."
  ]
}

You can also now add validation attributes to top level parameters of your action method. For example, you can mark a query string parameter as required like this:

[HttpGet("test/{testId}")]
public ActionResult<TestResult> Get(string testId, [Required]string name)

Problem Details

In this release we added support for RFC 7808 – Problem Details for HTTP APIs as a standardized format for returning machine readable error responses from HTTP APIs.

To update your Web API controllers to return Problem Details responses for invalid requests you can add the following code to your ConfigureServices method:

services.Configure<ApiBehaviorOptions>(options =>
{
    options.InvalidModelStateResponseFactory = context =>
    {
        var problemDetails = new ValidationProblemDetails(context.ModelState)
        {
            Instance = context.HttpContext.Request.Path,
            Status = StatusCodes.Status400BadRequest,
            Type = "https://asp.net/core",
            Detail = "Please refer to the errors property for additional details."
        };
        return new BadRequestObjectResult(problemDetails)
        {
            ContentTypes = { "application/problem+json", "application/problem+xml" }
        };
    };
});

You can also return a Problem Details response from your API action for an invalid request using the ValidationProblem() helper method.

An example Problem Details response for an invalid request looks like this (where the content type is application/problem+json):

{
  "errors": {
    "Text": [
      "The Text field is required."
    ]
  },
  "type": "https://asp.net/core",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "detail": "Please refer to the errors property for additional details.",
  "instance": "/api/values"
}

JSON Patch improvements

JSON Patch defines a JSON document structure for implementing HTTP PATCH semantics. A JSON Patch document defines a sequence of operations (add, remove, replace, copy, etc.) that can be applied to a JSON resource.

ASP.NET Core has supported JSON Patch since it first shipped, but in 2.1 we've added support for the test operation. The test operation allows to check for specific values before applying the patch. If any test operations fail then the whole patch fails.

A Web API controller action that supports JSON Patch looks like this:

[HttpPatch("{id}")]
public ActionResult<Value> Patch(int id, JsonPatchDocument<Value> patch)
{
    var value = new Value { ID = id, Text = "Do" };

    patch.ApplyTo(value, ModelState);

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

    return value;
}

Where the Value type is defined as follows:

public class Value
{
    public int ID { get; set; }

    public string Text { get; set; }

    public IDictionary<int, string> Status { get; } = new Dictionary<int, string>();
}

The following JSON Patch request successfully adds a value to the Status dictionary (note that we've also added support for non-string dictionary keys, like int, Guid, etc.):

Successful request

[
  { "op": "test", "path": "/text", "value": "Do" },
  { "op": "add", "path": "/status/1", "value": "Done!" }
]

Successful response

{
  "id": 123,
  "text": "Do",
  "status": {
    "1": "Done!"
  }
}

Conversely the following JSON Patch request fails because the value of the text property doesn't match:

Failed request

[
  { "op": "test", "path": "/text", "value": "Do not" },
  { "op": "add", "path": "/status/1", "value": "Done!" }
]

Failed response

{
  "Value": [
    "The current value 'Do' at path 'text' is not equal to the test value 'Do not'."
  ]
}

Summary

We hope you enjoy these Web API improvements. Please give them a try and let us know what you think. If you hit any issues or have feedback please file issues on GitHub.