LevelUp.ServiceDefaults.Caching
0.5.0
dotnet add package LevelUp.ServiceDefaults.Caching --version 0.5.0
NuGet\Install-Package LevelUp.ServiceDefaults.Caching -Version 0.5.0
<PackageReference Include="LevelUp.ServiceDefaults.Caching" Version="0.5.0" />
<PackageVersion Include="LevelUp.ServiceDefaults.Caching" Version="0.5.0" />
<PackageReference Include="LevelUp.ServiceDefaults.Caching" />
paket add LevelUp.ServiceDefaults.Caching --version 0.5.0
#r "nuget: LevelUp.ServiceDefaults.Caching, 0.5.0"
#:package LevelUp.ServiceDefaults.Caching@0.5.0
#addin nuget:?package=LevelUp.ServiceDefaults.Caching&version=0.5.0
#tool nuget:?package=LevelUp.ServiceDefaults.Caching&version=0.5.0
LevelUp.ServiceDefaults.Caching
Opt-in caching satellite for LevelUp.ServiceDefaults. Wires FusionCache's
.AsHybridCache() adapter as the application's HybridCache, giving an in-process
L1 with an optional Redis L2 and a Redis backplane for cross-instance invalidation.
The FusionCache/Redis dependencies are isolated here so they stay out of the
AOT-safe core.
PHI cache keys
Use PhiCacheKey for any cache entry that can identify or contain protected health information.
It returns a PhiCacheIdentity containing both the digest-addressed key and the full, canonical,
unhashed subject discriminator. Store values in PhiCacheEntry<T> and compare the stored subject
before returning a hit. Because both the discriminator and value are stored in Redis, require
AES-256-GCM encryption and register a production key provider before enabling this path:
builder.Services.AddLevelUpKeyVaultCacheEncryptionKey(options =>
options.VaultUri = "https://my-vault.vault.azure.net/");
builder.AddLevelUpHybridCache(options =>
{
options.RequireDistributedCache = true;
options.Encrypt = true;
});
const string patientId = "Patient/123";
string canonicalPatient = $"patient-id:v1:{patientId}";
PhiCacheIdentity identity = PhiCacheKey.ForOperation(
"fhir:v1",
nameof(ReadPatientAsync),
canonicalPatient);
PhiCacheEntry<Patient>? cached = await cache.GetOrCreateAsync(
identity.Key,
async cancellationToken => new PhiCacheEntry<Patient>(
identity.Subject,
await ReadPatientAsync(patientId, cancellationToken)));
if (!PhiCacheKey.Matches(cached?.Subject, identity.Subject))
{
// Treat a missing or mismatched discriminator as a cache miss.
}
AddLevelUpKeyVaultCacheEncryptionKey comes from LevelUp.ServiceDefaults.Azure. The static key
provider in this package is for development and tests only. Do not put a PhiCacheEntry<T> in a
distributed cache unless Encrypt = true and an ICacheEncryptionKeyProvider is registered; an
unhashed subject is identifying material even when the cached value looks harmless.
The PhiCacheKey factories accept only strings deliberately. The caller owns a versioned,
culture-invariant canonical representation that includes every field capable of changing the
answer; do not pass an object or tuple and rely on default serializer behavior. The factories
length-frame their own prefix/operation/payload segments, then hash the exact full subject later
used by Matches.
PhiCacheKey routes all of its hashed key material through HashGenerator, which uses SHA-256
truncated to 128 bits and emits {prefix}:{base64 digest}. The construction is deterministic
across processes and runs. It accepts only a caller-owned canonical string; it does not serialize
objects. Do not hash key material inline. A truncated digest does not make collisions impossible,
and SHA-256 is neither encryption nor an HMAC: it does not make predictable key material private.
The same subject feeds the digest, but the discriminator stored in the envelope remains unhashed;
consumers must compare it before returning a value and encrypt the distributed-cache envelope at
rest.
This package provides the independent ServiceDefaults primitive only. It does not implement the E-A.9a keyed-HMAC decision and does not adopt the primitive in the Health FHIR read path; that health integration and its planted-collision proof remain separate #174 work.
Usage
builder.AddLevelUpServiceDefaults()
.AddLevelUpHybridCache(); // L1 always; L2 + backplane only with ConnectionStrings:cache
The L2 distributed cache and the invalidation backplane self-disable when no Redis
connection resolves: the call registers an L1-only cache and does not throw, keeping
the canonical shim uniform across services. At host start a log line reports the
selected mode (Information for L1 + L2 + backplane, Warning for L1-only) so the
disable is never silent.
Entry points
Three overloads route through one private core, so the wiring is identical across surfaces:
// 1. Host builder (returns the builder, so the fluent chain keeps compiling).
builder.AddLevelUpHybridCache(o => o.ConnectionName = "redis");
// 2. IServiceCollection + IConfiguration — resolves ConnectionStrings:{ConnectionName}.
// Use this when caching is registered in an IServiceCollection extension.
services.AddLevelUpHybridCache(configuration, o => o.ConnectionName = "redis");
// 3. IServiceCollection + an explicit connection string (used verbatim, no lookup).
services.AddLevelUpHybridCache(redisConnectionString);
The builder overload returns TBuilder (so
builder.AddLevelUpServiceDefaults().AddLevelUpHybridCache() chains); the
IServiceCollection overloads return IServiceCollection.
Overload resolution: a bare
nullliteral is ambiguous (CS0121) between theIConfigurationandstring?overloads. Cast it to pick one, e.g.services.AddLevelUpHybridCache((string?)null).
Options (LevelUpCacheOptions)
| Option | Default | Purpose |
|---|---|---|
ConnectionName |
"cache" |
The ConnectionStrings key whose value supplies the Redis L2 + backplane. Set this when your Redis resource is named something other than cache (for example AddRedis("redis") ⇒ ConnectionName = "redis"). A blank value at the resolved key self-disables to L1-only. |
RequireDistributedCache |
false |
When true, an unresolved connection throws eagerly at registration (inside the Add… call) with an actionable message instead of self-disabling — for production topologies that must not silently lose the backplane. |
ConfigureRedis |
null |
An Action<RedisCacheOptions> escape hatch onto the standard Redis options surface (InstanceName, ConfigurationOptions, ssl/abort-connect, …). |
Encrypt |
false |
Require AES-256-GCM at-rest encryption of L2 payloads (needs a registered ICacheEncryptionKeyProvider; a missing provider fails fast at start). |
LocalDuration / DistributedDuration |
1 min / 10 min | The L1 / L2 default entry durations. |
Namespacing shared Redis with InstanceName
When several apps share one Redis, set InstanceName via ConfigureRedis to prefix
this app's L2 keys:
services.AddLevelUpHybridCache(configuration, o =>
{
o.ConnectionName = "redis";
o.ConfigureRedis = redis => redis.InstanceName = "tenantA:";
});
There is no implicit key-prefix default, so the at-rest key shape is unchanged unless you opt in here.
ConfigureRedis precedence vs the shared multiplexer
The L2 and the backplane share one connect-once IConnectionMultiplexer (one
connection, per FusionCache's own guidance). The opinionated defaults — including the
shared-multiplexer factory and the resolved connection — are applied first; your
ConfigureRedis callback runs last, so consumer values win. Note the precedence
consequence: a callback that re-sets ConnectionMultiplexerFactory or Configuration
replaces the shared factory and forfeits connection sharing with the backplane.
Prefer tuning ConfigurationOptions/InstanceName over replacing the factory.
The L2 stays private to FusionCache
The Redis L2 is constructed inline and held by FusionCache only — the satellite
registers no application-wide IDistributedCache. This is deliberate: registering
a global IDistributedCache would silently re-home ASP.NET session / data-protection /
output-cache onto Redis and let sibling IDistributedCache consumers write
plaintext into the encrypted L2's Redis. GetService<IDistributedCache>() is
therefore unaffected by AddLevelUpHybridCache. Program against the GA HybridCache
abstraction the satellite registers.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- LevelUp.ServiceDefaults (>= 0.5.0)
- Microsoft.Extensions.Caching.Hybrid (>= 10.9.0)
- Microsoft.Extensions.Caching.StackExchangeRedis (>= 10.0.11)
- StackExchange.Redis (>= 3.1.13)
- ZiggyCreatures.FusionCache (>= 2.6.0)
- ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis (>= 2.6.0)
- ZiggyCreatures.FusionCache.Serialization.SystemTextJson (>= 2.6.0)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on LevelUp.ServiceDefaults.Caching:
| Package | Downloads |
|---|---|
|
LevelUp.ServiceDefaults.Azure
Azure satellite for LevelUp.ServiceDefaults: Azure Monitor OpenTelemetry distro and Key Vault configuration provider, each gated on config. No-op otherwise. |
GitHub repositories
This package is not used by any popular GitHub repositories.