Manch einer wird jetzt sagen: “Wird doch bereits verwendet”. Zum Teil stimmt das auch. Wenn man sich die Kommunikation einer ASP.NET Seite zwischen Client und Server einmal in Fiddler oder einem ähnlichem Werkzeug ansieht, fällt schnell auf, dass die Ressourcen der Seite wie etwa Images, CSS-Dateien udgl. zwar meist den Statuscode 304 (not modified) aber die aspx-Seiten immer 200 (OK) zurückgeben. Genau hier kann angesetzt werden.
In Zeiten von URL-Rewrite und der Allgegenwart von SEO versucht heute jeder eine eindeutige URL für jede einzelne Seite zu erzeugen. In diesem Moment kann man auch die für conditional GET benötigten Daten verwalten. Man benötigt hier lediglich den Zeitpunkt der letzten Änderung einer Seite oder deren Inhalt, sowie eine eindeutige Zeichenfolge die als sogenannter ETag verwendet wird. Was ist nun zu tun? Eigentlich nicht sehr viel:
- Die Werte des Änderungsdatums und des ETag als Http-Header schreiben.
- Zwei Http-Header vom Client lesen.
- Entscheiden ob die Seite oder der Statuscode 304 gesendet wird.
Das ist die ganze Kunst von conditional GET und funktioniert sehr gut.
Im folgenden Screenshot kann man sehr gut die Request- und Responseheader des ersten Aufruf der Seite AboutMe.aspx sehen. Da sich die Seite noch nicht im Cache des Browser befindet, sendet der Browser im Cache-Header: pragma no-cache. Der Server hat im Responseheader bereits den Zeitpunkt der letzten Änderung und eine Zeichenfolge als ETag an den Browser gesendet, so dass diese bei der nächsten Anfrage dem Browser zur Verfügung stehen.

Nun noch einmal der Aufruf der gleichen Seite. Jetzt, da sich die Seite bereits im Cache des Browser befindet, sendet der Browser im Cache-Header den Zeitpunkt der letzten Änderung der Seite die sich in seinem Cache befindet sowie den gespeicherten ETag im Header “If-None-Match”. Auf dem Server werden nun die übermittelten Header des Client ausgewertet und je nach Zustand der Seite entweder 304, oder die geänderte Seite mit neuen Daten im Header gesendet.

Bei der Entscheidung conditional GET auch für aspx-Seiten zu verwenden, sollte man aber auch bedenken, dass nicht jede Seite der Website dafür geeignet ist. Man sollte also tunlichst darauf achten, Seiten mit ständig wechselndem Inhalt, wie etwas Fehlerseite oder Suchergebnisse, unbedingt auszuschließen. Es gibt bestimmt genug Mechanismen um den Ausschluss der jeweiligen Seite zu gewährleisten.
Wie die Lösung eines einzelnen aussieht, wird wohl sehr unterschiedlich sein. Der eine wird vielleicht ein Http-Modul verwenden, ein anderer einen Http-Handler. Ich habe mich für eine einfache Methode in einer Helper-Klasse entschieden, die ich in der Masterpage verwenden kann. Um die aufgerufene Seite in der Masterpage zu bestimmen, habe ich die Eigenschaft CurrentExecutionFilePath verwendet.
// Dateipfad und Namen der Clientseite bestimmen
string clientPath = this.Page.Request.CurrentExecutionFilePath;
// andere Code hier
// conditional GET verarbeiten
Utils.HasConditionalGet(clientPath);
Die Methode HasConditionalGet der Helper-Klasse Utils ist vom Typ bool und gibt true zurück wenn 304 gesendet wurde.
public static bool HasConditionalGet(string site)
{
// prüfen ob die Seite von conditional Get ausgenommen ist
if (SiteSettings.Instance.NonCachedSites.Contains(Path.GetFileName(site)))
{
return false;
}
bool value = false;
DateTime modifiedSince;
HttpResponse response = HttpContext.Current.Response;
HttpRequest request = HttpContext.Current.Request;
// bestimmen wann die Datei zuletzt geändert wurde
DateTime modifyDate = Utils.LastModified(request.MapPath(site));
// prüfen ob das Datum nicht in der Zukunft liegt
if (modifyDate > DateTime.Now)
{
modifyDate = DateTime.Now;
}
// einen ETag aus Seitennamen und Datum erstellen
string eTag = Utils.GetETagFromFile(site, modifyDate);
// prüfen ob ein Datum aus dem Header geparst werden kann
if (DateTime.TryParse(
request.Headers["If-Modified-Since"],
CultureInfo.InvariantCulture,
DateTimeStyles.AdjustToUniversal,
out modifiedSince))
{
// prüfen ob das Datum der letzten Änderung neuer ist
if (modifyDate > modifiedSince)
{
TimeSpan modifiedDiff = modifyDate - modifiedSince;
// Differenz größer einer Sekunde melden value = modifiedDiff > TimeSpan.FromSeconds(1);
}
}
// ETag aus dem Browser-Header lesen
string ifNoneMatch = request.Headers["If-None-Match"];
// prüfen ob die ETag's gleich sind
if (string.Compare(ifNoneMatch, eTag) == 0)
{ value = true;
}
// Cacheability muss public sein, da sonst die Methoden
// SetLastModified und SetETag die Header nicht schreiben.
response.Cache.SetCacheability(HttpCacheability.Public);
// die Cache-Header zum senden an den Client setzen
response.Cache.SetLastModified(modifyDate.ToUniversalTime());
response.Cache.SetETag(eTag);
// keine Änderung; 304 senden
if (value)
{
response.Clear();
response.StatusCode = (int)HttpStatusCode.NotModified;
response.SuppressContent = true;
}
return value;
}Wie bereits weiter oben erwähnt, wird außer dem Datum der letzten Änderung auch eine eindeutige Zeichenfolge für den ETag-Header benötigt. In meiner Lösung verwende ich eine Hashwert, der aus dem Dateinamen der Seite und dem Datum der letzten Änderung erzeugt wird. Diesen Hashwert lasse ich als Zeichenfolge im Hex-Format darstellen. Es ist unbedingt erforderlich, die Zeichenfolge in Anführungszeichen zu setzen und mit diesen umschließenden Anführungszeichen in den Header zu schreiben.
Bei der Umsetzung dieser Lösung bin ich auf eine Eigenart des .NET Framework gestoßen: Wenn eine anderer HttpCacheability Wert als public verwendet wird, private ist der Default Wert, werden die Header von den Methoden SetLastModified und SetETag nicht gesetzt.
Sehr geholfen beim Testen der Funktion meiner Seite, hat mir HTTP compression and HTTP conditional GET test tool und natürlich Fiddler.
Wenn ihnen der Artikel gefallen hat oder er für sie hilfreich war, bitte "kicken" sie ihn.
