We're having discussion around some of the implementation. We welcome your feedback!
A carefully-designed RESTful web API defines the resources, relationships, and navigation schemes that are accessible to client applications. When you implement and deploy a web API, you should consider the physical requirements of the environment hosting the web API and the way in which the web API is constructed rather than the logical structure of the data. This guidance focusses on best practices for implementing a web API and publishing it to make it available to client applications. Security concerns are described separately in the API Security Guidance document. You can find detailed information about web API design in the API Design Guidance document.
The following sections illustrate best practice for using the ASP.NET Web API template to build a RESTful web API. For detailed information on using the Web API template, visit the Learn About ASP.NET Web API page on the Microsoft website.
Note: This guidance is accompanied by a sample solution that provides a complete, working solution of the principles described in the following sections. The solution is called Orders, and it includes a web API, test client, and unit tests. The web API is based on the scenario of an e-commerce system and exposes information about customers, orders, and products.
In a service implemented by using the ASP.NET Web API, each request is routed to a method in a controller class. The Web API framework provides two primary options for implementing routing; convention-based routing and attribute-based routing. Consider the following points when you determine the best way to route requests in your web API:
-
Understand the limitations and requirements of convention-based routing.
By default, the Web API framework uses convention-based routing. The Web API framework creates an initial routing table that contains the following entry:
config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } );
Routes can be generic, comprising literals such as api and variables such as {controller} and {id}. Convention-based routing allows some elements of the route to be optional. The Web API framework determines which method to invoke in the controller by matching the HTTP verb in the request to the initial part of the method name, and then by matching any optional parameters. For example, if a controller named orders contains the methods GetAllOrders() or GetOrderByInt(int id) then the GET request http://www.adventure-works.com/api/orders/ will be directed to the method GetAlllOrders() and the GET request http://www.adventure-works.com/api/orders/99 will be routed to the method GetOrderByInt(int id). If there is no matching method available that begins with the prefix Get in the controller, the Web API framework replies with an HTTP 405 (Method Not Allowed) message. Additionally, name of the parameter (id) specified in the routing table must be the same as the name of the parameter for the GetOrderById method, otherwise the Web API framework will reply with an HTTP 404 (Not Found) response.
The same rules apply to POST, PUT, and DELETE HTTP requests; a PUT request that updates the details of order 101 would be directed to the URI http://www.adventure-works.com/api/orders/101, the body of the message will contain the new details of the order, and this information will be passed as a parameter to a method in the orders controller with a name that starts with the prefix Put, such as PutOrder.
The default routing table will not match a request that references child resources in a RESTful web API, such as http://www.adventure-works.com/api/customers/1/orders (find the details of all orders placed by customer 1). To handle these cases, you can add custom routes to the routing table:
config.Routes.MapHttpRoute( name: "CustomerOrdersRoute", routeTemplate: "api/customers/{custId}/orders", defaults: new { controller="Customers", action="GetOrdersForCustomer" }) );
This route directs requests that match the URI to the GetOrdersForCustomer method in the Customers controller. This method must take a single parameter named custI:
public class CustomersController : ApiController { ... public IEnumerable<Order> GetOrdersForCustomer(int custId) { // Find orders for the specified customer var orders = ... return orders; } ... }
Tip: Utilize the default routing wherever possible and avoid defining many complicated custom routes as this can result in brittleness (it is very easy to add methods to a controller that result in ambiguous routes) and reduced performance (the bigger the routing table, the more work the Web API framework has to do to work out which route matches a given URI). Keep the API and routes simple. For more information, see the section Organizing the Web API Around Resources in the API Design Guidance. If you must define custom routes, a preferable approach is to use attribute-based routing described later in this section.
For more information about convention-based routing, see the page Routing in ASP.NET Web API on the Microsoft website.
-
Avoid ambiguity in routing.
Convention-based routing can result in ambiguous pathways if multiple methods in a controller match the same route. In these situations, the Web API framework responds with an HTTP 500 (Internal Server Error) response message containing the text "Multiple actions were found that match the request".
-
Prefer attribute-based routing.
Attribute-based routing provides an alternative means for connecting routes to methods in a controller. Rather than relying on the pattern-matching features of convention-based routing, you can explicitly annotate methods in a controller with the details of the route to which they should be associated. This approach help to remove possible ambiguities. Furthermore, as explicit routes are defined at design time this approach is more efficient than convention-based routing at runtime. The following code shows how to apply the Route attribute to methods in the Customers controller. These methods also use the HttpGet attribute to indicate that they should respond to HTTP GET requests. This attribute enables you to name your methods using any convenient naming scheme rather than that expected by convention-based routing. You can also annotate methods with the HttpPost, HttpPut, and HttpDelete attributes to define methods that respond to other types of HTTP requests.
public class CustomersController : ApiController { ... [Route("api/customers/{id}")] [HttpGet] public Customer FindCustomerByID(int id) { // Find the matching customer var customer = ... return customer; } ... [Route("api/customers/{id}/orders")] [HttpGet] public IEnumerable<Order> FindOrdersForCustomer(int id) { // Find orders for the specified customer var orders = ... return orders; } ... }
Attribute-based routing also has the useful side-effect of acting as documentation for developers needing to maintain the code in the future; it is immediately clear which method belongs to which route, and the HttpGet attribute clarifies the type of HTTP request to which the method responds.
Attribute-based routing enables you to define constraints which restrict how the parameters are matched. Constraints can specify the type of the parameter, and in some cases they can also indicate the acceptable range of parameter values. In the following example, the id parameter to the FindCustomerByID method must be a non-negative integer. If an application submits an HTTP GET request with a negative customer number, the Web API framework will respond with an HTTP 405 (Method Not Allowed) message:
public class CustomersController : ApiController { ... [Route("api/customers/{id:int:min(0)}")] [HttpGet] public Customer FindCustomerByID(int id) { // Find the matching customer var customer = ... return customer; } ... }
For more information on attribute-based routing, see the page Attribute Routing in Web API 2 on the Microsoft website.
-
Support Unicode characters in routes.
The keys used to identify resources in GET requests could be strings. Therefore, in a global application, you may need to support URIs that contain non-English characters.
-
Distinguish methods that should not be routed.
If you are using convention-based routing, indicate methods that do not correspond to HTTP actions by decorating them with the NonAction attribute. This typically applies to helper methods defined for use by other methods within a controller, and this attribute will prevent these methods from being matched and invoked by an errant HTTP request.
-
Consider using the OData protocol to route and filter requests.
OData is a standard protocol for building data-based APIs that can follow RESTful conventions. OData supports a modified URI syntax for retrieving data, but provides significant facilities for filtering data and supporting common query options such as sorting, projecting, and selecting subsets of data. For example, if the Adventure-Works web API was implemented as an OData service, you could use the following URI to find the details for order number 99:
http://www.adventure-works.com/api/orders(99)
If you only wanted to find the value of the order (assuming that orderValue is a field in the orders resource), you could issue the following HTTP GET request:
http://www.adventure-works.com/api/orders(99)/orderValue
You can also limit the orders returned by using a filter. The following request finds all orders with a value greater than 100:
http://www.adventure-works.com/api/orders?$filter=orderValue gt 100
OData also provides metadata describing the resources that the web API exposes. This information can be used by client applications to help parse the response from GET requests, and construct PUT, POST, and DELETE requests containing the appropriate fields. This metadata can also be used to generate client-side SDKs; this feature is discussed further in the section Considerations for Publishing and Managing a Web API later in this guidance.
The ASP.NET Web API framework directly supports the OData protocol. Visual Studio provides wizards that enable you to quickly generate a default OData controller that provides a skeleton set of methods to read and write a set of resources. You add your own business logic to access the data.
For more information about the OData protocol, visit the Basic Tutorial page on the OData website. For examples showing how to implement an OData web API, visit the ASP.NET Web API OData page on the Microsoft website.
-
Consider the benefits and tradeoffs of placing the API in a subdomain.
By default, the ASP.NET web API organizes APIs into the /api directory in a domain, such as http://www.adventure-works.com/api/orders. This directory resides in the same domain as any other services exposed by the same host. It may be beneficial to split the web API out into its own subdomain running on a separate host, with URIs such as http://api.adventure-works.com/orders. This separation enables you to partition and scale the web API more effectively without affecting any other web applications or services running in the www.adventure-works.com domain.
However, placing a web API in a different subdomain can also lead to security concerns. Any web applications or services hosted at www.adventure-works.com that invoke a web API running elsewhere may violate the same-origin policy of many web browsers. In this situation, it will be necessary to enable cross-origin resource sharing (CORS) between the hosts. For more information, see the API Security Guidance document.
Once a request from a client application has been successfully routed to a method in a web API, the request must be processed in as efficient manner as possible. Consider the following points when you implement the code to handle requests:
GET, PUT, DELETE, HEAD, and PATCH actions must be idempotent.
The code that implements these requests must not impose any side-effects. The same request repeated over the same resource must have the same result.
Note: The article Idempotency Patterns on Jonathan Oliver’s blog provides an overview of idempotency and how it relates to data management operations.
-
POST actions should create new resources without additional side-effects.
A POST request is intended to create a new resource, and the effects of the request should be limited to creating the new resource only. The request should not modify other resources or have any other side-effects on the overall state of the system.
-
Implement batch operations to avoid chattiness.
Support POST, PUT and DELETE requests over resource collections. A POST request can contain the details for multiple new resources and add them all to the collection, a PUT request can replace the entire set of resources in a collection, and a DELETE request can remove an entire collection.
Note that the OData support included in ASP.NET Web API 2 provides the ability to batch requests. A client application can package up several web API requests and send them to the server in a single HTTP request, and receive a single HTTP response that contains the replies to each request. For more information, see the page Introducing Batch Support in Web API and Web API OData on the Microsoft website.
-
Abide by the HTTP protocol when sending a response back to a client application.
A web API must return messages that contain the correct HTTP status code to enable the client to determine how to handle the result, the appropriate HTTP headers so that the client understands the nature of the result, and a suitably formatted body to enable the client to parse the result. If you are using the ASP.NET Web API template, the default strategy for implementing methods that respond to HTTP POST requests is simply to return a copy of the newly created resource, as illustrated by the following example:
public class CustomersController : ApiController { ... [Route("api/customers")] [HttpPost] public Customer CreateNewCustomer(Customer customerDetails) { // Add the new customer to the repository // This method returns a customer with a unique ID allocated // by the repository var newCust = repository.Add(customerDetails); // Return the newly added customer return newCust; } ... }
If the POST operation is successful, the Web API framework creates an HTTP response with status code 200 (OK) and the details of the customer as the message body. However, according to the HTTP protocol, a POST operation should return status code 201 (Created) and the response message should include the URI of the newly created resource in the Location header of the response message.
To provide these features, return your own HTTP response message by using an HttpResponseMessage object. This approach gives you fine control over the HTTP status code, the headers in the response message, and even the format of the data in the response message body, as shown in the following code example. This version of the CreateNewCustomer method conforms more closely to the expectations of client following the HTTP protocol:
public class CustomersController : ApiController { ... [Route("api/customers")] [HttpPost] public HttpResponseMessage CreateNewCustomer(Customer customerDetails) { // Add the new customer to the repository var newCust = repository.Add(customerDetails); // Construct the response var response = Request.CreateResponse(HttpStatusCode.Created, newCust); // Add the Location header to the response // The URI should be the location of the customer including its ID, // such as http://adventure-works.com/api/customers/99 response.Headers.Location = new Uri(...); return response; } ... }
-
Support content negotiation.
The body of a response message may contain data in a variety of formats. For example, an HTTP GET request could return data in JSON, or XML format. When the client submits a request, it can include an Accept header that specifies the data formats that it can handle. These formats are specified as media types. For example, a client that issues a GET request that retrieves an image can specify an Accept header that lists the media types that the client can handle, such as "image/jpeg, image/gif, image/png". When the web API returns the result, it should format the data by using one of these media types and specify the format in the Content-Type header of the response.
If the client does not specify an Accept header, then use a sensible default format for the response body. As an example, the ASP.NET Web API framework defaults to JSON for text-based data.
Note: The ASP.NET Web API framework performs some automatic detection of Accept headers and handles them itself based on the type of the data in the body of the response message. For example, if the body of a response message contains a CLR (common language runtime) object, the ASP.NET Web API automatically formats the response as JSON with the Content-Type header of the response set to "application/json" unless the client indicates that it requires the results as XML, in which case the ASP.NET Web API framework formats the response as XML and sets the Content-Type header of the response to "text/xml". However, it may be necessary to handle Accept headers that specify different media types explicitly in the implementation code for an operation.
-
Provide links to support HATEOAS-style navigation and discovery of resources.
The API Design Guidance describes how following the HATEOAS approach enables a client to navigate and discover resources from an initial starting point. This is achieved by using links containing URIs; when a client issues an HTTP GET request to obtain a resource, the response should contain URIs that enable a client application to quickly locate any directly related resources. For example, in a web API that supports an e-commerce solution, a customer may have placed many orders. When a client application retrieves the details for a customer, the response should include links that enable the client application to send HTTP GET requests that can retrieve these orders. Additionally, HATEOAS-style links should describe the other operations (POST, PUT, DELETE, and so on) that each linked resource supports together with the corresponding URI to perform each request. This approach is described in more detail in the API Design Guidance document.
Currently there are no standards that govern the implementation of HATEOAS, but the following example illustrates one possible approach. In this example, an HTTP GET request that finds the details for a customer returns a response that include HATEOAS links that reference the orders for that customer:
GET http://adventure-works.com/customers/2 HTTP/1.1 Accept: text/json ...
HTTP/1.1 200 OK ... Content-Type: application/json; charset=utf-8 ... Content-Length: ... {"CustomerID":2,"CustomerName":"Bert","Links":[ {"Relationship":"self", "HRef":"http://adventure-works.com/customers/2", "Action":"GET", "LinkedResourceMIMETypes":["text/xml","application/json"]}, {"Relationship":"self", "HRef":"http://adventure-works.com/customers/2", "Action":"PUT", "LinkedResourceMIMETypes":["application/x-www-form-urlencoded"]}, {"Relationship":"self", "HRef":"http://adventure-works.com/customers/2", "Action":"DELETE", "LinkedResourceMIMETypes":[]}, {"Relationship":"orders", "HRef":"http://adventure-works.com/customers/2/orders", "Action":"GET", "LinkedResourceMIMETypes":["text/xml","application/json"]}, {"Relationship":"orders", "HRef":"http://adventure-works.com/customers/2/orders", "Action":"POST", "LinkedResourceMIMETypes":["application/x-www-form-urlencoded"]} ]}
In this example, the customer data is represented by the Customer class shown in the following code snippet. The HATEOAS links are held in the Links collection property:
public class Customer { public int CustomerID { get; set; } public string CustomerName { get; set; } public List<Link> Links { get; set; } ... } public class Link { public string Relationship { get; set; } public string HRef { get; set; } public string Action { get; set; } public string [] LinkedResourceMIMETypes { get; set; } }
The HTTP GET operation retrieves the customer data from storage and constructs a Customer object, and then populates the Links collection. The result is formatted as a JSON response message. Each link comprises the following fields:
a. The relationship between the object being returned and the object described by the link. In this case "self" indicates that the link is a reference back to the object itself (similar to a this pointer in many object-oriented languages), and "orders" is the name of a collection containing the related order information.
b. The hyperlink (HRef) for the object being described by the link in the form of a URI.
c. The type of HTTP request (Action) that can be sent to this URI.
d. The format of any data (LinkedResourceMIMETypes) that should be provided in the HTTP request or that can be returned in the response, depending on the type of the request.
The HATEOAS links shown in the example HTTP response indicate that a client application can perform the following operations:
a. An HTTP GET request to the URI http://adventure-works.com/customers/2 to fetch the details of the customer (again). The data can be returned as XML or JSON.
b. An HTTP PUT request to the URI http://adventure-works.com/customers/2 to modify the details of the customer. The new data must be provided in the request message in x-www-form-urlencoded format.
c. An HTTP DELETE request to the URI http://adventure-works.com/customers/2 to delete the customer. The request does not expect any additional information or return data in the response message body.
d. An HTTP GET request to the URI http://adventure-works.com/customers/2/orders to find all the orders for the customer. The data can be returned as XML or JSON.
e. An HTTP PUT request to the URI http://adventure-works.com/customers/2/orders to create a new order for this customer. The data must be provided in the request message in x-www-form-urlencoded format.
By default, in the ASP.NET Web API framework, if an operation throws an uncaught exception the framework returns a response message with HTTP status code 500 (Internal Server Error). In many cases, this simplistic approach is not useful in isolation, and makes determining the cause of the exception difficult. Therefore you should adopt a more comprehensive approach to handling exceptions, considering the following points:
-
Capture exceptions and return a meaningful response to clients.
The code that implements an HTTP operation should provide comprehensive exception handling rather than letting uncaught exceptions propagate to the Web API framework. If an exception makes it impossible to complete the operation successfully, the exception can be passed back in the response message, but it should include a meaningful description of the error that caused the exception. The exception should also include the appropriate HTTP status code rather than simply returning status code 500 for every situation. For example, if a user request causes a database update that violates a constraint (such as attempting to delete a customer that has outstanding orders), you should return status code 409 (Conflict) and a message body indicating the reason for the conflict. If some other condition renders the request unachievable, you can return status code 400 (Bad Request). You can find a full list of HTTP status codes on the Status Code Definitions page on the W3C website. The following code shows an example that traps different conditions and returns an appropriate response.
[HttpDelete] [Route("customers/{id:int}")] public HttpResponseMessage DeleteCustomer(int id) { try { // Find the customer to be deleted in the repository var customerToDelete = repository.GetCustomer(id); // If there is no such customer, return an error response // with status code 404 (Not Found) and // the message "Customer not found" if (customerToDelete == null) { return Request.CreateErrorResponse( HttpStatusCode.NotFound, Strings.CustomerNotFound); } // Remove the customer from the repository // The DeleteCustomer method returns true if the customer // was successfully deleted if (repository.DeleteCustomer(id)) { // Return a response message with status code 204 (No Content) // To indicate that the operation was successful return Request.CreateResponse(HttpStatusCode.NoContent); } else { // Otherwise return an error response with status code // 400 (Bad Request) and the message "Customer not deleted" return Request.CreateResponse( HttpStatusCode.BadRequest, Strings.CustomerNotDeleted); } } catch { // If an uncaught exception occurs, return an error response // with status code 500 (Internal Server Error) // and a meaningful message return Request.CreateErrorResponse( HttpStatusCode.InternalServerError, Strings.ServerError); } }
Tip: Do not include information that could be useful to an attacker attempting to penetrate your web API.
For further information, visit the Exception Handling in ASP.NET Web API page on the Microsoft website.
Note: Many web servers trap error conditions themselves before they reach the web API. For example, if you configure authentication for a web site and the user fails to provide the correct authentication information, the web server should respond with status code 401 (Unauthorized). Once a client has been authenticated, your code can perform its own checks to verify that the client should be able access the requested resource. If this authorization fails, you should return status code 403 (Forbidden).
-
Handle exceptions in a consistent manner and log information about errors.
To handle exceptions in a consistent manner, consider implementing a global error handling strategy across the entire web API. You can achieve part of this by creating an exception filter that runs whenever a controller throws any unhandled exception that is not an HttpResponseException exception. This approach is described on the Exception Handling in ASP.NET Web API page on the Microsoft website.
However, there are several situations where an exception filter will not catch an exception, including:
a. Exceptions thrown from controller constructors.
b. Exceptions thrown from message handlers.
c. Exceptions thrown during routing.
d. Exceptions thrown while serializing the content for a response message.
To handle these cases, you may need to implement a more customized approach. You should also incorporate error logging which captures the full details of each exception; this error log can contain detailed information as long as it is not made accessible over the web to clients. The article Web API Global Error Handling on the Microsoft website shows one way of performing this task.
-
Distinguish between client-side errors and server-side errors.
The HTTP protocol distinguishes between errors that occur due to the client application (the HTTP 4xx status codes), and errors that are caused by a mishap on the server (the HTTP 5xx status codes). Make sure that you respect this convention in any error response messages.
In a distributed environment such as that involving a web server and client applications, one of the primary sources of concern is the network. This can act as a considerable bottleneck, especially if a client application is frequently sending requests or receiving data. Therefore you should aim to minimize the amount of traffic that flows across the network. Consider the following points when you implement the code to retrieve and maintain data:
-
Support client-side caching.
The HTTP 1.1 protocol supports client-side caching through the use of the Cache-Control header. When a client application sends an HTTP GET request to the web API, the response can include a Cache-Control header that indicates whether the data in the body of the response can be safely cached by the client, and for how long before it should expire and be considered out-of-date. The following example shows an HTTP GET request and the corresponding response that includes a Cache-Control header:
GET http://adventure-works.com/orders/2 HTTP/1.1 ...
HTTP/1.1 200 OK ... Cache-Control: max-age=600, private Content-Type: text/json; charset=utf-8 Content-Length: ... {"OrderID":2,"ProductID":4,"Quantity":2,"OrderValue":10.00}
In this example, the Cache-Control header specifies that the data returned should be expired after 600 seconds, and is only suitable for a single client and must not be stored in a shared cache used by other clients (it is private). The Cache-Control header could specify public rather than private in which case the data can be stored in a shared cache, or it could specify no-cache in which case the data should not be cached. The following code example shows how to construct a Cache-Control header in a response message:
public class OrdersController : ApiController { ... [Route("api/orders/{id:int:min(0)}")] [HttpGet] public HttpResponseMessage FindOrderByID(int id) { // Find the matching order var order = ... // Construct the response var response = Request.CreateResponse(HttpStatusCode.OK, order); // Add the Cache-Control header to the response response.Headers.CacheControl = new CacheControlHeaderValue(); response.Headers.CacheControl.Private = true; response.Headers.CacheControl.MaxAge = new TimeSpan(0, 10, 0); return response; } ... }
Cache management is the responsibility of the client application, but if properly implemented it can save bandwidth and improve performance by removing the need to fetch data that has already been recently retrieved.
The max-age value in the Cache-Control header is only a guide and not a guarantee that the corresponding data won't change during the specified time. The web API should set the max-age to a suitable value depending on the expected volatility of the data. When this period expires, the client should discard the object from the cache.
Note: Most modern web browsers support client-side caching by adding the appropriate cache-control headers to requests and examining the headers of the results, as described. However, some older browsers will not cache the values returned from a URL that includes a query string. This is not usually an issue for custom client applications which implement their own cache management strategy based on the protocol discussed here.
Some older proxies exhibit the same behavior and might not cache requests based on URLs with query strings. This could be an issue for custom client applications that connect to a web server through such a proxy.
-
Provide ETags to Optimize Query Processing.
When a client application retrieves an object, the response message can also include an ETag (Entity Tag). An ETag is an opaque string that indicates the version of a resource; each time a resource changes the Etag is also modified. This ETag should be cached as part of the data by the client application. The following code example shows how to add an ETag as part of the response to an HTTP GET request. This code uses the GetHashCode method of an object to generate a numeric value that identifies the object (you can override this method if necessary and generate your own hash using a technology such as MD5) :
public class OrdersController : ApiController { ... [Route("api/orders/{id:int:min(0)}")] [HttpGet] public HttpResponseMessage FindOrderByID(int id) { // Find the matching order var order = ... // Construct the response var response = Request.CreateResponse(HttpStatusCode.OK, order); // Add the Cache-Control header to the response ... // Generate an ETag and add it to the response var hashedOrder = order.GetHashCode(); response.Headers.ETag = new EntityTagHeaderValue(String.Format("\"{0}\"", hashedOrder)); return response; } ... }
The response message posted by the web API looks like this:
HTTP/1.1 200 OK ... Cache-Control: max-age=600, private Content-Type: text/json; charset=utf-8 ETag: "2147483648" Content-Length: ... {"OrderID":2,"ProductID":4,"Quantity":2,"OrderValue":10.00}
Tip: For security reasons, do not allow sensitive data or data returned over an authenticated (HTTPS) connection to be cached.
A client application can issue a subsequent GET request to retrieve the same resource at any time, and if the resource has changed (it has a different ETag) the cached version should be discarded and the new version added to the cache. If a resource is large and requires a significant amount of bandwidth to transmit back to the client, repeated requests to fetch the same data can become inefficient. To combat this, the HTTP protocol defines the following process for optimizing GET requests that you should support in a web API:
a. The client constructs a GET request containing the ETag for the currently cached version of the resource referenced in an If-None-Match HTTP header:
GET http://adventure-works.com/orders/2 HTTP/1.1 If-None-Match: "2147483648" ...
b. The GET operation in the web API obtains the current ETag for the requested data (order 2 in the above example), and compares it to the value in the If-None-Match header.
c. If the current ETag for the requested data matches the ETag provided by the request, the resource has not changed and the web API should return an HTTP response with an empty message body and a status code of 304 (Not Modified).
d. If the current ETag for the requested data does not match the ETag provided by the request, then the data has changed and the web API should return an HTTP response with the new data in the message body and a status code of 200 (OK).
e. If the requested data no longer exists then the web API should return an HTTP response with the status code of 404 (Not Found).
f. The client uses the status code to maintain the cache. If the data has not changed (status code 304) then the object can remain cached and the client application should continue to use this version of the object. If the data has changed (status code 200) then the cached object should be discarded and the new one inserted. If the data is no longer available (status code 404) then the object should be removed from the cache.
Note: If the response header contains the Cache-Control header no-cache then the object should always be removed from the cache regardless of the HTTP status code.
The code below shows the FindOrderByID method extended to support the If-None-Match header. Notice that if the If-None-Match header is omitted, the specified order is always retrieved:
public class OrdersController : ApiController { ... [Route("api/orders/{id:int:min(0)}")] [HttpGet] public HttpResponseMessage FindOrderByID(int id) { // Find the matching order var order = ... // If there is no matching order, then return an HTTP Not Found error if (order == null) { return Request.CreateErrorResponse( HttpStatusCode.NotFound, Strings.OrderNotFound); } ... var hashedOrder = order.GetHashCode(); string hashedOrderEtag = String.Format("\"{0}\"", hashedOrder); HttpResponseMessage response = null; // Retrieve the If-None-Match header from the request (if it exists) var nonMatchEtags = Request.Headers.IfNoneMatch; // If there is an Etag in the If-None-Match header and // this etag matches that of the order just retrieved, // then create a Not Modified response message if (nonMatchEtags.Count > 0 && String.Compare(nonMatchEtags.First().Tag, hashedOrderEtag) == 0) { response = Request.CreateResponse(HttpStatusCode.NotModified); } // Otherwise create a response message that contains the order details else { response = Request.CreateResponse(HttpStatusCode.OK, order); } // Add the Cache-Control and Etag headers to the response response.Headers.CacheControl = new CacheControlHeaderValue(); response.Headers.CacheControl.Private = true; response.Headers.CacheControl.MaxAge = new TimeSpan(0, 10, 0); response.Headers.ETag = new EntityTagHeaderValue(hashedOrderEtag); return response; } ... }
Tip: In this example, the ETag for the data is generated by hashing the data retrieved from the underlying data source. If the ETag can be computed in some other way, then the process can be optimized further and the data only needs to be fetched from the data source if it has changed. This approach is especially useful if the data is large or accessing the data source can result in significant latency (for example, if the data source is a remote database).
-
Use ETags to Support Optimistic Concurrency.
To enable updates over previously cached data, the HTTP protocol supports an optimistic concurrency strategy. If, after fetching and caching a resource, the client application subsequently sends a PUT or DELETE request to change or remove the resource, it should include in If-Match header that references the ETag. The web API can then use this information to determine whether the resource has already been changed by another user since it was retrieved and send an appropriate response back to the client application as follows:
a. The client constructs a PUT request containing the new details for the resource and the ETag for the currently cached version of the resource referenced in an If-Match HTTP header. The following example shows a PUT request that updates an order:
PUT http://adventure-works.com/orders/1 HTTP/1.1 If-None-Match: "2282343857" Content-Type: application/x-www-form-urlencoded ... Date: Fri, 12 Sep 2014 09:18:37 GMT Content-Length: ... ProductID=3&Quantity=5&OrderValue=250
b. The PUT operation in the web API obtains the current ETag for the requested data (order 1 in the above example), and compares it to the value in the If- Match header.
c. If the current ETag for the requested data matches the ETag provided by the request, the resource has not changed and the web API should perform the update, returning a message with HTTP status code 204 (No Content) if it is successful. The response can include Cache-Control and ETag headers for the updated version of the resource. The response should always include the Location header that references the URI of the newly updated resource.
d. If the current ETag for the requested data does not match the ETag provided by the request, then the data has been changed by another user since it was fetched and the web API should return an HTTP response with an empty message body and a status code of 412 (Precondition Failed).
e. If the resource to be updated no longer exists then the web API should return an HTTP response with the status code of 404 (Not Found).
f. The client uses the status code and response headers to maintain the cache. If the data has been updated changed (status code 204) then the object can remain cached (as long as the Cache-Control header does not specify no-cache) but the ETag should be updated. If the data was changed by another user changed (status code 412) or not found (status code 404) then the cached object should be discarded.
The next code example shows an implementation of the PUT operation for the Orders controller:
public class OrdersController : ApiController { ... [HttpPut] [Route("api/orders/{id:int}")] public HttpResponseMessage UpdateExistingOrder(int id, Order order) { // Find the order to update var orderToUpdate = ... // If there is no matching order, then return an HTTP Not Found error if (orderToUpdate == null) { return Request.CreateErrorResponse( HttpStatusCode.NotFound, Strings.OrderNotFound); } // Calculate the ETag for the order to be updated var hashedOrder = orderToUpdate.GetHashCode(); string hashedOrderEtag = String.Format("\"{0}\"", hashedOrder); // Retrieve the If-Match header from the request (if it exists) var matchEtags = Request.Headers.IfMatch; // If there is an Etag in the If-Match header and // this etag matches that of the order just retrieved, // or if there is no etag, then update the Order if (((matchEtags.Count > 0 && String.Compare(matchEtags.First().Tag, hashedOrderEtag) == 0)) || matchEtags.Count == 0) { // Modify the order orderToUpdate.OrderValue = order.OrderValue; orderToUpdate.ProductID = order.ProductID; orderToUpdate.Quantity = order.Quantity; ... // Save the order back to the data store ... // Create the No Content response with Cache-Control, // ETag, and Location headers var response = Request.CreateResponse(HttpStatusCode.NoContent); response.Headers.CacheControl = new CacheControlHeaderValue(); response.Headers.CacheControl.Private = true; response.Headers.CacheControl.MaxAge = new TimeSpan(0, 10, 0); hashedOrder = order.GetHashCode(); hashedOrderEtag = String.Format("\"{0}\"", hashedOrder); response.Headers.ETag = new EntityTagHeaderValue(hashedOrderEtag); response.Headers.Location = new Uri(...); return response; } // Otherwise return a Precondition Failed response return Request.CreateErrorResponse(HttpStatusCode.PreconditionFailed, Strings.ConflictingOrderUpdate); } ... }
Tip: Use of the If-Match header is entirely optional, and if it is omitted the web API will always attempt to update the specified order, possibly blindly overwriting an update made by another user. To avoid problems due to lost updates, always provide an If-Match header.
There may be occasions when a client application needs to issue requests that send or receive data that may be several megabytes (or bigger) in size. Waiting while this amount of data is transmitted could cause the client application to become unresponsive. Consider the following points when you need to handle requests that include significant amounts of data:
-
Optimize requests and responses that involve large objects.
Some resources may be large objects or include large fields, such as graphics images or other types of binary data. A web API should support streaming to enable optimized uploading and downloading of these resources.
The HTTP protocol provides the chunked transfer encoding mechanism to stream large data objects back to a client. When the client sends an HTTP GET request for a large object, the web API can send the reply back in piecemeal chunks over a persistent HTTP connection. The length of the data in the reply may not be known initially (it might be generated), so the server hosting the web API should send a response message with each chunk that specifies the Transfer-Encoding: Chunked header rather than a Content-Length header. The client application can receive each chunk in turn to build up the complete response. The data transfer completes when the server sends back a final chunk with zero size.
You can implement chunking in the ASP.NET Web API by using the PushStreamContent class. The following example shows an operation that responds to HTTP GET requests for product images. Each product has its own image held in blob storage, and the Get method uses a PushStreamContent object to read the image data from appropriate blob and transmit it asynchronously as the content of the response message:
public class ProductImagesController : ApiController { ... [HttpGet] [Route("productimages/{id:int}")] public HttpResponseMessage Get(int id) { try { var container = ConnectToBlobContainer(...); if (!BlobExists(container, string.Format("image{0}.jpg", id))) { return Request.CreateErrorResponse( HttpStatusCode.NotFound, Strings.NoSuchImageFile); } var response = Request.CreateResponse(); response.Content = new PushStreamContent( async (outputStream, httpContent, transportContent) => { try { CloudBlockBlob blockBlob = container.GetBlockBlobReference( String.Format("image{0}.jpg", id)); await blockBlob.DownloadToStreamAsync(outputStream); } finally { outputStream.Close(); } }); response.StatusCode = HttpStatusCode.OK; response.Content.Headers.ContentType = new MediaTypeHeaderValue("image/jpeg"); return response; } catch { return Request.CreateErrorResponse( HttpStatusCode.InternalServerError, Strings.ServerError); } } ... }
In this example, ConnectBlobToContainer is a helper method that connects to a specified container (name not shown) in Azure Blob storage. BlobExists is another helper method that returns a Boolean value that indicates whether a blob with the specified name exists in the blob storage container.
You can also apply streaming to upload operations if a client needs to POST a new resource that includes a large object. The next example shows the Post method for the ProductImages controller. This method enables the client to upload a new product image:
public class ProductImagesController : ApiController { ... [HttpPost] [Route("productimages")] public async Task<HttpResponseMessage> Post() { try { if (!Request.Content.Headers.ContentType.MediaType.Equals( "image/jpeg")) { return Request.CreateErrorResponse( HttpStatusCode.UnsupportedMediaType, Strings.MediaNotSupported); } var container = ConnectToBlobContainer(...); int id = ... // Create a new ID for the image CloudBlockBlob blockBlob = container.GetBlockBlobReference( String.Format("image{0}.jpg", id)); await blockBlob.UploadFromStreamAsync( await Request.Content.ReadAsStreamAsync()); var response = Request.CreateResponse(HttpStatusCode.OK); var baseUri = string.Format("{0}://{1}:{2}", Request.RequestUri.Scheme, Request.RequestUri.Host, Request.RequestUri.Port); response.Headers.Location = new Uri(string.Format("{0}/productimages/{1}", baseUri, id)); return response; } catch { return Request.CreateErrorResponse( HttpStatusCode.InternalServerError, Strings.ServerError); } } ... }
Tip: The volume of data that you can upload to a web service is not constrained by streaming, and a single request could conceivably result in a massive object that consumes considerable resources. If, during the streaming process, the web API determines that the amount of data in a request has exceeded some acceptable bounds, it can abort the operation and return a response message with status code 413 (Request Entity Too Large).
You can minimize the size of large objects transmitted over the network by using HTTP compression. This approach helps to reduce the amount of network traffic and the associated network latency, but at the cost of requiring additional processing at the client and the server hosting the web API. For example, a client application that expects to receive compressed data can include an Accept-Encoding: gzip request header (other data compression algorithms can also be specified). If the server supports compression it should respond with the content held in gzip format in the message body and the Content-Encoding: gzip response header.
Tip: You can combine encoded compression with streaming; compress the data first before streaming it, and specify the gzip content encoding and chunked transfer encoding in the message headers. Also note that some web servers (such as Internet Information Server) can be configured to automatically compress HTTP responses regardless of whether the web API compresses the data or not.
-
Implement partial responses for clients that do not support asynchronous operations.
As an alternative to asynchronous streaming, a client application can explicitly request data for large objects in chunks, known as partial responses. The client application sends an HTTP HEAD request to obtain information about the object. If the web API supports partial responses if should respond to the HEAD request with a response message that contains an Accept-Ranges header and a Content-Length header that indicates the total size of the object, but the body of the message should be empty. The client application can use this information to construct a series of GET requests that specify a range of bytes to receive. The web API should return a response message with HTTP status 206 (Partial Content), a Content-Length header that specifies the actual amount of data included in the body of the response message, and a Content-Range header that indicates which part (such as bytes 4000 to 8000) of the object this data represents.
HTTP HEAD requests and partial responses are described in more detail in the API Design Guidance document.
-
Enable Continue status messages.
A client application that is about to send or receive a large amount of data to a server may determine first whether the server is actually willing to accept the request. Prior to sending the data, the client application can submit an HTTP request with an Expect: 100-Continue header and an empty body. If the server is willing to handle the request, it should respond with a message that specifies the HTTP status 100 (Continue). The client application can then proceed and send the complete request.
-
Support pagination for requests that may return large numbers of objects.
If a collection contains a large number of resources, issuing a GET request to the corresponding URI could result in significant processing on the server hosting the web API affecting performance, and generate a significant amount of network traffic resulting in increased latency.
To handle these cases, the web API should support query strings that enable the client application to refine requests or fetch data in more manageable, discrete blocks (or pages). The ASP.NET Web API framework parses query strings and splits them up into a series of parameter/value pairs which are passed to the appropriate method, following the routing rules described earlier. The method should be implemented to accept these parameters using the same names specified in the query string. Additionally, these parameters should be optional (in case the client omits the query string from a request) and have meaningful default values. The code below shows the GetAllOrders method in the Orders controller. This method retrieves the details of orders. If this method was unconstrained, it could conceivably return a large amount of data. The limit and offset parameters are intended to reduce the volume of data to a smaller subset, in this case only the first 10 orders by default:
public class OrdersController : ApiController { ... [Route("api/orders")] [HttpGet] public IEnumerable<Order> GetAllOrders(int limit=10, int offset=0) { // Find the number of orders specified by the limit parameter // starting with the order specified by the offset parameter var orders = ... return orders; } ... }
A client application can issue a request to retrieve 30 orders starting at offset 50 by using the URI http://www.adventure-works.com/api/orders?limit=30&offset=50.
Tip: Avoid enabling client applications to specify query strings that result in a URI that is more than 2000 characters long. Many web clients and servers cannot handle URIs that are this long.
If you are using the OData protocol support provided with the ASP.NET Web API you get this functionality built-in. The $top query string parameter allows a client application to limit the number of items returned, while the $skip parameter enables an application to specify an offset into a collection from where to start retrieving items. The URI /orders?$skip=20&$top=10 fetches 10 orders starting with the 20th order in the collection.
The same web API might be utilized by many client applications running anywhere in the world. It is important to ensure that the web API is implemented to maintain responsiveness under a heavy load, to be scalable to support a highly varying workload, and to guarantee availability for clients that perform business-critical operations. Consider the following points when determining how to meet these requirements:
-
Provide Asynchronous Support for Long-Running Requests.
A request that might take a long time to process should be performed without blocking the client that submitted the request. The web API can perform some initial checking to validate the request, initiate a separate task to perform the work, and then return a response message with HTTP code 202 (Accepted). The task could run asynchronously as part of the web API processing, or it could be offloaded to an Azure WebJob (if the web API is hosted by an Azure Website) or a worker role (if the web API is implemented as an Azure cloud service).
Note: For more information about using WebJobs with Azure Website, visit the page Use WebJobs to run background tasks in Microsoft Azure Websites on the Microsoft website.
The web API should also provide a mechanism to return the results of the processing to the client application. You can achieve this by providing a polling mechanism for client applications to periodically query whether the processing has finished and obtain the result, or enabling the web API to send a notification when the operation has completed.
You can implement a simple polling mechanism by providing a polling URI that acts as a virtual resource using the following approach:
a. The client application sends the initial request to the web API.
b. The web API stores information about the request in a table held in table storage or Microsoft Azure Cache, and generates a unique key for this entry, possibly in the form of a GUID.
c. The web API initiates the processing as a separate task. The web API records the state of the task in the table as Running.
d. The web API returns a response message with HTTP status code 202 (Accepted), and the GUID of the table entry in the body of the message.
e. When the task has completed, the web API stores the results in the table, and sets the state of the task to Complete. Note that if the task fails, the web API could also store information about the failure and set the status to Failed.
f. While the task is running, the client can continue performing its own processing. It can periodically send a request to the URI /polling/{guid} where {guid} is the GUID returned in the 202 response message by the web API.
g. The web API at the /polling{guid} URI queries the state of the corresponding task in the table and returns a response message with HTTP status code 200 (OK) containing this state (Running, Complete, or Failed). If the task has completed or failed, the response message can also include the results of the processing or any information available about the reason for the failure.
If you prefer to implement notifications, the options available include:
a. Using an Azure Notification Hub to push asynchronous responses to client applications. The page Azure Notification Hubs Notify Users on the Microsoft website provides further details.
b. Using the Comet model to retain a persistent network connection between the client and the server hosting the web API, and using this connection to push messages from the server back to the client. The MSDN magazine article Building a Simple Comet Application in the Microsoft .NET Framework describes an example solution.
c. Using SignalR to push data in real-time from the web server to the client over a persistent network connection. SignalR is available for ASP.NET web applications as a NuGet package. You can find more information on the ASP.NET SignalR website.
Note: Comet and SignalR both utilize persistent network connections between the web server and the client application. This can affect scalability as a large number of clients may require an equally large number of concurrent connections.
-
Ensure that each request is stateless.
Each request should be considered atomic. There should be no dependencies between one request made by a client application and any subsequent requests submitted by the same client. This approach assists in scalability; instances of the web service can be deployed on a number of servers. Client requests can be directed at any of these instances and the results should always be the same. It also improves availability for a similar reason; if a web server fails requests can be routed to another instance (by using Azure Traffic Manager) while the server is restarted with no ill effects on client applications.
-
Track clients and implement throttling to reduce the chances of DOS attacks.
If a specific client makes a large number of requests within a given period of time it might monopolize the service and affect the performance of other clients. To mitigate this issue, a web API can monitor calls from client applications either by tracking the IP address of all incoming requests or by logging each authenticated access. You can use this information to limit resource access. If a client exceeds a defined limit, the web API can return a response message with status 503 (Service Unavailable) and include a Retry-After header that specifies when the client can send the next request without it being declined. This strategy can help to reduce the chances of a Denial Of Service (DOS) attack from a set of clients stalling the system.
-
Enable Persistent HTTP connections with Keep-Alive response messages.
The HTTP protocol supports persistent HTTP connections where they are available. When a client application sends an HTTP request to a service the response can include the Keep-Alive header to indicate that the client application can use the same connection to send subsequent requests rather than open new ones. The connection closes automatically if the client does not reuse the connection within a period defined by the host.
Using Keep-Alive response messages can help to improve responsiveness by reducing latency and network congestion, but it can be detrimental to scalability by keeping unnecessary connections open for longer than required, limiting the ability of other concurrent clients to connect.
Note: Persistent HTTP connections are a purely optional feature to reduce the network overhead associated with repeatedly establishing a communications channel. Neither the web API nor the client application should depend on a persistent HTTP connection being available. Do not use persistent HTTP connections to implement Comet-style notification systems; instead you should utilize sockets (or websockets if available) at the TCP layer.
To make a web API available for client applications, the web API must be deployed to a host environment. This environment is typically a web server, although it may be some other type of host process. You should consider the following points when publishing a web API:
- All requests must be authenticated and authorized, and the appropriate level of access control must be enforced.
- A commercial web API might be subject to various quality guarantees concerning response times. It is important to ensure that host environment is scalable if the load can vary significantly over time.
- If may be necessary to meter requests for monetization purposes.
- It might be necessary to regulate the flow of traffic to the web API, and implement throttling for specific clients that have exhausted their quotas.
- Regulatory requirements might mandate logging and auditing of all requests and responses.
- To ensure availability, it may be necessary to monitor the health of the server hosting the web API and restart it if necessary.
These concerns are described in more detail in the API Design Guidance document.
It is useful to be able to decouple these issues from the technical issues concerning the implementation of the web API. For this reason, consider creating a façade, running as a separate process and that routes requests to the web API. The façade can provide the management operations and forward validated requests to the web API. Using a façade can also bring many functional advantages, including:
- Acting as an integration point for multiple web APIs.
- Transforming messages and translating communications protocols for clients built by using varying technologies.
- Caching requests and responses to reduce load on the server hosting the web API.
Azure provides the API Management Service which you can use to publish and manage a web API. Using this facility, you can generate a service that acts a façade for one or more web APIs. The service is itself a scalable web service that you can create and configure by using the Azure Management portal. You can use this service to publish and manage a web API as follows:
-
Deploy the web API to a website, Azure cloud service, or Azure virtual machine.
-
Connect the API management service to the web API. Requests sent to the URL of the management API are mapped to URIs in the web API. The same API management service can route requests to more than one web API. This enables you to aggregate multiple web APIs into a single management service. Similarly, the same web API can be referenced from more than one API management service if you need to restrict or partition the functionality available to different applications.
Note: The URIs in HATEOAS links generated as part of the response for HTTP GET requests should reference the URL of the API management service and not the web server hosting the web API.
-
For each web API, specify the HTTP operations that the web API exposes together with any optional parameters that an operation can take as input. You can also configure whether the API management service should cache the response received from the web API to optimize repeated requests for the same data. Record the details of the HTTP responses that each operation can generate. This information is used to generate documentation for developers, so it is important that it is accurate and complete.
You can either define operations manually using the wizards provided by the Azure Management portal, or you can import them from a file containing the definitions in WADL or Swagger format.
-
Configure the security settings for communications between the API management service and the web server hosting the web API. The API management service currently supports Basic authentication and mutual authentication using certificates, and OAuth 2.0 user authorization.
-
Create a product. A product is the unit of publication; you add the web APIs that you previously connected to the management service to the product. When the product is published, the web APIs become available to developers.
Note: Prior to publishing a product, you can also define user-groups that can access the product and add users to these groups. This gives you control over the developers and applications that can use the web API. If a web API is subject to approval, prior to being able to access it a developer must send a request to the product administrator. The administrator can grant or deny access to the developer. Existing developers can also be blocked if circumstances change.
-
Configure policies for each web API. Policies govern aspects such as whether cross-domain calls should be allowed, how to authenticate clients, whether to convert between XML and JSON data formats transparently, whether to restrict calls from a given IP range, usage quotas, and whether to limit the call rate. Policies can be applied globally across the entire product, for a single web API in a product, or for individual operations in a web API.
You can find full details describing how to perform these tasks on the API Management page on the Microsoft website. The Azure API Management Service also provides its own REST interface, enabling you to build a custom interface for simplifying the process of configuring a web API. For more information, visit the Azure API Management REST API Reference page on the Microsoft website.
Tip: Azure provides the Azure Traffic Manager which enables you to implement failover and load-balancing, and reduce latency across multiple instances of a web site hosted in different geographic locations. You can use Azure Traffic Manager in conjunction with the API Management Service; the API Management Service can route requests to instances of a web site through Azure Traffic Manager. For more information, visit the About Traffic Manager Load Balancing Methods page on the Microsoft website.
In this structure, if you are using custom DNS names for your web sites, you should configure the appropriate CNAME record for each web site to point to the DNS name of the Azure Traffic Manager web site.
Developers constructing client applications typically require information on how to access the web API, and documentation concerning the parameters, data types, return types, and return codes that describe the different requests and responses between the web service and the client application.
The Azure API Management Service includes a developer portal that describes the REST operations exposed by a web API. When a product has been published it appears on this portal. Developers can use this portal to sign up for access; the administrator can then approve or deny the request. If the developer is approved, they are assigned a subscription key that is used to authenticate calls from the client applications that they develop. This key must be provided with each web API call otherwise it will be rejected.
This portal also provides:
- Documentation for the product, listing the operations that it exposes, the parameters required, and the different responses that can be returned. Note that this information is generated from the details provided in step 3 in the list in the section Publishing a web API by using the Microsoft Azure API Management Service.
- Code snippets that show how to invoke operations from several languages, including JavaScript, C#, Java, Ruby, Python, and PHP.
- A developers' console that enables a developer to send an HTTP request to test each operation in the product and view the results.
- A page where the developer can report any issues or problems found.
The Azure Management portal enables you to customize the developer portal to change the styling and layout to match the branding of your organization.
Building a client application that invokes REST requests to access a web API requires writing a significant amount of code to construct each request and format it appropriately, send the request to the server hosting the web service, and parse the response to work out whether the request succeeded or failed and extract any data returned. To insulate the client application from these concerns, you can provide an SDK that wraps the REST interface and abstracts these low-level details inside a more functional set of methods. A client application uses these methods, which transparently convert calls into REST requests and then convert the responses back into method return values. This is a common technique that is implemented by many services, including the Azure SDK.
Creating a client-side SDK is a considerable undertaking as it has to be implemented consistently and tested carefully. However, much of this process can be made mechanical, and many vendors supply tools that can automate many of these tasks.
Depending on how you have published and deployed your web API you can monitor the web API directly, or you can gather usage and health information by analyzing the traffic that passes through the API Management service.
If you have implemented your web API by using the ASP.NET Web API template (either as a Web API project or as a Web role in an Azure cloud service) and Visual Studio 2013, you can gather availability, performance, and usage data by using ASP.NET Application Insights. Application Insights is a package that transparently tracks and records information about requests and responses when the web API is deployed to the cloud; once the package is installed and configured, you don't need to amend any code in your web API to use it. When you deploy the web API to an Azure web site, all traffic is examined and the following statistics are gathered:
- Server response time.
- Number of server requests and the details of each request.
- The top slowest requests in terms of average response time.
- The details of any failed requests.
- The number of sessions initiated by different browsers and user agents.
- The most frequently viewed pages (primarily useful for web applications rather than web APIs).
- The different user roles accessing the web API.
You can view this data in real time from the Azure Management portal. You can also create webtests that monitor the health of the web API. A webtest sends a periodic request to a specified URI in the web API and captures the response. You can specify the definition of a successful response (such as HTTP status code 200), and if the request does not return this response you can arrange for an alert to be sent to an administrator. If necessary, the administrator can restart the server hosting the web API if it has failed.
The Application Insights - Start monitoring your app's health and usage page on the Microsoft website provides more information.
If you have published your web API by using the API Management service, the API Management page on the Azure Management portal contains a dashboard that enables you to view the overall performance of the service. The Analytics page enables you to drill down into the details of how the product is being used. This page contains the following tabs:
- Usage. This tab provides information about the number of API calls made and the bandwidth used to handle these calls over time. You can filter usage details by product, API, and operation.
- Health. This tab enables you view the outcome of API requests (the HTTP status codes returned), the effectiveness of the caching policy, the API response time, and the service response time. Again, you can filter health data by product, API, and operation.
- Activity. This tab provides a text summary of the numbers of successful calls, failed called, blocked calls, average response time, and response times for each product, web API, and operation. This page also lists the number of calls made by each developer.
- At a glance. This tab displays a summary of the performance data, including the developers responsible for making the most API calls, and the products, web APIs, and operations that received these calls.
You can use this information to determine whether a particular web API or operation is causing a bottleneck, and if necessary scale the host environment and add more servers. You can also ascertain whether one or more applications are using a disproportionate volume of resources and apply the appropriate policies to set quotas and limit call rates.
Note: You can change the details for a published product, and the changes are applied immediately. For example, you can add or remove an operation from a web API without requiring that you republish the product that contains the web API.
A web API should be tested as thoroughly as any other piece of software. You should consider creating unit tests to validate the functionality of each operation, as you would with any other type of application. For more information, see the page Verifying Code by Using Unit Tests on the Microsoft website.
Note: The sample web API available with this guidance includes a test project that shows how to perform unit testing over selected operations.
The nature of a web API brings its own additional requirements to verify that it operates correctly. You should pay particular attention to the following aspects:
-
Test all routes to verify that they invoke the correct operations. Be especially aware of HTTP status code 405 (Method Not Allowed) being returned unexpectedly as this can indicate a mismatch between a route and the HTTP verbs (GET, POST, PUT, DELETE) that can be dispatched to that route.
Send HTTP requests to routes that do not support them, such as submitting a POST request to a specific resource (POST requests should only be sent to resource collections). In these cases, the only valid response should be status code 405 (Not Allowed).
-
Verify that all routes are protected properly and are subject to the appropriate authentication and authorization checks.
Note: Some aspects of security such as user authentication are most likely to be the responsibility of the host environment rather than the web API, but it is still necessary to include security tests as part of the deployment process.
-
Test the exception handling performed by each operation and verify that an appropriate and meaningful HTTP response is passed back to the client application.
-
Verify that request and response messages are well-formed. For example, if an HTTP POST request contains the data for a new resource in x-www-form-urlencoded format, confirm that the corresponding operation correctly parses the data, creates the resources, and returns a response containing the details of the new resource, including the correct Location header.
-
Verify all links and URIs in response messages. For example, an HTTP POST message should return the URI of the newly-created resource. All HATEOAS links should be valid.
Important: If you publish the web API through an API Management Service, then these URIs should reflect the URL of the management service and not that of the web server hosting the web API.
-
Ensure that each operation returns the correct status codes for different combinations of input. For example:
- If a query is successful, it should return status code 200 (OK)
- If a resource is not found, the operation should returs HTTP status code 404 (Not Found).
- If the client sends a request that successfully deletes a resource, the status code should be 204 (No Content).
- If the client sends a request that creates a new resource, the status code should be 201 (Created)
Watch out for unexpected response status codes in the 5xx range. These messages are usually reported by the host server to indicate that it was unable to fulfil a valid request.
- Test the different request header combinations that a client application can specify and ensure that the web API returns the expected information in response messages. Examples of client request headers include:
- Accept. The format of the data in the body of the response should match the format specified by the client request. The Content-Type header of the response should specify the format of the data returned.
- ETag and If-None-Match. If the client application implements client-side caching it can provide these headers to indicate which version of an object it currently has in cache, and how the web API should handle the request. The section Considerations for Optimizing Client-Side Data Access earlier in this guidance describes how these requests should be handled.
- Range. If an operation supports partial responses (if a request can return a large amount of data, for example), verify that the operation respects any client Range headers and responds with the correct data and the appropriate Accept-Ranges, Content-Length, and Content-Range headers. The section Considerations for Handling Large Requests and Responses in this guidance provides more information on these requests.
- Test query strings. If an operation can take optional parameters (such as pagination requests), test the different combinations and order of parameters.
- Verify that asynchronous operations complete successfully. If the web API supports streaming for requests that return large binary objects (such as video or audio), ensure that client requests are not blocked while the data is streamed. If the web API implements polling for long-running data modification operations, verify that that the operations report their status correctly as they proceed. For more information, see the section Considerations for Maintaining Responsiveness, Scalability, and Availability in this guidance.
You should also create and run performance tests to check that the web API operates satisfactorily under duress. You can build a web performance and load test project by using Visual Studio Ultimate. For more information, see the page Run performance tests on an application before a release on the Microsoft website.
- The façade pattern describes how to provide an interface to a web API.…
- The page Learn About ASP.NET Web API on the Microsoft website provides a detailed introduction to building RESTful web services by using the Web API.
- The page Routing in ASP.NET Web API on the Microsoft website describes how convention-based routing works in the ASP.NET Web API framework.
- For more information on attribute-based routing, see the page Attribute Routing in Web API 2 on the Microsoft website.
- The Basic Tutorial page on the OData website provides an introduction to the features of the OData protocol.
- The ASP.NET Web API OData page on the Microsoft website contains examples and further information on implementing an OData web API by using ASP.NET.
- The page Introducing Batch Support in Web API and Web API OData on the Microsoft website describes how to implement batch operations in a web API by using OData.
- The article Idempotency Patterns on Jonathan Oliver’s blog provides an overview of idempotency and how it relates to data management operations.
- The Status Code Definitions page on the W3C website contains a full list of HTTP status codes and their descriptions.
- For detailed information on handling HTTP exceptions with the ASP.NET Web API, visit the Exception Handling in ASP.NET Web API page on the Microsoft website.
- The article Web API Global Error Handling on the Microsoft website describes how to implement a global error handling and logging strategy for a web API.
- The page Use WebJobs to run background tasks in Microsoft Azure Websites on the Microsoft website provides information and examples on using WebJobs to perform background operations on an Azure Website.
- The page Azure Notification Hubs Notify Users on the Microsoft website shows how you can use an Azure Notification Hub to push asynchronous responses to client applications.
- The API Management page on the Microsoft website describes how to publish a product that provides controlled and secure access to a web API.
- The Azure API Management REST API Reference page on the Microsoft website describes how to use the API Management REST API to build custom management applications.
- The About Traffic Manager Load Balancing Methods page on the Microsoft website summarizes how Azure Traffic Manager can be used to load-balance requests across multiple instances of a website hosting a web API.
- The Application Insights - Start monitoring your app's health and usage page on the Microsoft website provides detailed information on installing and configuring Application Insights in an ASP.NET Web API project.
- The page Verifying Code by Using Unit Tests on the Microsoft website provides detailed information on creating and managing unit tests by using Visual Studio.
- The page Run performance tests on an application before a release on the Microsoft website describes how to use Visual Studio Ultimate to create a web performance and load test project.