CHAPTER 16

image

Optimization and Performance

Having a fully functional HTTP API is the main concern when transmitting data over HTTP, but once lots of traffic starts to come in, you should realize that there might be bottlenecks and they could affect the performance of an application. Most concerns are application-specific, but there are some common scenarios and best practices that ought to be taken into consideration when you are building an application.

This chapter will look at both server- and HTTP-level concerns to see what parts of the application can be optimized for high performance and how that optimization can be accomplished. After a first look at asynchronous processing inside the controller action methods, we will take a closer look at HTTP-level caching and how to apply it with ASP.NET Web API most efficiently.

ASP.NET Web API Asynchronous Actions

ASP.NET Web API supports asynchronous actions using the task-based asynchronous pattern, or TAP. As was mentioned in Chapter 2, TAP was introduced in .NET v4.0. The programming model was improved in .NET v4.5, and it is also supported by C# 5.0 language features using the new async and await keywords. The beauty of these new features is that we can take advantage of them in ASP.NET Web API with asynchronous controller actions.

When you have long-running I/O-intensive operations inside the controller actions, you will be blocking the thread that the operation is running on. To prevent this blocking, you can implement the controller actions asynchronously. Listing 16-1 shows a simple asynchronous Web API controller action.

Listing 16-1.  Simple Web API Asynchronous Controller Action

public async Task<IEnumerable<Foo>> Get() {

    using(HttpClient httpClient = new HttpClient()) {
    
        var response = await httpClient.GetAsync("http://example.com/api");
        return await response.Content.ReadAsAsync<IEnumerable<Foo>>();
    }
}

The method, a standard asynchronous function, is marked with the async modifier and returns Task<T> for some T. In fact, Web API controller actions don’t need to be marked with the async modifier. So using await expressions is not compulsory, but it simplifies the code structure a lot, as was discussed in Chapter 2. If the action method’s return type is Task or Task<T> for some T, the Web API recognizes and processes the request asynchronously. Listing 16-1 exposes a collection of Foo entity, but one can also simply return HttpResponseMessage asynchronously (see Listing 16-2).

Listing 16-2.  An Asynchronous Controller Action Returning Task<HttpResponseMessage>

public Task<HttpResponseMessage> GetContent(string topic) {

    //Implementation goes here
}

ASP.NET Web API has been designed asynchronously from top to bottom. This is a very important part of the framework. Knowing now how asynchronous actions can be implemented, we can move on and take advantage of some real-world use cases.

Scenarios and Use Cases

It was mentioned several times in Chapter 2 that trying to leverage asynchrony for every scenario is not a good idea, especially for server applications, such as any ASP.NET Web API application. This section of the chapter will guide you through some common scenarios suitable for asynchronous controller actions in an ASP.NET Web API application.

Asynchronous Database Calls

It is common to come across arguments about asynchronous database calls in ASP.NET web applications. Most of the time synchronous database calls work pretty well, but in some cases processing database queries asynchronously has a very important impact on the application’s performance. One reason why asynchronous programming is not recommended for database queries is that it is extremely hard to get it right, even if TAP is adopted. However, with the new asynchronous language features of C# 5.0, it is easier but still complex.

Let’s assume that we have an SQL Server database out there somewhere for our car gallery application and we want to query that database to get the cars list. The database has basically nothing inside it except for a table and a stored procedure. We will try to get the cars list inside the Cars table through the stored procedure, and we will wait for one second inside that stored procedure in order to simulate the long-running database call. Listing 16-3 shows the whole database script.

Listing 16-3.  CarGallery SQL Server Database Script

CREATE TABLE dbo.[Cars] (
    Id INT IDENTITY(1000,1) NOT NULL,
    Model NVARCHAR(50) NULL,
    Make NVARCHAR(50) NULL,
    [Year] INT NOT NULL,
    Price REAL NOT NULL,
    CONSTRAINT [PK_Cars] PRIMARY KEY CLUSTERED (Id) ON [PRIMARY]
) ON [PRIMARY];
GO

CREATE PROCEDURE [dbo].[sp$GetCars]
AS
-- wait for 1 second
WAITFOR DELAY '00:00:01';
SELECT * FROM Cars;
GO

INSERT INTO dbo.Cars VALUES('Car1', 'Model1', 2006, 24950);
INSERT INTO dbo.Cars VALUES('Car2', 'Model1', 2003, 56829);
INSERT INTO dbo.Cars VALUES('Car3', 'Model2', 2006, 17382);
INSERT INTO dbo.Cars VALUES('Car4', 'Model3', 2002, 72733);

Before starting to implement the application to query this database, we have some additional configurations to make in order to write efficient asynchronous queries. First of all, ADO.NET does not process requests asynchronously by default even if code is written that way. Set the Asynchronous Processing property to true inside the connection string in order to allow the issuing of async requests through ADO.NET objects.

By default, the ADO.NET connection pool is also limited to 100 concurrent connections. In order to perform more asynchronous queries against the SQL Server instance, increase this number through the connection string by setting Max Pool Size value. Listing 16-4 shows the connection string we’ll be using.

Listing 16-4.  CarGallery Connection String Optimized for Asynchronous Processing

<connectionStrings>
    <add name="CarGalleryConnStr"
         connectionString="Data Source=.SQLEXPRESS;initial catalog=CarGallery;Max Pool Size=500;Integrated Security=true;Asynchronous Processing=True;"
         providerName="System.Data.SqlClient" />
</connectionStrings>

The configuration is now ready, and so let’s create a class that will do the query operations and returns results as .NET CLR objects (see Listing 16-5).

Listing 16-5.  GalleryContext Class That Does the Query Operations and Returns Results As C# CLR Objects

using System;
using System.Collections.Generic;
using System.Linq;
using System.Data.SqlClient;
using System.Configuration;
using System.Data;
using System.Threading.Tasks;

public class GalleryContext {

    readonly string _spName = "sp$GetCars";

    readonly string _connectionString =
        ConfigurationManager.ConnectionStrings["CarGalleryConnStr"].ConnectionString;

    public IEnumerable<Car> GetCarsViaSP() {

        using (var conn = new SqlConnection(_connectionString)) {
            using (var cmd = new SqlCommand()) {

                cmd.Connection = conn;
                cmd.CommandText = _spName;
                cmd.CommandType = CommandType.StoredProcedure;

                conn.Open();

                using (var reader = cmd.ExecuteReader()) {

                    return reader.Select(r => carBuilder(r)).ToList();
                }
            }
        }
    }

    public async Task<IEnumerable<Car>> GetCarsViaSPAsync() {

        using (var conn = new SqlConnection(_connectionString)) {
            using (var cmd = new SqlCommand()) {

                cmd.Connection = conn;
                cmd.CommandText = _spName;
                cmd.CommandType = CommandType.StoredProcedure;

                conn.Open();

                using (var reader = await cmd.ExecuteReaderAsync()) {
                    
                    return reader.Select(r => carBuilder(r)).ToList();
                }
            }
        }
    }

    //private helpers

    private Car carBuilder(SqlDataReader reader) {

        return new Car {

            Id = int.Parse(reader["Id"].ToString()),
            Make = reader["Make"] is DBNull ? null : reader["Make"].ToString(),
            Model = reader["Model"] is DBNull ? null : reader["Model"].ToString(),
            Year = int.Parse(reader["Year"].ToString()),
            Price = float.Parse(reader["Price"].ToString()),
        };
    }
}

The Car class is nothing but a simple POCO class (see Listing 16-6).

Listing 16-6.  Car Entity Class

public class Car {

    public int Id { get; set; }
    public string Make { get; set; }
    public string Model { get; set; }
    public int Year { get; set; }
    public float Price { get; set; }
}

You may also notice that a Select method was used on the SqlDataReader, and it does not exist. It is a small extension method that makes the code look prettier (see Listing 16-7).

Listing 16-7.  Select Extension Method for SqlDataReader

using System;
using System.Collections.Generic;
using System.Data.SqlClient;

public static class Extensions {

    public static IEnumerable<T> Select<T>(
        this SqlDataReader reader, Func<SqlDataReader, T> projection) {

        while (reader.Read()) {
            yield return projection(reader);
        }
    }
}

Look at the GalleryContext class, and you will see that there are two methods: GetCarsViaSP and GetCarsViaSPAsync. Both consume the same stored procedure and return the data. However, the GetCarsViaSPAsync method is an asynchronous version of this operation. Inside the GetCarsViaSPAsync method, the SqlCommand.ExecuteReaderAsync method is called instead of SqlCommand.ExecuteReader method, and ExecuteReaderAsync returns Task<SqlDataReader>, which represents an ongoing operation. As this is an asynchronous function, await it by using the await keyword in order to suspend the execution of the GetCarsViaSPAsync method till the operation is completed.

Now we should be able to query the SQL Server database inside the Web API controller actions. The implementation of our controller action is fairly simple for our sample (see Listing 16-8).

Listing 16-8.  SPCarsAsyncController Controller

public class SPCarsAsyncController : ApiController {

    readonly GalleryContext galleryContext = new GalleryContext();

    public async Task<IEnumerable<Car>> Get() {

        return await galleryContext.GetCarsViaSPAsync();
    }
}

With this implementation, the database is now being queried (a long-running operation in this case) without blocking any ASP.NET threads. Also, we will create another controller that will have only one action method as SPCarsAsyncController, and that action method will return the same result—using the synchronous GetCarsViaSP method this time, however (see Listing 16-9).

Listing 16-9.  SpCarsSyncController Controller

public class SPCarsSyncController : ApiController {

    readonly GalleryContext galleryContext = new GalleryContext();

    public IEnumerable<Car> Get() {

        return galleryContext.GetCarsViaSP();
    }
}

For this sample, the route shown in Listing 16-10 has been registered.

Listing 16-10.  Default Route for the Sample

protected void Application_Start(object sender, EventArgs e) {

    var config = GlobalConfiguration.Configuration;
    config.Routes.MapHttpRoute(
        "DefaultHttpRoute",
        "api/{controller}/{id}",
        new { id = RouteParameter.Optional }
    );
}

When the project is run, we will hit one of the endpoints of the API to warm up the application. To measure the difference here, we will perform a small load test on two of the endpoints with the Apache HTTP Server Benchmarking Tool, also known as ab.exe. A hundred concurrent requests will be made to each endpoint, and ab.exe will give the results. First, let’s hit /api/SPCarsSync, which makes a synchronous call to the database (see Figure 16-1).

9781430247258_Fig16-01.jpg

Figure 16-1. 100 concurrent requests to /api/SPCarsSync

image Note  The Apache HTTP Server Benchmarking Tool is a tiny benchmarking utility that allows multiple concurrent requests to be made to an HTTP endpoint. While ab.exe comes with Apache server installation, you don’t have to install Apache to get ab.exe. Download the zip file for Apache server, and you will find ab.exe there. It is a stand-alone executable file, one that can be used anywhere on your machine. (See http://httpd.apache.org/docs/2.2/programs/ab.html for more about ab.exe.) As an alternative, check out the Web Capacity Analysis Tool (WCAT), a lightweight HTTP load generation tool primarily designed to measure web server performance within a controlled environment (http://www.iis.net/downloads/community/2007/05/wcat-63-(x64 )).

Let’s do the same test on /api/SPCarsAsync, which makes an asynchronous call to the database (see Figure 16-2).

9781430247258_Fig16-02.jpg

Figure 16-2. 100 concurrent requests to /api/SPCarsAsync

image Note  Depending on the computer’s capabilities, numbers can change. This test was run on the Windows 8 operating system with Intel Core i5-2410M CPU @ 2.30GHz on an SATA drive.

Comparing the “Request per Seconds” field results shows that there is a major difference between the two. While the synchronous endpoint performs 19.30 requests per second, the asynchronous endpoint performs 80.09 requests per second—a huge performance increase.

However, querying an SQL Server database asynchronously may not be a good choice if the operation takes only a little time (for example, less than 40 milliseconds) and if your application has no high capacity requirements. In a real-world application, test your endpoints to see the performance results and adjust your application architecture accordingly.

Asynchronous HTTP Requests

Another scenario where asynchrony plays a key role involves HTTP requests, especially long-running HTTP requests. In Chapter 4, you saw a nice HTTP client API called HttpClient, which is under the System.Net.Http namespace and has no synchronous method making a network call. Use this API to consume an HTTP web service inside ASP.NET Web API controller actions. However, in this section we will use the System.Net.WebClient because new HttpClient has no synchronous methods to compare against. This section’s main purpose is to emphasize the performance impact of the asynchronous operations.

If you are on .NET 4.0, you may need to make a few configuration changes to leverage asynchrony efficiently for TCP connections. If the ASP.NET application uses System.Net to communicate over HTTP, the maximum connection limit may need to be increased. For ASP.NET, this is limited by default to 12 times the number of CPUs by the autoConfig feature set in the ASP.NET process model settings. That is, on a quad-core processor, you can have at most 12 × 4, or 48, concurrent connections to an IP endpoint. The easiest way to increase maxconnection in an ASP.NET application is to set System.Net.ServicePointManager.DefaultConnectionLimit programmatically. In an ASP.NET Web API application, this can be done inside the Application_Start method in the Global.asax file (see Listing 16-11). The reason why this setting has been configured programmatically is that autoConfig also needs to be disabled if configuration is to be set inside the configuration file. If autoConfig is disabled, other properties tied to autoConfig, including maxWorkerThreads and maxIoThreads, need to be set as well. You can find more about this setting in the following knowledge base article: http://support.microsoft.com/kb/821268

However, under .NET 4.5, System.Net.ServicePointManager.DefaultConnectionLimit value is set to Int32.MaxValue by default under ASP.NET. On the other hand, regardless of the framework version, this number is set to 2 in other application platforms rather than ASP.NET (a console application, a WPF application, etc.) and as other application platforms don’t have the autoConfig feature available, changing this value through the application configuration file should work out fine.

Listing 16-11.  Increasing the connectionManagement/maxconnection Limit Inside the Application_Start Method

protected void Application_Start(object sender, EventArgs e) {

    //increases the connectionManagement/maxconnection limit
    System.Net.ServicePointManager.DefaultConnectionLimit = int.MaxValue;
    
    //Lines omitted for brevity . . .
}

The sample needs to employ an HTTP web service, which returns a cars list with details. What users will get are cars whose price value is greater than $30,000 (see Listing 16-12).

image Note  Don’t worry about the implementation of this service. It is enough to know that the service, exposed through the http://localhost:11338/api/cars URI, will produce a list of cars. You’ll find the code for this simple service inside the source code provided for this book.

Listing 16-12.  Asynchronous HTTP Request Sample Inside a Controller Action

public class AsyncCarsController : ApiController {

    //HTTP service base address
    const string CountryAPIBaseAddress = "http://localhost:11338/api/cars";

    public async Task<IEnumerable<Car>> Get() {

        using (WebClient client = new WebClient()) {

            var content = await client.DownloadStringTaskAsync(CountryAPIBaseAddress);
            var cars = JsonConvert.DeserializeObject<List<Car>>(content);
            return cars.Where(x => x.Price > 30000.00F);
        }
    }
}

Listing 16-13 shows a synchronous version of the same action.

Listing 16-13.  Synchronous HTTP Request Sample Inside a Controller Action

public class SyncCarsController : ApiController {

    const string CountryAPIBaseAddress = "http://localhost:11338/api/cars";

    public IEnumerable<Car> Get() {

        using (WebClient client = new WebClient()) {
                
            var content = client.DownloadString(CountryAPIBaseAddress);
            var cars = JsonConvert.DeserializeObject<List<Car>>(content);
            return cars.Where(x => x.Price > 30000.00F);
        }
    }
}

After warming the application up, let’s generate a load test against each endpoint. We will send 500 requests this time and set the concurrency level to 100. Figure 16-3 shows the load test result for the synchronous endpoint.

9781430247258_Fig16-03.jpg

Figure 16-3. 500 requests with the concurrency level 100 to /api/SyncCars

We can see that we get 24.07 requests per second. Let’s do the same test on /api/AsyncCars which makes an asynchronous call to the web service (Figure 16-4).

9781430247258_Fig16-04.jpg

Figure 16-4. 500 requests with a concurrency level of 100 to /api/AsyncCars

We get 49.35 requests per second this time—an overwhelming result and a huge performance increase. With network operations like these inside your Web API application, making network calls asynchronously could improve performance dramatically.

Asynchronous File Uploads

You probably upload several files daily from web sites via HTML form file upload and develop several applications that enable this functionality. However, in this section, we will look at how to handle form file uploads in ASP.NET Web API asynchronously.

ASP.NET Web API has APIs that can handle data encoded with MIME multipart. We will use it in our sample. So the request arriving at our server should be of the multipart/form-data type. Listing 16-14 shows the sample application’s API controller.

Listing 16-14.  UploadController, FileResult, and CustomMultipartFormDataStreamProvider Classes

Fpublic class FileResult {

    public IEnumerable<string> FileNames { get; set; }
    public string Submitter { get; set; }
}

public class UploadController : ApiController {

    public async Task<FileResult> Post() {

        //Check whether it is an HTML form file upload request
        if (!Request.Content.IsMimeMultipartContent("form-data"))
        {
            //return UnsupportedMediaType response back if not
            throw new HttpResponseException(
                new HttpResponseMessage(
                    HttpStatusCode.UnsupportedMediaType)
            );
        }

        //Determine the upload path
        var uploadPath =
            HttpContext.Current.Server.MapPath("∼/Files");

        var multipartFormDataStreamProvider =
            new CustomMultipartFormDataStreamProvider(uploadPath);

        // Read the MIME multipart content asynchronously
        // using the stream provider just created.
        await Request.Content.ReadAsMultipartAsync(
            multipartFormDataStreamProvider);

        // Create response
        return new FileResult {

            FileNames =
                multipartFormDataStreamProvider
                .FileData.Select(
                    entry => entry.LocalFileName),

            Submitter =
                multipartFormDataStreamProvider
                .FormData["submitter"]
        };
    }
}

public class CustomMultipartFormDataStreamProvider
    : MultipartFormDataStreamProvider {

    public CustomMultipartFormDataStreamProvider(
        string rootPath) : base(rootPath) { }

    public override string GetLocalFileName(
        HttpContentHeaders headers) {

        if (headers != null &&
            headers.ContentDisposition != null) {

            return headers
                .ContentDisposition
                .FileName.TrimEnd('"').TrimStart('"'),
        }

        return base.GetLocalFileName(headers);
    }
}

First, check whether the request is an HTML form file upload request or not. If not, an UnsupportedMediaType response is getting returned. Then create a new MultipartFormDataStreamProvider instance. This class is suited for use with HTML file uploads for writing file content to a System.IO.FileStream. The stream provider looks at the Content-Disposition header field and determines an output System.IO.Stream based on the presence of a file name parameter. This makes it convenient to process MIME Multipart HTML Form data, which combine form data and file content. Finally, invoke the ReadAsMultipartAsync extension method of HttpContent to actually read and save the file to the specified place.

In order to test the functionality of this sample, here’s a console client that uploads the file to this HTTP service (see Figure 16-5).

9781430247258_Fig16-05.jpg

Figure 16-5. File upload console client application after upload is completed

After the upload is completed, the file can be seen inside the Files folder.

image Note  You’ll find the full source code of the file upload console client application inside the source code provided for this book.

Multiple Asynchronous Operations in One

Some situations may require you to run multiple asynchronous operations inside a controller action, each one of them to be executed separately. You can leverage WhenAll, one of the handy utilities of the Task class. The Task.WhenAll method receives a collection of tasks and creates another task that will complete when all the supplied tasks have completed (you have already seen this in Chapter 2).

In order to demonstrate a sample usage, we have created an API that exposes the cars list with two different resources. One resource returns the list of cheap cars, and the other, the list of expensive ones (see Listing 16-15). This API is the one that will be employed in our client application.

Listing 16-15.  Cars API Controller

public class CarsController : ApiController {

    readonly CarsContext _carsContext = new CarsContext();

    [HttpGet]
    public IEnumerable<Car> Cheap() {

        Thread.Sleep(1000);

        return _carsContext.GetCars(car => car.Price < 50000);
    }

    [HttpGet]
    public IEnumerable<Car> Expensive() {

        Thread.Sleep(1000);

        return _carsContext.GetCars(car => car.Price >= 50000);
    }
}

The CarsContext class is a class that has the mock-up data and displays that data with a public method (see Listing 16-16). Also, as you can see, we are hanging the thread for one second in order to see the results clearly when testing against client implementation.

Listing 16-16.  CarsContext Class

public class Car {

    public int Id { get; set; }
    public string Make { get; set; }
    public string Model { get; set; }
    public int Year { get; set; }
    public float Price { get; set; }
}

public class CarsContext {

    //data
    static List<Car> cars = new List<Car> {
        new Car {
            Id = 1,
            Make = "Make1",
            Model = "Model1",
            Year = 2010,
            Price = 10732.2F
        },
        new Car {
            Id = 2,
            Make = "Make2",
            Model = "Model2",
            Year = 2008,
            Price = 27233.1F
        },

        //Lines omitted for brevity
    };

    public IEnumerable<Car> GetCars(Func<Car, bool> predicate) {

        return cars.Where(predicate);
    }
}

The point in the client application is to get both the cheap and expensive cars and display them together at once. This goal will be achieved by consuming the APIs separately. Listing 16-17 shows the complete implementation for this.

Listing 16-17.  Cars API Controller in the Client Application

public class Car {

    public int Id { get; set; }
    public string Make { get; set; }
    public string Model { get; set; }
    public int Year { get; set; }
    public float Price { get; set; }
}

public class CarsController : ApiController {

    readonly List<string> _payloadSources = new Liszt<string> {
        "http://localhost:2700/api/cars/cheap",
        "http://localhost:2700/api/cars/expensive"
    };

    readonly HttpClient _httpClient = new HttpClient();

    [HttpGet]
    public async Task<IEnumerable<Car>> AllCars() {

        var carsResult = new List<Car>();

        foreach (var uri in _payloadSources) {

            var cars = await getCars(uri);
            carsResult.AddRange(cars);
        }

        return carsResult;
    }

    //private helper which gets the payload and hands it back as IEnumerable<Car>
    private async Task<IEnumerable<Car>> getCars(string uri) {

        var response = await _httpClient.GetAsync(uri);
        var content = await response.Content.ReadAsAsync<IEnumerable<Car>>();

        return content;
    }
}

To make this example work, the so-called action-based route is used (you are familiar with it from Chapter 9) (see Listing 16-18).

Listing 16-18.  Action-Based Route Used for the Sample in Listing 16-17

protected void Application_Start(object sender, EventArgs e) {

    GlobalConfiguration.Configuration.Routes.MapHttpRoute(
        "DefaultHttpRoute",
        "api/{controller}/{action}/{id}",
        new { id = RouteParameter.Optional }
    );
}

Inside the CarsController, there is a public method named AllCars, which will be our action. The AllCars method is, as usual, marked with the async modifier for all asynchronous Web API controller actions. Its return type is Task<IEnumerable<Car>>. Inside the AllCars action method, loop through the API source list, which has been defined as a List<string> object, and inside the foreach loop, get the cars list from the HTTP service asynchronously. Notice that the await keyword is used inside the foreach loop without any worries. This action was very hard to accomplish before, but the new C# asynchronous language features handle it now. When tested, this API should complete in roughly two seconds, because each HTTP call takes about one second. Figure 16-6 shows the benchmarking result issued with the Apache Benchmarking Tool.

9781430247258_Fig16-06.jpg

Figure 16-6. Benchmarking result of Cars API AllCars controller action

Let’s see the implementation of the same operation with the help of Task.WhenAll (see Listing 16-19).

Listing 16-19.  Cars API AllCarsInParallel Controller Action Using Task.WhenAll

[HttpGet]
public async Task<IEnumerable<Car>> AllCarsInParallel() {

    var allTasks = _payloadSources.Select(uri =>
        getCars(uri)
    );

    IEnumerable<Car>[] allResults = await Task.WhenAll(allTasks);

    return allResults.SelectMany(cars => cars);
}

First of all, get all of the tasks that are to be run in parallel. This is done here with the LINQ Select method and a lambda expression, and the type of allTasks variable is IEnumerable<Task<IEnumerable<Car>>>. As a second action, pass the collection of tasks into Task.WhenAll method to get a result of the type IEnumerable<Car> array. Actually, the Task.WhenAll method returns Task<IEnumerable<Car>[]> in this case, but as we “await” on this, we get the result as IEnumerable<Car> array. Task.WhenAll will run the supplied collection of tasks in parallel, and this will shorten the time for completion. Finally, we use another LINQ method, SelectMany, to return the result. SelectMany takes a series of collections and combines them all into one. Measure this action’s completion time, and you’ll see that it is reduced by half (see Figure 16-7).

9781430247258_Fig16-07.jpg

Figure 16-7. Benchmarking the result of Cars API AllCarsInParallel controller action

HTTP Caching

Caching, in general, is an optimization to improve performance and scalability by keeping a resource inside a fast-reachable store—a computer’s random-access memory (RAM), for example. If you have resources that aren’t likely to change over a given amount of time, making those resources cacheable provides huge performance benefits.

In the scope of ASP.NET Web API, the benefits of caching can be leveraged along with HTTP, a widely used and understood transport protocol with a very detailed specification. Inside this specification, HTTP caching plays a big part. It is controlled by the HTTP caching headers, which are sent by the web server to specify how long a resource is valid and when it last changed.

This section will take a close look at HTTP caching and how to implement this mechanism.

HTTP Specification for Caching

Caching’s main purpose in HTTP/1.11 is to eliminate the need for sending requests in most cases and for sending full responses in most other cases. The HTTP caching mechanism can be divided into several parts:

  • control of cachable resource by the origin server
  • validation of a cached resource
  • invalidation after updates and deletions

Let’s look at each of these topics separately before implementing the logic with ASP.NET Web API.

image Note  Although most parts of HTTP caching are covered in this section, there is more information about the HTTP caching specification (RFC 2616 Section 13, Caching in HTTP) at www.w3.org/Protocols/rfc2616/rfc2616-sec13.html.

Control of the Cachable Resource by the Origin Server

In order for caching to work with HTTP, the origin server of the resource must indicate that it supports caching. This action is performed by specific HTTP response headers. If the resource can be cached, the origin server also dictates by whom the content can be cached and for how long.

The main header that controls the cache for the server and the client is the Cache-Control2 general header. The Cache-Control header dictates the cache behavior of the resource, which must be obeyed by all caching mechanisms along the request/response chain. Either the request or the response message may carry the Cache-Control header.

If the response message has the Cache-Control header, the Cache-Control header carries the cacheability information for the resource, including how long and by whom it can be cached. Figure 16-8 shows a sample GET request and its response message, which carries the Cache-Control header.

9781430247258_Fig16-08.jpg

Figure 16-8. A GET request and its response message, which has the Cache-Control header

In the response header, the origin server states that the resource can be cached for a day (in delta-seconds) with max-age directive, but it must not be cached by a shared cache, such as a proxy server. A client with this response message can cache the resource for a day and never needs to hit the origin server during that time.

Also, using the no-cache directive, the origin server can state that the response is noncacheable. Figure 16-9 shows a GET request and its response message, which carries the Cache-Control header with a no-cache directive.

9781430247258_Fig16-09.jpg

Figure 16-9. A GET Request and its response message, which has the Cache-Control header with a no-cache directive

As the content owners, we may state that the resource is cacheable for a day through the Cache-Control header so that the client can cache the response and serve it for a day. But what if the resource is changed during that cache period? The client would serve the stale resource (as indicated, the resource is cacheable only for a day). This is where cached resource validation—the next topic—enters the scene.

Validation of a Cached Resource

Consumers of our resource would prefer to have a fresh copy of the resource’s representation. To ensure that they get what they want, consumers need to have some way of validating the resource with the origin server. Several HTTP headers enable them to do this.

Along with the response message, the origin server can append either the ETag or Last-Modified header (or both of them). The ETag (short for “entity tag”)3 is a response header that represents the state of a resource at a given point in time. An ETag is also just an opaque, server-generated string, one that the client shouldn’t try to infer any meaning from. On the other hand, from the syntactic standpoint the ETag should, according to HTTP specification, always be in quotes.

The ETag is used for comparing two or more entities from the same requested resource, a resource identified by its unique URI, and one of the parts of the HTTP validation model. An entity tag has two main roles inside the HTTP specification. One is to validate the cached resource (the main subject of this section), and the other is to make concurrency checks.

When a consumer sends a request and receives a response that includes the ETag value, s/he can then send that ETag value in a conditional GET request to server. The server then looks at the received ETag value and decides whether the resource is up to date or stale. The way that the server informs the consumer of the result of this validation is determined via the HTTP response status code. If the origin server decides that the resource is still fresh, the server can send a “304 Not Modified” response with no message body. The ability to send a 304 response can save a lot of bandwidth and reduce response time. Besides, the origin server doesn’t have to process the resource creation logic, which might involve a database lookup or a similar operation.

To see a sample showing this in action, let’s send a GET request to a URI and respond with an ETag header (see Figure 16-10).

9781430247258_Fig16-10.jpg

Figure 16-10. A GET Request and its response message, which has the ETag header

As you can see, the response has both Cache-Control and ETag headers. The Cache-Control header indicates that the resource must not be cached by a shared cache and that the resource can be cached for 0 seconds. The origin server stated that this resource representation can be cached for 0 seconds because the response also contains an ETag key and the server wants the client to revalidate the resource when the cache expires—instantly, as it happens, since the max-age is 0.

Let’s assume that we, as the client, cached the resource and need to send another request to the same URI. When  we send a request this time, we will append the ETag header value, previously obtained from the server, to the request message with the If-None-Match4 header. This means that we will send a conditional GET request to the origin server. The If-None-Match header will carry the entity tag and tell the server to return the full response only if this ETag is not fresh. If the resource is up to date, the server should respond with “304 Not Modified” response (see Figure 16-11).

9781430247258_Fig16-11.jpg

Figure 16-11. A conditional GET request and its “304 Not Modified” response

As you can see, the response status code is now “304 Not Modified,” and the response body is empty. This response tells the client to go ahead and use the cached resource, as it is still up to date. Then the client will pull the resource out of the cache and use it.

Besides the ETag header, there is one more validation header that can be used: the Last-Modified5 entity header. This header states the date and time at which the resource was last modified. The client can later use this value to check the state of the resource. Figure 16-12 shows a response that carries a Last-Modified header.

9781430247258_Fig16-12.jpg

Figure 16-12. A GET Request and its response message, which has the Last-Modified header

With this header value in place, the client can now send a conditional GET request to the origin server with the Last-Modified header value with an If-Modified-Since6 header. If the requested resource has not been modified since the time specified in the If-Modified-Since header, a full response will not be returned from the server. Instead, a “304 Not Modified” response will be returned without any message body (see Figure 16-13).

9781430247258_Fig16-13.jpg

Figure 16-13. A conditional GET request with an If-Modified-Since header and its “304 Not Modified” response

You probably noticed that requests have been sent without an Accept header and responses have been returned in application/json format, the default format of the origin server. However, if a request were sent with an Accept header for application/xml, the XML representation of the resource would be returned. If the client has any validation header values from the response whose format is application/json, it will try to use the same validation value for a request whose Accept header states application/xml. If the server has an up-to-date copy of the resource, it will respond with a “304 Not Modified” status code. This is apperently not the desired behavior, because the resource format is different and they should be cached separately.

Another response header can be used to solve this problem. The Vary7 header value indicates the set of request-header fields that determines whether a cache is permitted to use the response to reply to a subsequent request. Figure 16-14 shows a response message that carries the Vary header.

9781430247258_Fig16-14.jpg

Figure 16-14. A GET Request and its response message, which has the ETag and Vary headers

The response message in this representation states that the Accept and the Accept-Charset header values are not changed. For example, if you try to send a conditional GET request with the ETag value obtained from this response and with the Accept header value of application/json, you should get a full “200 OK” response along with the response body, because the Accept header is different from that in the previous request (see Figure 16-15).

9781430247258_Fig16-15.jpg

Figure 16-15. A conditional GET request and its response message, which has the ETag and Vary headers

You see that the ETag value is different in this representation of the resource. If you were to send a conditional GET request with this ETag value for a resource in application/json format, you would get “304 Not Modified” response if the resource is still up to date (see Figure 16-16).

9781430247258_Fig16-16.jpg

Figure 16-16. A conditional GET request and its “304 Not Modified” response message

Invalidation After Updates and Deletions

As was explained in Chapter 3, POST, PUT, and DELETE are not recognized as safe verbs. They can change the state of a resource. If the origin server generates ETags by computing a hash value out of the resource content, then invalidating the cache would be easy, because if the content has changed, the hash value will be changed. However, as you will see, when implementing the HTTP caching in ASP.NET Web API, generating the ETag value by computing the hash of the content is unnecessary if the application’s architecture is crafted correctly.

Assuming that /api/cars/1 URI represents the Car 1 object in our HTTP API, if we send a PUT or DELETE request to /api/cars/1, it is obvious that Car 1’s state will change, and the previously created ETags can be invalidated. This requires storing the ETags in a separate place, such as memory collection, but this makes it much more efficient.

Applying HTTP Caching in ASP.NET Web API

ASP.NET Web API extensibility points make it easy to implement HTTP caching and to plug it into our application. With such rich HTTP API elements as strongly typed headers, it becomes even easier. What needs to be done now is to create a message handler to handle the caching and cache invalidation.

Let’s look at a sample ASP.NET Web API application where we can retrieve a list of cars, get an individual car, update a car, and delete a car. Figure 16-17 shows the GET request against /api/cars and the corresponding response message with the headers.

9781430247258_Fig16-17.jpg

Figure 16-17. A GET request against /api/cars

Notice that the response contains neither the ETag nor the Last-Modified header, and the Cache-Control header states that this resource representation cannot be cached. Let’s see how we can implement our HttpCachingHandler to enable caching for the application.

HTTP Caching Message Handler

First off, a separate class is needed to keep a record of the cached resource. For this purpose, we create a CacheableEntity class (see Listing 16-20).

Listing 16-20.  The CacheableEntity Class

public class CacheableEntity {

    public CacheableEntity(string resourceKey) {
            
        ResourceKey = resourceKey;
    }

    public string ResourceKey { get; private set; }
    public EntityTagHeaderValue EntityTag { get; set; }
    public DateTimeOffset LastModified { get; set; }

    public bool IsValid(DateTimeOffset modifiedSince) {

        var lastModified = LastModified.UtcDateTime;
        return (lastModified.AddSeconds(−1) < modifiedSince.UtcDateTime);
    }
}

The CacheableEntity class basically holds all the necessary information about a cacheable resource. The ResourceKey property of the CacheableEntity class will be the ID of the cached entity, against which others can be compared. The EntityTag and LastModified properties will hold corresponding header values. A method called IsValid will be used when drawing comparisons against the last-modified date and time of the resource.

image Note  This section’s main concern is, not to implement a fully functional and extensible caching handler, but to point you in the right direction. If you want a more flexible and robust caching handler, one you can plug into your ASP.NET Web API, Ali Kheyrollahi maintains an open source project called CacheCow on GitHub (https://github.com/aliostad/CacheCow). It allows you to extend the project-provided caching handler to fit your needs.

Let’s have a look at the core structure of the HttpCachingHandler message handler (see Listing 16-21).

Listing 16-21.  The Initial Structure of the HttpCachingHandler

public class HttpCachingHandler : DelegatingHandler {

    private readonly string[] _varyHeaders;

    public HttpCachingHandler(params string[] varyHeaders) {

        _varyHeaders = varyHeaders;
    }

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken) {

        return base.SendAsync(request, cancellationToken);
    }

    private string GetResourceKey(
        Uri uri,
        string[] varyHeaders,
        HttpRequestMessage request) {

        throw new NotImplementedException();
    }

    private string GetResourceKey(
        string trimedRequestUri,
        string[] varyHeaders,
        HttpRequestMessage request) {

        throw new NotImplementedException();
    }

    private EntityTagHeaderValue GenerateETag(string[] varyHeaders) {

        throw new NotImplementedException();
    }

    private string GetRequestUri(Uri requestUri) {

        throw new NotImplementedException();
    }
}

First of all, there is a constructor method that takes an array of strings as a parameter for Vary header values. There are also four private helper methods that are not yet implemented (we will implement them before moving further).

The GetRequestUri method will trim the RequestUri as the /api/cars URI, and the /api/cars/ URI will represent the same resource. So we will trim the “/” character at the end, if it is present. Listing 16-22 shows the implementation of the GetRequestUri method.

Listing 16-22.  The Implementation of the GetRequestUri Method

private string GetRequestUri(Uri requestUri) {
    return string.Concat(
        requestUri.LocalPath.TrimEnd('/'),
        requestUri.Query).ToLower(CultureInfo.InvariantCulture);

}

The GenerateETag method is just a helper class where the ETag value is actually created. As Listing 16-23 shows, a Guid is used to generate the ETag.

Listing 16-23.  The GenerateETag Method Implementation

private EntityTagHeaderValue GenerateETag() {
    var eTag = string.Concat(
        """, Guid.NewGuid().ToString("N"), """);
    return new EntityTagHeaderValue(eTag);

}

Notice that we put the ETag value in quotes. If this action wasn’t performed, there would be a runtime error by the EntityTagHeaderValue class because it forces you to generate the ETag in quotes.

Last is the GetResourceKey method, which is a little bit more complicated than the others. Listing 16-24 shows the implementations of the GetResourceKey method and its only overload.

Listing 16-24.  Implementation of the GetResourceKey Method

private string GetResourceKey(
    Uri uri,
    string[] varyHeaders,
    HttpRequestMessage request) {

    return GetResourceKey(GetRequestUri(uri), varyHeaders, request);
}

private string GetResourceKey(
    string trimedRequestUri,
    string[] varyHeaders,
    HttpRequestMessage request) {

    var requestedVaryHeaderValuePairs = request.Headers
        .Where(x => varyHeaders.Contains(x.Key))
        .Select(x => string.Format("{0}:{1}", x.Key, string.Join(";", x.Value)));

    return string.Format(
        "{0}:{1}",
        trimedRequestUri,
        string.Join("_", requestedVaryHeaderValuePairs)).ToLower(
            CultureInfo.InvariantCulture);
}

The difference between these two methods is that one accepts a Uri type as parameter and the other accepts a string as parameter for a trimmed URI. Actual implementation is inside the second method. Inside the second GetResourceKey method, first get all values of the headers whose keys will be passed through the constructor as Vary headers keys, and then join them to create a string value. Second, join those with the trimmed request URI to generate a unique key for our resource.

With our private helper methods in place, we can now go ahead and implement the actual logic inside a SendAsync method. But before moving further, let’s create a static ConcurrentDictionary<string, CacheableEntity> instance inside the HttpCachingHandler class to keep track of the generated CacheableEntity instances (see Listing 16-25).

Listing 16-25.  Static Dictionary Instance for the CacheEntity Values Inside the HttpCachingHandler Class

public class HttpCachingHandler : DelegatingHandler {

    private static ConcurrentDictionary<string, CacheableEntity> _eTagCacheDictionary =
            new ConcurrentDictionary<string, CacheableEntity>();

    //Lines omitted for brevity
}

First, you ought to be able to send “304 Not Modified” responses if the cache is still valid. To do that, follow this approach:

  1. Inspect whether the request is a GET request or not.
  2. Generate the ResourceKey according to the request URI and Vary headers.
  3. If the request is a GET request, retrieve the If-None-Match, If-Modified-Since header values.
  4. Inspect whether the request contains any ETag values or not.
  5. If the request contains any ETag values, check whether there are any CacheableEntity instances for this request inside our CacheableEntity dictionary.
  6. If there are any CacheableEntity instances for this request inside our CacheableEntity dictionary, go ahead and compare them with the ETag values obtained from the request.
  7. If there is a match, return a “304 Not Modified” response.
  8. If the request doesn’t contain any ETag values, check whether the request contains the If-Modified-Since header.
  9. If the request contains the If-Modified-Since header, check whether there are any CacheableEntity instances for this request inside our CacheableEntity dictionary.
  10. If there are any CacheableEntity instances for this request inside our CacheableEntity dictionary, check the validity of the cache according to the If-Modified-Since header value.
  11. If the cache is valid, return a “304 Not Modified” response.
  12. If the request is not a GET request or if any of the cache validations doesn’t pass, continue processing the request.

Listing 16-26 expresses these requirements with the code inside the SendAsync method.

Listing 16-26.  Initial Implementation of the SendAsync Method

protected override async Task<HttpResponseMessage> SendAsync(
    HttpRequestMessage request,
    CancellationToken cancellationToken) {

    var resourceKey = GetResourceKey(request.RequestUri, _varyHeaders, request);
    CacheableEntity cacheableEntity = null;
    var cacheControlHeader = new CacheControlHeaderValue {
        Private = true,
        MustRevalidate = true,
        MaxAge = TimeSpan.FromSeconds(0)
    };

    if (request.Method == HttpMethod.Get) {

        var eTags = request.Headers.IfNoneMatch;
        var modifiedSince = request.Headers.IfModifiedSince;
        var anyEtagsFromTheClientExist = eTags.Any();
        var doWeHaveAnyCacheableEntityForTheRequest =
            _eTagCacheDictionary.TryGetValue(resourceKey, out cacheableEntity);

        if (anyEtagsFromTheClientExist) {

            if (doWeHaveAnyCacheableEntityForTheRequest) {
                if (eTags.Any(x => x.Tag == cacheableEntity.EntityTag.Tag)) {

                    var tempResp = new HttpResponseMessage(HttpStatusCode.NotModified);
                    tempResp.Headers.CacheControl = cacheControlHeader;
                    return tempResp;
                }
            }
        }
        else if (modifiedSince.HasValue) {

            if (doWeHaveAnyCacheableEntityForTheRequest) {
                if (cacheableEntity.IsValid(modifiedSince.Value)) {

                    var tempResp = new HttpResponseMessage(HttpStatusCode.NotModified);
                    tempResp.Headers.CacheControl = cacheControlHeader;
                    return tempResp;
                }
            }
        }
    }

    try {

        return await base.SendAsync(request, cancellationToken);
    }
    catch (Exception ex) {
                
        return request.CreateErrorResponse(
            HttpStatusCode.InternalServerError, ex);
    }
}

Everything done inside the SendAsync method here accords with the just-listed requirements, but there are a couple of parts worth pointing out. First of all, we have marked the method with an async modifier because the await operator will be used inside our method. Also, we  put the base.SendAsync method inside a try/catch block because we want to catch the exception, which might occur inside the pipeline (e.g., inside another message handler), and return a formatted error message instead of just terminating the process.

So far, so good—but there’s a problem. The logic to generate the cacheable entities and send them back as responses hasn’t been implemented. The place to perform this action is where the response comes back through the pipeline—the last place where there’s a chance to modify the response. We need to perform this action because we want to have the full response, which is ready to go out the door, to make the modifications (see Listing 16-27).

Listing 16-27.  Implementation of the Generating Cacheable Response Messages

protected override async Task<HttpResponseMessage> SendAsync(
    HttpRequestMessage request,
    CancellationToken cancellationToken) {

    //Lines omitted for brevity

    HttpResponseMessage response;
    try {

        response = await base.SendAsync(request, cancellationToken);
    }
    catch (Exception ex) {
                
        response = request.CreateErrorResponse(
            HttpStatusCode.InternalServerError, ex);
    }

    if (response.IsSuccessStatusCode) {

        if (request.Method == HttpMethod.Get &&
            !_eTagCacheDictionary.TryGetValue(
                resourceKey, out cacheableEntity)) {

            cacheableEntity = new CacheableEntity(resourceKey);
            cacheableEntity.EntityTag = GenerateETag();
            cacheableEntity.LastModified = DateTimeOffset.Now;

            _eTagCacheDictionary.AddOrUpdate(
                resourceKey, cacheableEntity, (k, e) => cacheableEntity);
        }

        if (request.Method == HttpMethod.Get) {

            response.Headers.CacheControl = cacheControlHeader;
            response.Headers.ETag = cacheableEntity.EntityTag;
            response.Content.Headers.LastModified = cacheableEntity.LastModified;

            _varyHeaders.ForEach(
                varyHeader => response.Headers.Vary.Add(varyHeader));
        }
    }

    return response;
}

A little extension method called ForEach is used here. Listing 16-28 shows its implementation.

Listing 16-28.  ForEach Extension Method

internal static class IEnumerableExtensions {

    public static void ForEach<T>(
        this IEnumerable<T> enumerable, Action<T> action) {

        foreach (var item in enumerable)
            action(item);
    }
}

The code stays the same before base.SendAsync is called, but the basic logic for creating the cacheable resource if the request is a GET request has been added. First, we check whether the response status code is a success status code. If it is, then we go ahead check whether there is a CacheableEntity for this request. If there isn’t, we generate one. Last, if the request is a GET request, we make the response cacheable and return it.

Before trying this out, we need to register the message handler. Generally, we would want to register this caching handler as the first message handler because this handler should see the request first and the response last. Registering it as the first message handler will provide this ability (see Listing 16-29).

Listing 16-29.  Registering the HttpCachingHandler

protected void Application_Start(object sender, EventArgs e) {

    HttpConfiguration config = GlobalConfiguration.Configuration;

    //Lines omitted for brevity

    config.MessageHandlers.Insert(0,
        new HttpCachingHandler("Accept", "Accept-Charset"));
}

image Note  If you have a message handler that performs the request authentication, you may want to register it before the caching handler, as you’d never want to deal with unauthenticated requests. Since everything depends on your scenario, consider the order while you are registering your message handlers.

You can see that Accept and Accept-Charset headers have to be passed in to be used as Vary header values. Let’s give this a try with Fiddler. When a request is sent to /api/cars, the cacheable response should be returned (see Figure 16-18).

9781430247258_Fig16-18.jpg

Figure 16-18. A GET request to /api/cars and its cacheable response

The cacheable response was returned. Now let’s use the ETag to make a conditional GET request and see whether a “304 Not Modified” response comes back (see Figure 16-19).

9781430247258_Fig16-19.jpg

Figure 16-19. A conditional GET request and “304 Not Modified” response

It worked as expected! Now let’s use the same ETag to make another conditional GET request, this time for the application/xml format. According to our requirement, there should be a full “200 OK” response and a new ETag (see Figure 16-20).

9781430247258_Fig16-20.jpg

Figure 16-20. A conditional GET request and full “200 OK” response

Again, it worked as expected. This time, let’s use the Last-Modified header value returned by the application/xml request to send a conditional GET request. A “304 Not Modified” response should come back (see Figure 16-21).

9781430247258_Fig16-21.jpg

Figure 16-21. A conditional GET request with an If-Modified-Since header and a “304 Not Modified” response

Let’s change the Accept-Charset value and send the same conditional GET request shown in Figure 16-21. Figure 16-22 shows the result.

9781430247258_Fig16-22.jpg

Figure 16-22. A conditional GET request with an If-Modified-Since header and a full “200 OK” response

A full “200 OK” response came back because the cache differs by the Accept-Charset header as well.

Our little handler works as expected, but it doesn’t cover invalidation of a cached entity. For example, let’s send a POST request to /api/cars to add a new car to the list (see Figure 16-23).

9781430247258_Fig16-23.jpg

Figure 16-23. A POST request to /api/cars

Now the collection is changed. Let’s send a conditional GET request to /api/cars with the previously obtained ETag value. Normally, a full “200 OK” response should come back, but because our handler doesn’t invalidate the cache, “304 Not Modified”, which wrongly states the freshness of the cache, will be returned (Figure 16-24).

9781430247258_Fig16-24.jpg

Figure 16-24. A conditional GET request with an If-None-Match header and a “304 Not Modified” response

The next section will show how to implement the invalidation scenario and also make it extensible, so that new invalidation logics can be plugged in.

Cache Invalidation with HttpCachingHandler

When we receive a POST, PUT or DELETE request to our resources, it is certain that our resource state has been changed if the response status code for the request is a success status code. In order to implement this functionality, we just need to make small changes to our previously implemented HttpCachingHandler class (Listing 16-30).

Listing 16-30.  Implementation of Initial Cache Invalidation Logic

protected override async Task<HttpResponseMessage> SendAsync(
    HttpRequestMessage request,
    CancellationToken cancellationToken) {

    //Lines omitted for brevity

    HttpResponseMessage response;
    try {

        response = await base.SendAsync(request, cancellationToken);
    }
    catch (Exception ex) {
                
        response = request.CreateErrorResponse(
            HttpStatusCode.InternalServerError, ex);
    }

    if (response.IsSuccessStatusCode) {

        if (request.Method == HttpMethod.Get &&
            !_eTagCacheDictionary.TryGetValue(resourceKey, out cacheableEntity)) {

            cacheableEntity = new CacheableEntity(resourceKey);
            cacheableEntity.EntityTag = GenerateETag();
            cacheableEntity.LastModified = DateTimeOffset.Now;

            _eTagCacheDictionary.AddOrUpdate(
                resourceKey, cacheableEntity, (k, e) => cacheableEntity);
        }

        if (request.Method == HttpMethod.Put ||
            request.Method == HttpMethod.Post ||
            request.Method == HttpMethod.Delete) {

            var cacheEntityKey = _eTagCacheDictionary.Keys.FirstOrDefault(
                x => x.StartsWith(
                    string.Format("{0}:", invalidCacheUri),
                    StringComparison.InvariantCultureIgnoreCase));
            
            if (!string.IsNullOrEmpty(cacheEntityKey)) {
            
                CacheableEntity outVal = null;
                _eTagCacheDictionary.TryRemove(key, out outVal);
            }
        }
        else {

            response.Headers.CacheControl = cacheControlHeader;
            response.Headers.ETag = cacheableEntity.EntityTag;
            response.Content.Headers.LastModified = cacheableEntity.LastModified;

            _varyHeaders.ForEach(
                varyHeader => response.Headers.Vary.Add(varyHeader));
        }
    }

    return response;
}

But these changes still don’t cover all the possibilities. For example, when there is a POST, PUT, or DELETE request to /api/cars/{id}, the /api/cars resource is also changed, but this code does not invalidate its cache. In order to be more flexible, let’s open up a extensibility point for our message handler to invalidate the cache resources. Listing 16-31 shows the complete changes to HttpCachingHandler to enable this feature.

Listing 16-31.  Final Implementation of the Cache Invalidation Logic

public class HttpCachingHandler : DelegatingHandler {

    private static ConcurrentDictionary<string, CacheableEntity> _eTagCacheDictionary =
            new ConcurrentDictionary<string, CacheableEntity>();

    public ICollection<Func<string, string[]>> CacheInvalidationStore =
        new Collection<Func<string, string[]>>();

    //Lines omitted for brevity
    
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken) {

        HttpResponseMessage response;
        try {

            response = await base.SendAsync(request, cancellationToken);
        }
        catch (Exception ex) {
                
            response = request.CreateErrorResponse(
                HttpStatusCode.InternalServerError, ex);
        }

        if (response.IsSuccessStatusCode) {

            if ((!_eTagCacheDictionary.TryGetValue(resourceKey, out cacheableEntity) ||
                request.Method == HttpMethod.Put ||
                request.Method == HttpMethod.Post) &&
                request.Method != HttpMethod.Delete) {

                cacheableEntity = new CacheableEntity(resourceKey);
                cacheableEntity.EntityTag = GenerateETag();
                cacheableEntity.LastModified = DateTimeOffset.Now;

                _eTagCacheDictionary.AddOrUpdate(
                    resourceKey, cacheableEntity, (k, e) => cacheableEntity);
            }

            if (request.Method == HttpMethod.Put ||
                request.Method == HttpMethod.Post ||
                request.Method == HttpMethod.Delete) {

                HashSet<string> invalidCaches = new HashSet<string>();
                invalidCaches.Add(GetRequestUri(request.RequestUri));

                CacheInvalidationStore.ForEach(
                    func => func(GetRequestUri(request.RequestUri))
                        .ForEach(uri => invalidCaches.Add(uri)));

                invalidCaches.ForEach(invalidCacheUri => {

                    var cacheEntityKeys = _eTagCacheDictionary.Keys.Where(
                        x => x.StartsWith(
                            string.Format("{0}:", invalidCacheUri),
                            StringComparison.InvariantCultureIgnoreCase));

                    cacheEntityKeys.ForEach(key => {
                        if (!string.IsNullOrEmpty(key)) {

                            CacheableEntity outVal = null;
                            _eTagCacheDictionary.TryRemove(key, out outVal);
                        }
                    });
                });
            }
            else {

                response.Headers.CacheControl = cacheControlHeader;
                response.Headers.ETag = cacheableEntity.EntityTag;
                response.Content.Headers.LastModified = cacheableEntity.LastModified;

                _varyHeaders.ForEach(
                    varyHeader => response.Headers.Vary.Add(varyHeader));
            }
        }

        return response;
    }

    //Lines omitted for brevity
}

First of all, a public property which is of type ICollection<Func<string, string[]>> has been added to our handler. If there is a POST, PUT, or DELETE request to a resource, each Func object will be called by passing the request URI, and a string array value will be expected in return. This string array will carry the resource URIs whose cache is no longer valid. Finally, those URIs will be aggregated, and each corresponding cache will be removed.

Listing 16-32 shows how to register this message handler.

Listing 16-32.  Registration of the HttpCachingHandler

protected void Application_Start(object sender, EventArgs e) {

    HttpConfiguration config = GlobalConfiguration.Configuration;
    
    //Lines omitted for brevity

    var eTagHandler = new HttpCachingHandler("Accept", "Accept-Charset");
    eTagHandler.CacheInvalidationStore.Add(requestUri => {

        if (requestUri.StartsWith(
            "/api/cars/",
            StringComparison.InvariantCultureIgnoreCase)) {

            return new[] { "/api/cars" };
        }

        return new string[0];
    });

    config.MessageHandlers.Insert(0, eTagHandler);
}

One cache invalidation rule has been added here. It indicates that the /api/cars resource should be invalid if there is a POST, PUT, or DELETE request to a resource URI that starts with /api/cars. Let’s give this a try. First, send a request to /api/cars to get back a cacheable resource (see Figure 16-25).

9781430247258_Fig16-25.jpg

Figure 16-25. A GET request to /api/cars and its cacheable response

Then use the obtained ETag value to send a conditional GET request to /api/cars (see Figure 16-26).

9781430247258_Fig16-26.jpg

Figure 16-26. A conditional GET request to /api/cars and its “304 Not Modified” response

We got a “304 Not Modified” response as expected. Now let’s send a POST request to /api/cars to add a new Car entity to the list (see Figure 16-27).

9781430247258_Fig16-27.jpg

Figure 16-27. A POST request against /api/cars

The new Car entity is added successfully. Let’s now try to send a conditional GET request to /api/cars with the previously obtained ETag value. This time there should be a “200 OK” response including the content, because the cars list in the cache should be stale now (see Figure 16-28).

9781430247258_Fig16-28.jpg

Figure 16-28. A conditional GET request to /api/cars and its full “200 OK” response

As expected, the previous cache has been invalidated, and a new ETag came back. Now let’s send a GET request to /api/cars/2 to get the single Car entity (see Figure 16-29).

9781430247258_Fig16-29.jpg

Figure 16-29. A GET request against /api/cars/2

Now let’s send a PUT request against /api/cars/2 to update the entity (see Figure 16-30).

9781430247258_Fig16-30.jpg

Figure 16-30. A PUT request against /api/cars/2

With one of the cars updated, the cars collection in the cache is now stale. If a conditional GET request were sent against /api/cars with the previously obtained ETag value, a “200 OK” response would come back because our custom invalidation logic has kicked in and invalidated the /api/cars cache when the PUT request occurred against /api/cars/2 (see Figure 16-31).

9781430247258_Fig16-31.jpg

Figure 16-31. A conditional GET request against /api/cars and its full “200 OK” response

This handler covers most of the functionality you should ever need, but you can also extend it according to your needs. For example, you might want to use an external store for such CacheableEntity objects as an SQL Server database table, Windows Azure Table Store, and the like, instead of an in-memory dictionary. Keep in mind that if you prefer to use an in-memory dictionary in production, you will face inconsistencies in your application when you scale out through multiple servers.

Summary

Asynchrony is very important if your application aims at scalability when you are consuming long-running I/O-intensive operations. In this chapter, you have seen that it allows you to easily be part of this asynchronous infrastructure with TAP (task-based asynchronous patterns) and C# 5.0 asynchronous language features. In addition, HTTP has pretty robust caching standards, which were explained in detail. As the ASP.NET Web API embraces HTTP, plugging in our caching implementation was easy to do.

1 www.w3.org/Protocols/rfc2616/rfc2616.html

2 www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9

3 www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.19

4 www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26

5 www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.29

6 www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.25

7 www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.44

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.58.121.8