Razor-Ansichten in ASP.NET Core mit dem neuen cache TagHelper zwischenspeichern

Razor-Ansichten in ASP.NET Core mit dem neuen cache TagHelper zwischenspeichern

Razor ist eine einfache, flexible, aber zugleich mächtige Template-Sprache für ASP.NET und ASP.NET Core Webanwendungen. Allerdings werden Razor-Ansichten erst zur Laufzeit in C# Code kompiliert. Dieser wiederum muss bei jeder Anfrage ausgeführt werden. Zwar erledigt ASP.NET Core dies sehr performant. Je nach Anwendung macht es dennoch Sinn, die kompilierten und gerenderten Razor-Ansichten zwischenzuspeichern. Insbesondere Teilansichten, die sich nicht verändern wie beispielsweise ein Footer ohne Inhalte. Für diesen Zweck gibt es einen neuen TagHelper, der genau dies ermöglicht.

Warum sollte ich Caching nutzen?

In einem Satz gesagt: Das Zwischenspeichern reduziert die Ladezeit und Serverlast. Eine Razor-Teilansicht muss kompiliert werden. Bei jeder Anfrage generiert sie ihren Inhalt zudem dynamisch, beides kostet Zeit und Rechenleistung. Dazu kommen möglicherweise noch externe Quellen, von denen Daten geladen werden müssen – etwa eine Datenbank. Durch das Zwischenspeichern wird diese Last erheblich reduziert, denn: Wenn die Ansicht einmal im Zwischenspeicher ist, wird nur noch das fertige HTML aus dem RAM geladen. Das geht sehr schnell und verbraucht nur sehr wenige Ressourcen.

Natürlich hängt der effektive Nutzen immer vom jeweiligen Projekt und dessen Aufbau ab. Manche profitieren stark durch die Zwischenspeicherung, andere weniger. Grundsätzlich macht es jedoch Sinn, sich mit dem Thema auseinanderzusetzen und Caching möglichst bereits bei der Entwicklung zu berücksichtigen.

Wie lassen sich Razor-Ansichten zwischenspeichern?

Hierfür wurde der cache Tag eingeführt. Er ist flexibel und kann direkt HTML/Razor-Code enthalten. Aber auch Teilansichten lassen sich wie gewohnt über Html.Partial() laden, um deren Inhalt zwischenzuspeichern. Am besten lässt sich dies demonstrieren, wenn man im cache Tag die aktuelle Uhrzeit ausgibt:

<p>
    Seite aufgerufen: @DateTime.Now.ToString()
</p>
<cache expires-after="TimeSpan.FromMinutes(5)">
    Cache-Zeitpunkt: @DateTime.Now.ToString()
</cache>

Was bedeutet das? Der obere Teil wird ganz normal bei jedem Seitenaufruf gerendet, und enthält somit den Zeitpunkt des Aufrufens der Seite. Der zweiten Teil im cache wird an dieser Stelle in den Cache gelegt. Öffnen wir die Seite und aktualisieren sie ein paar Sekunden später, wird der Cache-Zeitpunkt unverändert bleiben:

Erst nach 5 Minuten rendert ASP.NET den Inhalt des Cache-Tags neu, wodurch die Uhrzeit aktualisiert wird. Zu beachten ist, dass der cache Tag rein serverseitig ausgeführt wird. Im HTML-Quellcode des Browsers ist er daher nicht zu sehen.

Ablaufzeit des Zwischenspeichers bestimmen

Um den Zwischenspeicher zu leeren, stehen uns verschiedene Möglichkeiten zur Verfügung. Die erste haben wir im obigen Beispiel bereits kennengelernt: Im Parameter expires-after übergeben wir die Lebensdauer als Zeitspanne in Form einer TimeSpan Instanz. Es gibt jedoch noch weitere Möglichkeiten.

expires-on: Absoluter Ablaufzeitpunkt

Über das expires-on Attribute kann die Gültigkeit als absolute DateTime Instanz angegeben werden. Sinn macht dies beispielsweise, wenn die Ansicht auf Daten zurückgreift, die als Cron/Aufgabe zu einem bestimmten Zeitpunkt neu geladen werden.

Folgendes Beispiel erneuert den Cache jeweils kurz vor Ende des aktuellen Tages:

<cache expires-on="@DateTime.Today.AddDays(1).AddTicks(-1)">
    @Html.Partial("DateTime")
</cache>
expires-sliding: Alte, nicht verwendete Elemente entfernen

Einen etwas anderen Ansatz geht expire-sliding: Hier wird das Element aus dem Cache gelöscht, wenn eine gewisse Zeit lang nicht darauf zugegriffen wurde. Dies ist besonders nützlich, wenn viele verschiedene Inhalte zwischengespeichert werden, aber nicht all zu viel Arbeitsspeicher auf dem Server zur Verfügung steht. Erwartet wird auch hier eine TimeSpan Instanz:

<cache expires-sliding="TimeSpan.FromMinutes(5)">
    Cache-Zeitpunkt: @DateTime.Now.ToString()
</cache>

In diesem Beispiel entfernt ASP.NET den zwischengespeicherten Inhalt, wenn 5 Minuten lang nicht darauf zugegriffen wurde.

Komplexere Cache-Schlüssel

In allen obigen Demos wird der Zwischenspeicher mit allen Nutzern geteilt. Dies ist sinnvoll für Elemente, die überall gleich angezeigt werden wie etwa ein News-Ticker Widget. Nicht selten werden Inhalte aber dynamisch anhand von Nutzerrollen und ähnlichen Kriterien generiert. Damit in solchen Fällen keinem Nutzer alte oder falsche Inhalte angezeigt werden, benötigt man verschiedene Cache-Schlüssel. Beispielsweise könnten diese die Id einer Gruppe beinhalten. Für Gruppe 1 wird dann ein anderer Cache-Eintrag erstellt wie für Gruppe 2.

Auch hier ist ASP.NET sehr flexibel und ermöglicht praktisch jede Kombinationsmöglichkeit. Zum Einsatz kommen verschiedene Attribute mit dem vary-by Präfix.

vary-by: Eigenen Cache-Schlüssel definieren

Am flexibelsten ist das vary-by Attribute. Es erlaubt die Nutzung jeder beliebigen C# Variable als Schlüssel:

@{ 
    var myCacheKey = ...
}
<cache vary-by="myCacheKey">
    Cache-Zeitpunkt: @DateTime.Now.ToString()
</cache>

Für häufig genutzte Schlüssel existieren allerdings noch weitere Hilfsattribute, die ich im folgenden vorstelle. Erwähnenswert ist auch, dass die folgenden Hilfsattribute beliebig Kombiniert werden können. ASP.NET setzt den Cache-Schlüssel dann aus den gegebenen Werten zusammen, beispielsweise Nutzer und Route.

vary-by-user: Eigener Cache für jeden Nutzer

Nutzen wir das Authentifizierungssystem von ASP.NET Core und möchten Inhalte separat für jeden angemeldeten Nutzer zwischenspeichern, genügt es, dieses Attribute auf true zu setzen. Sinn macht dies beispielsweise bei einer dynamisch generierten Navigation, die den Nutzer mit seinem Namen begrüßt.

vary-by-route: URL-Routen verwenden

Nutzt den Wert von Route-Parametern als Schlüssel. Es muss die gleiche Bezeichnung verwendet werden, wie beim definieren der Route. Das kombinieren mehrere Route-Parameter können mittels Komma getrennt übergeben werden. Folgendes Beispiel erzeugt einen Cache-Schlüssel aus Controller, Action und Id:

app.UseMvc(routes => {
    routes.MapRoute(
        name: "default",
        template: "{controller=Home}/{action=Index}/{id?}"
    );
});
<cache vary-by-route="controller,action,id">
    Cache-Zeitpunkt: @DateTime.Now.ToString()
</cache>
vary-by-query: URL-Parameter, die nicht als Route definiert wurden

Erzänzend zu vary-by-route können hier verschiedene URL-Parameter angegeben werden. Im Unterschied zur Route sind sie nicht darin festgelegt, sondern werden daran angehangen. Ein Beispiel, das von der oben definierten Standard-Route ausgeht: http://localhost:5000/home/index/?foo=bar

Der Parameter foo kann wie folgt als Schlüssel für den Zwischenspeicher genutzt werden:

<cache vary-by-query="foo">
    Cache-Zeitpunkt: @DateTime.Now.ToString()
</cache>
vary-by-cookie: Den Wert eines Cookies als Schlüssel

Gleiches Prinzip wie bei den vorherigen Attributen auch, hier muss der Name des Cookies übergeben werden. Wie immer bei Daten von Außen ist hier natürlich Vorsicht geboten. Es sollten keine Cookies genutzt werden, die sich serverseitig nicht verifizieren lassen. Die Benutzergruppe an dieser Stelle als Schlüssel zu setzen wäre beispielsweise keine Idee. So könnte sich der Nutzer durch einfache Manipulation des Cookies Inhalte anzeigen lassen, die nicht für ihn bestimmt sind.

vary-by-header: Beliebige HTTP-Header nutzen

Nutzt den Name des übergebenen HTTP-Headers, beispielsweise User-Agent. Ähnlich wie bei vary-by-cookie sollte man im Hinterkopf behalten, dass sich dieser Wert leicht vom Nutzer verändern lässt.

Priorität des Caches festlegen

Wie bereits zu Beginn erwähnt, wird der Inhalt des Caches im Arbeitsspeicher abgelegt. Insbesondere wenn der Cache pro Nutzer aufgebaut wird, kann er schnell wachsen. Ist der RAM des Servers erschöpft, wird ASP.NET aufräumen. Über das priority Attribute können die Inhalte gewichtet werden:

@using Microsoft.Extensions.Caching.Memory
<cache vary-by-route="id" priority="CacheItemPriority.Low">
    Cache-Zeitpunkt: @DateTime.Now.ToString()
</cache>

Das CacheItemPriority Enum bietet die Werte High, Normal, Low und NeverRemove. Ein Element mit niedriger Priorität wird also vor einem mit normaler oder hoher entfernt. Im Falle von NeverRemove wird der Server nach anderen Elementen suchen.

Mögliche Probleme des Zwischenspeichers

Durch die neue Caching-Möglichkeit von ASP.NET Core kann die Leistung einfach und effektiv erhöht werden. Allerdings sollte man nicht vergessen, dass es sich um einen flüchtigen Zwischenspeicher handelt. Sobald der ASP.NET Webserver bzw. der komplette Host-Server neu gestartet wird, geht sein Inhalt verloren. In der Regel passiert dies nicht all zu häufig und ist auch nicht weiter schlimm – denn schließlich wird der Cache bei der nächsten Anfrage automatisch wieder neu aufgebaut.

In Umgebungen mit mehreren Servern sind Caches ähnlich wie Sessions grundsätzlich ein Problem: Der Cache wird pro Server erstellt, und ist somit auf anderen Maschinen nicht gültig. Landet ein Nutzer bei seiner ersten Anfrage auf Server 1, baut sich dort der Cache auf. Wird er beim zweiten Seitenaufruf an Server 2 geroutet, hat dieser keinen Zugriff auf den Cache von Server 1 und baut ihn somit ebenfalls auf. Am Ende leidet die Effektivität und man hat den gleichen Inhalt X-Mal auf verschiedenen Servern im Zwischenspeicher.

In solchen Umgebungen sollte man auf einen serverübergreifenden Cache-Provider zurückgreifen, wie etwa Redis. Hier hat man einen zentralen Cache-Server, auf den alle Webserver zugreifen können. Im obigen Beispiel würde Server 1 den Inhalt in den Redis-Cache legen, und Server 2 greift bei der nächsten Anfrage darauf zu, anstatt ihn unnötig neu aufzubauen.

Leave a Reply