Middleware für die Ausgabezwischenspeicherung in ASP.NET Core

Von Tom Dykstra

Hinweis

Dies ist nicht die neueste Version dieses Artikels. Informationen zum aktuellen Release finden Sie in der .NET 8-Version dieses Artikels.

Wichtig

Diese Informationen beziehen sich auf ein Vorabversionsprodukt, das vor der kommerziellen Freigabe möglicherweise noch wesentlichen Änderungen unterliegt. Microsoft gibt keine Garantie, weder ausdrücklich noch impliziert, hinsichtlich der hier bereitgestellten Informationen.

Informationen zum aktuellen Release finden Sie in der .NET 8-Version dieses Artikels.

In diesem Artikel wird erläutert, wie die Middleware für die Ausgabezwischenspeicherung in einer ASP.NET Core-App konfiguriert wird. Eine Einführung in die Ausgabezwischenspeicherung finden Sie unter Ausgabezwischenspeicherung.

Die Middleware für die Ausgabezwischenspeicherung kann in allen Arten von ASP.NET Core-Apps verwendet werden: Minimal-API, Web-API mit Controllern, MVC und Razor Pages. Die Beispiel-App ist eine Minimal-API, aber die darin demonstrierten Zwischenspeicherungsfeatures werden auch in den anderen App-Typen unterstützt.

Hinzufügen der Middleware zur App

Fügen Sie die Middleware für die Ausgabezwischenspeicherung zur Dienstsammlung hinzu, indem Sie AddOutputCache aufrufen.

Fügen Sie die Middleware zur Anforderungsverarbeitungspipeline hinzu, indem Sie UseOutputCache aufrufen.

Hinweis

  • In Apps, die CORS-Middleware verwenden, muss UseOutputCache nach UseCors aufgerufen werden.
  • In Razor Pages-Apps und Apps mit Controllern muss UseOutputCache nach UseRouting aufgerufen werden.
  • Durch Aufrufen von AddOutputCache und UseOutputCache wird nicht das Zwischenspeicherungsverhalten gestartet, sondern das Zwischenspeichern verfügbar gemacht. Das Zwischenspeichern von Antwortdaten muss wie in den folgenden Abschnitten gezeigt konfiguriert werden.

Konfigurieren eines Endpunkts oder einer Seite

Bei minimalen API-Apps konfigurieren Sie einen Endpunkt für die Zwischenspeicherung, indem Sie CacheOutput aufrufen oder das [OutputCache]-Attribut anwenden, wie in den folgenden Beispielen gezeigt:

app.MapGet("/cached", Gravatar.WriteGravatar).CacheOutput();
app.MapGet("/attribute", [OutputCache] (context) => 
    Gravatar.WriteGravatar(context));

Bei Apps mit Controllern wenden Sie das [OutputCache]-Attribut auf die Aktionsmethode an. Bei Razor Pages-Apps wenden Sie das Attribut auf die Razor-Seitenklasse an.

Konfigurieren mehrerer Endpunkte oder Seiten

Erstellen Sie Richtlinien beim Aufrufen von AddOutputCache, um eine Zwischenspeicherungskonfiguration anzugeben, die für mehrere Endpunkte gilt. Eine Richtlinie kann für bestimmte Endpunkte ausgewählt werden, während eine Basisrichtlinie eine Standardzwischenspeicherungskonfiguration für eine Sammlung von Endpunkten bereitstellt.

Der folgende hervorgehobene Code konfiguriert die Zwischenspeicherung für alle Endpunkte der App mit einer Ablaufzeit von 10 Sekunden. Wenn keine Ablaufzeit angegeben ist, wird standardmäßig eine Minute festgelegt.

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => 
        builder.Expire(TimeSpan.FromSeconds(10)));
    options.AddPolicy("Expire20", builder => 
        builder.Expire(TimeSpan.FromSeconds(20)));
    options.AddPolicy("Expire30", builder => 
        builder.Expire(TimeSpan.FromSeconds(30)));
});

Der folgende hervorgehobene Code erstellt zwei Richtlinien, die jeweils eine andere Ablaufzeit angeben. Ausgewählte Endpunkte können die Ablaufzeit von 20 Sekunden verwenden, und für andere Endpunkte kann die Ablaufzeit von 30 Sekunden gelten.

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => 
        builder.Expire(TimeSpan.FromSeconds(10)));
    options.AddPolicy("Expire20", builder => 
        builder.Expire(TimeSpan.FromSeconds(20)));
    options.AddPolicy("Expire30", builder => 
        builder.Expire(TimeSpan.FromSeconds(30)));
});

Sie können eine Richtlinie für einen Endpunkt auswählen, wenn Sie die CacheOutput-Methode aufrufen oder das [OutputCache]-Attribut verwenden:

app.MapGet("/20", Gravatar.WriteGravatar).CacheOutput("Expire20");
app.MapGet("/30", [OutputCache(PolicyName = "Expire30")] (context) => 
    Gravatar.WriteGravatar(context));

Bei Apps mit Controllern wenden Sie das [OutputCache]-Attribut auf die Aktionsmethode an. Bei Razor Pages-Apps wenden Sie das Attribut auf die Razor-Seitenklasse an.

Standardrichtlinie für die Ausgabezwischenspeicherung

Standardmäßig folgt die Ausgabezwischenspeicherung den folgenden Regeln:

  • Es werden nur HTTP 200-Antworten zwischengespeichert.
  • Es werden nur HTTP GET- oder HEAD-Anforderungen zwischengespeichert.
  • Antworten, die cookies festlegen, werden nicht zwischengespeichert.
  • Antworten auf authentifizierte Anforderungen werden nicht zwischengespeichert.

Der folgende Code wendet alle Standardregeln für die Zwischenspeicherung auf alle Endpunkte einer App an:

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder.Cache());
});

Überschreiben der Standardrichtlinie

Der folgende Code zeigt, wie die Standardregeln überschrieben werden. Die hervorgehobenen Zeilen im folgenden benutzerdefinierten Richtliniencode ermöglichen die Zwischenspeicherung für HTTP POST-Methoden und HTTP 301-Antworten:

using Microsoft.AspNetCore.OutputCaching;
using Microsoft.Extensions.Primitives;

namespace OCMinimal;

public sealed class MyCustomPolicy : IOutputCachePolicy
{
    public static readonly MyCustomPolicy Instance = new();

    private MyCustomPolicy()
    {
    }

    ValueTask IOutputCachePolicy.CacheRequestAsync(
        OutputCacheContext context, 
        CancellationToken cancellationToken)
    {
        var attemptOutputCaching = AttemptOutputCaching(context);
        context.EnableOutputCaching = true;
        context.AllowCacheLookup = attemptOutputCaching;
        context.AllowCacheStorage = attemptOutputCaching;
        context.AllowLocking = true;

        // Vary by any query by default
        context.CacheVaryByRules.QueryKeys = "*";

        return ValueTask.CompletedTask;
    }

    ValueTask IOutputCachePolicy.ServeFromCacheAsync
        (OutputCacheContext context, CancellationToken cancellationToken)
    {
        return ValueTask.CompletedTask;
    }

    ValueTask IOutputCachePolicy.ServeResponseAsync
        (OutputCacheContext context, CancellationToken cancellationToken)
    {
        var response = context.HttpContext.Response;

        // Verify existence of cookie headers
        if (!StringValues.IsNullOrEmpty(response.Headers.SetCookie))
        {
            context.AllowCacheStorage = false;
            return ValueTask.CompletedTask;
        }

        // Check response code
        if (response.StatusCode != StatusCodes.Status200OK && 
            response.StatusCode != StatusCodes.Status301MovedPermanently)
        {
            context.AllowCacheStorage = false;
            return ValueTask.CompletedTask;
        }

        return ValueTask.CompletedTask;
    }

    private static bool AttemptOutputCaching(OutputCacheContext context)
    {
        // Check if the current request fulfills the requirements
        // to be cached
        var request = context.HttpContext.Request;

        // Verify the method
        if (!HttpMethods.IsGet(request.Method) && 
            !HttpMethods.IsHead(request.Method) && 
            !HttpMethods.IsPost(request.Method))
        {
            return false;
        }

        // Verify existence of authorization headers
        if (!StringValues.IsNullOrEmpty(request.Headers.Authorization) || 
            request.HttpContext.User?.Identity?.IsAuthenticated == true)
        {
            return false;
        }

        return true;
    }
}

Um diese benutzerdefinierte Richtlinie zu verwenden, erstellen Sie eine benannte Richtlinie:

builder.Services.AddOutputCache(options =>
{
    options.AddPolicy("CachePost", MyCustomPolicy.Instance);
});

Wählen Sie dann die benannte Richtlinie für einen Endpunkt aus:

app.MapPost("/cachedpost", Gravatar.WriteGravatar)
    .CacheOutput("CachePost");

Alternative Außerkraftsetzung von Standardrichtlinien

Alternativ können Sie die Abhängigkeitsinjektion (Dependency Injection, DI) zum Initialisieren einer Instanz verwenden, indem Sie folgende Änderungen an der benutzerdefinierten Richtlinienklasse vornehmen:

  • Verwenden Sie einen öffentlichen Konstruktor anstelle eines privaten Konstruktors.
  • Entfernen Sie die Instance-Eigenschaft in der benutzerdefinierten Richtlinienklasse.

Beispiel:

public sealed class MyCustomPolicy2 : IOutputCachePolicy
{

    public MyCustomPolicy2()
    {
    }

Der Rest der Klasse ist identisch mit der zuvor gezeigten. Fügen Sie die benutzerdefinierte Richtlinie wie im folgenden Beispiel gezeigt hinzu:

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => 
        builder.AddPolicy<MyCustomPolicy2>(), true);
});

Der vorangehende Code verwendet DI, um die Instanz der benutzerdefinierten Richtlinienklasse zu erstellen. Alle öffentlichen Argumente im Konstruktor werden aufgelöst.

Wenn Sie eine benutzerdefinierte Richtlinie als Basisrichtlinie verwenden, rufen Sie OutputCache() (ohne Argumente) nicht für einen Endpunkt auf, für den die Basisrichtlinie gelten soll. Durch Aufrufen von OutputCache() wird dem Endpunkt die Standardrichtlinie hinzugefügt.

Angeben des Cacheschlüssels

Standardmäßig ist jeder Teil der URL als Schlüssel für einen Cacheeintrag enthalten, d. h. Schema, Host, Port, Pfad und Abfragezeichenfolge. Möglicherweise möchten Sie den Cacheschlüssel jedoch explizit steuern. Angenommen, Sie verfügen über einen Endpunkt, der nur für jeden eindeutigen Wert der culture-Abfragezeichenfolge eine eindeutige Antwort zurückgibt. Variationen in anderen Teilen der URL, z. B. andere Abfragezeichenfolgen, sollten nicht zu unterschiedlichen Cacheeinträgen führen. Sie können solche Regeln in einer Richtlinie angeben, wie im folgenden hervorgehobenen Code gezeigt:

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});

Anschließend können Sie die VaryByQuery-Richtlinie für einen Endpunkt auswählen:

app.MapGet("/query", Gravatar.WriteGravatar).CacheOutput("Query");

Im Folgenden sind einige Optionen zum Steuern des Cacheschlüssels aufgeführt:

  • SetVaryByQuery: Geben Sie einen oder mehrere Abfragezeichenfolgennamen an, die dem Cacheschlüssel hinzugefügt werden sollen.

  • SetVaryByHeader: Geben Sie einen oder mehrere HTTP-Header an, die dem Cacheschlüssel hinzugefügt werden sollen.

  • VaryByValue: Geben Sie einen Wert an, der dem Cacheschlüssel hinzugefügt werden soll. Im folgenden Beispiel wird ein Wert verwendet, der angibt, ob die aktuelle Serverzeit in Sekunden ungerade oder gerade ist. Eine neue Antwort wird nur generiert, wenn die Anzahl der Sekunden von ungerade zu gerade oder von gerade zu ungerade wechselt.

    app.MapGet("/varybyvalue", Gravatar.WriteGravatar)
        .CacheOutput(c => c.VaryByValue((context) => 
            new KeyValuePair<string, string>(
                "time", (DateTime.Now.Second % 2)
                    .ToString(CultureInfo.InvariantCulture))));
    

Verwenden Sie OutputCacheOptions.UseCaseSensitivePaths, um anzugeben, dass für den Pfadteil des Schlüssels die Groß-/Kleinschreibung relevant ist. In der Standardeinstellung wird die Groß-/Kleinschreibung nicht berücksichtigt.

Weitere Optionen finden Sie in der OutputCachePolicyBuilder-Klasse.

Erneute Cacheüberprüfung

Eine erneute Cacheüberprüfung bedeutet, dass der Server anstelle des gesamten Antworttexts einen 304 Not Modified-HTTP-Statuscode zurückgeben kann. Dieser Statuscode informiert den Client darüber, dass die Antwort auf die Anforderung mit der zuvor vom Client empfangenen Antwort identisch ist.

Der folgende Code veranschaulicht die Verwendung eines Etag-Headers zum Aktivieren der erneuten Cacheüberprüfung. Wenn der Client einen If-None-Match-Header mit dem ETag-Wert einer früheren Antwort sendet und der Cacheeintrag neu ist, gibt der Server 304 Nicht geändert anstelle der vollständigen Antwort zurück:

app.MapGet("/etag", async (context) =>
{
    var etag = $"\"{Guid.NewGuid():n}\"";
    context.Response.Headers.ETag = etag;
    await Gravatar.WriteGravatar(context);

}).CacheOutput();

Eine weitere Möglichkeit zur erneuten Cacheüberprüfung besteht darin, das Erstellungsdatum des Cacheeintrags im Vergleich zum Datum der Clientanforderung zu überprüfen. Wenn der Anforderungsheader If-Modified-Since bereitgestellt wird, gibt die Ausgabezwischenspeicherung 304 zurück, wenn der zwischengespeicherte Eintrag älter und nicht abgelaufen ist.

Die erneute Cacheüberprüfung erfolgt automatisch als Reaktion auf diese vom Client gesendeten Header. Abgesehen von der Aktivierung der Ausgabezwischenspeicherung ist keine spezielle Konfiguration auf dem Server erforderlich, um dieses Verhalten zu aktivieren.

Verwenden von Tags zum Entfernen von Cacheeinträgen

Sie können Tags verwenden, um eine Gruppe von Endpunkten zu identifizieren und alle Cacheeinträge für die Gruppe zu entfernen. Der folgende Code erstellt beispielsweise ein Paar von Endpunkten, deren URLs mit „blog“ beginnen, und markiert sie mit „tag-blog“:

app.MapGet("/blog", Gravatar.WriteGravatar)
    .CacheOutput(builder => builder.Tag("tag-blog")); ;
app.MapGet("/blog/post/{id}", Gravatar.WriteGravatar)
    .CacheOutput(builder => builder.Tag("tag-blog")); ;

Eine alternative Möglichkeit zum Zuweisen von Tags für dasselbe Endpunktpaar besteht darin, eine Basisrichtlinie zu definieren, die für Endpunkte gilt, die mit blog beginnen:

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});

Eine weitere Alternative besteht im Aufrufen von MapGroup:

var blog = app.MapGroup("blog")
    .CacheOutput(builder => builder.Tag("tag-blog"));
blog.MapGet("/", Gravatar.WriteGravatar);
blog.MapGet("/post/{id}", Gravatar.WriteGravatar);

In den obigen Tagzuweisungsbeispielen werden beide Endpunkte durch das tag-blog-Tag identifiziert. Anschließend können Sie die Cacheeinträge für diese Endpunkte mit einer einzigen Anweisung entfernen, die auf dieses Tag verweist:

app.MapPost("/purge/{tag}", async (IOutputCacheStore cache, string tag) =>
{
    await cache.EvictByTagAsync(tag, default);
});

Mit diesem Code werden Cacheeinträge für diese Endpunkte durch eine HTTP POST-Anforderung entfernt, die an https://localhost:<port>/purge/tag-blog gesendet wird.

Möglicherweise benötigen Sie eine Methode zum Entfernen aller Cacheeinträge für alle Endpunkte. Erstellen Sie hierzu eine Basisrichtlinie für alle Endpunkte, wie im folgenden Code gezeigt:

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});

Mit dieser Basisrichtlinie können Sie das Tag „tag-all“ verwenden, um alle Einträge im Cache zu entfernen.

Deaktivieren der Ressourcensperre

Standardmäßig ist die Ressourcensperre aktiviert, um das Risiko von Cacheüberlastung (Stampede) und Thundering Herd zu verringern. Weitere Informationen finden Sie unter Ausgabezwischenspeicherung.

Um die Ressourcensperre zu deaktivieren, rufen Sie beim Erstellen einer Richtlinie SetLocking(false) auf, wie im folgenden Beispiel gezeigt:

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});

Im folgenden Beispiel wird die Richtlinie ohne Sperren für einen Endpunkt ausgewählt:

app.MapGet("/nolock", Gravatar.WriteGravatar)
    .CacheOutput("NoLock");

Grenzwerte

Mit den folgenden Eigenschaften von OutputCacheOptions können Sie Grenzwerte konfigurieren, die für alle Endpunkte gelten:

  • SizeLimit: Maximale Größe des Cachespeichers. Wenn dieser Grenzwert erreicht ist, werden keine neuen Antworten zwischengespeichert, bis ältere Einträge entfernt werden. Der Standardwert ist 100 MB.
  • MaximumBodySize - Wenn der Antworttext diesen Grenzwert überschreitet, wird er nicht zwischengespeichert. Der Standardwert ist 64 MB.
  • DefaultExpirationTimeSpan: Die Dauer der Ablaufzeit, die gilt, wenn keine Richtlinienangabe vorliegt. Der Standardwert ist 60 Sekunden.

Cachespeicher

IOutputCacheStore wird für die Speicherung verwendet. Diese Angabe wird standardmäßig mit MemoryCache verwendet. Zwischengespeicherte Antworten werden „In-Process“ gespeichert, sodass jeder Server über einen separaten Cache verfügt, der bei jedem Neustart des Serverprozesses verloren geht.

Redis Cache

Eine Alternative ist die Verwendung des Redis-Caches. Der Redis-Cache bietet Konsistenz zwischen Serverknoten über einen freigegebenen Cache, der einzelne Serverprozesse überdauert. So verwenden Sie Redis für die Ausgabezwischenspeicherung:

  • Installieren Sie das NuGet-Paket Microsoft.AspNetCore.OutputCaching.StackExchangeRedis.

  • Rufen Sie builder.Services.AddStackExchangeRedisOutputCache (nicht AddStackExchangeRedisCache) auf und stellen Sie eine Verbindungszeichenfolge bereit, die auf einen Redis-Server verweist.

    Beispiel:

    builder.Services.AddStackExchangeRedisOutputCache(options =>
    {
        options.Configuration = 
            builder.Configuration.GetConnectionString("MyRedisConStr");
        options.InstanceName = "SampleInstance";
    });
    
    builder.Services.AddOutputCache(options =>
    {
        options.AddBasePolicy(builder => 
            builder.Expire(TimeSpan.FromSeconds(10)));
    });
    
    • options.Configuration – Eine Verbindungszeichenfolge mit einem lokalen Redis-Server oder einem gehosteten Angebot wie Azure Cache für Redis. Zum Beispiel <instance_name>.redis.cache.windows.net:6380,password=<password>,ssl=True,abortConnect=False für Azure Cache für Redis.
    • options.InstanceName - Optional, gibt eine logische Partition für den Cache an.

    Die Konfigurationsoptionen sind identisch mit den Redis-basierten Optionen für verteilte Zwischenspeicherung.

Es wird nicht empfohlen IDistributedCache zur Ausgabezwischenspeicherung zu verwenden. IDistributedCache verfügt nicht über atomische Features, die für das Tagging erforderlich sind. Es wird empfohlen, die integrierte Unterstützung für Redis zu verwenden oder benutzerdefinierte IOutputCacheStore-Implementierungen zu erstellen, indem Sie direkte Abhängigkeiten vom zugrunde liegenden Speichermechanismus verwenden.

Siehe auch

In diesem Artikel wird erläutert, wie die Middleware für die Ausgabezwischenspeicherung in einer ASP.NET Core-App konfiguriert wird. Eine Einführung in die Ausgabezwischenspeicherung finden Sie unter Ausgabezwischenspeicherung.

Die Middleware für die Ausgabezwischenspeicherung kann in allen Arten von ASP.NET Core-Apps verwendet werden: Minimal-API, Web-API mit Controllern, MVC und Razor Pages. Die Beispiel-App ist eine Minimal-API, aber die darin demonstrierten Zwischenspeicherungsfeatures werden auch in den anderen App-Typen unterstützt.

Hinzufügen der Middleware zur App

Fügen Sie die Middleware für die Ausgabezwischenspeicherung zur Dienstsammlung hinzu, indem Sie AddOutputCache aufrufen.

Fügen Sie die Middleware zur Anforderungsverarbeitungspipeline hinzu, indem Sie UseOutputCache aufrufen.

Hinweis

  • In Apps, die CORS-Middleware verwenden, muss UseOutputCache nach UseCors aufgerufen werden.
  • In Razor Pages-Apps und Apps mit Controllern muss UseOutputCache nach UseRouting aufgerufen werden.
  • Durch Aufrufen von AddOutputCache und UseOutputCache wird nicht das Zwischenspeicherungsverhalten gestartet, sondern das Zwischenspeichern verfügbar gemacht. Das Zwischenspeichern von Antwortdaten muss wie in den folgenden Abschnitten gezeigt konfiguriert werden.

Konfigurieren eines Endpunkts oder einer Seite

Bei minimalen API-Apps konfigurieren Sie einen Endpunkt für die Zwischenspeicherung, indem Sie CacheOutput aufrufen oder das [OutputCache]-Attribut anwenden, wie in den folgenden Beispielen gezeigt:

app.MapGet("/cached", Gravatar.WriteGravatar).CacheOutput();
app.MapGet("/attribute", [OutputCache] (context) => 
    Gravatar.WriteGravatar(context));

Bei Apps mit Controllern wenden Sie das [OutputCache]-Attribut auf die Aktionsmethode an. Bei Razor Pages-Apps wenden Sie das Attribut auf die Razor-Seitenklasse an.

Konfigurieren mehrerer Endpunkte oder Seiten

Erstellen Sie Richtlinien beim Aufrufen von AddOutputCache, um eine Zwischenspeicherungskonfiguration anzugeben, die für mehrere Endpunkte gilt. Eine Richtlinie kann für bestimmte Endpunkte ausgewählt werden, während eine Basisrichtlinie eine Standardzwischenspeicherungskonfiguration für eine Sammlung von Endpunkten bereitstellt.

Der folgende hervorgehobene Code konfiguriert die Zwischenspeicherung für alle Endpunkte der App mit einer Ablaufzeit von 10 Sekunden. Wenn keine Ablaufzeit angegeben ist, wird standardmäßig eine Minute festgelegt.

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => 
        builder.Expire(TimeSpan.FromSeconds(10)));
    options.AddPolicy("Expire20", builder => 
        builder.Expire(TimeSpan.FromSeconds(20)));
    options.AddPolicy("Expire30", builder => 
        builder.Expire(TimeSpan.FromSeconds(30)));
});

Der folgende hervorgehobene Code erstellt zwei Richtlinien, die jeweils eine andere Ablaufzeit angeben. Ausgewählte Endpunkte können die Ablaufzeit von 20 Sekunden verwenden, und für andere Endpunkte kann die Ablaufzeit von 30 Sekunden gelten.

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => 
        builder.Expire(TimeSpan.FromSeconds(10)));
    options.AddPolicy("Expire20", builder => 
        builder.Expire(TimeSpan.FromSeconds(20)));
    options.AddPolicy("Expire30", builder => 
        builder.Expire(TimeSpan.FromSeconds(30)));
});

Sie können eine Richtlinie für einen Endpunkt auswählen, wenn Sie die CacheOutput-Methode aufrufen oder das [OutputCache]-Attribut verwenden:

app.MapGet("/20", Gravatar.WriteGravatar).CacheOutput("Expire20");
app.MapGet("/30", [OutputCache(PolicyName = "Expire30")] (context) => 
    Gravatar.WriteGravatar(context));

Bei Apps mit Controllern wenden Sie das [OutputCache]-Attribut auf die Aktionsmethode an. Bei Razor Pages-Apps wenden Sie das Attribut auf die Razor-Seitenklasse an.

Standardrichtlinie für die Ausgabezwischenspeicherung

Standardmäßig folgt die Ausgabezwischenspeicherung den folgenden Regeln:

  • Es werden nur HTTP 200-Antworten zwischengespeichert.
  • Es werden nur HTTP GET- oder HEAD-Anforderungen zwischengespeichert.
  • Antworten, die cookies festlegen, werden nicht zwischengespeichert.
  • Antworten auf authentifizierte Anforderungen werden nicht zwischengespeichert.

Der folgende Code wendet alle Standardregeln für die Zwischenspeicherung auf alle Endpunkte einer App an:

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder.Cache());
});

Überschreiben der Standardrichtlinie

Der folgende Code zeigt, wie die Standardregeln überschrieben werden. Die hervorgehobenen Zeilen im folgenden benutzerdefinierten Richtliniencode ermöglichen die Zwischenspeicherung für HTTP POST-Methoden und HTTP 301-Antworten:

using Microsoft.AspNetCore.OutputCaching;
using Microsoft.Extensions.Primitives;

namespace OCMinimal;

public sealed class MyCustomPolicy : IOutputCachePolicy
{
    public static readonly MyCustomPolicy Instance = new();

    private MyCustomPolicy()
    {
    }

    ValueTask IOutputCachePolicy.CacheRequestAsync(
        OutputCacheContext context, 
        CancellationToken cancellationToken)
    {
        var attemptOutputCaching = AttemptOutputCaching(context);
        context.EnableOutputCaching = true;
        context.AllowCacheLookup = attemptOutputCaching;
        context.AllowCacheStorage = attemptOutputCaching;
        context.AllowLocking = true;

        // Vary by any query by default
        context.CacheVaryByRules.QueryKeys = "*";

        return ValueTask.CompletedTask;
    }

    ValueTask IOutputCachePolicy.ServeFromCacheAsync
        (OutputCacheContext context, CancellationToken cancellationToken)
    {
        return ValueTask.CompletedTask;
    }

    ValueTask IOutputCachePolicy.ServeResponseAsync
        (OutputCacheContext context, CancellationToken cancellationToken)
    {
        var response = context.HttpContext.Response;

        // Verify existence of cookie headers
        if (!StringValues.IsNullOrEmpty(response.Headers.SetCookie))
        {
            context.AllowCacheStorage = false;
            return ValueTask.CompletedTask;
        }

        // Check response code
        if (response.StatusCode != StatusCodes.Status200OK && 
            response.StatusCode != StatusCodes.Status301MovedPermanently)
        {
            context.AllowCacheStorage = false;
            return ValueTask.CompletedTask;
        }

        return ValueTask.CompletedTask;
    }

    private static bool AttemptOutputCaching(OutputCacheContext context)
    {
        // Check if the current request fulfills the requirements
        // to be cached
        var request = context.HttpContext.Request;

        // Verify the method
        if (!HttpMethods.IsGet(request.Method) && 
            !HttpMethods.IsHead(request.Method) && 
            !HttpMethods.IsPost(request.Method))
        {
            return false;
        }

        // Verify existence of authorization headers
        if (!StringValues.IsNullOrEmpty(request.Headers.Authorization) || 
            request.HttpContext.User?.Identity?.IsAuthenticated == true)
        {
            return false;
        }

        return true;
    }
}

Um diese benutzerdefinierte Richtlinie zu verwenden, erstellen Sie eine benannte Richtlinie:

builder.Services.AddOutputCache(options =>
{
    options.AddPolicy("CachePost", MyCustomPolicy.Instance);
});

Wählen Sie dann die benannte Richtlinie für einen Endpunkt aus:

app.MapPost("/cachedpost", Gravatar.WriteGravatar)
    .CacheOutput("CachePost");

Alternative Außerkraftsetzung von Standardrichtlinien

Alternativ können Sie die Abhängigkeitsinjektion (Dependency Injection, DI) zum Initialisieren einer Instanz verwenden, indem Sie folgende Änderungen an der benutzerdefinierten Richtlinienklasse vornehmen:

  • Verwenden Sie einen öffentlichen Konstruktor anstelle eines privaten Konstruktors.
  • Entfernen Sie die Instance-Eigenschaft in der benutzerdefinierten Richtlinienklasse.

Beispiel:

public sealed class MyCustomPolicy2 : IOutputCachePolicy
{

    public MyCustomPolicy2()
    {
    }

Der Rest der Klasse ist identisch mit der zuvor gezeigten. Fügen Sie die benutzerdefinierte Richtlinie wie im folgenden Beispiel gezeigt hinzu:

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => 
        builder.AddPolicy<MyCustomPolicy2>(), true);
});

Der vorangehende Code verwendet DI, um die Instanz der benutzerdefinierten Richtlinienklasse zu erstellen. Alle öffentlichen Argumente im Konstruktor werden aufgelöst.

Wenn Sie eine benutzerdefinierte Richtlinie als Basisrichtlinie verwenden, rufen Sie OutputCache() (ohne Argumente) nicht für einen Endpunkt auf, für den die Basisrichtlinie gelten soll. Durch Aufrufen von OutputCache() wird dem Endpunkt die Standardrichtlinie hinzugefügt.

Angeben des Cacheschlüssels

Standardmäßig ist jeder Teil der URL als Schlüssel für einen Cacheeintrag enthalten, d. h. Schema, Host, Port, Pfad und Abfragezeichenfolge. Möglicherweise möchten Sie den Cacheschlüssel jedoch explizit steuern. Angenommen, Sie verfügen über einen Endpunkt, der nur für jeden eindeutigen Wert der culture-Abfragezeichenfolge eine eindeutige Antwort zurückgibt. Variationen in anderen Teilen der URL, z. B. andere Abfragezeichenfolgen, sollten nicht zu unterschiedlichen Cacheeinträgen führen. Sie können solche Regeln in einer Richtlinie angeben, wie im folgenden hervorgehobenen Code gezeigt:

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});

Anschließend können Sie die VaryByQuery-Richtlinie für einen Endpunkt auswählen:

app.MapGet("/query", Gravatar.WriteGravatar).CacheOutput("Query");

Im Folgenden sind einige Optionen zum Steuern des Cacheschlüssels aufgeführt:

  • SetVaryByQuery: Geben Sie einen oder mehrere Abfragezeichenfolgennamen an, die dem Cacheschlüssel hinzugefügt werden sollen.

  • SetVaryByHeader: Geben Sie einen oder mehrere HTTP-Header an, die dem Cacheschlüssel hinzugefügt werden sollen.

  • VaryByValue: Geben Sie einen Wert an, der dem Cacheschlüssel hinzugefügt werden soll. Im folgenden Beispiel wird ein Wert verwendet, der angibt, ob die aktuelle Serverzeit in Sekunden ungerade oder gerade ist. Eine neue Antwort wird nur generiert, wenn die Anzahl der Sekunden von ungerade zu gerade oder von gerade zu ungerade wechselt.

    app.MapGet("/varybyvalue", Gravatar.WriteGravatar)
        .CacheOutput(c => c.VaryByValue((context) => 
            new KeyValuePair<string, string>(
                "time", (DateTime.Now.Second % 2)
                    .ToString(CultureInfo.InvariantCulture))));
    

Verwenden Sie OutputCacheOptions.UseCaseSensitivePaths, um anzugeben, dass für den Pfadteil des Schlüssels die Groß-/Kleinschreibung relevant ist. In der Standardeinstellung wird die Groß-/Kleinschreibung nicht berücksichtigt.

Weitere Optionen finden Sie in der OutputCachePolicyBuilder-Klasse.

Erneute Cacheüberprüfung

Eine erneute Cacheüberprüfung bedeutet, dass der Server anstelle des gesamten Antworttexts einen 304 Not Modified-HTTP-Statuscode zurückgeben kann. Dieser Statuscode informiert den Client darüber, dass die Antwort auf die Anforderung mit der zuvor vom Client empfangenen Antwort identisch ist.

Der folgende Code veranschaulicht die Verwendung eines Etag-Headers zum Aktivieren der erneuten Cacheüberprüfung. Wenn der Client einen If-None-Match-Header mit dem ETag-Wert einer früheren Antwort sendet und der Cacheeintrag neu ist, gibt der Server 304 Nicht geändert anstelle der vollständigen Antwort zurück:

app.MapGet("/etag", async (context) =>
{
    var etag = $"\"{Guid.NewGuid():n}\"";
    context.Response.Headers.ETag = etag;
    await Gravatar.WriteGravatar(context);

}).CacheOutput();

Eine weitere Möglichkeit zur erneuten Cacheüberprüfung besteht darin, das Erstellungsdatum des Cacheeintrags im Vergleich zum Datum der Clientanforderung zu überprüfen. Wenn der Anforderungsheader If-Modified-Since bereitgestellt wird, gibt die Ausgabezwischenspeicherung 304 zurück, wenn der zwischengespeicherte Eintrag älter und nicht abgelaufen ist.

Die erneute Cacheüberprüfung erfolgt automatisch als Reaktion auf diese vom Client gesendeten Header. Abgesehen von der Aktivierung der Ausgabezwischenspeicherung ist keine spezielle Konfiguration auf dem Server erforderlich, um dieses Verhalten zu aktivieren.

Verwenden von Tags zum Entfernen von Cacheeinträgen

Sie können Tags verwenden, um eine Gruppe von Endpunkten zu identifizieren und alle Cacheeinträge für die Gruppe zu entfernen. Der folgende Code erstellt beispielsweise ein Paar von Endpunkten, deren URLs mit „blog“ beginnen, und markiert sie mit „tag-blog“:

app.MapGet("/blog", Gravatar.WriteGravatar)
    .CacheOutput(builder => builder.Tag("tag-blog")); ;
app.MapGet("/blog/post/{id}", Gravatar.WriteGravatar)
    .CacheOutput(builder => builder.Tag("tag-blog")); ;

Eine alternative Möglichkeit zum Zuweisen von Tags für dasselbe Endpunktpaar besteht darin, eine Basisrichtlinie zu definieren, die für Endpunkte gilt, die mit blog beginnen:

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});

Eine weitere Alternative besteht im Aufrufen von MapGroup:

var blog = app.MapGroup("blog")
    .CacheOutput(builder => builder.Tag("tag-blog"));
blog.MapGet("/", Gravatar.WriteGravatar);
blog.MapGet("/post/{id}", Gravatar.WriteGravatar);

In den obigen Tagzuweisungsbeispielen werden beide Endpunkte durch das tag-blog-Tag identifiziert. Anschließend können Sie die Cacheeinträge für diese Endpunkte mit einer einzigen Anweisung entfernen, die auf dieses Tag verweist:

app.MapPost("/purge/{tag}", async (IOutputCacheStore cache, string tag) =>
{
    await cache.EvictByTagAsync(tag, default);
});

Mit diesem Code werden Cacheeinträge für diese Endpunkte durch eine HTTP POST-Anforderung entfernt, die an https://localhost:<port>/purge/tag-blog gesendet wird.

Möglicherweise benötigen Sie eine Methode zum Entfernen aller Cacheeinträge für alle Endpunkte. Erstellen Sie hierzu eine Basisrichtlinie für alle Endpunkte, wie im folgenden Code gezeigt:

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});

Mit dieser Basisrichtlinie können Sie das Tag „tag-all“ verwenden, um alle Einträge im Cache zu entfernen.

Deaktivieren der Ressourcensperre

Standardmäßig ist die Ressourcensperre aktiviert, um das Risiko von Cacheüberlastung (Stampede) und Thundering Herd zu verringern. Weitere Informationen finden Sie unter Ausgabezwischenspeicherung.

Um die Ressourcensperre zu deaktivieren, rufen Sie beim Erstellen einer Richtlinie SetLocking(false) auf, wie im folgenden Beispiel gezeigt:

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});

Im folgenden Beispiel wird die Richtlinie ohne Sperren für einen Endpunkt ausgewählt:

app.MapGet("/nolock", Gravatar.WriteGravatar)
    .CacheOutput("NoLock");

Grenzwerte

Mit den folgenden Eigenschaften von OutputCacheOptions können Sie Grenzwerte konfigurieren, die für alle Endpunkte gelten:

  • SizeLimit: Maximale Größe des Cachespeichers. Wenn dieser Grenzwert erreicht ist, werden keine neuen Antworten zwischengespeichert, bis ältere Einträge entfernt werden. Der Standardwert ist 100 MB.
  • MaximumBodySize: Wenn der Antworttext diesen Grenzwert überschreitet, wird er nicht zwischengespeichert. Der Standardwert ist 64 MB.
  • DefaultExpirationTimeSpan: Die Dauer der Ablaufzeit, die gilt, wenn keine Richtlinienangabe vorliegt. Der Standardwert ist 60 Sekunden.

Cachespeicher

IOutputCacheStore wird für die Speicherung verwendet. Diese Angabe wird standardmäßig mit MemoryCache verwendet. Es wird nicht empfohlen IDistributedCache zur Ausgabezwischenspeicherung zu verwenden. IDistributedCache verfügt nicht über atomische Features, die für das Tagging erforderlich sind. Es wird empfohlen, benutzerdefinierte IOutputCacheStore-Implementierungen zu erstellen, indem Sie direkte Abhängigkeiten vom zugrunde liegenden Speichermechanismus (wie z. B. Redis) verwenden. Oder verwenden Sie die integrierte Unterstützung für Redis Cache in .NET 8..

Siehe auch