Partilhar via


Armazenamento em cache no .NET

Neste artigo, aprende 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 uma consideração de design. O armazenamento em cache pode melhorar significativamente o desempenho do aplicativo, tornando os dados que mudam com pouca frequência (ou são caros para recuperar) mais prontamente disponíveis. Este artigo apresenta três abordagens de cache e fornece 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 Microsoft.Extensions.* pacotes vêm prontos para injeção de dependências (DI). As interfaces IMemoryCache, HybridCache e IDistributedCache podem ser utilizadas como serviços.

Cache na memória

Nesta secção, aprende sobre o pacote Microsoft.Extensions.Caching.Memory . A implementação atual do IMemoryCache é um wrapper em torno do ConcurrentDictionary<TKey,TValue>, expondo uma API rica em recursos. As entradas dentro da cache são representadas por ICacheEntry e podem ser quaisquer object. A solução de cache em memória é ótima para aplicações que são executadas num único servidor, onde os dados em cache ocupam memória no processo da aplicação.

Sugestão

Para cenários de cache multisservidor, 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 as expirações deslizantes e absolutas:

Definir uma expiração faz com que as entradas na cache sejam despejadas se não forem acedidas dentro do tempo de expiração. Os consumidores têm opções adicionais para controlar entradas de cache, através de MemoryCacheEntryOptions. Cada ICacheEntry é emparelhado com MemoryCacheEntryOptions, que expõe a funcionalidade de expiração e despejo com IChangeToken, as definições de prioridade com CacheItemPriority e o controlo do ICacheEntry.Size. Os métodos de extensão relevantes são:

Exemplo de cache na memória

Para usar a implementação padrão IMemoryCache , chame o AddMemoryCache método de extensão 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 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 carga de trabalho do .NET, pode-se aceder ao IMemoryCache de forma diferente, como a injeção de construtor. Neste exemplo, utiliza 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 começar a armazenar em cache. Esta amostra passa pelas letras do alfabeto inglês de 'A' a '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.";
}

Sugestão

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

O exemplo inclui uma função auxiliar que itera através das 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 C# anterior:

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

Para adicionar itens ao cache, chame 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 C# anterior:

  • A variável addLettersToCacheTask delega responsabilidades a IterateAlphabetAsync e está em espera.
  • O Func<char, Task> asyncFunc é discutido com uma lambda.
  • O MemoryCacheEntryOptions é instanciado com uma expiração absoluta em relação a agora.
  • Um callback pós-despejo é registado.
  • Um AlphabetLetter objeto é instanciado e passado para Set junto com letter e options.
  • A carta é gravada no console como sendo armazenada em cache.
  • Finalmente, um Task.Delay é devolvido.

Para cada letra do alfabeto, é escrita uma entrada de cache com callback para expiração e pós-despejo.

O callback pós-despejo escreve para o console os detalhes do valor que foi despejado.

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 a cache está preenchida, espera-se outra chamada para IterateAlphabetAsync, mas desta vez, deve chamar 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 a chave cache estiver presente no letter e o value for uma instância de um AlphabetLetter, será registado no console. Quando a letter chave não está no cache, ela foi excluída e seu retorno de chamada pós-exclusão foi invocado.

Métodos de extensão adicionais

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

Ponha 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.";
}

Pode ajustar os valores de MillisecondsDelayAfterAdd e MillisecondsAbsoluteExpiration para observar as alterações no comportamento da expiração e expulsão de entradas em cache. A seguir está a saída de exemplo da execução deste código. (Devido à natureza não determinística dos eventos .NET, a 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.

Uma vez que a expiração absoluta (MemoryCacheEntryOptions.AbsoluteExpirationRelativeToNow) é 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 a cache independentemente dos serviços de dados que o consomem. O modelo Worker Service é um ótimo exemplo, pois corre BackgroundService de forma independente (ou em segundo plano) do outro código da aplicação. Quando um aplicativo começa a ser executado que hospeda uma implementação do IHostedService, a implementação correspondente (neste caso, o BackgroundService ou "trabalhador") começa a ser executado no mesmo processo. Esses serviços hospedados são registados com DI como singletons através do método de extensão AddHostedService<THostedService>(IServiceCollection). Outros serviços podem ser registrados na DI com qualquer vida útil.

Importante

É importante compreender a duração da vida útil. Quando você invoca AddMemoryCache para registrar todos os serviços de cache de memória, os serviços são registrados como singletons.

Cenário do serviço de fotografia

Imagine que você está desenvolvendo um serviço de fotos que depende de API de terceiros acessível via HTTP. Estes dados fotográficos não mudam frequentemente, mas há uma grande quantidade deles. 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 seguinte, vê-se vários serviços a serem registados na 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 C# anterior:

O PhotoService é responsável por obter fotos que correspondam a determinados critérios (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 C# anterior:

  • O construtor requer um IMemoryCache, CacheSignal<Photo>e ILogger.
  • O GetPhotosAsync método:
    • Define um parâmetro Func<Photo, bool> filter e retorna um IAsyncEnumerable<Photo>.
    • Chama e espera que o _cacheSignal.WaitAsync() seja liberado; isto garante que a cache está preenchida antes de ser acedida.
    • Invoca _cache.GetOrCreateAsync(), para obter de forma assíncrona todas as fotos armazenadas no cache.
    • O factory argumento regista um aviso e devolve um array de fotos vazio – isto nunca deveria acontecer.
    • Cada foto no cache é iterada, filtrada e materializada com yield return.
    • Finalmente, o sinal de cache é redefinido.

Os consumidores deste serviço são livres para invocar o método GetPhotosAsync e lidar com as fotos conforme necessário. HttpClient não é necessário, já que o cache contém as fotos.

O sinal assíncrono é baseado em uma instância encapsulada SemaphoreSlim, dentro de um singleton de tipo genérico restrito. O CacheSignal<T> baseia-se em 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 envolver uma ocorrência do SemaphoreSlim. Como o CacheSignal<T> é registado como singleton, pode ser utilizado em todos os ciclos de vida do serviço com qualquer tipo genérico—neste caso, o Photo. É responsável por sinalizar o semeador da cache.

O 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 C# anterior:

  • O construtor requer um ILogger, HttpCliente IMemoryCache.
  • O _updateInterval é definido por três horas.
  • O ExecuteAsync método:
    • Loops 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 para Task.Delay é aguardada, dado o intervalo de atualização.
    • Depois de atrasar por três horas, o cache é atualizado novamente.

Os consumidores que estão no mesmo processo poderiam solicitar as IMemoryCache para as fotos, sendo o CacheWorker responsável por atualizar o cache.

Cache híbrido

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

Principais características

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

  • Cache de dois níveis: Gere automaticamente tanto as camadas de cache em memória (L1) como as distribuídas (L2). Os dados são recuperados primeiro da cache em memória para maior velocidade, depois da cache distribuída se necessário, e finalmente da fonte.
  • Proteção contra sobrecarga: Impede que múltiplos pedidos simultâneos executem a mesma operação dispendiosa. Apenas um pedido recupera os dados enquanto os outros esperam pelo resultado.
  • Serialização configurável: Suporta múltiplos formatos de serialização, incluindo JSON (por defeito), protobuf e XML.
  • Invalidação baseada em tags: Agrupa entradas de cache relacionadas com tags para uma eficiente invalidação em lote.
  • API simplificada: O GetOrCreateAsync método gere automaticamente falhas de cache, serialização e armazenamento.

Quando usar o HybridCache

Considere usar HybridCache quando:

  • Precisas tanto de cache local (em memória) como distribuído num ambiente multi-servidor.
  • Queres proteção contra cenários de estampida no cache.
  • Prefere uma API simplificada a ter de coordenar manualmente IMemoryCache e IDistributedCache.
  • Precisamos de invalidação de cache baseada em tags para entradas relacionadas.

Sugestão

Para aplicações de servidor único com necessidades simples de cache, a cache em memória pode ser suficiente. Para aplicações multi-servidor sem necessidade de proteção contra avalanches ou invalidação baseada em etiquetas, considere 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

Registar o serviço HybridCache no DI ligando para AddHybridCache:

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

O código anterior regista HybridCache com opções padrão. 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)
    };
});

Utilização básica

O método principal para interagir com HybridCache é GetOrCreateAsync. Este método verifica a cache para uma entrada com a chave especificada e, se não for encontrada, 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 C# anterior:

  • O GetOrCreateAsync método utiliza uma chave única e um método de fábrica.
  • Se os dados não estiverem na cache, é chamado o método de fábrica para os recuperar.
  • Os dados são armazenados automaticamente tanto em caches em memória como distribuídas.
  • Apenas um pedido concorrente executa o método de fábrica; outros aguardam o resultado.

Opções de entrada

Pode sobrescrever 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-lhe configurar:

Invalidação baseada em etiquetas

As etiquetas permitem agrupar entradas de cache relacionadas e invalidá-las juntas. Isto é útil para cenários em que os dados relacionados precisam de ser atualizados como 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 etiqueta específica:

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

Também pode invalidar múltiplas etiquetas ao mesmo tempo:

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

Observação

A invalidação baseada em etiquetas é uma operação lógica. Não remove ativamente valores da cache, mas garante que as entradas marcadas são tratadas como falhas de cache. As inscrições acabam por expirar consoante a sua vida útil configurada.

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 em cache, utilize a tag reservada de wildcard "*".

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

Serialização

Para cenários de cache distribuído, HybridCache requer serialização. Por defeito, trata string e byte[] internamente e utiliza System.Text.Json para outros tipos. 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 cache distribuída

HybridCache utiliza a implementação configurada IDistributedCache para a sua cache distribuída (L2). Mesmo sem configurar um IDistributedCache, o HybridCache ainda fornece cache em memória e proteção contra tempestades de requisições. Para adicionar o Redis como uma cache distribuída:

// 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 mais informações sobre implementações de cache distribuída, veja Cache distribuída.

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 suporte a uma expansão maior do que a abordagem de cache na memória. O uso de um cache distribuído descarrega a memória 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ída fazem parte do Microsoft.Extensions.Caching.Memory pacote NuGet, e existe até um AddDistributedMemoryCache método de extensão.

Atenção

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

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

API de cache distribuído

As APIs de cache distribuído são um pouco mais primitivas do que suas contrapartes 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 baseadas em um string. Com armazenamento em cache na memória, o valor pode ser qualquer tipo genérico fortemente definido, enquanto os valores no armazenamento em cache distribuído são mantidos como byte[]. Isso não quer dizer que não haja implementações que exponham valores genéricos fortemente tipados; ainda assim, isso é um detalhe de implementação.

Criar valores

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

Usando o AlphabetLetter registro do exemplo de cache na memória, você pode serializar o objeto para JSON e, em seguida, 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 em memória, as entradas de cache podem ter opções para ajudar a ajustar a sua presença no cache — neste caso, o DistributedCacheEntryOptions.

Criar métodos de extensão

Existem vários métodos de extensão baseados na conveniência para criar valores. Estes métodos ajudam a evitar codificar string representações de objetos em :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);
}

Uma vez que uma entrada de cache é lida do cache, pode obter a representação string codificada em UTF8 a partir do byte[].

Compreender os métodos de extensão

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

Atualizar valores

Não há forma de atualizar os valores na cache distribuída com uma única chamada de API. Em vez disso, os valores podem ter as suas expirações deslizantes reiniciadas com uma das APIs de atualização:

Se o valor real precisar de ser atualizado, deve apagar o valor e depois voltar a adicioná-lo.

Excluir valores

Para eliminar valores na cache distribuída, chame uma das Remove APIs:

Sugestão

Embora existam versões síncronas destas APIs, considere o facto de as implementações de caches distribuídas dependerem da E/S de rede. Por esta razão, normalmente é preferível usar APIs assíncronas.

Ver também