Zwischenspeichern in .NET

In diesem Artikel erfahren Sie mehr über verschiedene Zwischenspeichermechanismen. Beim Zwischenspeichern werden Daten auf einer Zwischenebene gespeichert, sodass nachfolgende Datenabrufe schneller erfolgen. Konzeptionell ist das Zwischenspeichern eine Strategie zur Leistungsoptimierung und eine Entwurfsüberlegung. Das Zwischenspeichern kann die App-Leistung erheblich verbessern, indem selten geänderte (oder aufwendig abzurufende) Daten leichter verfügbar gemacht werden. In diesem Artikel werden die beiden primären Arten des Zwischenspeicherns vorgestellt und Beispielquellcode für beide vorgestellt:

Wichtig

Es gibt zwei MemoryCache-Klassen in .NET, eine im System.Runtime.Caching-Namespace und die andere im Microsoft.Extensions.Caching-Namespace:

Dieser Artikel konzentriert sich auf das Zwischenspeichern, berücksichtigt jedoch nicht das System.Runtime.Caching-NuGet-Paket. Alle Verweise auf MemoryCache befinden sich innerhalb des Microsoft.Extensions.Caching-Namespace.

Alle Microsoft.Extensions.*-Pakete sind für die Abhängigkeitsinjektion (Dependency Injection, DI) bereit. Beide Schnittstellen IMemoryCache und IDistributedCache können als Dienste verwendet werden.

In-Memory-Caching

In diesem Abschnitt erfahren Sie mehr über das Paket Microsoft.Extensions.Caching.Memory. Die aktuelle Implementierung von IMemoryCache ist ein Wrapper um das ConcurrentDictionary<TKey,TValue>, wobei eine funktionsreiche API verfügbar gemacht wird. Einträge im Cache werden durch den ICacheEntry dargestellt und können ein beliebiges object sein. Die In-Memory-Cachelösung eignet sich hervorragend für Apps, die auf einem einzelnen Server ausgeführt werden, auf dem alle zwischengespeicherten Daten Arbeitsspeicher im Prozess der App belegen.

Tipp

Für Szenarien mit multiserverbasiertem Zwischenspeichern sollten Sie den Ansatz des verteilten Zwischenspeicherns als Alternative zum In-Memory-Zwischenspeichern in Betracht ziehen.

In-Memory-Zwischenspeicher-API

Der Nutzer des Caches hat die Kontrolle über gleitende und absolute Abläufe:

Das Festlegen eines Ablaufs führt dazu, dass Einträge im Cache entfernt werden, wenn nicht innerhalb der zugewiesenen Ablaufzeit auf sie zugegriffen wird. Nutzer haben zusätzliche Optionen zum Steuern von Cacheeinträgen über die MemoryCacheEntryOptions. Jeder ICacheEntry ist mit MemoryCacheEntryOptions gekoppelt, wodurch die Funktionalität zum Entfernen von Ablaufzeit mit IChangeToken, Prioritätseinstellungen mit CacheItemPriority und das Steuern des ICacheEntry.Size verfügbar gemacht werden. Betrachten Sie die folgenden Erweiterungsmethoden:

In-Memory-Cache: Beispiel

Um die IMemoryCache-Standardimplementierungen zu verwenden, rufen Sie die AddMemoryCache-Erweiterungsmethode auf, um alle erforderlichen Dienste bei DI zu registrieren. Im folgenden Codebeispiel wird der generische Host verwendet, um die DI-Funktionalität verfügbar zu machen:

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

Abhängig von Ihrer .NET-Workload können Sie anders auf den IMemoryCache zugreifen, z. B. als Konstruktorinjektion. In diesem Beispiel verwenden Sie die IServiceProvider-Instanz auf dem host und rufen die generische GetRequiredService<T>(IServiceProvider)-Erweiterungsmethode auf:

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

Mit registrierten In-Memory-Cachediensten und aufgelöst über DI – Sie können mit dem Zwischenspeichern beginnen. Im Beispiel werden im englischen Alphabet die Buchstaben „A“ bis „Z“ durchlaufen. Es gibt einen record AlphabetLetter-Typ, in dem der Verweis auf den Buchstaben festgehalten wird, und der eine Nachricht generiert.

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

Tipp

Der file-Zugriffsmodifizierer wird für den AlphabetLetter-Typ verwendet, da er innerhalb definiert ist und nur über die Program.cs-Datei darauf zugegriffen wird. Weitere Informationen finden Sie unter file (C# Reference). Informationen zum vollständigen Quellcode finden Sie im Abschnitt Program.cs.

Das Beispiel enthält eine Hilfsfunktion, die die Buchstaben des Alphabets durchläuft:

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

    Console.WriteLine();
}

Im oben stehenden C#-Code ist Folgendes passiert:

  • Die Func<char, Task> asyncFunc wird bei jeder Iteration erwartet, wobei der aktuelle letter übergeben wird.
  • Nachdem alle Buchstaben verarbeitet wurden, wird eine leere Zeile in die Konsole geschrieben.

Rufen Sie zum Hinzufügen von Elementen zum Cache eine der Create- oder Set-APIs auf:

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;

Im oben stehenden C#-Code ist Folgendes passiert:

  • Die Variable addLettersToCacheTask delegiert an IterateAlphabetAsync und wird benötigt.
  • Func<char, Task> asyncFunc ist durch eine Lambda-Funktion vertreten.
  • MemoryCacheEntryOptions wird mit einem zum aktuellen Zeitpunkt relativen absoluten Ablauf instanziiert.
  • Ein Rückruf nach dem Entfernen wird registriert.
  • Ein AlphabetLetter-Objekt wird instanziiert und zusammen mit letter und options an Set übergeben.
  • Der Buchstabe wird als zwischengespeichert in die Konsole geschrieben.
  • Schließlich wird ein Task.Delay zurückgegeben.

Für jeden Buchstaben im Alphabet wird ein Cacheeintrag mit einem Ablauf und Rückruf nach dem Entfernen geschrieben.

Der Rückruf nach dem Entfernen schreibt die Details des entfernten Werts in die Konsole:

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

Nach dem Auffüllen des Caches wird ein weiterer Aufruf von IterateAlphabetAsync erwartet, aber dieses Mal rufen Sie IMemoryCache.TryGetValue auf:

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;

Wenn der cache den letter-Schlüssel enthält und der value eine Instanz eines AlphabetLetter ist, wird er in die Konsole geschrieben. Wenn sich der letter-Schlüssel nicht im Cache befindet, wurde er entfernt, und der Rückruf nach dem Entfernen wurde aufgerufen.

Zusätzliche Erweiterungsmethoden

Der IMemoryCache verfügt über viele benutzerfreundliche Erweiterungsmethoden einschließlich eines asynchronen GetOrCreateAsync:

Korrektes Zusammenfügen

Der gesamte Quellcode der Beispiel-App ist ein Programm der obersten Ebene und erfordert zwei NuGet Pakete:

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

Sie können die Werte MillisecondsDelayAfterAdd und MillisecondsAbsoluteExpiration anpassen, um die Verhaltensänderungen beim Ablauf und beim Entfernen von zwischengespeicherten Einträgen zu beobachten. Im Folgenden sehen Sie eine Beispielausgabe aus der Ausführung dieses Codes. Aufgrund der nicht deterministischen Natur von .NET-Ereignissen kann ihre Ausgabe unterschiedlich sein.

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.

Da der absolute Ablauf (MemoryCacheEntryOptions.AbsoluteExpirationRelativeToNow) festgelegt ist, werden alle zwischengespeicherten Elemente schließlich entfernt.

Zwischenspeichern des Workerdiensts

Eine gängige Strategie für das Zwischenspeichern von Daten ist das von den nutzenden Datendiensten unabhängige Aktualisieren des Caches. Die Workerdienst-Vorlage ist ein gutes Beispiel, da der BackgroundService unabhängig vom übrigen Anwendungscode (d. h. im Hintergrund) ausgeführt wird. Wenn eine Anwendung, die eine Implementierung des IHostedService hostet, mit der Ausführung beginnt, beginnt die Ausführung der entsprechenden Implementierung (in diesem Fall der BackgroundService oder „Worker“) im selben Prozess. Diese gehosteten Dienste werden mithilfe der AddHostedService<THostedService>(IServiceCollection)-Erweiterungsmethode bei DI als Singletons registriert. Andere Dienste können mit einer beliebigen Dienstlebensdauer bei DI registriert werden.

Wichtig

Es ist sehr wichtig, mit der Lebensdauer des Diensts vertraut zu sein. Wenn Sie AddMemoryCache aufrufen, um alle In-Memory-Zwischenspeicherdienste zu registrieren, werden die Dienste als Singletons registriert.

Fotodienstszenario

Stellen Sie sich vor, Sie würden einen Fotodienst entwickeln, der auf einer Drittanbieter-API basiert, auf die über HTTP zugegriffen werden kann. Diese Fotodaten ändern sich nicht sehr oft, aber es gibt viele davon. Jedes Foto wird durch einen einfachen record dargestellt:

namespace CachingExamples.Memory;

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

Im folgenden Beispiel sehen Sie, dass mehrere Dienste bei DI registriert werden. Jeder Dienst hat eine einzelne Aufgabe.

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

Im oben stehenden C#-Code ist Folgendes passiert:

Der PhotoService ist für das Abrufen von Fotos verantwortlich, die einem bestimmten Kriterium (oder filter) entsprechen:

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

Im oben stehenden C#-Code ist Folgendes passiert:

  • Der Konstruktor erfordert einen IMemoryCache, ein CacheSignal<Photo> und einen ILogger.
  • Die GetPhotosAsync-Methode:
    • Definiert einen Func<Photo, bool> filter-Parameter und gibt ein IAsyncEnumerable<Photo> zurück.
    • Ruft auf und wartet auf die Freigabe von _cacheSignal.WaitAsync(). Dadurch wird sichergestellt, dass der Cache aufgefüllt wird, bevor auf den Cache zugegriffen wird.
    • Ruft _cache.GetOrCreateAsync() auf, und ruft asynchron alle Fotos im Cache ab.
    • Das factory-Argument protokolliert eine Warnung und gibt ein leeres Fotoarray zurück. Dies sollte nie geschehen.
    • Jedes Foto im Cache wird durchlaufen, gefiltert und mit yield return angezeigt.
    • Schließlich wird das Cachesignal zurückgesetzt.

Nutzer dieses Diensts können die GetPhotosAsync-Methode aufrufen und Fotos entsprechend behandeln. Kein HttpClient ist erforderlich, da der Cache die Fotos enthält.

Das asynchrone Signal basiert auf einer gekapselten SemaphoreSlim-Instanz innerhalb eines eingeschränkten Singletons generischen Typs. Das CacheSignal<T> basiert auf einer Instanz von 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();
}

Im vorangehenden C#-Code wird das Decoratormuster verwendet, um eine Instanz von SemaphoreSlim zu umschließen. Da das CacheSignal<T> als Singleton registriert ist, kann es über alle Dienstlebensdauern hinweg mit einem beliebigen generischen Typ verwendet werden – in diesem Fall Photo. Es ist für das Signalisieren des Seedings des Caches verantwortlich.

Der CacheWorker ist eine Unterklasse von 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;
            }
        }
    }
}

Im oben stehenden C#-Code ist Folgendes passiert:

  • Der Konstruktor erfordert einen ILogger, ein HttpClient und einen IMemoryCache.
  • Das _updateInterval auf drei Stunden festgelegt.
  • Die ExecuteAsync-Methode:
    • Führt während der Ausführung der App Schleifendurchläufe durch.
    • Stellt eine HTTP-Anforderung an "https://jsonplaceholder.typicode.com/photos" und ordnet die Antwort als Array von Photo-Objekten zu.
    • Das Array von Fotos wird im IMemoryCache unter dem "Photos"-Schlüssel platziert.
    • _cacheSignal.Release() wird aufgerufen und gibt alle Nutzer frei, die auf das Signal gewartet haben.
    • Der Aufruf von Task.Delay wird erwartet, wenn das Aktualisierungsintervall angegeben ist.
    • Nach einer Verzögerung von drei Stunden wird der Cache erneut aktualisiert.

Consumer im selben Prozess können IMemoryCache nach den Fotos fragen, aber der CacheWorker ist für die Aktualisierung des Caches verantwortlich.

Verteiltes Caching

In einigen Szenarien ist ein verteilter Cache erforderlich, z. B. bei mehreren App-Servern. Ein verteilter Cache unterstützt eine höhere horizontale Skalierung als der In-Memory-Zwischenspeicheransatz. Die Verwendung eines verteilten Caches lädt den Cachespeicher an einen externen Prozess aus, erfordert jedoch zusätzliche Netzwerk-E/A und führt zu einer etwas größeren Latenz (auch wenn sie nominell ist).

Die Abstraktionen des verteilten Zwischenspeicherns sind Teil des Microsoft.Extensions.Caching.Memory-NuGet-Pakets, und es gibt sogar eine AddDistributedMemoryCache-Erweiterungsmethode.

Achtung

Der AddDistributedMemoryCache sollte nur in Entwicklungs- und/oder Testszenarien verwendet werden und ist keine praktikable Produktionsimplementierung.

Betrachten Sie eine der verfügbaren Implementierungen des IDistributedCache aus den folgenden Paketen:

API für verteiltes Zwischenspeichern

Die APIs für verteiltes Zwischenspeichern sind etwas primitiver als ihre In-Memory-Zwischenspeicher-API-Entsprechungen. Die Schlüssel-Wert-Paare sind etwas einfacher. Die Schlüssel für In-Memory-Zwischenspeichern basieren auf einem object, während die verteilten Schlüssel ein string sind. Mit In-Memory-Zwischenspeichern kann der Wert ein beliebiger stark typisierter generischer Wert sein, während Werte beim verteilten Zwischenspeichern als byte[] beibehalten werden. Das heißt nicht, dass verschiedene Implementierungen keine stark typisierten generischen Werte verfügbar machen, aber dies wäre ein Implementierungsdetail.

Erstellen von Werten

Um Werte im verteilten Cache zu erstellen, rufen Sie eine der APIs zum Festlegen auf:

Mithilfe des AlphabetLetter-Datensatzes aus dem In-Memory-Cachebeispiel könnten Sie das Objekt in JSON serialisieren und dann den string als byte[] codieren:

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

Ähnlich wie beim In-Memory-Zwischenspeichern können Cacheeinträge Optionen bieten, um ihr Vorhandensein im Cache zu optimieren – in diesem Fall DistributedCacheEntryOptions.

Erstellen von Erweiterungsmethoden

Es gibt mehrere benutzerfreundliche Erweiterungsmethoden zum Erstellen von Werten, mit denen die Codierung von string-Darstellungen von Objekten in byte[] vermieden werden kann:

Lesen von Werten

Um Werte aus dem verteilten Cache zu lesen, rufen Sie eine der get-APIs auf:

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

Sobald ein Cacheeintrag aus dem Cache gelesen wurde, können Sie die UTF8-codierte string-Darstellung vom byte[] abrufen.

Lesen von Erweiterungsmethoden

Es gibt mehrere benutzerfreundliche Erweiterungsmethoden zum Lesen von Werten, mit denen die Decodierung von byte[] in string-Darstellungen von Objekten vermieden werden kann:

Aktualisieren von Werten

Es gibt keine Möglichkeit, die Werte im verteilten Cache mit einem einzigen API-Aufruf zu aktualisieren. Stattdessen können die gleitenden Abläufe von Werten mit einer der Aktualisierungs-APIs zurückgesetzt werden:

Wenn der tatsächliche Wert aktualisiert werden muss, müssen Sie den Wert löschen und dann erneut hinzufügen.

Löschen von Werten

Um Werte im verteilten Cache zu löschen, rufen Sie eine der APIs zum Entfernen auf:

Tipp

Es gibt zwar synchrone Versionen der oben genannten APIs, aber Sie sollten bedenken, dass Implementierungen verteilter Caches von der Netzwerk-E/A abhängig sind. Aus diesem Grund werden in der Regel die asynchronen APIs verwendet.

Siehe auch