Caché en memoria en ASP.NET Core

Por Rick Anderson, John Luo y Steve Smith

El almacenamiento en caché puede mejorar significativamente el rendimiento y la escalabilidad de una aplicación al reducir el trabajo necesario para generar contenido. El almacenamiento en caché funciona mejor con datos que cambian con poca frecuencia y es caro de generar. El almacenamiento en caché hace una copia de los datos que puede devolverse mucho más rápido que desde la fuente. Las aplicaciones deberían escribirse y probarse para que nunca dependan de datos almacenados en caché.

ASP.NET Core admite varias cachés diferentes. La caché más sencilla se basa en el IMemoryCache. IMemoryCache representa una caché almacenada en la memoria del servidor web. Las aplicaciones que se ejecutan en una granja de servidores (varios servidores) deben asegurarse de que las sesiones son fijas cuando se utiliza la caché en memoria. Las sesiones fijas garantizan que todas las solicitudes de un cliente se dirijan al mismo servidor. Por ejemplo, las aplicaciones Azure Web utilizan Application Request Routing (ARR) para dirigir todas las solicitudes al mismo servidor.

Las sesiones no fijas en una granja de servidores web requieren una caché distribuida para evitar problemas de consistencia de la caché. Para algunas aplicaciones, una caché distribuida puede admitir una mayor escalabilidad horizontal que una caché en memoria. El uso de una caché distribuida descarga la memoria caché a un proceso externo.

La caché en memoria puede almacenar cualquier objeto. La interfaz de la caché distribuida está limitada a byte[]. La caché en memoria y la caché distribuida almacenan los elementos de la caché como pares clave-valor.

System.Runtime.Caching/MemoryCache

System.Runtime.Caching/MemoryCache (Paquete NuGet) puede utilizarse con:

  • .NET Standard 2.0 o posterior.
  • Cualquier implementación .NET orientada a .NET Standard 2.0 o posterior. Por ejemplo, ASP.NET Core 3.1 o posterior.
  • .NET Framework 4.5 o superior.

Microsoft.Extensions.Caching.Memory/IMemoryCache (descrito en este artículo) se recomienda sobre System.Runtime.Caching/MemoryCache porque está mejor integrado en ASP.NET Core. Por ejemplo, IMemoryCache funciona de forma nativa con ASP.NET Core inyección de dependencias.

Utilice System.Runtime.Caching/MemoryCache como puente de compatibilidad al portar código de ASP.NET 4.x a ASP.NET Core.

Guía de caché

  • El código siempre debe tener una opción de recuperación para obtener datos y no depender de que un valor almacenado en caché esté disponible.
  • La caché utiliza un recurso escaso, la memoria. Limite el crecimiento de la caché:
    • No introduzca datos externos en la caché. Por ejemplo, no se recomienda utilizar una entrada arbitraria proporcionada por el usuario como clave de la caché, ya que la entrada podría consumir una cantidad impredecible de memoria.
    • Utilizar expiraciones para limitar el crecimiento de la caché.
    • Utilizar SetSize, Size y SizeLimit para limitar el tamaño de la caché. El tiempo de ejecución de ASP.NET Core no limita el tamaño de la caché en función de la presión de la memoria. Depende del desarrollador limitar el tamaño de la caché.

Utilizar IMemoryCache

Advertencia

Utilizar una cache de memoria compartida de Inyección de Dependencia y llamar a SetSize, Size, o SizeLimit para limitar el tamaño de la cache puede causar que la aplicación no funcione. Cuando se establece un límite de tamaño en una caché, todas las entradas deben especificar un tamaño cuando se añaden. Esto puede dar lugar a problemas, ya que los desarrolladores pueden no tener un control total sobre lo que utiliza la caché compartida. Cuando utilice SetSize, Size, o SizeLimit para limitar la caché, cree un singleton de caché para el almacenamiento en caché. Para más información y un ejemplo, consulte Use SetSize, Size y SizeLimit para limitar el tamaño de la caché. Una caché compartida es aquella compartida por otros frameworks o librerías.

La caché en memoria es un servicio al que se hace referencia desde una aplicación utilizando inyección de dependencias. Solicita la instancia IMemoryCache en el constructor:

public class IndexModel : PageModel
{
    private readonly IMemoryCache _memoryCache;

    public IndexModel(IMemoryCache memoryCache) =>
        _memoryCache = memoryCache;

    // ...

El siguiente código utiliza TryGetValue para comprobar si una hora está en la caché. Si una hora no está en la caché, se crea una nueva entrada y se añade a la caché con Set:

public void OnGet()
{
    CurrentDateTime = DateTime.Now;

    if (!_memoryCache.TryGetValue(CacheKeys.Entry, out DateTime cacheValue))
    {
        cacheValue = CurrentDateTime;

        var cacheEntryOptions = new MemoryCacheEntryOptions()
            .SetSlidingExpiration(TimeSpan.FromSeconds(3));

        _memoryCache.Set(CacheKeys.Entry, cacheValue, cacheEntryOptions);
    }

    CacheCurrentDateTime = cacheValue;
}

En el código anterior, la entrada de la caché se configura con una caducidad variable de tres segundos. Si no se accede a la entrada de la caché durante más de tres segundos, se expulsa de la caché. Cada vez que se accede a la entrada de la caché, ésta permanece en la caché durante 3 segundos más. La clase CacheKeys forma parte de la muestra de descarga.

Se muestra la hora actual y la hora en caché:

<ul>
    <li>Current Time: @Model.CurrentDateTime</li>
    <li>Cached Time: @Model.CacheCurrentDateTime</li>
</ul>

El siguiente código utiliza el método de extensión Set para almacenar en caché datos durante un tiempo relativo sin MemoryCacheEntryOptions:

_memoryCache.Set(CacheKeys.Entry, DateTime.Now, TimeSpan.FromDays(1));

En el código anterior, la entrada de caché se configura con una caducidad relativa de un día. La entrada de caché se expulsa de la memoria caché después de un día, incluso si se accede a ella dentro de este período de tiempo de espera.

El siguiente código utiliza GetOrCreate y GetOrCreateAsync para almacenar datos en caché.

public void OnGetCacheGetOrCreate()
{
    var cachedValue = _memoryCache.GetOrCreate(
        CacheKeys.Entry,
        cacheEntry =>
        {
            cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(3);
            return DateTime.Now;
        });

    // ...
}

public async Task OnGetCacheGetOrCreateAsync()
{
    var cachedValue = await _memoryCache.GetOrCreateAsync(
        CacheKeys.Entry,
        cacheEntry =>
        {
            cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(3);
            return Task.FromResult(DateTime.Now);
        });

    // ...
}

El siguiente código llama a Get para obtener la hora en caché:

var cacheEntry = _memoryCache.Get<DateTime?>(CacheKeys.Entry);

El siguiente código obtiene o crea un elemento en caché con caducidad absoluta:

var cachedValue = _memoryCache.GetOrCreate(
    CacheKeys.Entry,
    cacheEntry =>
    {
        cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(20);
        return DateTime.Now;
    });

Un elemento en caché con una caducidad variable corre el riesgo de no caducar nunca. Si se accede repetidamente al elemento en caché dentro del intervalo de caducidad variable, el elemento nunca caduca. Combina una caducidad variable con una absoluta para garantizar que el elemento caduca. La caducidad absoluta establece un límite superior sobre el tiempo que el elemento puede estar en caché, al tiempo que permite que el elemento caduque antes si no se solicita dentro del intervalo de caducidad variable. Si el intervalo de caducidad o el tiempo de caducidad absoluta pasan, el elemento se expulsa de la caché.

El siguiente código obtiene o crea un elemento de la caché con un intervalo de caducidad y absoluto:

var cachedValue = _memoryCache.GetOrCreate(
    CacheKeys.CallbackEntry,
    cacheEntry =>
    {
        cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(3);
        cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(20);
        return DateTime.Now;
    });

El código anterior garantiza que los datos no se almacenarán en caché durante más tiempo que el tiempo absoluto.

GetOrCreate, GetOrCreateAsync y Get son métodos de extensión de la clase CacheExtensions. Estos métodos amplían la capacidad de IMemoryCache.

MemoryCacheEntryOptions

En el ejemplo siguiente:

  • Establece la prioridad de la caché en CacheItemPriority.NeverRemove.
  • Establece un PostEvictionDelegate que es llamado después de que la entrada es desalojada de la caché. La devolución de llamada se ejecuta en un subproceso diferente del código que elimina el elemento de la caché.
public void OnGetCacheRegisterPostEvictionCallback()
{
    var memoryCacheEntryOptions = new MemoryCacheEntryOptions()
        .SetPriority(CacheItemPriority.NeverRemove)
        .RegisterPostEvictionCallback(PostEvictionCallback, _memoryCache);

    _memoryCache.Set(CacheKeys.CallbackEntry, DateTime.Now, memoryCacheEntryOptions);
}

private static void PostEvictionCallback(
    object cacheKey, object cacheValue, EvictionReason evictionReason, object state)
{
    var memoryCache = (IMemoryCache)state;

    memoryCache.Set(
        CacheKeys.CallbackMessage,
        $"Entry {cacheKey} was evicted: {evictionReason}.");
}

Utilizar SetSize, Size y SizeLimit para limitar el tamaño de la caché

Una instancia MemoryCache puede especificar y aplicar opcionalmente un límite de tamaño. El límite de tamaño de la caché no tiene una unidad de medida definida porque la caché no tiene un mecanismo para medir el tamaño de las entradas. Si se establece el límite de tamaño de la caché, todas las entradas deben especificar el tamaño. El tiempo de ejecución de ASP.NET Core no limita el tamaño de la caché en función de la presión de la memoria. Depende del desarrollador limitar el tamaño de la caché. El tamaño especificado está en unidades que el desarrollador elige.

Por ejemplo:

  • Si la aplicación web almacenara principalmente cadenas, el tamaño de cada entrada de la caché podría ser la longitud de la cadena.
  • La aplicación podría especificar el tamaño de todas las entradas como 1, y el límite de tamaño sería el número de entradas.

Si SizeLimit no se establece, la caché crece sin límite. El tiempo de ejecución de ASP.NET Core no recorta la caché cuando la memoria del sistema es baja. Las aplicaciones deben diseñarse para:

  • Limitar el crecimiento de la caché.
  • Llamar a Compact o Remove cuando la memoria disponible es limitada.

El siguiente código crea un MemoryCache de tamaño fijo sin unidades accesible por inyección de dependencia:

public class MyMemoryCache
{
    public MemoryCache Cache { get; } = new MemoryCache(
        new MemoryCacheOptions
        {
            SizeLimit = 1024
        });
}

SizeLimit no tiene unidades. Las entradas de caché deben especificar el tamaño en las unidades que consideren más apropiadas si se ha establecido el límite de tamaño de caché. Todos los usuarios de una instancia de caché deben utilizar el mismo sistema de unidades. Una entrada no se almacenará en caché si la suma de los tamaños de las entradas almacenadas en caché supera el valor especificado por SizeLimit. Si no se establece ningún límite de tamaño de caché, se ignora el tamaño de caché establecido en la entrada.

El siguiente código registra MyMemoryCache con el contenedor de inyección de dependencia:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddSingleton<MyMemoryCache>();

MyMemoryCache se crea como una caché de memoria independiente para los componentes que son conscientes de esta caché de tamaño limitado y saben cómo establecer el tamaño de la entrada de caché adecuadamente.

El tamaño de la entrada de la caché puede establecerse mediante el método de extensión SetSize o la propiedad Size:

if (!_myMemoryCache.Cache.TryGetValue(CacheKeys.Entry, out DateTime cacheValue))
{
    var cacheEntryOptions = new MemoryCacheEntryOptions()
        .SetSize(1);

    // cacheEntryOptions.Size = 1;

    _myMemoryCache.Cache.Set(CacheKeys.Entry, cacheValue, cacheEntryOptions);
}

En el código anterior, las dos líneas resaltadas consiguen el mismo resultado al establecer el tamaño de la entrada de la caché. SetSize se proporciona por conveniencia cuando se encadenan llamadas a new MemoryCacheOptions().

MemoryCache.Compact

MemoryCache.Compact intenta eliminar el porcentaje especificado de la caché en el siguiente orden:

  • Todos los elementos caducados.
  • Elementos por prioridad. Los elementos de menor prioridad se eliminan primero.
  • Objetos utilizados menos recientemente.
  • Objetos con la caducidad absoluta más temprana.
  • Objetos con la fecha de caducidad más temprana.

Los elementos anclados con prioridad NeverRemove no se eliminan nunca. El siguiente código elimina un elemento de la caché y llama a Compact para eliminar el 25% de las entradas de la caché:

_myMemoryCache.Cache.Remove(CacheKeys.Entry);
_myMemoryCache.Cache.Compact(.25);

Para más información, consulte el código fuente de Compact en GitHub.

Copia en caché de las dependencias

El siguiente ejemplo muestra cómo caducar una entrada de la caché si caduca una entrada dependiente. Se añade un CancellationChangeToken al elemento en caché. Cuando se llama a Cancel sobre CancellationTokenSource, ambas entradas de la caché son desalojadas:

public void OnGetCacheCreateDependent()
{
    var cancellationTokenSource = new CancellationTokenSource();

    _memoryCache.Set(
        CacheKeys.DependentCancellationTokenSource,
        cancellationTokenSource);

    using var parentCacheEntry = _memoryCache.CreateEntry(CacheKeys.Parent);

    parentCacheEntry.Value = DateTime.Now;

    _memoryCache.Set(
        CacheKeys.Child,
        DateTime.Now,
        new CancellationChangeToken(cancellationTokenSource.Token));
}

public void OnGetCacheRemoveDependent()
{
    var cancellationTokenSource = _memoryCache.Get<CancellationTokenSource>(
        CacheKeys.DependentCancellationTokenSource);

    cancellationTokenSource.Cancel();
}

El uso de un CancellationTokenSource permite desalojar varias entradas de la caché en grupo. Con el patrón using en el código anterior, las entradas de caché creadas dentro del ámbito using heredan los disparadores y la configuración de caducidad.

Notas adicionales

  • La caducidad no se produce en segundo plano. No hay ningún temporizador que escanee activamente la caché en busca de elementos caducados. Cualquier actividad en la caché (Get, Set, Remove) puede desencadenar una búsqueda en segundo plano de elementos caducados. Un temporizador en CancellationTokenSource (CancelAfter) también elimina la entrada y activa una búsqueda de elementos caducados. El siguiente ejemplo utiliza CancellationTokenSource(TimeSpan) para el token registrado. Cuando este token se activa, elimina la entrada inmediatamente y activa las devoluciones de llamada de desalojo:

    if (!_memoryCache.TryGetValue(CacheKeys.Entry, out DateTime cacheValue))
    {
        cacheValue = DateTime.Now;
    
        var cancellationTokenSource = new CancellationTokenSource(
            TimeSpan.FromSeconds(10));
    
        var cacheEntryOptions = new MemoryCacheEntryOptions()
            .AddExpirationToken(
                new CancellationChangeToken(cancellationTokenSource.Token))
            .RegisterPostEvictionCallback((key, value, reason, state) =>
            {
                ((CancellationTokenSource)state).Dispose();
            }, cancellationTokenSource);
    
        _memoryCache.Set(CacheKeys.Entry, cacheValue, cacheEntryOptions);
    }
    
  • Cuando se utiliza una devolución de llamada para repoblar un elemento en caché:

    • Múltiples peticiones pueden encontrar el valor de la clave en caché vacío porque la llamada de retorno no se ha completado.
    • Esto puede provocar que varios subprocesos repoblen el elemento de la caché.
  • Cuando una entrada de la caché se utiliza para crear otra, la entrada hija copia los tokens de caducidad y los ajustes de caducidad basados en el tiempo de la entrada primaria. El elemento secundario no caduca por la eliminación o actualización manual de la entrada primaria.

  • Utilice PostEvictionCallbacks para establecer las devoluciones de llamada que se activarán después de que la entrada de la caché sea desalojada de la caché.

  • Para la mayoría de las aplicaciones, IMemoryCache está activado. Por ejemplo, llamar a AddMvc, AddControllersWithViews, AddRazorPages, AddMvcCore().AddRazorViewEngine, y muchos otros métodos Add{Service} en Program.cs, habilita IMemoryCache. Para aplicaciones que no llamen a uno de los métodos Add{Service} anteriores, puede ser necesario llamar a AddMemoryCache en Program.cs.

Actualización de la caché en segundo plano

Utiliza un servicio en segundo plano como IHostedService para actualizar la caché. El servicio en segundo plano puede volver a calcular las entradas y luego asignarlas a la caché solo cuando estén listas.

Recursos adicionales

Vea o descargue el código de ejemplo (cómo descargarlo)

Conceptos básicos del almacenamiento en caché

El almacenamiento en caché puede mejorar significativamente el rendimiento y la escalabilidad de una aplicación al reducir el trabajo necesario para generar contenido. El almacenamiento en caché funciona mejor con datos que cambian con poca frecuencia y es caro de generar. El almacenamiento en caché hace una copia de los datos que puede devolverse mucho más rápido que desde la fuente. Las aplicaciones deberían escribirse y probarse para que nunca dependan de datos almacenados en caché.

ASP.NET Core admite varias cachés diferentes. La caché más sencilla se basa en el IMemoryCache. IMemoryCache representa una caché almacenada en la memoria del servidor web. Las aplicaciones que se ejecutan en una granja de servidores (varios servidores) deben asegurarse de que las sesiones son fijas cuando se utiliza la caché en memoria. Las sesiones fijas garantizan que todas las solicitudes posteriores de un cliente se dirijan al mismo servidor. Por ejemplo, las aplicaciones web de Azure utilizan Application Request Routing (ARR) para dirigir todas las solicitudes posteriores al mismo servidor.

Las sesiones no fijas en una granja de servidores web requieren una caché distribuida para evitar problemas de consistencia de la caché. Para algunas aplicaciones, una caché distribuida puede admitir una mayor escalabilidad horizontal que una caché en memoria. El uso de una caché distribuida descarga la memoria caché a un proceso externo.

La caché en memoria puede almacenar cualquier objeto. La interfaz de la caché distribuida está limitada a byte[]. La caché en memoria y la caché distribuida almacenan los elementos de la caché como pares clave-valor.

System.Runtime.Caching/MemoryCache

System.Runtime.Caching/MemoryCache (Paquete NuGet) puede utilizarse con:

  • .NET Standard 2.0 o posterior.
  • Cualquier implementación .NET orientada a .NET Standard 2.0 o posterior. Por ejemplo, ASP.NET Core 3.1 o posterior.
  • .NET Framework 4.5 o superior.

Microsoft.Extensions.Caching.Memory/IMemoryCache (descrito en este artículo) se recomienda sobre System.Runtime.Caching/MemoryCache porque está mejor integrado en ASP.NET Core. Por ejemplo, IMemoryCache funciona de forma nativa con ASP.NET Core inyección de dependencias.

Utilice System.Runtime.Caching/MemoryCache como puente de compatibilidad al portar código de ASP.NET 4.x a ASP.NET Core.

Guía de caché

  • El código siempre debe tener una opción de recuperación para obtener datos y no depender de que un valor almacenado en caché esté disponible.
  • La caché utiliza un recurso escaso, la memoria. Limite el crecimiento de la caché:
    • Noutilice entradas externas como claves de caché.
    • Utilizar expiraciones para limitar el crecimiento de la caché.
    • Utilizar SetSize, Size y SizeLimit para limitar el tamaño de la caché. El tiempo de ejecución de ASP.NET Core no limita el tamaño de la caché en función de la presión de la memoria. Depende del desarrollador limitar el tamaño de la caché.

Utilizar IMemoryCache

Advertencia

Utilizar una cache de memoria compartida de Inyección de Dependencia y llamar a SetSize, Size, o SizeLimit para limitar el tamaño de la cache puede causar que la aplicación no funcione. Cuando se establece un límite de tamaño en una caché, todas las entradas deben especificar un tamaño cuando se añaden. Esto puede dar lugar a problemas, ya que los desarrolladores pueden no tener un control total sobre lo que utiliza la caché compartida. Cuando utilice SetSize, Size, o SizeLimit para limitar la caché, cree un singleton de caché para el almacenamiento en caché. Para más información y un ejemplo, consulte Use SetSize, Size y SizeLimit para limitar el tamaño de la caché. Una caché compartida es aquella compartida por otros frameworks o librerías.

La caché en memoria es un servicio al que se hace referencia desde una aplicación utilizando inyección de dependencias. Solicita la instancia IMemoryCache en el constructor:

public class HomeController : Controller
{
    private IMemoryCache _cache;

    public HomeController(IMemoryCache memoryCache)
    {
        _cache = memoryCache;
    }

El siguiente código utiliza TryGetValue para comprobar si una hora está en la caché. Si una hora no se almacena en caché, se crea una nueva entrada y se añade a la caché con Set. La clase CacheKeys forma parte de la muestra de descarga.

public static class CacheKeys
{
    public static string Entry => "_Entry";
    public static string CallbackEntry => "_Callback";
    public static string CallbackMessage => "_CallbackMessage";
    public static string Parent => "_Parent";
    public static string Child => "_Child";
    public static string DependentMessage => "_DependentMessage";
    public static string DependentCTS => "_DependentCTS";
    public static string Ticks => "_Ticks";
    public static string CancelMsg => "_CancelMsg";
    public static string CancelTokenSource => "_CancelTokenSource";
}
public IActionResult CacheTryGetValueSet()
{
    DateTime cacheEntry;

    // Look for cache key.
    if (!_cache.TryGetValue(CacheKeys.Entry, out cacheEntry))
    {
        // Key not in cache, so get data.
        cacheEntry = DateTime.Now;

        // Set cache options.
        var cacheEntryOptions = new MemoryCacheEntryOptions()
            // Keep in cache for this time, reset time if accessed.
            .SetSlidingExpiration(TimeSpan.FromSeconds(3));

        // Save data in cache.
        _cache.Set(CacheKeys.Entry, cacheEntry, cacheEntryOptions);
    }

    return View("Cache", cacheEntry);
}

Se muestra la hora actual y la hora en caché:

@model DateTime?

<div>
    <h2>Actions</h2>
    <ul>
        <li><a asp-controller="Home" asp-action="CacheTryGetValueSet">TryGetValue and Set</a></li>
        <li><a asp-controller="Home" asp-action="CacheGet">Get</a></li>
        <li><a asp-controller="Home" asp-action="CacheGetOrCreate">GetOrCreate</a></li>
        <li><a asp-controller="Home" asp-action="CacheGetOrCreateAsynchronous">CacheGetOrCreateAsynchronous</a></li>
        <li><a asp-controller="Home" asp-action="CacheRemove">Remove</a></li>
        <li><a asp-controller="Home" asp-action="CacheGetOrCreateAbs">CacheGetOrCreateAbs</a></li>
        <li><a asp-controller="Home" asp-action="CacheGetOrCreateAbsSliding">CacheGetOrCreateAbsSliding</a></li>

    </ul>
</div>

<h3>Current Time: @DateTime.Now.TimeOfDay.ToString()</h3>
<h3>Cached Time: @(Model == null ? "No cached entry found" : Model.Value.TimeOfDay.ToString())</h3>

El siguiente código utiliza el método de extensión Set para almacenar en caché los datos de una hora relativa sin crear el objeto MemoryCacheEntryOptions:

public IActionResult SetCacheRelativeExpiration()
{
    DateTime cacheEntry;

    // Look for cache key.
    if (!_cache.TryGetValue(CacheKeys.Entry, out cacheEntry))
    {
        // Key not in cache, so get data.
        cacheEntry = DateTime.Now;

        // Save data in cache and set the relative expiration time to one day
        _cache.Set(CacheKeys.Entry, cacheEntry, TimeSpan.FromDays(1));
    }

    return View("Cache", cacheEntry);
}

El valor DateTime almacenado en caché permanece en la caché mientras haya peticiones dentro del periodo de tiempo de espera.

El siguiente código utiliza GetOrCreate y GetOrCreateAsync para almacenar datos en caché.

public IActionResult CacheGetOrCreate()
{
    var cacheEntry = _cache.GetOrCreate(CacheKeys.Entry, entry =>
    {
        entry.SlidingExpiration = TimeSpan.FromSeconds(3);
        return DateTime.Now;
    });

    return View("Cache", cacheEntry);
}

public async Task<IActionResult> CacheGetOrCreateAsynchronous()
{
    var cacheEntry = await
        _cache.GetOrCreateAsync(CacheKeys.Entry, entry =>
        {
            entry.SlidingExpiration = TimeSpan.FromSeconds(3);
            return Task.FromResult(DateTime.Now);
        });

    return View("Cache", cacheEntry);
}

El siguiente código llama a Get para obtener la hora en caché:

public IActionResult CacheGet()
{
    var cacheEntry = _cache.Get<DateTime?>(CacheKeys.Entry);
    return View("Cache", cacheEntry);
}

El siguiente código obtiene o crea un elemento en caché con caducidad absoluta:

public IActionResult CacheGetOrCreateAbs()
{
    var cacheEntry = _cache.GetOrCreate(CacheKeys.Entry, entry =>
    {
        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(10);
        return DateTime.Now;
    });

    return View("Cache", cacheEntry);
}

Un elemento en caché con una caducidad variable corre el riesgo de no caducar nunca. Si se accede repetidamente al elemento en caché dentro del intervalo de caducidad variable, el elemento nunca caduca. Combina una caducidad variable con una absoluta para garantizar que el elemento caduca. La caducidad absoluta establece un límite superior sobre el tiempo que el elemento puede estar en caché, al tiempo que permite que el elemento caduque antes si no se solicita dentro del intervalo de caducidad variable. Si el intervalo de caducidad o el tiempo de caducidad absoluta pasan, el elemento se expulsa de la caché.

El siguiente código obtiene o crea un elemento de la caché con un intervalo de caducidad y absoluto:

public IActionResult CacheGetOrCreateAbsSliding()
{
    var cacheEntry = _cache.GetOrCreate(CacheKeys.Entry, entry =>
    {
        entry.SetSlidingExpiration(TimeSpan.FromSeconds(3));
        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(20);
        return DateTime.Now;
    });

    return View("Cache", cacheEntry);
}

El código anterior garantiza que los datos no se almacenarán en caché durante más tiempo que el tiempo absoluto.

GetOrCreate, GetOrCreateAsync y Get son métodos de extensión de la clase CacheExtensions. Estos métodos amplían la capacidad de IMemoryCache.

MemoryCacheEntryOptions

La siguiente muestra:

  • Establece un tiempo de expiración variable. Las peticiones que accedan a este elemento en caché reiniciarán el reloj de caducidad variable.
  • Establece la prioridad de la caché en CacheItemPriority.NeverRemove.
  • Establece un PostEvictionDelegate que será llamado después de que la entrada sea desalojada de la caché. La devolución de llamada se ejecuta en un subproceso diferente del código que elimina el elemento de la caché.
public IActionResult CreateCallbackEntry()
{
    var cacheEntryOptions = new MemoryCacheEntryOptions()
        // Pin to cache.
        .SetPriority(CacheItemPriority.NeverRemove)
        // Add eviction callback
        .RegisterPostEvictionCallback(callback: EvictionCallback, state: this);

    _cache.Set(CacheKeys.CallbackEntry, DateTime.Now, cacheEntryOptions);

    return RedirectToAction("GetCallbackEntry");
}

public IActionResult GetCallbackEntry()
{
    return View("Callback", new CallbackViewModel
    {
        CachedTime = _cache.Get<DateTime?>(CacheKeys.CallbackEntry),
        Message = _cache.Get<string>(CacheKeys.CallbackMessage)
    });
}

public IActionResult RemoveCallbackEntry()
{
    _cache.Remove(CacheKeys.CallbackEntry);
    return RedirectToAction("GetCallbackEntry");
}

private static void EvictionCallback(object key, object value,
    EvictionReason reason, object state)
{
    var message = $"Entry was evicted. Reason: {reason}.";
    ((HomeController)state)._cache.Set(CacheKeys.CallbackMessage, message);
}

Utilizar SetSize, Size y SizeLimit para limitar el tamaño de la caché

Una instancia MemoryCache puede especificar y aplicar opcionalmente un límite de tamaño. El límite de tamaño de la caché no tiene una unidad de medida definida porque la caché no tiene un mecanismo para medir el tamaño de las entradas. Si se establece el límite de tamaño de la caché, todas las entradas deben especificar el tamaño. El tiempo de ejecución de ASP.NET Core no limita el tamaño de la caché en función de la presión de la memoria. Depende del desarrollador limitar el tamaño de la caché. El tamaño especificado está en unidades que el desarrollador elige.

Por ejemplo:

  • Si la aplicación web almacenara principalmente cadenas, el tamaño de cada entrada de la caché podría ser la longitud de la cadena.
  • La aplicación podría especificar el tamaño de todas las entradas como 1, y el límite de tamaño sería el número de entradas.

Si SizeLimit no se establece, la caché crece sin límite. El tiempo de ejecución de ASP.NET Core no recorta la caché cuando la memoria del sistema es baja. Las aplicaciones deben diseñarse para:

  • Limitar el crecimiento de la caché.
  • Llamar a Compact o Remove cuando la memoria disponible es limitada:

El siguiente código crea un MemoryCache de tamaño fijo sin unidades accesible por inyección de dependencia:

// using Microsoft.Extensions.Caching.Memory;
public class MyMemoryCache 
{
    public MemoryCache Cache { get; private set; }
    public MyMemoryCache()
    {
        Cache = new MemoryCache(new MemoryCacheOptions
        {
            SizeLimit = 1024
        });
    }
}

SizeLimit no tiene unidades. Las entradas de la caché deben especificar el tamaño en las unidades que consideren más apropiadas si se ha establecido el límite de tamaño de la caché. Todos los usuarios de una instancia de caché deben utilizar el mismo sistema de unidades. Una entrada no se almacenará en caché si la suma de los tamaños de las entradas almacenadas en caché supera el valor especificado por SizeLimit. Si no se ha establecido un límite de tamaño de caché, se ignorará el tamaño de caché establecido en la entrada.

El siguiente código registra MyMemoryCache con el contenedor de inyección de dependencia.

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddSingleton<MyMemoryCache>();
}

MyMemoryCache se crea como una caché de memoria independiente para los componentes que son conscientes de esta caché de tamaño limitado y saben cómo establecer el tamaño de la entrada de caché adecuadamente.

El código siguiente usa MyMemoryCache:

public class SetSize : PageModel
{
    private MemoryCache _cache;
    public static readonly string MyKey = "_MyKey";

    public SetSize(MyMemoryCache memoryCache)
    {
        _cache = memoryCache.Cache;
    }

    [TempData]
    public string DateTime_Now { get; set; }

    public IActionResult OnGet()
    {
        if (!_cache.TryGetValue(MyKey, out string cacheEntry))
        {
            // Key not in cache, so get data.
            cacheEntry = DateTime.Now.TimeOfDay.ToString();

            var cacheEntryOptions = new MemoryCacheEntryOptions()
                // Set cache entry size by extension method.
                .SetSize(1)
                // Keep in cache for this time, reset time if accessed.
                .SetSlidingExpiration(TimeSpan.FromSeconds(3));

            // Set cache entry size via property.
            // cacheEntryOptions.Size = 1;

            // Save data in cache.
            _cache.Set(MyKey, cacheEntry, cacheEntryOptions);
        }

        DateTime_Now = cacheEntry;

        return RedirectToPage("./Index");
    }
}

El tamaño de la entrada de caché puede establecerse mediante Size o los métodos de extensión SetSize:

public IActionResult OnGet()
{
    if (!_cache.TryGetValue(MyKey, out string cacheEntry))
    {
        // Key not in cache, so get data.
        cacheEntry = DateTime.Now.TimeOfDay.ToString();

        var cacheEntryOptions = new MemoryCacheEntryOptions()
            // Set cache entry size by extension method.
            .SetSize(1)
            // Keep in cache for this time, reset time if accessed.
            .SetSlidingExpiration(TimeSpan.FromSeconds(3));

        // Set cache entry size via property.
        // cacheEntryOptions.Size = 1;

        // Save data in cache.
        _cache.Set(MyKey, cacheEntry, cacheEntryOptions);
    }

    DateTime_Now = cacheEntry;

    return RedirectToPage("./Index");
}

MemoryCache.Compact

MemoryCache.Compact intenta eliminar el porcentaje especificado de la caché en el siguiente orden:

  • Todos los elementos caducados.
  • Elementos por prioridad. Los elementos de menor prioridad se eliminan primero.
  • Objetos utilizados menos recientemente.
  • Objetos con la caducidad absoluta más temprana.
  • Objetos con la fecha de caducidad más temprana.

Los elementos anclados con prioridad NeverRemove nunca se eliminan. El siguiente código elimina un elemento de la caché y llama a Compact:

_cache.Remove(MyKey);

// Remove 33% of cached items.
_cache.Compact(.33);   
cache_size = _cache.Count;

Para más información, consulta el código fuente de Compact en GitHub.

Copia en caché de las dependencias

El siguiente ejemplo muestra cómo caducar una entrada de la caché si caduca una entrada dependiente. Se añade un CancellationChangeToken al elemento en caché. Cuando se llama a Cancel sobre CancellationTokenSource, ambas entradas de la caché son desalojadas.

public IActionResult CreateDependentEntries()
{
    var cts = new CancellationTokenSource();
    _cache.Set(CacheKeys.DependentCTS, cts);

    using (var entry = _cache.CreateEntry(CacheKeys.Parent))
    {
        // expire this entry if the dependant entry expires.
        entry.Value = DateTime.Now;
        entry.RegisterPostEvictionCallback(DependentEvictionCallback, this);

        _cache.Set(CacheKeys.Child,
            DateTime.Now,
            new CancellationChangeToken(cts.Token));
    }

    return RedirectToAction("GetDependentEntries");
}

public IActionResult GetDependentEntries()
{
    return View("Dependent", new DependentViewModel
    {
        ParentCachedTime = _cache.Get<DateTime?>(CacheKeys.Parent),
        ChildCachedTime = _cache.Get<DateTime?>(CacheKeys.Child),
        Message = _cache.Get<string>(CacheKeys.DependentMessage)
    });
}

public IActionResult RemoveChildEntry()
{
    _cache.Get<CancellationTokenSource>(CacheKeys.DependentCTS).Cancel();
    return RedirectToAction("GetDependentEntries");
}

private static void DependentEvictionCallback(object key, object value,
    EvictionReason reason, object state)
{
    var message = $"Parent entry was evicted. Reason: {reason}.";
    ((HomeController)state)._cache.Set(CacheKeys.DependentMessage, message);
}

El uso de un CancellationTokenSource permite desalojar varias entradas de la caché en grupo. Con el patrón using en el código anterior, las entradas de caché creadas dentro del bloque using heredarán los desencadenadores y la configuración de expiración.

Notas adicionales

  • La caducidad no se produce en segundo plano. No hay ningún temporizador que escanee activamente la caché en busca de elementos caducados. Cualquier actividad en la caché (Get, Set, Remove) puede desencadenar una búsqueda en segundo plano de elementos caducados. Un temporizador en CancellationTokenSource (CancelAfter) también elimina la entrada y activa una búsqueda de elementos caducados. El siguiente ejemplo utiliza CancellationTokenSource(TimeSpan) para el token registrado. Cuando este token se activa, elimina la entrada inmediatamente y activa las devoluciones de llamada de desalojo:

    public IActionResult CacheAutoExpiringTryGetValueSet()
    {
        DateTime cacheEntry;
    
        if (!_cache.TryGetValue(CacheKeys.Entry, out cacheEntry))
        {
            cacheEntry = DateTime.Now;
    
            var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
    
            var cacheEntryOptions = new MemoryCacheEntryOptions()
                .AddExpirationToken(new CancellationChangeToken(cts.Token));
    
            _cache.Set(CacheKeys.Entry, cacheEntry, cacheEntryOptions);
        }
    
        return View("Cache", cacheEntry);
    }
    
  • Cuando se utiliza una devolución de llamada para repoblar un elemento en caché:

    • Múltiples peticiones pueden encontrar el valor de la clave en caché vacío porque la llamada de retorno no se ha completado.
    • Esto puede provocar que varios subprocesos repoblen el elemento de la caché.
  • Cuando una entrada de la caché se utiliza para crear otra, la entrada hija copia los tokens de caducidad y los ajustes de caducidad basados en el tiempo de la entrada primaria. El elemento secundario no caduca por la eliminación o actualización manual de la entrada primaria.

  • Utilice PostEvictionCallbacks para establecer las devoluciones de llamada que se activarán después de que la entrada de la caché sea desalojada de la caché. En el código de ejemplo, CancellationTokenSource.Dispose() se llama a para liberar los recursos no administrados usados por CancellationTokenSource. Sin embargo, CancellationTokenSource no se elimina inmediatamente porque la entrada de caché todavía la usa. CancellationToken se pasa a MemoryCacheEntryOptions para crear una entrada de caché que expira después de un tiempo determinado. Por lo tanto Dispose, no se debe llamar a hasta que se quite o expire la entrada de caché. El código de ejemplo llama al RegisterPostEvictionCallback método para registrar una devolución de llamada que se invocará cuando se expulsa la entrada de caché y elimina en CancellationTokenSource esa devolución de llamada.

  • Para la mayoría de las aplicaciones, IMemoryCache está activado. Por ejemplo, llamar a AddMvc, AddControllersWithViews, AddRazorPages, AddMvcCore().AddRazorViewEngine, y muchos otros métodos Add{Service} en ConfigureServices, habilita IMemoryCache. Para aplicaciones que no llamen a uno de los métodos Add{Service} anteriores, puede ser necesario llamar a AddMemoryCache en ConfigureServices.

Actualización de la caché en segundo plano

Utiliza un servicio en segundo plano como IHostedService para actualizar la caché. El servicio en segundo plano puede volver a calcular las entradas y luego asignarlas a la caché solo cuando estén listas.

Recursos adicionales