Udostępnij za pośrednictwem


Oprogramowanie pośredniczące buforowania danych wyjściowych w programie ASP.NET Core

Autor: Tom Dykstra

Uwaga

Nie jest to najnowsza wersja tego artykułu. Aby zapoznać się z bieżącą wersją, zobacz wersję tego artykułu platformy .NET 9.

Ostrzeżenie

Ta wersja ASP.NET Core nie jest już obsługiwana. Aby uzyskać więcej informacji, zobacz zasady pomocy technicznej platformy .NET i platformy .NET Core. Aby zapoznać się z bieżącą wersją, zobacz wersję tego artykułu platformy .NET 9.

Ważne

Te informacje odnoszą się do produktu w wersji wstępnej, który może zostać znacząco zmodyfikowany, zanim zostanie wydany komercyjnie. Firma Microsoft nie udziela żadnych gwarancji, jawnych lub domniemanych, w odniesieniu do informacji podanych w tym miejscu.

Aby zapoznać się z bieżącą wersją, zobacz wersję tego artykułu platformy .NET 9.

W tym artykule wyjaśniono, jak skonfigurować oprogramowanie pośredniczące buforowania danych wyjściowych w aplikacji ASP.NET Core. Aby zapoznać się z wprowadzeniem do buforowania danych wyjściowych, zobacz Buforowanie danych wyjściowych.

Oprogramowanie pośredniczące buforowania danych wyjściowych może być używane we wszystkich typach aplikacji ASP.NET Core: minimalny interfejs API, internetowy interfejs API z kontrolerami, MVC i Razor stronami. Przykłady kodu są udostępniane dla minimalnych interfejsów API i interfejsów API opartych na kontrolerach. Przykłady interfejsu API opartego na kontrolerze pokazują, jak używać atrybutów do konfigurowania buforowania. Te atrybuty mogą być również używane w aplikacjach MVC i Razor Pages.

Przykłady kodu odnoszą się do klasy Gravatar, która generuje obraz i udostępnia datę i godzinę generowania. Klasa jest zdefiniowana i używana tylko w przykładowej aplikacji. Jego celem jest ułatwienie sprawdzenia, kiedy są używane buforowane dane wyjściowe. Aby uzyskać więcej informacji, zobacz Jak pobrać przykładowe dyrektywy i preprocesora w przykładowym kodzie.

Dodawanie oprogramowania pośredniczącego do aplikacji

Dodaj oprogramowanie pośredniczące buforowania danych wyjściowych do kolekcji usług, wywołując metodę AddOutputCache.

Dodaj oprogramowanie pośredniczące do potoku przetwarzania żądań, wywołując metodę UseOutputCache.

Na przykład:

builder.Services.AddOutputCache();
var app = builder.Build();

// Configure the HTTP request pipeline.
app.UseHttpsRedirection();
app.UseOutputCache();
app.UseAuthorization();

Wywoływanie AddOutputCachei UseOutputCache nie rozpoczyna buforowania, dzięki czemu buforowanie jest dostępne. Aby tworzyć odpowiedzi pamięci podręcznej aplikacji, buforowanie musi być skonfigurowane, jak pokazano w poniższych sekcjach.

Uwaga

  • W aplikacjach korzystających z oprogramowaniaUseOutputCache pośredniczącego CORS należy wywołać metodę po UseCors.
  • W Razor aplikacjach Stron i aplikacjach z kontrolerami UseOutputCache należy wywołać metodę po UseRouting.

Konfigurowanie jednego punktu końcowego lub strony

W przypadku minimalnych aplikacji interfejsu API skonfiguruj punkt końcowy do buforowania, wywołując CacheOutputmetodę , lub [OutputCache] stosując atrybut, jak pokazano w poniższych przykładach:

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

W przypadku aplikacji z kontrolerami zastosuj [OutputCache] atrybut do metody akcji, jak pokazano poniżej:

[ApiController]
[Route("/[controller]")]
[OutputCache]
public class CachedController : ControllerBase
{
    public async Task GetAsync()
    {
        await Gravatar.WriteGravatar(HttpContext);
    }
}

W przypadku Razor aplikacji Pages zastosuj atrybut do Razor klasy strony.

Konfigurowanie wielu punktów końcowych lub stron

Utwórz zasady podczas wywoływania AddOutputCache w celu określenia konfiguracji buforowania, która ma zastosowanie do wielu punktów końcowych. Zasady można wybrać dla określonych punktów końcowych, podczas gdy zasady podstawowe zapewniają domyślną konfigurację buforowania dla kolekcji punktów końcowych.

Poniższy wyróżniony kod konfiguruje buforowanie dla wszystkich punktów końcowych aplikacji z czasem wygaśnięcia 10 sekund. Jeśli nie określono czasu wygaśnięcia, wartość domyślna to jedna minuta.

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)));
});

Poniższy wyróżniony kod tworzy dwie zasady, z których każda określa inny czas wygaśnięcia. Wybrane punkty końcowe mogą używać 20-sekundowego wygaśnięcia, a inne mogą korzystać z 30-sekundowego wygaśnięcia.

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)));
});

Zasady dla punktu końcowego można wybrać podczas wywoływania CacheOutput metody lub przy użyciu atrybutu [OutputCache] .

W minimalnej aplikacji interfejsu API następujący kod konfiguruje jeden punkt końcowy z 20-sekundowym wygaśnięciem i jednym z 30-sekundowym wygaśnięciem:

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

W przypadku aplikacji z kontrolerami zastosuj [OutputCache] atrybut do metody akcji, aby wybrać zasady:

[ApiController]
[Route("/[controller]")]
[OutputCache(PolicyName = "Expire20")]
public class Expire20Controller : ControllerBase
{
    public async Task GetAsync()
    {
        await Gravatar.WriteGravatar(HttpContext);
    }
}

W przypadku Razor aplikacji Pages zastosuj atrybut do Razor klasy strony.

Domyślne zasady buforowania danych wyjściowych

Domyślnie buforowanie danych wyjściowych jest zgodne z następującymi regułami:

  • Buforowane są tylko odpowiedzi HTTP 200.
  • Buforowane są tylko żądania HTTP GET lub HEAD.
  • Odpowiedzi, które ustawiają pliki cookie, nie są buforowane.
  • Odpowiedzi na uwierzytelnione żądania nie są buforowane.

Poniższy kod stosuje wszystkie domyślne reguły buforowania do wszystkich punktów końcowych aplikacji:

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

Zastępowanie zasad domyślnych

Poniższy kod pokazuje, jak zastąpić reguły domyślne. Wyróżnione wiersze w poniższym niestandardowym kodzie zasad umożliwiają buforowanie metod HTTP POST i odpowiedzi HTTP 301:

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;
    }
}

Aby użyć tych zasad niestandardowych, utwórz nazwane zasady:

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

Wybierz nazwane zasady dla punktu końcowego. Poniższy kod wybiera zasady niestandardowe dla punktu końcowego w minimalnej aplikacji interfejsu API:

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

Poniższy kod wykonuje to samo w przypadku akcji kontrolera:

[ApiController]
[Route("/[controller]")]
[OutputCache(PolicyName = "CachePost")]
public class PostController : ControllerBase
{
    public async Task GetAsync()
    {
        await Gravatar.WriteGravatar(HttpContext);
    }
}

Zastępowanie alternatywnych zasad domyślnych

Alternatywnie użyj iniekcji zależności (DI), aby zainicjować wystąpienie, z następującymi zmianami w niestandardowej klasie zasad:

  • Publiczny konstruktor zamiast prywatnego konstruktora.
  • Wyeliminuj Instance właściwość w niestandardowej klasie zasad.

Na przykład:

public sealed class MyCustomPolicy2 : IOutputCachePolicy
{

    public MyCustomPolicy2()
    {
    }

Pozostała część klasy jest taka sama, jak pokazano wcześniej. Dodaj zasady niestandardowe, jak pokazano w poniższym przykładzie:

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

Powyższy kod używa di do utworzenia wystąpienia niestandardowej klasy zasad. Wszystkie publiczne argumenty w konstruktorze są rozpoznawane.

W przypadku używania zasad niestandardowych jako zasad podstawowych nie należy wywoływać OutputCache() (bez argumentów) ani używać atrybutu [OutputCache] w żadnym punkcie końcowym, do którego mają być stosowane zasady podstawowe. Wywoływanie OutputCache() lub używanie atrybutu powoduje dodanie domyślnych zasad do punktu końcowego.

Określanie klucza pamięci podręcznej

Domyślnie każda część adresu URL jest dołączana jako klucz do wpisu pamięci podręcznej, czyli schematu, hosta, portu, ścieżki i ciągu zapytania. Można jednak jawnie kontrolować klucz pamięci podręcznej. Załóżmy na przykład, że masz punkt końcowy, który zwraca unikatową odpowiedź tylko dla każdej unikatowej culture wartości ciągu zapytania. Odmiana w innych częściach adresu URL, takich jak inne ciągi zapytania, nie powinna powodować różnych wpisów pamięci podręcznej. Takie reguły można określić w zasadach, jak pokazano w poniższym wyróżnionym kodzie:

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));
});

Następnie możesz wybrać VaryByQuery zasady dla punktu końcowego. W minimalnej aplikacji interfejsu API poniższy kod wybiera VaryByQuery zasady dla punktu końcowego, który zwraca unikatową odpowiedź tylko dla każdej unikatowej culture wartości ciągu zapytania:

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

Poniższy kod wykonuje to samo w przypadku akcji kontrolera:

[ApiController]
[Route("/[controller]")]
[OutputCache(PolicyName = "Query")]
public class QueryController : ControllerBase
{
    public async Task GetAsync()
    {
        await Gravatar.WriteGravatar(HttpContext);
    }
}

Poniżej przedstawiono niektóre opcje kontrolowania klucza pamięci podręcznej:

  • SetVaryByQuery — Określ co najmniej jedną nazwę ciągu zapytania, która ma zostać dodana do klucza pamięci podręcznej.

  • SetVaryByHeader — Określ co najmniej jeden nagłówek HTTP, który ma zostać dodany do klucza pamięci podręcznej.

  • VaryByValue— Określ wartość, która ma zostać dodana do klucza pamięci podręcznej. W poniższym przykładzie użyto wartości, która wskazuje, czy bieżący czas serwera w sekundach jest dziwny, czy nawet. Nowa odpowiedź jest generowana tylko wtedy, gdy liczba sekund przechodzi od nieparzystego do parzystego, a nawet do dziwnego.

    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));
        options.AddPolicy("VaryByValue", builder => 
            builder.VaryByValue((context) =>
                new KeyValuePair<string, string>(
                "time", (DateTime.Now.Second % 2)
                    .ToString(CultureInfo.InvariantCulture))));
    });
    

Użyj OutputCacheOptions.UseCaseSensitivePaths polecenia , aby określić, że część ścieżki klucza uwzględnia wielkość liter. Wartość domyślna to bez uwzględniania wielkości liter.

Aby uzyskać więcej opcji, zobacz klasę OutputCachePolicyBuilder .

Ponowne dodawanie pamięci podręcznej

Zmiana pamięci podręcznej oznacza, że serwer może zwrócić 304 Not Modified kod stanu HTTP zamiast pełnej treści odpowiedzi. Ten kod stanu informuje klienta, że odpowiedź na żądanie nie zmienia się od tego, co klient otrzymał wcześniej.

Poniższy kod ilustruje użycie nagłówka Etag w celu włączenia ponownego stosowania pamięci podręcznej. Jeśli klient wysyła If-None-Match nagłówek z wartością etag wcześniejszej odpowiedzi, a wpis pamięci podręcznej jest świeży, serwer zwraca wartość 304 Not Modified zamiast pełnej odpowiedzi. Poniżej przedstawiono sposób ustawiania wartości elementu etag w zasadach w aplikacji interfejsu API minimalnej:

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

}).CacheOutput();

Oto jak ustawić wartość etag w interfejsie API opartym na kontrolerze:

[ApiController]
[Route("/[controller]")]
[OutputCache]
public class EtagController : ControllerBase
{
    public async Task GetAsync()
    {
        var etag = $"\"{Guid.NewGuid():n}\"";
        HttpContext.Response.Headers.ETag = etag;
        await Gravatar.WriteGravatar(HttpContext);
    }
}

Innym sposobem na ponowne zaktualizowanie pamięci podręcznej jest sprawdzenie daty utworzenia wpisu pamięci podręcznej w porównaniu z datą żądaną przez klienta. Po podaniu nagłówka If-Modified-Since żądania buforowanie danych wyjściowych zwraca wartość 304, jeśli buforowany wpis jest starszy i nie wygasł.

Ponowne aktualizowanie pamięci podręcznej jest automatyczne w odpowiedzi na te nagłówki wysyłane z klienta. Nie jest wymagana żadna specjalna konfiguracja na serwerze, aby włączyć to zachowanie, oprócz włączania buforowania danych wyjściowych.

Eksmitowanie wpisów pamięci podręcznej za pomocą tagów

Za pomocą tagów można zidentyfikować grupę punktów końcowych i wykluczyć wszystkie wpisy pamięci podręcznej dla grupy. Na przykład poniższy minimalny kod interfejsu API tworzy parę punktów końcowych, których adresy URL zaczynają się od "bloga" i tagują je jako "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"));

Poniższy kod pokazuje, jak przypisać tagi do punktu końcowego w interfejsie API opartym na kontrolerze:

[ApiController]
[Route("/[controller]")]
[OutputCache(Tags = new[] { "tag-blog", "tag-all" })]
public class TagEndpointController : ControllerBase
{
    public async Task GetAsync()
    {
        await Gravatar.WriteGravatar(HttpContext);
    }
}

Alternatywnym sposobem przypisywania tagów dla punktów końcowych z trasami rozpoczynającymi się od blog jest zdefiniowanie podstawowych zasad, które mają zastosowanie do wszystkich punktów końcowych z tą trasą. Poniższy kod pokazuje, jak to zrobić:

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));
});

Inną alternatywą dla minimalnych aplikacji interfejsu API jest wywołanie metody MapGroup:

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

W poprzednich przykładach przypisania tagów oba punkty końcowe są identyfikowane przez tag-blog tag . Następnie można wykluczyć wpisy pamięci podręcznej dla tych punktów końcowych za pomocą pojedynczej instrukcji, która odwołuje się do tego tagu:

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

Za pomocą tego kodu żądanie HTTP POST wysłane do https://localhost:<port>/purge/tag-blog eksmituje wpisy pamięci podręcznej dla tych punktów końcowych.

Możesz chcieć wykluczyć wszystkie wpisy pamięci podręcznej dla wszystkich punktów końcowych. W tym celu należy utworzyć podstawowe zasady dla wszystkich punktów końcowych, tak jak w poniższym kodzie:

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));
});

Te podstawowe zasady umożliwiają eksmitowanie wszystkich elementów w pamięci podręcznej za pomocą tagu "tag-all".

Wyłączanie blokowania zasobów

Domyślnie blokada zasobów jest włączona w celu ograniczenia ryzyka stemplowania pamięci podręcznej i grzmotu stada. Aby uzyskać więcej informacji, zobacz Buforowanie danych wyjściowych.

Aby wyłączyć blokowanie zasobów, wywołaj metodę SetLocking(false) podczas tworzenia zasad, jak pokazano w poniższym przykładzie:

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));
});

Poniższy przykład wybiera zasady braku blokowania dla punktu końcowego w minimalnej aplikacji interfejsu API:

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

W interfejsie API opartym na kontrolerze użyj atrybutu , aby wybrać zasady:

[ApiController]
[Route("/[controller]")]
[OutputCache(PolicyName = "NoLock")]
public class NoLockController : ControllerBase
{
    public async Task GetAsync()
    {
        await Gravatar.WriteGravatar(HttpContext);
    }
}

Limity

Następujące właściwości OutputCacheOptions umożliwiają skonfigurowanie limitów, które mają zastosowanie do wszystkich punktów końcowych:

  • SizeLimit — Maksymalny rozmiar magazynu pamięci podręcznej. Po osiągnięciu tego limitu żadne nowe odpowiedzi nie są buforowane do momentu eksmitowania starszych wpisów. Wartość domyślna to 100 MB.
  • MaximumBodySize — Jeśli treść odpowiedzi przekroczy ten limit, nie jest buforowana. Wartość domyślna to 64 MB.
  • DefaultExpirationTimeSpan - Czas wygaśnięcia, który ma zastosowanie, gdy nie zostanie określony przez zasady. Wartość domyślna to 60 sekund.

Magazyn pamięci podręcznej

IOutputCacheStore jest używany do magazynowania. Domyślnie jest używany z MemoryCache. Buforowane odpowiedzi są przechowywane w procesie, dlatego każdy serwer ma oddzielną pamięć podręczną, która zostanie utracona przy każdym ponownym uruchomieniu procesu serwera.

Redis Cache

Alternatywą jest użycie pamięci podręcznej Redis Cache. Pamięć podręczna Redis Cache zapewnia spójność między węzłami serwera za pośrednictwem udostępnionej pamięci podręcznej, która przeżywa poszczególne procesy serwera. Aby użyć usługi Redis do buforowania danych wyjściowych:

  • Zainstaluj pakiet NuGet Microsoft.AspNetCore.OutputCaching.StackExchangeRedis.

  • Wywołaj builder.Services.AddStackExchangeRedisOutputCache metodę (nie AddStackExchangeRedisCache), a następnie podaj parametry połączenia wskazującą serwer Redis.

    Na przykład:

    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— parametry połączenia do lokalnego serwera Redis lub do oferty hostowanej, takiej jak Azure Cache for Redis. Na przykład <instance_name>.redis.cache.windows.net:6380,password=<password>,ssl=True,abortConnect=False w przypadku usługi Azure Cache for Redis.
    • options.InstanceName — Opcjonalnie określa partycję logiczną pamięci podręcznej.

    Opcje konfiguracji są identyczne z opcjami buforowania rozproszonego opartego na usłudze Redis.

Nie zalecamy IDistributedCache używania z buforowaniem danych wyjściowych. IDistributedCache nie ma funkcji niepodzielnych, które są wymagane do tagowania. Zalecamy korzystanie z wbudowanej obsługi usługi Redis lub tworzenia niestandardowych IOutputCacheStore implementacji przy użyciu bezpośrednich zależności od podstawowego mechanizmu magazynu.

Zobacz też

W tym artykule wyjaśniono, jak skonfigurować oprogramowanie pośredniczące buforowania danych wyjściowych w aplikacji ASP.NET Core. Aby zapoznać się z wprowadzeniem do buforowania danych wyjściowych, zobacz Buforowanie danych wyjściowych.

Oprogramowanie pośredniczące buforowania danych wyjściowych może być używane we wszystkich typach aplikacji ASP.NET Core: minimalny interfejs API, internetowy interfejs API z kontrolerami, MVC i Razor stronami. Przykładowa aplikacja jest minimalnym interfejsem API, ale każda funkcja buforowania, która ilustruje, jest również obsługiwana w innych typach aplikacji.

Dodawanie oprogramowania pośredniczącego do aplikacji

Dodaj oprogramowanie pośredniczące buforowania danych wyjściowych do kolekcji usług, wywołując metodę AddOutputCache.

Dodaj oprogramowanie pośredniczące do potoku przetwarzania żądań, wywołując metodę UseOutputCache.

Uwaga

  • W aplikacjach korzystających z oprogramowaniaUseOutputCache pośredniczącego CORS należy wywołać metodę po UseCors.
  • W Razor aplikacjach Stron i aplikacjach z kontrolerami UseOutputCache należy wywołać metodę po UseRouting.
  • Wywoływanie AddOutputCachei UseOutputCache nie rozpoczyna buforowania, dzięki czemu buforowanie jest dostępne. Dane odpowiedzi buforowania muszą być skonfigurowane, jak pokazano w poniższych sekcjach.

Konfigurowanie jednego punktu końcowego lub strony

W przypadku minimalnych aplikacji interfejsu API skonfiguruj punkt końcowy do buforowania, wywołując CacheOutputmetodę , lub [OutputCache] stosując atrybut, jak pokazano w poniższych przykładach:

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

W przypadku aplikacji z kontrolerami zastosuj [OutputCache] atrybut do metody akcji. W przypadku Razor aplikacji Pages zastosuj atrybut do Razor klasy strony.

Konfigurowanie wielu punktów końcowych lub stron

Utwórz zasady podczas wywoływania AddOutputCache w celu określenia konfiguracji buforowania, która ma zastosowanie do wielu punktów końcowych. Zasady można wybrać dla określonych punktów końcowych, podczas gdy zasady podstawowe zapewniają domyślną konfigurację buforowania dla kolekcji punktów końcowych.

Poniższy wyróżniony kod konfiguruje buforowanie dla wszystkich punktów końcowych aplikacji z czasem wygaśnięcia 10 sekund. Jeśli nie określono czasu wygaśnięcia, wartość domyślna to jedna minuta.

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)));
});

Poniższy wyróżniony kod tworzy dwie zasady, z których każda określa inny czas wygaśnięcia. Wybrane punkty końcowe mogą używać 20 sekundowego wygaśnięcia, a inne mogą korzystać z 30-sekundowego wygaśnięcia.

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)));
});

Zasady dla punktu końcowego można wybrać podczas wywoływania CacheOutput metody lub przy użyciu atrybutu [OutputCache] :

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

W przypadku aplikacji z kontrolerami zastosuj [OutputCache] atrybut do metody akcji. W przypadku Razor aplikacji Pages zastosuj atrybut do Razor klasy strony.

Domyślne zasady buforowania danych wyjściowych

Domyślnie buforowanie danych wyjściowych jest zgodne z następującymi regułami:

  • Buforowane są tylko odpowiedzi HTTP 200.
  • Buforowane są tylko żądania HTTP GET lub HEAD.
  • Odpowiedzi, które ustawiają pliki cookie, nie są buforowane.
  • Odpowiedzi na uwierzytelnione żądania nie są buforowane.

Poniższy kod stosuje wszystkie domyślne reguły buforowania do wszystkich punktów końcowych aplikacji:

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

Zastępowanie zasad domyślnych

Poniższy kod pokazuje, jak zastąpić reguły domyślne. Wyróżnione wiersze w poniższym niestandardowym kodzie zasad umożliwiają buforowanie metod HTTP POST i odpowiedzi HTTP 301:

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;
    }
}

Aby użyć tych zasad niestandardowych, utwórz nazwane zasady:

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

Wybierz nazwane zasady dla punktu końcowego:

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

Zastępowanie alternatywnych zasad domyślnych

Alternatywnie użyj iniekcji zależności (DI), aby zainicjować wystąpienie, z następującymi zmianami w niestandardowej klasie zasad:

  • Publiczny konstruktor zamiast prywatnego konstruktora.
  • Wyeliminuj Instance właściwość w niestandardowej klasie zasad.

Na przykład:

public sealed class MyCustomPolicy2 : IOutputCachePolicy
{

    public MyCustomPolicy2()
    {
    }

Pozostała część klasy jest taka sama, jak pokazano wcześniej. Dodaj zasady niestandardowe, jak pokazano w poniższym przykładzie:

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

Powyższy kod używa di do utworzenia wystąpienia niestandardowej klasy zasad. Wszystkie publiczne argumenty w konstruktorze są rozpoznawane.

W przypadku używania zasad niestandardowych jako zasad podstawowych nie należy wywoływać OutputCache() (bez argumentów) w żadnym punkcie końcowym, do którego mają być stosowane zasady podstawowe. Wywołanie OutputCache() powoduje dodanie domyślnych zasad do punktu końcowego.

Określanie klucza pamięci podręcznej

Domyślnie każda część adresu URL jest dołączana jako klucz do wpisu pamięci podręcznej, czyli schematu, hosta, portu, ścieżki i ciągu zapytania. Można jednak jawnie kontrolować klucz pamięci podręcznej. Załóżmy na przykład, że masz punkt końcowy, który zwraca unikatową odpowiedź tylko dla każdej unikatowej culture wartości ciągu zapytania. Odmiana w innych częściach adresu URL, takich jak inne ciągi zapytania, nie powinna powodować różnych wpisów pamięci podręcznej. Takie reguły można określić w zasadach, jak pokazano w poniższym wyróżnionym kodzie:

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));
});

Następnie możesz wybrać VaryByQuery zasady dla punktu końcowego:

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

Poniżej przedstawiono niektóre opcje kontrolowania klucza pamięci podręcznej:

  • SetVaryByQuery — Określ co najmniej jedną nazwę ciągu zapytania, która ma zostać dodana do klucza pamięci podręcznej.

  • SetVaryByHeader — Określ co najmniej jeden nagłówek HTTP, który ma zostać dodany do klucza pamięci podręcznej.

  • VaryByValue— Określ wartość, która ma zostać dodana do klucza pamięci podręcznej. W poniższym przykładzie użyto wartości, która wskazuje, czy bieżący czas serwera w sekundach jest dziwny, czy nawet. Nowa odpowiedź jest generowana tylko wtedy, gdy liczba sekund przechodzi od nieparzystego do parzystego, a nawet do dziwnego.

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

Użyj OutputCacheOptions.UseCaseSensitivePaths polecenia , aby określić, że część ścieżki klucza uwzględnia wielkość liter. Wartość domyślna to bez uwzględniania wielkości liter.

Aby uzyskać więcej opcji, zobacz klasę OutputCachePolicyBuilder .

Ponowne dodawanie pamięci podręcznej

Zmiana pamięci podręcznej oznacza, że serwer może zwrócić 304 Not Modified kod stanu HTTP zamiast pełnej treści odpowiedzi. Ten kod stanu informuje klienta, że odpowiedź na żądanie nie zmienia się od tego, co klient otrzymał wcześniej.

Poniższy kod ilustruje użycie nagłówka Etag w celu włączenia ponownego stosowania pamięci podręcznej. Jeśli klient wysyła If-None-Match nagłówek z wartością etag wcześniejszej odpowiedzi, a wpis pamięci podręcznej jest świeży, serwer zwraca wartość 304 Not Modified zamiast pełnej odpowiedzi:

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

}).CacheOutput();

Innym sposobem na ponowne zaktualizowanie pamięci podręcznej jest sprawdzenie daty utworzenia wpisu pamięci podręcznej w porównaniu z datą żądaną przez klienta. Po podaniu nagłówka If-Modified-Since żądania buforowanie danych wyjściowych zwraca wartość 304, jeśli buforowany wpis jest starszy i nie wygasł.

Ponowne aktualizowanie pamięci podręcznej jest automatyczne w odpowiedzi na te nagłówki wysyłane z klienta. Nie jest wymagana żadna specjalna konfiguracja na serwerze, aby włączyć to zachowanie, oprócz włączania buforowania danych wyjściowych.

Eksmitowanie wpisów pamięci podręcznej za pomocą tagów

Za pomocą tagów można zidentyfikować grupę punktów końcowych i wykluczyć wszystkie wpisy pamięci podręcznej dla grupy. Na przykład poniższy kod tworzy parę punktów końcowych, których adresy URL zaczynają się od "bloga" i tagują je jako "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"));

Alternatywnym sposobem przypisywania tagów dla tej samej pary punktów końcowych jest zdefiniowanie podstawowych zasad, które mają zastosowanie do punktów końcowych rozpoczynających się od blog:

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));
});

Inną alternatywą jest wywołanie metody MapGroup:

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

W poprzednich przykładach przypisania tagów oba punkty końcowe są identyfikowane przez tag-blog tag . Następnie można wykluczyć wpisy pamięci podręcznej dla tych punktów końcowych za pomocą pojedynczej instrukcji, która odwołuje się do tego tagu:

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

Za pomocą tego kodu żądanie HTTP POST wysłane do programu https://localhost:<port>/purge/tag-blog spowoduje wykluczenie wpisów pamięci podręcznej dla tych punktów końcowych.

Możesz chcieć wykluczyć wszystkie wpisy pamięci podręcznej dla wszystkich punktów końcowych. W tym celu należy utworzyć podstawowe zasady dla wszystkich punktów końcowych, tak jak w poniższym kodzie:

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));
});

Te podstawowe zasady umożliwiają eksmitowanie wszystkich elementów w pamięci podręcznej za pomocą tagu "tag-all".

Wyłączanie blokowania zasobów

Domyślnie blokada zasobów jest włączona w celu ograniczenia ryzyka stemplowania pamięci podręcznej i grzmotu stada. Aby uzyskać więcej informacji, zobacz Buforowanie danych wyjściowych.

Aby wyłączyć blokowanie zasobów, wywołaj metodę SetLocking(false) podczas tworzenia zasad, jak pokazano w poniższym przykładzie:

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));
});

Poniższy przykład wybiera zasady braku blokowania dla punktu końcowego:

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

Limity

Następujące właściwości OutputCacheOptions umożliwiają skonfigurowanie limitów, które mają zastosowanie do wszystkich punktów końcowych:

  • SizeLimit — Maksymalny rozmiar magazynu pamięci podręcznej. Po osiągnięciu tego limitu żadne nowe odpowiedzi nie będą buforowane do czasu eksmitowania starszych wpisów. Wartość domyślna to 100 MB.
  • MaximumBodySize — Jeśli treść odpowiedzi przekroczy ten limit, nie zostanie ona buforowana. Wartość domyślna to 64 MB.
  • DefaultExpirationTimeSpan - Czas wygaśnięcia, który ma zastosowanie, gdy nie zostanie określony przez zasady. Wartość domyślna to 60 sekund.

Magazyn pamięci podręcznej

IOutputCacheStore jest używany do magazynowania. Domyślnie jest używany z MemoryCache. Nie zalecamy IDistributedCache używania z buforowaniem danych wyjściowych. IDistributedCache nie ma funkcji niepodzielnych, które są wymagane do tagowania. Zalecamy tworzenie niestandardowych IOutputCacheStore implementacji przy użyciu bezpośrednich zależności od podstawowego mechanizmu magazynu, takiego jak Redis. Możesz też użyć wbudowanej obsługi pamięci podręcznej Redis Cache na platformie .NET 8..

Zobacz też