diff --git a/maturity-level-two/README.md b/maturity-level-two/README.md index 66ef997..1dfed86 100644 --- a/maturity-level-two/README.md +++ b/maturity-level-two/README.md @@ -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) @@ -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 +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)). diff --git a/maturity-level-two/docs/content-negotiation.md b/maturity-level-two/docs/content-negotiation.md new file mode 100644 index 0000000..b1bf9a7 --- /dev/null +++ b/maturity-level-two/docs/content-negotiation.md @@ -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(); + var jsonInputFormatter = jsonInputFormatters.First(); + options.InputFormatters.Clear(); + options.InputFormatters.Add(jsonInputFormatter); + var jsonOutputFormatters = options.OutputFormatters.OfType(); + 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. + ```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 +/// +/// Create a car +/// +/// New car information +/// Create a car +/// a Car instance +[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 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: +```csharp + /// +/// Get all cars +/// +/// /// Filter a specific body Type (optional) +/// Get all cars +/// List of cars +[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 GetCars([FromQuery] CarBodyType? bodyType) +{ + return File(GetCars(), "application/pdf", "carlist.pdf"); +} +``` + +## Error response codes +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. + +