Introduction
Previous article: HTTP 304 Not Modified - An Introduction
In my last article, we discussed the basic HTTP caching mechanism provided by HTTP 304 Not Modified status code and several relevant headers. The mechanism could be summarized with a statement:
Quote:
If something does not change, it will not be sent.
In this article, an example project will be shown that is implemented in ASP.NET Web API.
Background
This example project is a material of teaching that I created a few months ago, to shows people how to create a Web API Controller with HTTP 304 support and how to consume it with jQuery. The scenario in this project is to let user view and edit a corporation's employee data. After user selecting an employee in drop-down box, the data queried from server or cached in browser will be shown on the left side and key response headers will be shown on the right side, as you can see in Figure 1:
Figure 1
Content cited from https://www.valvesoftware.com/company/people.html
In following sections, we will start from back-end to front-end, from the design pattern behind the scenes to creating an API Controller. And in the last one we will move on the jQuery part as an ending.
The Observer Pattern
The Observer Design Pattern acts a crucial role in this example project. The basic idea is to observe the change on an instance and receive the notification. Figure 2 is the simplified UML diagram that shows how it is employed in this project.
Figure 2
The observed subject is the instances of Employee
class. This is the content that will be viewed and edited by users. Employee
implements the INotifyPropertyChanged
interface so EmployeeChangeObserver
is able to observe it by subscribing PropertyChanged
event. Once the event gets invoked, EmployeeChangeObserver
immediately marks it with a timestamp and a Guid
and updates its LastChange
property. As you have seen from the property names in the class Change
, the timestamp will be used as Last-Modified header value and the Guid
will be used as ETag header value for upcoming HTTP requests.
Following is the Employee
class (some of members are abbreviated). The PropertyChanged
event is invoked whenever one of properties is changed.
public class Employee : INotifyPropertyChanged
{
#region Fields
private Guid id;
private string firstName;
private string lastName;
#endregion
#region Properties
public Guid ID
{
get { return this.id; }
set { this.ChangeProperty(ref this.id, value, "ID"); }
}
public string FirstName
{
get { return this.firstName; }
set { this.ChangeProperty(ref this.firstName, value, "FirstName"); }
}
public string LastName
{
get { return this.lastName; }
set { this.ChangeProperty(ref this.lastName, value, "LastName"); }
}
#endregion
#region Event
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region Event Raiser
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, e);
}
#endregion
#region Others
protected void ChangeProperty<T>(ref T currentValue, T newValue,
string propertyName)
{
if (!EqualityComparer<T>.Default.Equals(currentValue, newValue))
{
currentValue = newValue;
this.OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
Next, we create a generic class ChangeObserver<TItem>
to observe any instances implementing INotifyPropertyChanged
interface. The Change
property is updated with DateTime.UtcNow
and Guid.NewGuid()
values whenever the PropertyChanged
event is invoked.
public abstract class ChangeObserver<TItem>
where TItem : class, INotifyPropertyChanged
{
#region Field
private readonly TItem item;
private Change lastChange;
#endregion
#region Property
public TItem Item
{
get { return this.item; }
}
public Change LastChange
{
get { return this.lastChange; }
}
#endregion
#region Constructure
public ChangeObserver(TItem item, Change lastChange)
{
if (item == null) throw new ArgumentNullException("item");
this.item = item;
this.item.PropertyChanged += this.item_PropertyChanged;
this.lastChange = lastChange;
}
#endregion
#region Event Handler
private void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
Interlocked.Exchange(ref this.lastChange, new Change(DateTime.UtcNow, Guid.NewGuid()));
}
#endregion
}
Third, we create an EmployeeChangeObserver
class derived from ChangeObserver<TItem>
especially for observing Employee
instance.
public class EmployeeChangeObserver : ChangeObserver<Employee>
{
public EmployeeChangeObserver(Employee item)
: base(item)
{
}
public EmployeeChangeObserver(Employee item, Change lastModified)
: base(item, lastModified)
{
}
}
Above is all what we need to do to employ the Observer Design Pattern. In next section, we will move on the Web API.
Implementation in Web API
Our Web API is defined in ValuesController
class. It provides a few methods that is able to select, create and update the instance of Employee
class. ValuesControllerExtensions
class defines a set of static methods, allowing ValuesController
to accomplish HTTP caching within few lines of code.
Let us take a look at the ValuesControllerExtensions
first. The extension method EndIfNotModified(HttpRequestMessage, DateTime)
decides whether the request is necessary to be handled in further. We also have CreateResponse<T>(HttpRequestMessage, HttpStatusCode, T, DateTime)
method that adds all necessary response headers for HTTP caching.
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Web.Http;
namespace HttpCaching.Controllers
{
public static class ValuesControllerExtensions
{
public static void EndIfNotModified(this HttpRequestMessage request, DateTime lastModified)
{
if (request == null)
throw new ArgumentNullException("request");
var ifModifiedSince = request.Headers.IfModifiedSince;
if (ifModifiedSince != null && ifModifiedSince.Value.DateTime >= lastModified)
throw new HttpResponseException(HttpStatusCode.NotModified);
}
public static HttpResponseMessage CreateResponse<T>(this HttpRequestMessage request,
HttpStatusCode statusCode, T value, DateTime lastModified, TimeSpan expires)
{
if (request == null)
throw new ArgumentNullException("request");
var response = request.CreateResponse<T>(statusCode, value);
response.Headers.CacheControl = new CacheControlHeaderValue();
response.Content.Headers.LastModified = new DateTimeOffset(lastModified);
response.Content.Headers.Expires = new DateTimeOffset(DateTime.UtcNow
+ expires.Duration());
return response;
}
}
}
The logic behind the combination of two extension methods could be summarized in following chart which you may be familiar with in our last article:
Figure 3
With these extension methods, building a method that supports HTTP caching under ValuesController
is much easier. As you can see in Select(Guid)
method:
using HttpCaching.Models;
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web;
using System.Web.Helpers;
using System.Web.Http;
namespace HttpCaching.Controllers
{
public class ValuesController : ApiController
{
#region Fields
public static readonly ConcurrentDictionary<Guid, EmployeeChangeObserver> Employees;
public static readonly TimeSpan DefaultExpires;
#endregion
#region Constructor
static ValuesController()
{
var fileInfo = new FileInfo(HttpContext.Current.Server.MapPath("~/App_Data/Valve.txt"));
var lastChange = new Change(fileInfo.LastWriteTimeUtc, Guid.NewGuid());
var employees = Json.Decode<Employee[]>(File.ReadAllText(fileInfo.FullName)).
ToDictionary(c => c.ID, c => new EmployeeChangeObserver(c, lastChange));
Employees = new ConcurrentDictionary<Guid, EmployeeChangeObserver>(employees);
DefaultExpires = TimeSpan.FromMinutes(1);
}
#endregion
#region Methods
[HttpGet]
public HttpResponseMessage Select(Guid id)
{
var employee = Employees.EndIfNotFound(id);
var lastChange = employee.LastChange;
if (lastChange == null)
return base.Request.CreateResponse(HttpStatusCode.OK, employee.Item);
base.Request.EndIfNotModified(lastChange.LastModifiedUtc);
return base.Request.CreateResponse(HttpStatusCode.OK, employee.Item,
lastChange.LastModifiedUtc, DefaultExpires);
}
#endregion
}
}
This is where we need the EmployeeChangeObserver.LastChange
property. Thanks to the Observer Design Pattern, whenever the Select(Guid)
method is called, it always indicates whether selected Employee
object is changed in the past. Also it provides the parameters that we need for EndIfNotModified(HttpRequestMessage, DateTime)
and CreateResponse<T>(HttpRequestMessage, HttpStatusCode, T, DateTime)
methods.
Calling Web API in jQuery
We are now able to consume the Web API using jQuery. In Figure 1, when select box Employee is changed, the function selectEmployee(id)
gets triggered with selected ID. Its mission is to retrieve employee information from server or cache, and fill the form with it.
function selectEmployee(id) {
$.ajax("/../api/values/select", {
data: { ID: id },
type: "GET",
ifModified: true, statusCode: {
304: function() {
$("#statusCode").val(304);
$("#cacheMessage").text("The content is rendered from cache.");
},
200: function () {
$("#statusCode").val(200);
$("#cacheMessage").text("The content is rendered from server.");
}
},
success: function (data, textStatus, jqXHR) {
if (jqXHR.status == 304) {
data = jQuery.data(mainForm, id);
} else {
jQuery.data(mainForm, data["ID"], data);
}
$("#firstName").val(data["FirstName"]);
$("#lastName").val(data["LastName"]);
$("#alias").val(data["Alias"]);
$("#steamId").val(data["SteamID"]);
$("#sex").val(data["Sex"]);
$("#description").val(data["Description"]);
$("#lastModified").val(jqXHR.getResponseHeader("Last-Modified"));
$("#expires").val(jqXHR.getResponseHeader("Expires"));
$("#eTag").val(jqXHR.getResponseHeader("ETag"));
}
});
}
In the callback function success
, we store the data that we just received from server into cache and recall the data from cache by using the data()
function. Note what we discussed in last article that the message body is empty in HTTP Status 304. Therefore, parameter data
is null and you should not access it.
So How It Actually Work?
Let us see how the project actually works in browser. We browse /Home/Index in Chrome, press F12 to open the Developer Tool, then go back to the browser and select the employee Gabe Newell.
Figure 3
Remember this is the first time that we select Gabe Newell (in Figure 3). The request and response headers are shown in Figure 4.
Figure 4
Next, let us try select another employee and reselect Gabe Newell again, or click Refresh button, and see what is happening in Developer Tool.
Figure 5
Notice that If-Modified-Since header is present in the request. The value is exactly equal to what we just saw in Figure 4 Last-Modified. This time we receive an HTTP 304 Not Modified response because the data is not changed since Sat, 07 Feb 2015 08:58:32 GMT. And of course, the response body is empty.
Figure 6
Conclusion
I hope this project could give you a rough idea that HTTP caching is not only made for static assets, it could be also applied to dynamic content by controlling headers "manually". When you look back and review the Web API you created, or you are designing new Web API, if the content is observable, you may situationally consider to let your API support HTTP caching.
You have probably noticed that there are few things not mentioned in sections above. Here are a couple things that you can try it by yourself.
- In this example project, we use Last-Modified header to accomplish the HTTP caching. But remember what we discussed in the last article, there is another option ETag that could be chosen too. Try to modify the method
ValuesController.Select(Guid)
and reach the same outcome.
- In this example,
Employee
instances are loaded from Valve.txt at very beginning and changes will not be saved in the file. Try to design and replace it with an actual table in database and make the changes be saved.
Further Reading