Azure Functions sollen ja dazu genutzt werden um Serverless, Stateless und Cloud-first Applikationen bzw. APIs zu erstellen. Daraus ergibt sich die Herausforderung so etwas wie einen Cache Layer für Http Trigger Functions einzubauen. Hier ist ein Beispiel, wie man mit dem HTTP Header Cache-Control trotzdem so etwas wie ein Caching in eine API einbauen kann.

Stateless Azure Functions

Der Grund warum Http Trigger in Azure Functions Stateless sind liegt in der Idee der Functions-as-a-Service (FaaS) ansich. Eine Function wird nur ausgeführt, wenn Sie wirklich benötigt wird. Passiert kein Request, benötigt es auch keine Instanz der Function. Deshalb kann es bei einem ersten Request zu einer Azure Function auch etwas dauern bis eine Antwort zurück kommt. Das liegt am Warm-Up der darunter liegenden Infrastruktur.

Schreibt man nun Informationen in den Ram, werden die Infos beim Cool-Down natürlich wieder vergessen. Der Cool-Down passiert spätestens nach 15min in den Standardeinstellungen. Somit müssten die Informationen bei jedem Ausführen überprüft werden.

Caching der HTTP Response auf Clients

Eine einfache Möglichkeit des cachens, ohne einen Cache-Layer in die Applikation einzubauen (z.B. Redis Cache), ist das Setzen des HTTP Headers Cache-Control in der HTTP Response. Der Header sagt dem Browser, dass eine Server-Antwort für eine Gewisse Zeit valide ist und der Browser keinen neuen Request zum Server machen soll.

Weitere Infos zu dem Header gibt es in der MDN: https://developer.mozilla.org/de/docs/Web/HTTP/Headers/Cache-Control

JSON Response mit Cache

Da die meisten APIs mittlerweile JSON zurückgeben und meine API das auch macht kann man in einer .NET Core 2.x Azure Function von Klasse JsonResult erben um sie um einen Cache zu erweitern. Die Klasse erstellt JSON aus .NET Objekten.

In der Funktion ExecuteResultAsync kann der Header Cache-Control zur Antwort hinzugefügt werden.

CachedJsonResponse-Klasse

In meiner Lösung sieht die neue Response Klasse dann so aus:

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Formatters.Json.Internal;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;

namespace Http.Helpers
{
    public class CachedJsonResult : JsonResult
    {
        private int _cacheDurationInSeconds = 0;

        public CachedJsonResult(object value) : base(value)
        {
        }

        public CachedJsonResult(object value, int cacheDurationInSeconds) : base(value)
        {
            _cacheDurationInSeconds = cacheDurationInSeconds;
        }

        public CachedJsonResult(object value, JsonSerializerSettings serializerSettings) : base(value, serializerSettings)
        {
        }

        public CachedJsonResult(object value, JsonSerializerSettings serializerSettings, int cacheDurationInSeconds) : base(value, serializerSettings)
        {
            _cacheDurationInSeconds = cacheDurationInSeconds;
        }

        public override Task ExecuteResultAsync(ActionContext context)
        {
            if (context == null)
                throw new ArgumentNullException(nameof (context));

            if(_cacheDurationInSeconds > 0)
            {
                var response = context.HttpContext.Response;
                response.Headers.Add("cache-control", "public, max-age=" + _cacheDurationInSeconds);
            }

            return context.HttpContext.RequestServices.GetRequiredService<JsonResultExecutor>().ExecuteAsync(context, this);
        }
    }
}

Die Reponse-Klasse kann dann wie jede andere Response-Klasse in einer Azure Function genutzt werden.
Hier ist ein Beispiel dazu:

[FunctionName("GetNames")]
public static async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Function, "get", Route = "names")] HttpRequest req,
    ILogger log)
{
    log.LogInformation("C# HTTP trigger function processed a request.");

    var data = new List<object>()
    {
        new { Name = "Jack Miller" },
        new { Name = "John Wick" }
    };

    return new CachedJsonResult(data, 60);
}

Caching prüfen

Ich bin ein Freund davon APIs mit Tools wie Postman zu prüfen. Deshalb zeige ich das Resultat der Function in Postman.

Wichtig für das Testen von HTTP Caching ist der Schalter Send no-cache header in den Postman-Einstellungen. Dieser muss für den Test deaktiviert werden, da Postman sonst automatisch den Header Cache-Control: no-cache mitsendet.

postman-no-cache

Führt man nun einen Request zu der Function aus, dauert es etwas und man erhält eine Antwort.

In den Headern der Response sieht man auch, dass Cache-Control auf public, max-age=60 gesetzt wurde. Die Antwort wird für 60 Sekunden zwischengespeichert.
Führt man innerhalb von 60 Sekunden einen weiteren Request aus, so ist die Antwortzeit des Requests drastisch reduziert. Da die Informationen aus dem Cache kommen.

postman-uncached-response
Abfrage ohne Cache

postman-cached-response
Abfrage nach dem Setzen des Caches

Fazit

Natürlich kann es je nach Komplexität und Größe einer Applikation besser sein einen Cache-Layer hinzuzufügen. Um den ersten Schritt in einem agilen Cloud-Projekt zu gehen ist diese Lösung eine einfache Möglichkeit Abfragen zu Cachen.

Happy Coding 👨‍💻