Skip to content
This repository has been archived by the owner on Nov 13, 2021. It is now read-only.

Provide guideline for content negotiation #135

Open
wants to merge 27 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9514baa
Update startup
FilipVanRaemdonck Oct 8, 2019
7ce78c7
Update formatter usage
FilipVanRaemdonck Oct 9, 2019
927802c
Fix typo
FilipVanRaemdonck Oct 9, 2019
6814c3c
rename
FilipVanRaemdonck Oct 9, 2019
bbaa4ed
Restructure
FilipVanRaemdonck Oct 9, 2019
68b3900
update structure
FilipVanRaemdonck Oct 9, 2019
9c3c5c8
Fix typo
FilipVanRaemdonck Oct 9, 2019
5682ea1
Fix typo
FilipVanRaemdonck Oct 9, 2019
8b7c9b2
typos
FilipVanRaemdonck Oct 9, 2019
5960ab1
typos
FilipVanRaemdonck Oct 9, 2019
2b4433a
Structure readme
FilipVanRaemdonck Oct 9, 2019
3b499e7
Updates in readme
FilipVanRaemdonck Oct 9, 2019
15fb8de
Update readme
FilipVanRaemdonck Oct 9, 2019
7e2b4a9
Review PR comments
FilipVanRaemdonck Oct 9, 2019
b7d8526
Add suggestions to improve readability
FilipVanRaemdonck Oct 9, 2019
969eb21
Update createCar example
FilipVanRaemdonck Oct 9, 2019
cb0fcee
Update - add CompaitbilityVersion
FilipVanRaemdonck Oct 10, 2019
c31adb6
Merge with master
FilipVanRaemdonck Oct 10, 2019
9bdd199
add summary + move long text to separate md
FilipVanRaemdonck Oct 10, 2019
8dca836
Undo code changes for readme
FilipVanRaemdonck Oct 10, 2019
23528ce
Update PR review
FilipVanRaemdonck Oct 10, 2019
4abfa89
Fix sentence in readme
FilipVanRaemdonck Oct 10, 2019
2df1802
Review readme contents
FilipVanRaemdonck Oct 10, 2019
fb67085
Remove startup changes + update readme
FilipVanRaemdonck Oct 10, 2019
04afee2
Add links to more information
FilipVanRaemdonck Oct 10, 2019
3cb9690
Review PR comments
FilipVanRaemdonck Oct 10, 2019
d2af233
Undo coding changes
FilipVanRaemdonck Oct 10, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions maturity-level-two/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Practical API Guidelines - Should have

1. [Validating OpenAPI Specifications](docs/validating-open-api-specs.md)
1. [Content Negotiation](docs/content-negotiation.md)
1. [APISecurity](docs/api-security.md)


Expand All @@ -14,6 +15,24 @@ You should:
- Validate changes to your OpenAPI specs to avoid specification violations ([user guide](docs/validating-open-api-specs.md))
- Unit test Open API validation to automatically detect breaking changes

## Content negotiation
MassimoC marked this conversation as resolved.
Show resolved Hide resolved
With [content negotiation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation) a consumer specifies in which format he/she will communicate (send and receive data) with the server. You can do this by using the following headers in your request:
- `Content-Type` - Specify the format of the payload you send to the server.
- `Accept` - Specify your preferred payload format(s) of the server response. A default format will be used when this header is not specified. Also other accept headers can be used, you can find more info about this [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation).

When you send a `Content-Type` the server doesn't understand, the server should return an HTTP 415: Unsupported Media Type. If the server cannot respond to your request in a format specified in your `Accept` header, it will return an HTTP 406: Not Acceptable.

When adding Content-Negotiation to your project you should:
* Think whether content negotiation is really necessary. For most of the cases you only need JSON and thus no content negotiation is needed.
* Remove input and output formatters when multi-format (JSON, XML, CSV, ...) is not necessary.
* Carefully evaluate whether you should use the [Produces] and [Consumes] attributes to further restrict the supported request and response media types for one specific acion or controller.
* [Produces] and [Consumes] are not meant to document the supported media types for all actions.
* It is strongly advised to not use these attributes if you are supporting more than one media type (e.g. application/json and application/problem+json).

Notes:
* `Content-Type` is not needed for HTTP GET requests since a GET request has no request body.
* If you want to explicitly specify which content types are produced/consumed in your swagger file, we advise to use a custom attribute (to be checked whether something).

## API Security
API security is an essential part when designing the API. All different levels of security are discussed within the API-Security document ([user guide](docs/api-security.md)).

Expand Down
70 changes: 70 additions & 0 deletions maturity-level-two/docs/content-negotiation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Content Negotiation

When no specific compatibility requirements regarding the rest request and response formats are set, it is recommended to use the JSON format (application/json). However in some situations a client might be restricted in the payload formats it can send to and receive from the server. With [content negotiation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation) we determine what format we'd like to use requests & response payloads. For REST API's, the most common formats are JSON and XML.
As an API consumer, you can specify what format you are expecting by adding HTTP headers:
- `Content-Type` - Specify the format of the payload you send to the server.
- `Accept` - Specify your preferred payload format(s) of the server response. A default format will be used when this header is not specified.
When a format is not supported, the API should return an [HTTP 406 Not Acceptable](https://httpstatuses.com/406).
Because of it's lightness and fastness, JSON has become the standard over the last couple of years but in certain situations other formats still have their advantages. In addition to this, ASP.NET Core also uses some additional formatters for special cases, such as the TextOutputFormatter and the HttpNoContentOutputFormatter.

When you would like to make sure your api only uses the JSON format, you can specify this in the startup of your ASP.NET Core project. This will make sure you'll refuse all non-JSON 'Accept' headers, including the plain text headers (on these requests you will return response code 406). If no 'Accept' header is specified you can return JSON as it is the only supported type. You can find an example of the changes you have to do in the startup example below:
```csharp
services.AddMvc(options =>
{
var jsonInputFormatters = options.InputFormatters.OfType<JsonInputFormatter>();
var jsonInputFormatter = jsonInputFormatters.First();
options.InputFormatters.Clear();
options.InputFormatters.Add(jsonInputFormatter);
var jsonOutputFormatters = options.OutputFormatters.OfType<JsonOutputFormatter>();
var jsonOutputFormatter = jsonOutputFormatters.First();
options.OutputFormatters.Clear();
options.OutputFormatters.Add(jsonOutputFormatter);
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
```
In here we have to add the 'SetCompatibilityVersion'as well to make sure the supported formats are documented correctly in e.g. the swagger. In case you'd like to add other formatting possibilities, it is possible to add these formatters to your api formatters. In case of xml you can use the XmlSerializerInputFormatter and the XmlSerializerOutputFormatter or add the xml formatters using the Mvc. With this approach the default format is still JSON.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One question : just imagine that i want to return XML for only a single operation. What should i do?
A better scenario is : I have an API with multiple operations, one of those is a GET that returns a list of items.
I want to be able to get the list of items in "text/csv" what should be the approach?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can uses the [Produces("text/csv") but this will not allow the "text/csv" as an acceptable "accept' header..

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry that was a mistake, you just shouldn't return an ok(...) and then you can work with the [Produces]

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I now added some information about it

```csharp
services.AddMvc()
.AddNewtonsoftJson()
.AddXmlSerializerFormatters();
```
Be aware that the formatters you specify in the above section are all the formatters your api will know. Thus if an api call is done towards an action in which an unknown request or response format is used/requested, the api will not answer the call with a success status code but rather with an HTTP 406, Not Acceptable, or an HTTP 415, Unsupported.

You can (not should) further restrict the request and respnse formats for one specific acion or controller by using the [Produces] and [Consumes] attributes. However you should be careful when using these attributes: if you use these attributes your method will not be able to return another response format then format specified in your attribute. If you return another response format the content-type of your response will be overwritten.
```csharp
/// <summary>
/// Create a car
/// </summary>
/// <param name="newCarRequest">New car information</param>
/// <remarks>Create a car</remarks>
/// <returns>a Car instance</returns>
[Produces("application/json")]
[Consumes("application/json")]
[HttpPost(Name = Constants.RouteNames.v1.CreateCar)]
[SwaggerResponse((int)HttpStatusCode.OK, "Car created", typeof(CarCreatedDto))]
[SwaggerResponse((int)HttpStatusCode.Conflict, "Car already exists")]
[SwaggerResponse((int)HttpStatusCode.InternalServerError, "API is not available")]
public async Task<IActionResult> CreateCar([FromBody] NewCarRequest newCarRequest)
```

In case you have an action which returns media type(s) only this action will return, you can use the [Produces] and [Consumes] keywords too. But be aware that in this case your api might not know how it should serialize the response, so you might have to take care of this yourself. In order to do so you can return a ContentResult (e.g. FileContentResult or ContentResult in the Microsoft.AspNetCore.Mvc namespace). An example is given below:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case you have an action which returns media type(s) only this action will return, you can use the [Produces] and [Consumes] keywords too.

Not clear

```csharp
/// <summary>
/// Get all cars
/// </summary>
/// /// <param name="bodyType">Filter a specific body Type (optional)</param>
/// <remarks>Get all cars</remarks>
/// <returns>List of cars</returns>
[HttpGet(Name = Constants.RouteNames.v1.GetCars)]
[SwaggerResponse((int)HttpStatusCode.OK, "List of Cars")]
[SwaggerResponse((int)HttpStatusCode.InternalServerError, "API is not available")]
// [Produces("application/json", "application/problem+json")]
public async Task<IActionResult> GetCars([FromQuery] CarBodyType? bodyType)
{
return File(GetCars(), "application/pdf", "carlist.pdf");
}
```

## Error response codes
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why "codes"? It's more error response format

By default error response codes in ASP.NET Core will use the application/problem+xml or application/problem+json content types. These return types will usually work well with the above mentioned way of working: if you remove the xml from the supported formats, your method will return a json content type instead. However the use of the content type application/problem+json will conflict with the use of the [Produces] attribute: the [Produces] attribute will overwrite the content type from your error response and set it to application/json.