Skip to main content

Microsoft caching

Respire integrates with Microsoft caching abstractions through two companion projects.

Distributed cache​

Respire.Caching provides IDistributedCache and IBufferDistributedCache:

builder.Services.AddRespireDistributedCache(
"redis://localhost",
instanceName: "myapp:");

Use ClientOptions when the cache needs its own fully configured client. The factory receives the service provider, and takes precedence if ConnectionString is also set:

builder.Services.AddRespireDistributedCache(options =>
{
options.ClientOptions = services => new RespireOptions
{
Endpoints = { new RespireEndpoint("redis.internal") },
Serializer = services.GetRequiredService<IRespireSerializer>(),
CommandTimeout = TimeSpan.FromSeconds(2),
};
options.InstanceName = "myapp:";
});

RespireDistributedCache uses atomic Lua reads to implement sliding expiration, so RESP3 client-side caching does not apply to IDistributedCache operations. Applications can still enable it on a separately registered IRespireClient used for direct eligible reads.

The cache owns and disposes clients created from ClientOptions or ConnectionString. If neither is set, it uses a separately registered IRespireClient without taking ownership.

To adapt an existing client directly, use AsDistributedCache:

using Respire.Caching;

await using var client = await RespireClient.ConnectAsync("redis://localhost");
await using var cache = client.AsDistributedCache(new RespireCacheOptions
{
InstanceName = "myapp:",
});
var cachedBytes = await cache.GetAsync("product:42");

Each call creates a new adapter without network I/O. InstanceName adds to any existing client key prefix, and ValueCodec configures the adapter's payload encoding. The caller retains ownership of the client; disposing the adapter does not dispose it. RespireCacheOptions holds only cache settings. A RespireCacheRegistrationOptions instance can also be passed here, but its connection settings are ignored because the adapter uses the supplied client. The registration methods take RespireCacheRegistrationOptions, which adds ConnectionString and ClientOptions. The existing RespireDistributedCache constructor remains available.

Inject the framework abstraction into application code:

public sealed class ProductCache(IDistributedCache cache)
{
public Task<byte[]?> GetAsync(string id, CancellationToken ct) =>
cache.GetAsync($"product:{id}", ct);
}

HybridCache​

Respire.Caching.Hybrid adds Respire as the L2 backend for HybridCache:

builder.Services.AddRespireHybridCache(
"redis://localhost",
instanceName: "myapp:");

This combines an in-process L1 with Redis-backed L2 storage.

Opt-in payload compression​

Set RespireCacheOptions.ValueCodec to encode the hash's data field. The default is null: cache bytes stay raw. This setting is independent of the client's serializer; RespireValueCodecSerializer on a shared or cache-owned client does not enable cache compression.

using Respire.Compression;

builder.Services.AddRespireDistributedCache(options =>
{
options.ConnectionString = "redis://localhost";
options.InstanceName = "myapp:compressed-v1:";
options.ValueCodec = new BrotliValueCodec(new RespireValueCodecOptions
{
MinimumLength = 1024,
MaximumDecodedLength = 4 * 1024 * 1024,
});
});

The same option applies to AddRespireHybridCache through its configureCache callback:

using Respire.Compression;

builder.Services.AddRespireHybridCache(options =>
{
options.ConnectionString = "redis://localhost";
options.InstanceName = "myapp:hybrid-compressed-v1:";
options.ValueCodec = new BrotliValueCodec();
});

With neither ConnectionString nor ClientOptions, these registrations use the registered IRespireClient. Direct construction also accepts new RespireCacheOptions { ValueCodec = codec }. The cache captures the codec reference at construction; changing the options afterward does not switch existing instances. Codecs must support concurrent calls. The cache does not dispose a shared codec or take ownership of a registered client.

Both synchronous and asynchronous array and buffer APIs apply the codec. HybridCache applies it only to serialized L2 payloads; L1 behavior and serialization remain HybridCache's responsibility. Buffer reads decode into the supplied IBufferWriter<byte>; built-in codecs avoid an intermediate decoded array. Writes retain an owned encoded array through the asynchronous send. Multi-segment input is combined before encoding. Compression is synchronous CPU work, not streaming or allocation-free. Cancellation is checked before encoding and again before sending; it cannot interrupt a codec call already executing. Existing accepted-send cancellation semantics still apply. For Brotli, start with the default quality 4; measure CPU time and stored size before increasing quality, especially for large values. The cache does not schedule codec work onto another thread.

The threshold determines whether compression is attempted, not whether a frame is written. Small, empty, or incompressible values still carry an uncompressed frame; those can coexist with compressed frames. Built-in settings are validated by the codec constructor. Maximum decoded length bounds original/decoded payload bytes; it does not cap total workspace. Oversized writes fail before publication. RespireDistributedCache throws for corrupt, oversized, or incompatible frames instead of returning a cache miss. HybridCache's handling of backend exceptions remains controlled by HybridCache. Built-in buffer decoding advances the destination only on success, though a decompression failure can modify uncommitted buffer memory. Custom codecs define their own decoding contract.

absexp, sldexp, TTLs, sliding refresh, and removal are unchanged. Refresh and removal never invoke a codec. Reads refresh sliding TTL through the existing script before decoding, so even a corrupt entry can have its sliding TTL refreshed. There is no extra Redis command for compression. See value codecs for frame layout and resource costs.

Migration compatibility​

With ValueCodec = null, cache entries use the same Redis layout and payload bytes as Microsoft.Extensions.Caching.StackExchangeRedis; existing entries remain interchangeable. Sliding-expiration reads refresh TTL atomically in the same round trip.

Opting into a codec changes the data payload contract. Built-in decoders reject unframed legacy entries; raw readers return encoded bytes and do not decode them. Every process sharing a namespace, including HybridCache instances, must use a compatible codec. Use a new InstanceName namespace or explicitly rewrite old entries with the appropriate reader and writer, preserving desired expiry. Enabling a reader first does not make legacy entries readable. Changing compression algorithms also requires compatible decoding or migration; there is no automatic detection/fallback to raw bytes.

Redis ACL commands​

Cache identities need these commands:

EVALSHA EVAL SET UNLINK HSET HMGET PTTL PEXPIRE PERSIST EXISTS

Timeout- or cancellation-safe operations also require CLIENT ID and CLIENT KILL.