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

Daniel Roth

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

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:

[<span class="hljs-name">Route</span>(<span class="hljs-string">"api/[controller]"</span>)]
[<span class="hljs-name">ApiController</span>]
public class ProductsController : ControllerBase
{
    private readonly ProductsRepository _repository<span class="hljs-comment">;</span>

    public ProductsController(<span class="hljs-name">ProductsRepository</span> repository)
    {
        _repository = repository<span class="hljs-comment">;</span>
    }

    [<span class="hljs-name">HttpGet</span>]
    public IEnumerable<Product> Get()
    {
        return _repository.GetProducts()<span class="hljs-comment">;</span>
    }

    [<span class="hljs-name">HttpGet</span>(<span class="hljs-string">"{id}"</span>)]
    public ActionResult<Product> Get(<span class="hljs-name">int</span> id)
    {
        if (<span class="hljs-name">!_repository.TryGetProduct</span>(<span class="hljs-name">id</span>, out var product))
        {
            return NotFound()<span class="hljs-comment">;</span>
        }
        return product<span class="hljs-comment">;</span>
    }

    [<span class="hljs-name">HttpPost</span>]
    [<span class="hljs-name">ProducesResponseType</span>(<span class="hljs-name">201</span>)]
    public ActionResult<Product> Post(<span class="hljs-name">Product</span> product)
    {
        _repository.AddProduct(<span class="hljs-name">product</span>)<span class="hljs-comment">;</span>
        return CreatedAtAction(<span class="hljs-name">nameof</span>(<span class="hljs-name">Get</span>), new { id = product.Id }, product)<span class="hljs-comment">;</span>
    }
}

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:

{
  <span class="hljs-attr">"count"</span>: [
    <span class="hljs-string">"The input was not valid."</span>
  ]
}

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

{
  <span class="hljs-attr">"count"</span>: [
    <span class="hljs-string">"Could not convert string to integer: abc. Path 'count', line 1, position 16."</span>
  ]
}

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

{
  <span class="hljs-attr">""</span>: [
    <span class="hljs-string">"Unexpected end when reading JSON. Path '', line 1, position 1."</span>
  ]
}

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:

[<span class="hljs-name">HttpGet</span>(<span class="hljs-string">"test/{testId}"</span>)]
public ActionResult<TestResult> Get(<span class="hljs-name"><span class="hljs-builtin-name">string</span></span> testId, [<span class="hljs-name">Required</span>]string name)

Problem Details

In this release we added support for RFC 7807 – 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 = <span class="hljs-built_in">context</span> =>
    {
        var problemDetails = new ValidationProblemDetails(<span class="hljs-built_in">context</span>.ModelState)
        {
            <span class="hljs-keyword">Instance </span>= <span class="hljs-built_in">context</span>.HttpContext.Request.Path,
            Status = StatusCodes.Status<span class="hljs-number">400B</span>adRequest,
            Type = <span class="hljs-string">"https://asp.net/core"</span>,
            Detail = <span class="hljs-string">"Please refer to the errors property for additional details."</span>
        }<span class="hljs-comment">;</span>
        return new <span class="hljs-keyword">BadRequestObjectResult(problemDetails)
</span>        {
            ContentTypes = { <span class="hljs-string">"application/problem+json"</span>, <span class="hljs-string">"application/problem+xml"</span> }
        }<span class="hljs-comment">;</span>
    }<span class="hljs-comment">;</span>
})<span class="hljs-comment">;</span>

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

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

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(<span class="hljs-string">"{id}"</span>)]
<span class="hljs-function"><span class="hljs-keyword">public</span> ActionResult<Value> <span class="hljs-title">Patch</span>(<span class="hljs-params"><span class="hljs-keyword">int</span> id, JsonPatchDocument<Value> patch</span>)
</span>{
    <span class="hljs-keyword">var</span> <span class="hljs-keyword">value</span> = <span class="hljs-keyword">new</span> Value { ID = id, Text = <span class="hljs-string">"Do"</span> };

    patch.ApplyTo(<span class="hljs-keyword">value</span>, ModelState);

    <span class="hljs-keyword">if</span> (!ModelState.IsValid)
    {
        <span class="hljs-keyword">return</span> BadRequest(ModelState);
    }

    <span class="hljs-keyword">return</span> <span class="hljs-keyword">value</span>;
}

Where the Value type is defined as follows:

<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">Value</span>
{
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> ID { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }

    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> Text { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }

    <span class="hljs-keyword">public</span> IDictionary<<span class="hljs-keyword">int</span>, <span class="hljs-keyword">string</span>> Status { <span class="hljs-keyword">get</span>; } = <span class="hljs-keyword">new</span> Dictionary<<span class="hljs-keyword">int</span>, <span class="hljs-keyword">string</span>>();
}

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

[
  { <span class="hljs-attr">"op"</span>: <span class="hljs-string">"test"</span>, <span class="hljs-attr">"path"</span>: <span class="hljs-string">"/text"</span>, <span class="hljs-attr">"value"</span>: <span class="hljs-string">"Do"</span> },
  { <span class="hljs-attr">"op"</span>: <span class="hljs-string">"add"</span>, <span class="hljs-attr">"path"</span>: <span class="hljs-string">"/status/1"</span>, <span class="hljs-attr">"value"</span>: <span class="hljs-string">"Done!"</span> }
]

Successful response

{
  <span class="hljs-attr">"id"</span>: <span class="hljs-number">123</span>,
  <span class="hljs-attr">"text"</span>: <span class="hljs-string">"Do"</span>,
  <span class="hljs-attr">"status"</span>: {
    <span class="hljs-attr">"1"</span>: <span class="hljs-string">"Done!"</span>
  }
}

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

Failed request

[
  { <span class="hljs-attr">"op"</span>: <span class="hljs-string">"test"</span>, <span class="hljs-attr">"path"</span>: <span class="hljs-string">"/text"</span>, <span class="hljs-attr">"value"</span>: <span class="hljs-string">"Do not"</span> },
  { <span class="hljs-attr">"op"</span>: <span class="hljs-string">"add"</span>, <span class="hljs-attr">"path"</span>: <span class="hljs-string">"/status/1"</span>, <span class="hljs-attr">"value"</span>: <span class="hljs-string">"Done!"</span> }
]

Failed response

{
  "Value": [
    "The current value '<span class="hljs-keyword">Do</span>' <span class="hljs-built_in">at</span> <span class="hljs-built_in">path</span> 'text' is <span class="hljs-keyword">not</span> equal to the test value '<span class="hljs-keyword">Do</span> <span class="hljs-keyword">not</span>'."
  ]
}

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.

4 comments

Discussion is closed. Login to edit/delete existing comments.

  • Angus Beare 0

    I’m porting an ASP.NET app over to .Net core. Problem is that the models have non nullable Guid’s. In the old version you could pass a null in the model and the binder would not complain. It would just create an empty Guid.  But in this .Net Core version (now using 3 preview) the controller immediately returns a 400. I don’t want to have to change all the client js to pass in an empty Guid. I could set the model Guids to Guid? which works but that breaks our standard of models matching the DB structures.  What do you suggest?

  • Willem Bos 0

    I’m localizing my ASP NET Core 2.2 Web API and that’s working fine, but the only message I cannot localize is the message: “The input was not valid.”.
    In the startup I set AllowInputFormatterExceptionMessages = false to get the generic message. I don’t want the detailed information.
    I searched and tried everything I could find on the internet, but nothing is working.
    I’ve changed all the properties of the ModelBindingMessageProvider, but with no effect.
    I use the IStringLocalizer functionality to localize the Web API.
    Do you have a sollution?

  • Michael Johnstone 0

    Hi, thanks for the info. I noticed a typo when trying to search through the document. The specified standard is RFC 7807 which links correctly to the document, but the hyperlink text is incorrect (mentions RFC 7808)

    • Daniel RothMicrosoft employee 0

      Thanks for pointing this out! Should be fixed now.

Feedback usabilla icon