Click here to Skip to main content
15,881,882 members
Articles / Web Development / HTML

HTTP 304 Not Modified In ASP.NET Web API

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
16 Feb 2015CPOL5 min read 37K   224   4   2
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:

Image 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.

Image 2

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.

C#
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.

C#
    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.

C#
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.

C#
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:

C#
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.

JavaScript
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

Image 4

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.

Image 5

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.

Image 6

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.

Image 7

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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Taiwan Taiwan
Back-end developer, English learner, drummer, game addict, Jazz fan, author of LINQ to A*

Comments and Discussions

 
QuestionifModifiedSince > lastModified Pin
el_mariachi21-Oct-15 22:47
el_mariachi21-Oct-15 22:47 
AnswerRe: ifModifiedSince > lastModified Pin
Robert Vandenberg Huang25-Oct-15 2:12
professionalRobert Vandenberg Huang25-Oct-15 2:12 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.