Compartilhar via


Processo de armazenamento em cache no .NET

Neste artigo, você aprenderá sobre vários mecanismos de cache. O cache é o ato de armazenar dados em uma camada intermediária, tornando as recuperações de dados subsequentes mais rápidas. Conceitualmente, o cache é uma estratégia de otimização de desempenho e consideração de design. O cache pode melhorar significativamente o desempenho do aplicativo, tornando os dados pouco alterados (ou caros para recuperar) mais facilmente disponíveis. Este artigo apresenta três abordagens de cache e fornece um código-fonte de exemplo para cada uma:

Importante

Há duas MemoryCache classes no .NET, uma no System.Runtime.Caching namespace e outra no Microsoft.Extensions.Caching namespace:

Embora este artigo se concentre no cache, ele não inclui o System.Runtime.Caching pacote NuGet. Todas as referências a MemoryCache estão dentro do Microsoft.Extensions.Caching namespace.

Todos os pacotes Microsoft.Extensions.* vêm prontos para injeção de dependência (DI). As interfaces IMemoryCache, HybridCache e IDistributedCache podem ser usadas como serviços.

Cache na memória

Nesta seção, você aprenderá sobre o pacote Microsoft.Extensions.Caching.Memory . A implementação atual do IMemoryCache é um envoltório em torno do ConcurrentDictionary<TKey,TValue>, expondo uma API rica em recursos. As entradas dentro do cache são representadas pelo ICacheEntry e podem ser qualquer object. A solução de cache na memória é ótima para aplicativos executados em um único servidor, em que os dados armazenados em cache alugam memória no processo do aplicativo.

Dica

Para cenários de cache de vários servidores, considere a abordagem de cache distribuído como uma alternativa ao cache na memória.

API de cache na memória

O consumidor do cache tem controle sobre expirações deslizantes e absolutas:

A definição de uma expiração faz com que as entradas no cache sejam removidas se não forem acessadas dentro do loteamento de tempo de expiração. Os consumidores têm opções adicionais para controlar entradas de cache por meio de MemoryCacheEntryOptions. Cada ICacheEntry é emparelhado com MemoryCacheEntryOptions, o que expõe a funcionalidade de remoção de expiração com IChangeToken, configurações de prioridade com CacheItemPriority, e o controle do ICacheEntry.Size. Os métodos de extensão relevantes são:

Exemplo de cache na memória

Para usar a implementação IMemoryCache padrão, chame o método de extensão AddMemoryCache para registrar todos os serviços necessários com DI. No exemplo de código a seguir, o host genérico é usado para expor a funcionalidade de DI:

using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddMemoryCache();
using IHost host = builder.Build();

Dependendo da sua carga de trabalho do .NET, você pode acessar o IMemoryCache de forma diferente, como injeção de construtor. Neste exemplo, você usa a instância IServiceProvider no host e chama o método de extensão genérico GetRequiredService<T>(IServiceProvider).

IMemoryCache cache =
    host.Services.GetRequiredService<IMemoryCache>();

Com os serviços de cache na memória registrados e resolvidos por meio de DI, você está pronto para iniciar o cache. Este exemplo itera através das letras no alfabeto inglês 'A' até 'Z'. O record AlphabetLetter tipo contém a referência à letra e gera uma mensagem.

file record AlphabetLetter(char Letter)
{
    internal string Message =>
        $"The '{Letter}' character is the {Letter - 64} letter in the English alphabet.";
}

Dica

O file modificador de acesso é usado no AlphabetLetter tipo, pois é definido e acessado apenas de dentro do arquivo Program.cs. Para obter mais informações, consulte o arquivo (Referência de C#). Para ver o código-fonte completo, consulte a seção Program.cs .

O exemplo inclui uma função auxiliar que percorre as letras do alfabeto.

static async ValueTask IterateAlphabetAsync(
    Func<char, Task> asyncFunc)
{
    for (char letter = 'A'; letter <= 'Z'; ++letter)
    {
        await asyncFunc(letter);
    }

    Console.WriteLine();
}

No código anterior do C#:

  • Func<char, Task> asyncFunc é aguardado em cada iteração, passando o atual letter.
  • Depois que todas as letras tiverem sido processadas, uma linha em branco será gravada no console.

Para adicionar itens ao cache, use uma das APIs Create ou Set.

var addLettersToCacheTask = IterateAlphabetAsync(letter =>
{
    MemoryCacheEntryOptions options = new()
    {
        AbsoluteExpirationRelativeToNow =
            TimeSpan.FromMilliseconds(MillisecondsAbsoluteExpiration)
    };

    _ = options.RegisterPostEvictionCallback(OnPostEviction);

    AlphabetLetter alphabetLetter =
        cache.Set(
            letter, new AlphabetLetter(letter), options);

    Console.WriteLine($"{alphabetLetter.Letter} was cached.");

    return Task.Delay(
        TimeSpan.FromMilliseconds(MillisecondsDelayAfterAdd));
});
await addLettersToCacheTask;

No código anterior do C#:

  • A variável addLettersToCacheTask delega para IterateAlphabetAsync e é aguardada.
  • Func<char, Task> asyncFunc é argumentado com um lambda.
  • MemoryCacheEntryOptions é instanciado com uma expiração absoluta em relação ao agora.
  • Um retorno de chamada pós-remoção é registrado.
  • Um objeto AlphabetLetter é instanciado e passado para Set junto com letter e options.
  • A carta é registrada no console como armazenada em cache.
  • Por fim, um Task.Delay retorna.

Para cada letra no alfabeto, uma entrada de cache é escrita com um retorno de chamada de expiração e pós-remoção.

O retorno de chamada pós-remoção grava os detalhes do valor que foi removido para o console:

static void OnPostEviction(
    object key, object? letter, EvictionReason reason, object? state)
{
    if (letter is AlphabetLetter alphabetLetter)
    {
        Console.WriteLine($"{alphabetLetter.Letter} was evicted for {reason}.");
    }
};

Agora que o cache está preenchido, uma nova chamada para IterateAlphabetAsync é aguardada, mas desta vez você chama IMemoryCache.TryGetValue:

var readLettersFromCacheTask = IterateAlphabetAsync(letter =>
{
    if (cache.TryGetValue(letter, out object? value) &&
        value is AlphabetLetter alphabetLetter)
    {
        Console.WriteLine($"{letter} is still in cache. {alphabetLetter.Message}");
    }

    return Task.CompletedTask;
});
await readLettersFromCacheTask;

Se cache contiver a chave letter e for uma instância value de uma AlphabetLetter, será gravada no console. Quando a letter chave não está no cache, ela foi removida e seu retorno de chamada pós-remoção foi invocado.

Métodos de extensão adicionais

IMemoryCache vem com muitos métodos de extensão baseados em conveniência, incluindo um GetOrCreateAsync assíncrono:

Coloque tudo junto

Todo o código-fonte do aplicativo de exemplo é um programa de nível superior e requer dois pacotes NuGet:

using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddMemoryCache();
using IHost host = builder.Build();

IMemoryCache cache =
    host.Services.GetRequiredService<IMemoryCache>();

const int MillisecondsDelayAfterAdd = 50;
const int MillisecondsAbsoluteExpiration = 750;

static void OnPostEviction(
    object key, object? letter, EvictionReason reason, object? state)
{
    if (letter is AlphabetLetter alphabetLetter)
    {
        Console.WriteLine($"{alphabetLetter.Letter} was evicted for {reason}.");
    }
};

static async ValueTask IterateAlphabetAsync(
    Func<char, Task> asyncFunc)
{
    for (char letter = 'A'; letter <= 'Z'; ++letter)
    {
        await asyncFunc(letter);
    }

    Console.WriteLine();
}

var addLettersToCacheTask = IterateAlphabetAsync(letter =>
{
    MemoryCacheEntryOptions options = new()
    {
        AbsoluteExpirationRelativeToNow =
            TimeSpan.FromMilliseconds(MillisecondsAbsoluteExpiration)
    };

    _ = options.RegisterPostEvictionCallback(OnPostEviction);

    AlphabetLetter alphabetLetter =
        cache.Set(
            letter, new AlphabetLetter(letter), options);

    Console.WriteLine($"{alphabetLetter.Letter} was cached.");

    return Task.Delay(
        TimeSpan.FromMilliseconds(MillisecondsDelayAfterAdd));
});
await addLettersToCacheTask;

var readLettersFromCacheTask = IterateAlphabetAsync(letter =>
{
    if (cache.TryGetValue(letter, out object? value) &&
        value is AlphabetLetter alphabetLetter)
    {
        Console.WriteLine($"{letter} is still in cache. {alphabetLetter.Message}");
    }

    return Task.CompletedTask;
});
await readLettersFromCacheTask;

await host.RunAsync();

file record AlphabetLetter(char Letter)
{
    internal string Message =>
        $"The '{Letter}' character is the {Letter - 64} letter in the English alphabet.";
}

Você pode ajustar os valores de MillisecondsDelayAfterAdd e MillisecondsAbsoluteExpiration para observar as mudanças no comportamento em relação à expiração e remoção de entradas armazenadas em cache. Veja a seguir a saída de exemplo da execução desse código. (Devido à natureza não determinística dos eventos do .NET, sua saída pode ser diferente.)

A was cached.
B was cached.
C was cached.
D was cached.
E was cached.
F was cached.
G was cached.
H was cached.
I was cached.
J was cached.
K was cached.
L was cached.
M was cached.
N was cached.
O was cached.
P was cached.
Q was cached.
R was cached.
S was cached.
T was cached.
U was cached.
V was cached.
W was cached.
X was cached.
Y was cached.
Z was cached.

A was evicted for Expired.
C was evicted for Expired.
B was evicted for Expired.
E was evicted for Expired.
D was evicted for Expired.
F was evicted for Expired.
H was evicted for Expired.
K was evicted for Expired.
L was evicted for Expired.
J was evicted for Expired.
G was evicted for Expired.
M was evicted for Expired.
N was evicted for Expired.
I was evicted for Expired.
P was evicted for Expired.
R was evicted for Expired.
O was evicted for Expired.
Q was evicted for Expired.
S is still in cache. The 'S' character is the 19 letter in the English alphabet.
T is still in cache. The 'T' character is the 20 letter in the English alphabet.
U is still in cache. The 'U' character is the 21 letter in the English alphabet.
V is still in cache. The 'V' character is the 22 letter in the English alphabet.
W is still in cache. The 'W' character is the 23 letter in the English alphabet.
X is still in cache. The 'X' character is the 24 letter in the English alphabet.
Y is still in cache. The 'Y' character is the 25 letter in the English alphabet.
Z is still in cache. The 'Z' character is the 26 letter in the English alphabet.

Como a expiração absoluta (MemoryCacheEntryOptions.AbsoluteExpirationRelativeToNow) está definida, todos os itens armazenados em cache serão eventualmente removidos.

Cache do Serviço de Trabalho

Uma estratégia comum para armazenar dados em cache é atualizar o cache independentemente dos serviços de dados de consumo. O template Worker Service é um ótimo exemplo, pois BackgroundService executa de forma independente (ou em segundo plano) do outro código do aplicativo. Quando um aplicativo começa a ser executado, que hospeda uma implementação da IHostedService, a implementação correspondente (nesse caso, o BackgroundService ou "trabalhador") começa a ser executada no mesmo processo. Esses serviços hospedados são registrados com DI como singletons, por meio do método de extensão AddHostedService<THostedService>(IServiceCollection). Outros serviços podem ser registrados com DI com qualquer tempo de vida do serviço.

Importante

Os ciclos de vida do serviço são importantes de se entender. Quando você chama AddMemoryCache para registrar todos os serviços de cache na memória, os serviços são registrados como singletons.

Cenário de serviço de fotografia

Imagine que você está desenvolvendo um serviço de fotos que depende da API de terceiros acessível via HTTP. Esses dados de foto não mudam com frequência, mas há muitos desses dados. Cada foto é representada por um simples record:

namespace CachingExamples.Memory;

public readonly record struct Photo(
    int AlbumId,
    int Id,
    string Title,
    string Url,
    string ThumbnailUrl);

No exemplo a seguir, você verá vários serviços sendo registrados com DI. Cada serviço tem uma única responsabilidade.

using CachingExamples.Memory;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddMemoryCache();
builder.Services.AddHttpClient<CacheWorker>();
builder.Services.AddHostedService<CacheWorker>();
builder.Services.AddScoped<PhotoService>();
builder.Services.AddSingleton(typeof(CacheSignal<>));

using IHost host = builder.Build();

await host.StartAsync();

No código anterior do C#:

O PhotoService responsável por obter fotos que correspondam aos critérios especificados (ou filter):

using Microsoft.Extensions.Caching.Memory;

namespace CachingExamples.Memory;

public sealed class PhotoService(
        IMemoryCache cache,
        CacheSignal<Photo> cacheSignal,
        ILogger<PhotoService> logger)
{
    public async IAsyncEnumerable<Photo> GetPhotosAsync(Func<Photo, bool>? filter = default)
    {
        try
        {
            await cacheSignal.WaitAsync();

            Photo[] photos =
                (await cache.GetOrCreateAsync(
                    "Photos", _ =>
                    {
                        logger.LogWarning("This should never happen!");

                        return Task.FromResult(Array.Empty<Photo>());
                    }))!;

            // If no filter is provided, use a pass-thru.
            filter ??= _ => true;

            foreach (Photo photo in photos)
            {
                if (!default(Photo).Equals(photo) && filter(photo))
                {
                    yield return photo;
                }
            }
        }
        finally
        {
            cacheSignal.Release();
        }
    }
}

No código anterior do C#:

  • O construtor requer um IMemoryCache, CacheSignal<Photo>e ILogger.
  • O método GetPhotosAsync:
    • Define um Func<Photo, bool> filter parâmetro e retorna um IAsyncEnumerable<Photo>.
    • Chama e aguarda pela liberação de _cacheSignal.WaitAsync() para garantir que o cache seja preenchido antes de acessá-lo.
    • Chama _cache.GetOrCreateAsync(), obtendo de forma assíncrona todas as fotos no cache.
    • O factory argumento registra um aviso e retorna uma matriz de fotos vazia. Isso nunca deve acontecer.
    • Cada foto no cache é iterada, filtrada e materializada com yield return.
    • Por fim, o sinal de cache é redefinido.

Os consumidores desse serviço são livres para chamar o método GetPhotosAsync e lidar com fotos adequadamente. O HttpClient não é necessário, pois o cache contém as fotos.

O sinal assíncrono baseia-se em uma instância de SemaphoreSlim encapsulada, dentro de um singleton restrito de tipo genérico. O CacheSignal<T> depende de uma instância de SemaphoreSlim.

namespace CachingExamples.Memory;

public sealed class CacheSignal<T>
{
    private readonly SemaphoreSlim _semaphore = new(1, 1);

    /// <summary>
    /// Exposes a <see cref="Task"/> that represents the asynchronous wait operation.
    /// When signaled (consumer calls <see cref="Release"/>), the 
    /// <see cref="Task.Status"/> is set as <see cref="TaskStatus.RanToCompletion"/>.
    /// </summary>
    public Task WaitAsync() => _semaphore.WaitAsync();

    /// <summary>
    /// Exposes the ability to signal the release of the <see cref="WaitAsync"/>'s operation.
    /// Callers who were waiting, will be able to continue.
    /// </summary>
    public void Release() => _semaphore.Release();
}

No código C# anterior, o padrão decorador é usado para encapsular uma instância do SemaphoreSlim. Como CacheSignal<T> é registrado como singleton, ele pode ser usado em todos os tempos de vida do serviço com qualquer tipo genérico, nesse caso, Photo. Ele é responsável por sinalizar a inicialização do cache.

É CacheWorker uma subclasse de BackgroundService:

using System.Net.Http.Json;
using Microsoft.Extensions.Caching.Memory;

namespace CachingExamples.Memory;

public sealed class CacheWorker(
    ILogger<CacheWorker> logger,
    HttpClient httpClient,
    CacheSignal<Photo> cacheSignal,
    IMemoryCache cache) : BackgroundService
{
    private readonly TimeSpan _updateInterval = TimeSpan.FromHours(3);

    private bool _isCacheInitialized = false;

    private const string Url = "https://jsonplaceholder.typicode.com/photos";

    public override async Task StartAsync(CancellationToken cancellationToken)
    {
        await cacheSignal.WaitAsync();
        await base.StartAsync(cancellationToken);
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            logger.LogInformation("Updating cache.");

            try
            {
                Photo[]? photos =
                    await httpClient.GetFromJsonAsync<Photo[]>(
                        Url, stoppingToken);

                if (photos is { Length: > 0 })
                {
                    cache.Set("Photos", photos);
                    logger.LogInformation(
                        "Cache updated with {Count:#,#} photos.", photos.Length);
                }
                else
                {
                    logger.LogWarning(
                        "Unable to fetch photos to update cache.");
                }
            }
            finally
            {
                if (!_isCacheInitialized)
                {
                    cacheSignal.Release();
                    _isCacheInitialized = true;
                }
            }

            try
            {
                logger.LogInformation(
                    "Will attempt to update the cache in {Hours} hours from now.",
                    _updateInterval.Hours);

                await Task.Delay(_updateInterval, stoppingToken);
            }
            catch (OperationCanceledException)
            {
                logger.LogWarning("Cancellation acknowledged: shutting down.");
                break;
            }
        }
    }
}

No código anterior do C#:

  • O construtor requer um ILogger, HttpCliente IMemoryCache.
  • O _updateInterval é definido por três horas.
  • O método ExecuteAsync:
    • Faz um loop enquanto o aplicativo está em execução.
    • Faz uma solicitação HTTP para "https://jsonplaceholder.typicode.com/photos", e mapeia a resposta como uma matriz de Photo objetos.
    • A matriz de fotos é colocada no IMemoryCache sob a chave "Photos".
    • O _cacheSignal.Release() é chamado, liberando todos os consumidores que estavam esperando pelo sinal.
    • A chamada a Task.Delay é aguardada, dado o intervalo de atualização.
    • Depois de atrasar por três horas, o cache será atualizado novamente.

Os consumidores no mesmo processo podem solicitar as fotos a IMemoryCache, mas CacheWorker é responsável por atualizar o cache.

Cache híbrido

A HybridCache biblioteca combina os benefícios do cache distribuído e na memória, ao mesmo tempo em que aborda os desafios comuns com as APIs de cache existentes. Introduzida no .NET 9, HybridCache fornece uma API unificada que simplifica a implementação de cache e inclui recursos internos, como proteção contra debandada e serialização configurável.

Características principais

HybridCache oferece várias vantagens em relação ao uso IMemoryCache e IDistributedCache separadamente:

  • Cache de dois níveis: gerencia automaticamente as camadas de cache L1 (na memória) e distribuída (L2). Os dados são recuperados primeiro da memória para maior rapidez, depois do cache distribuído, se necessário, e, finalmente, da origem.
  • Proteção contra Stampede: Impede que múltiplas solicitações simultâneas executem a mesma operação de alto custo. Apenas uma solicitação busca os dados enquanto outras pessoas esperam pelo resultado.
  • Serialização configurável: dá suporte a vários formatos de serialização, incluindo JSON (padrão), protobuf e XML.
  • Invalidação baseada em tag: agrupa entradas de cache relacionadas com tags para eficiente invalidação em lote.
  • API simplificada: o GetOrCreateAsync método manipula erros de cache, serialização e armazenamento automaticamente.

Quando usar HybridCache

Considere usar HybridCache quando:

  • Você precisa de cache local (na memória) e distribuído em um ambiente de vários servidores.
  • Você deseja proteção contra cenários de debandada de cache.
  • Você prefere uma API simplificada a coordenar manualmente IMemoryCache e IDistributedCache.
  • Você precisa de invalidação de cache baseada em tags para registros relacionados.

Dica

Para aplicativos de servidor único com necessidades simples de cache, o cache na memória pode ser suficiente. Para aplicativos de vários servidores sem a necessidade de proteção contra debandada ou invalidação baseada em tags, considere o cache distribuído.

Configuração do HybridCache

Para usar HybridCache, instale o Microsoft.Extensions.Caching.Hybrid pacote NuGet:

dotnet add package Microsoft.Extensions.Caching.Hybrid

Registre o HybridCache serviço com DI chamando AddHybridCache:

var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHybridCache();

O código anterior registra HybridCache com opções padrão. Você também pode configurar opções globais:

var builderWithOptions = Host.CreateApplicationBuilder(args);
builderWithOptions.Services.AddHybridCache(options =>
{
    options.MaximumPayloadBytes = 1024 * 1024; // 1 MB
    options.MaximumKeyLength = 1024;
    options.DefaultEntryOptions = new HybridCacheEntryOptions
    {
        Expiration = TimeSpan.FromMinutes(5),
        LocalCacheExpiration = TimeSpan.FromMinutes(2)
    };
});

Uso Básico

O método principal para interagir com HybridCache é GetOrCreateAsync. Esse método verifica o cache de uma entrada com a chave especificada e, se não for encontrado, chama o método de fábrica para recuperar os dados:

async Task<WeatherData> GetWeatherDataAsync(HybridCache cache, string city)
{
    return await cache.GetOrCreateAsync(
        $"weather:{city}",
        async cancellationToken =>
        {
            // Simulate fetching from an external API
            await Task.Delay(100, cancellationToken);
            return new WeatherData(city, 72, "Sunny");
        }
    );
}

No código anterior do C#:

  • O GetOrCreateAsync método usa uma chave exclusiva e um método de fábrica.
  • Se os dados não estiverem no cache, o método de fábrica será chamado para recuperá-los.
  • Os dados são armazenados automaticamente em caches distribuídos e na memória.
  • Apenas uma solicitação simultânea executa o método de fábrica; outros esperam pelo resultado.

Opções de entrada

Você pode substituir os padrões globais para entradas de cache específicas usando HybridCacheEntryOptions:

async Task<WeatherData> GetWeatherWithOptionsAsync(HybridCache cache, string city)
{
    var entryOptions = new HybridCacheEntryOptions
    {
        Expiration = TimeSpan.FromMinutes(10),
        LocalCacheExpiration = TimeSpan.FromMinutes(5)
    };

    return await cache.GetOrCreateAsync(
        $"weather:{city}",
        async cancellationToken => new WeatherData(city, 72, "Sunny"),
        entryOptions
    );
}

As opções de entrada permitem que você configure:

Invalidação baseada em tags

Tags permitem agrupar entradas de cache relacionadas e invalidá-las juntas. Isso é útil para cenários em que os dados relacionados precisam ser atualizados como uma unidade:

async Task<CustomerData> GetCustomerAsync(HybridCache cache, int customerId)
{
    var tags = new[] { "customer", $"customer:{customerId}" };

    return await cache.GetOrCreateAsync(
        $"customer:{customerId}",
        async cancellationToken => new CustomerData(customerId, "John Doe", "john@example.com"),
        new HybridCacheEntryOptions { Expiration = TimeSpan.FromMinutes(30) },
        tags
    );
}

Para invalidar todas as entradas com uma marca específica:

async Task InvalidateCustomerCacheAsync(HybridCache cache, int customerId)
{
    await cache.RemoveByTagAsync($"customer:{customerId}");
}

Você também pode invalidar várias marcas ao mesmo tempo:

async Task InvalidateAllCustomersAsync(HybridCache cache)
{
    await cache.RemoveByTagAsync(new[] { "customer", "orders" });
}

Observação

A invalidação baseada em etiqueta é uma operação lógica. Ele não remove ativamente os valores do cache, mas garante que as entradas marcadas sejam tratadas como erros de cache. As entradas eventualmente expiram com base no tempo de vida configurado.

Remover entradas de cache

Para remover uma entrada de cache específica por chave, use o RemoveAsync método:

async Task RemoveWeatherDataAsync(HybridCache cache, string city)
{
    await cache.RemoveAsync($"weather:{city}");
}

Para invalidar todas as entradas armazenadas em cache, use a tag curinga reservada "*":

async Task InvalidateAllCacheAsync(HybridCache cache)
{
    await cache.RemoveByTagAsync("*");
}

Serialização

Para cenários de cache distribuído, HybridCache requer serialização. Por padrão, ele manipula string e byte[] internamente e usa System.Text.Json para outros tipos. Você pode configurar serializadores personalizados para tipos específicos ou usar um serializador de uso geral:

// Custom serialization example
// Note: This requires implementing a custom IHybridCacheSerializer<T>
var builderWithSerializer = Host.CreateApplicationBuilder(args);
builderWithSerializer.Services.AddHybridCache(options =>
{
    options.DefaultEntryOptions = new HybridCacheEntryOptions
    {
        Expiration = TimeSpan.FromMinutes(10),
        LocalCacheExpiration = TimeSpan.FromMinutes(5)
    };
});
// To add a custom serializer, uncomment and provide your implementation:
// .AddSerializer<WeatherData, CustomWeatherDataSerializer>();

Configurar o cache distribuído

HybridCache usa a implementação configurada IDistributedCache para seu cache distribuído (L2). Mesmo sem um IDistributedCache configurado, HybridCache ainda fornece proteção contra cache e debandada na memória. Para adicionar o Redis como um cache distribuído:

// Distributed cache with Redis
var builderWithRedis = Host.CreateApplicationBuilder(args);
builderWithRedis.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "localhost:6379";
});
builderWithRedis.Services.AddHybridCache(options =>
{
    options.DefaultEntryOptions = new HybridCacheEntryOptions
    {
        Expiration = TimeSpan.FromMinutes(30),
        LocalCacheExpiration = TimeSpan.FromMinutes(5)
    };
});

Para obter mais informações sobre implementações de cache distribuído, consulte Cache distribuído.

Cache distribuído

Em alguns cenários, um cache distribuído é necessário, como é o caso de vários servidores de aplicativos. Um cache distribuído oferece maior escalabilidade do que a abordagem de cache na memória. O uso de um cache distribuído descarrega a memória de cache para um processo externo, mas requer E/S de rede extra e introduz um pouco mais de latência (mesmo que nominal).

As abstrações de cache distribuídas fazem parte do Microsoft.Extensions.Caching.Memory pacote NuGet e há até mesmo um AddDistributedMemoryCache método de extensão.

Cuidado

AddDistributedMemoryCache deve ser usado apenas em cenários de desenvolvimento ou teste e não é uma implementação de produção viável.

Considere qualquer uma das implementações disponíveis dos IDistributedCache seguintes pacotes:

API de cache distribuído

As APIs de cache distribuídas são um pouco mais primitivas do que seus equivalentes de API de cache na memória. Os pares chave-valor são um pouco mais básicos. As chaves de cache na memória são baseadas em um object, enquanto as chaves distribuídas são um string. Com o cache na memória, o valor pode ser qualquer tipo genérico fortemente tipado, enquanto no cache distribuído, os valores são armazenados como byte[]. Isso não significa que várias implementações não exponham valores genéricos fortemente tipados, mas isso é apenas um detalhe de implementação.

Criar valores

Para criar valores no cache distribuído, chame uma das APIs definidas:

Usando o registro do exemplo de cache em memória AlphabetLetter, você pode serializar o objeto em JSON e então codificar o string como um byte[].

DistributedCacheEntryOptions options = new()
{
    AbsoluteExpirationRelativeToNow =
        TimeSpan.FromMilliseconds(MillisecondsAbsoluteExpiration)
};

AlphabetLetter alphabetLetter = new(letter);
string json = JsonSerializer.Serialize(alphabetLetter);
byte[] bytes = Encoding.UTF8.GetBytes(json);

await cache.SetAsync(letter.ToString(), bytes, options);

Assim como o cache na memória, as entradas de cache podem ter opções para ajudar a ajustar sua existência no cache, nesse caso, o DistributedCacheEntryOptions.

Criar métodos de extensão

Há vários métodos de extensão baseados em conveniência para criar valores. Esses métodos ajudam a evitar a codificação string de representações de objetos em um byte[]:

Ler valores

Para ler valores do cache distribuído, chame uma das Get APIs:

AlphabetLetter? alphabetLetter = null;
byte[]? bytes = await cache.GetAsync(letter.ToString());
if (bytes is { Length: > 0 })
{
    string json = Encoding.UTF8.GetString(bytes);
    alphabetLetter = JsonSerializer.Deserialize<AlphabetLetter>(json);
}

Depois que uma entrada de cache for lida fora do cache, você poderá obter a representação codificada string em UTF8 do byte[].

Localizar métodos de extensão

Há vários métodos de extensão baseados em conveniência para ler valores. Esses métodos ajudam a evitar a decodificação de byte[] em representações de objetos string.

Atualizar valores

Não há como atualizar os valores no cache distribuído com uma única chamada à API. Em vez disso, os valores podem ter suas expirações deslizantes redefinidas com uma das APIs de atualização:

Se o valor real precisar ser atualizado, você deverá excluir o valor e adicioná-lo novamente.

Excluir valores

Para excluir valores no cache distribuído, chame uma das Remove APIs:

Dica

Embora existam versões síncronas dessas APIs, considere o fato de que as implementações de caches distribuídos dependem da E/S da rede. Por esse motivo, geralmente é preferível usar as APIs assíncronas.

Consulte também