Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

HTTP 304 Not Modified In ASP.NET Web API

0.00/5 (No votes)
16 Feb 2015 1  
A example project about how to manually control HTTP caching in Web API.

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;

    // Other fields abbreviated.

    #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"); }
    }

    // Other properties abbreviated.

    #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

        // Default constructor abbreviated.

        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)
        {
            // Update the latest change information whenever the change occurs.
            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
    {
        // Some methods abbreviated.

        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

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()
        {
            // Read JSON data from text file as the default content of the dictionary.
            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);

            // The default expiration time in clients' cache is 1 minute.
            DefaultExpires = TimeSpan.FromMinutes(1);
        }

        #endregion

        #region Methods

        [HttpGet]
        public HttpResponseMessage Select(Guid id)
        {
            var employee = Employees.EndIfNotFound(id);
            var lastChange = employee.LastChange;

            // If change information is not available, will end here.
            if (lastChange == null)
                return base.Request.CreateResponse(HttpStatusCode.OK, employee.Item);

            // If nothing changed, will raise an HttpResponseException in status 304.
            base.Request.EndIfNotModified(lastChange.LastModifiedUtc);

            // Give the latest change information.
            return base.Request.CreateResponse(HttpStatusCode.OK, employee.Item,
                lastChange.LastModifiedUtc, DefaultExpires);
        }

        // Other methods abbreviated.

        #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,  // Remember to turn this option on.
        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) { 
            // Parameter data is null if status is 304. 
            if (jqXHR.status == 304) {
                // Render data from cache.
                data = jQuery.data(mainForm, id);
            } else {
                // Save data into cache.
                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"]);

            // Show response headers. 
            $("#lastModified").val(jqXHR.getResponseHeader("Last-Modified"));
            $("#expires").val(jqXHR.getResponseHeader("Expires"));
            $("#eTag").val(jqXHR.getResponseHeader("ETag"));
        }
    });
}

// Other functions abbreviated. 

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.

  1. 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. 
  2. 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

 

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here