The first stable release since v0.3.0, and the release the preview line was leading to. CodeCargo.Nats.DistributedCache and CodeCargo.Nats.HybridCacheExtensions both ship as 1.0.0. Target frameworks are unchanged (net8.0 and net10.0), as is the minimum server version (NATS 2.11+).
Coming from v1.0.0-preview.1? The only change is documentation (#62): the README now explains that nats.cache.operation.duration records seconds while OpenTelemetry's default bucket bounds are millisecond-shaped, so quantile queries need bounds configured for the instrument — without that, the p99 example returned ~4.95s no matter the real latency. The assemblies are otherwise identical; no code change, no action required beyond the version bump.
Coming from v0.3.0? Read the breaking changes below — in particular, expect a cold cache on first start.
New since v0.3.0
- Telemetry (#56). Metrics and traces via
System.Diagnostics.MetricsandActivitySource, with no OpenTelemetry dependency taken by the package — nothing is timed or allocated per operation until a listener subscribes. RegisterNatsCacheTelemetryNames.MeterName/.ActivitySourceNameto opt in. Instruments arenats.cache.operation.duration(histogram, seconds) andnats.cache.misses(counter, by reason), plus spans carrying the same tags. See Telemetry — including Histogram buckets, which you need before the quantile queries mean anything. - Bulk purge by prefix (#60). The new
INatsCacheMaintenance.PurgeByPrefixAsync(prefix)evicts every entry beneath a key sub-prefix — one tenant's keys, say — in a single subject-filtered JetStream stream purge, and returns the number of messages purged. It is irreversible, and it needs stream-purge permission onKV_<bucket>beyond the cache's ordinary KV access. See Multi-tenant Key Prefixes and Bulk Purge. - Automatic bucket creation (#53).
CreateBucketIfNotExists = truecreates a missing bucket on first use with the settings per-key TTL requires (History = 1, non-zeroLimitMarkerTTL);ConfigureBucketOnCreatecustomizes storage, replication, or limits. An existing bucket is used as-is and never modified, so operator-managed settings are preserved. See Automatic bucket creation. - Zero-copy reads (#59).
TryGetAsync(string, IBufferWriter<byte>, CancellationToken)writes the payload straight into the caller's buffer, so a hit no longer allocates an intermediate array. It reports asoperation=getin telemetry, since the buffer overload is an implementation detail rather than a different cache operation. - Trimmable and AOT-clean (#58). Both packages set
<IsAotCompatible>true</IsAotCompatible>and build with zero IL2xxx/IL3xxx warnings on net8.0 and net10.0. Key validation moved fromRegexOptions.Compiledto a source-generated[GeneratedRegex].
⚠️ Breaking changes since v0.3.0
Cache entries use a new binary envelope (#48, #54)
The CacheEntry envelope changed from JSON-with-a-base64-payload to compact binary framing —
[version:1][flags:1][absExpTicks:8?][sldExpTicks:8?][raw payload]. There is no read shim for the old format.
- Entries written by v0.3.0 or earlier are treated as a cache miss (logged at
Debug,EventId 102 UndeserializableEntry), never an error. A v0.3.0 node cannot read this release's envelope either, so during a rolling deploy each version only sees entries written in its own format. - Nothing is evicted and nothing throws — a node never deletes bytes it cannot read, so rolling deployments are safe and there is no migration step. TTL'd entries are reaped by NATS; TTL-less entries are overwritten by the next
Set. - Impact: expect a cold cache after upgrading. If that is unacceptable, pre-warm it or move to a new bucket /
CacheKeyPrefix. - Details: Cache Entry Format and Upgrades.
NATS.Client.KeyValueStore bumped 2.8.2 → 3.0.1 (major) (#56)
Apps that also reference NATS.Net packages directly (for example NATS.Extensions.Microsoft.DependencyInjection) must move to the matching 3.x versions. The bump required no source changes in this library. The minimum server version is unchanged (NATS 2.11+); integration tests now run against 2.14.3.
CacheEntryJsonContext removed (#48)
The public JsonSerializerContext for CacheEntry is gone, and CacheEntry is now a plain POCO — the [JsonPropertyName] attributes went with it. Code referencing CacheEntryJsonContext, or depending on the stored absexp / sldexp / data JSON property names, no longer compiles or no longer applies. The envelope is an internal storage detail.
A missing BucketName now fails fast (#51)
AddNatsDistributedCache — and AddNatsHybridCache, which delegates to it — now registers options with .Validate(...).ValidateOnStart().
- Under the Generic Host, a null / empty / whitespace
BucketNamethrowsOptionsValidationExceptionat host startup instead of surfacing at first cache use. - Direct
new NatsCache(...)throwsArgumentException("BucketName must be set") instead ofNullReferenceException. Anycatchkeyed onNullReferenceExceptionneeds updating.
TryGetAsync propagates caller cancellation (#52)
NatsCache.TryGetAsync(string, IBufferWriter<byte>, CancellationToken) previously caught everything and returned false silently. It still returns false for read failures — now logged at Warning (EventId 101) rather than silently — but an OperationCanceledException from the caller's cancelled token now propagates instead of masquerading as a cache miss.
Relatedly, Remove failures are now consistently logged at Error (previously inconsistent or not logged at all), and every failure is logged exactly once.
Expirations beyond ~68 years are rejected (#54)
NATS encodes message TTLs as (int)ttl.TotalSeconds, which overflows above int.MaxValue seconds. SlidingExpiration, AbsoluteExpiration, and AbsoluteExpirationRelativeToNow windows exceeding 24855.03:14:07 now throw ArgumentOutOfRangeException from Set/SetAsync, instead of writing an entry with an overflowed TTL header. The read path fails closed on the same ceiling, so nothing that can be written reads back as an undeserializable miss.
The MaxValue sentinels are the deliberate exception: DateTimeOffset.MaxValue (absolute) and TimeSpan.MaxValue (sliding / relative) are now normalized to "never expire" rather than producing an enormous TTL.
Expiration is computed from TimeProvider, in UTC, on an inclusive boundary (#46)
Every clock read moved from DateTimeOffset.Now to TimeProvider.GetUtcNow().
- If your container registers a
TimeProvider, the cache now uses it for all expiration math. See Controlling Expiration Timing. - The absolute-expiration read check is now inclusive (
>=): an entry whose absolute expiration is exactly "now" reads as expired, matching TTL computation and BCLMemoryCache. - The public
NatsCacheconstructor is unchanged (the clock is an internalinit-only property), so this is not an ABI break.
Also worth knowing (not breaking)
- DI registration shape changed (#60).
AddNatsDistributedCachenow registers the concreteNatsCacheas the singleton and forwards bothIDistributedCacheand the newINatsCacheMaintenanceto that one instance, so a purge and a cache read share the same KV store, key prefix, and key encoder. ResolvingIDistributedCachebehaves as before. - New direct dependency:
System.Diagnostics.DiagnosticSource10.0.9 for the telemetry types (#56). It was already in the graph transitively, so this adds no new nodes. - Target frameworks are unchanged: net8.0 and net10.0.
What's Changed
- #40 Inject TimeProvider by @matthewdevenny in #46
- Add cache-envelope serialization benchmarks + manual workflow by @matthewdevenny in #47
- #37 Replace JSON+base64 cache envelope with compact binary framing by @matthewdevenny in #48
- #42 validate options by @matthewdevenny in #51
- #43 Log swallowed TryGetAsync exceptions by @matthewdevenny in #52
- #38 add bucket auto creation option by @matthewdevenny in #53
- CodeCargo: update code-cargo/cargowall-action to v1.3.3 by @matthewdevenny in #55
- #49 Address unserializable entries by @matthewdevenny in #54
- #39 Add OpenTelemetry by @matthewdevenny in #56
- CodeCargo: update code-cargo/cargowall-action to v1.3.5 by @matthewdevenny in #57
- #45 Add purge-by-prefix cache maintenance helper by @matthewdevenny in #60
- #45 Write TryGetAsync payload directly into the caller buffer by @matthewdevenny in #59
- #45 Enable AOT/trim analyzers and source-generate key regex by @matthewdevenny in #58
- #61 Document histogram bucket bounds for the duration instrument by @matthewdevenny in #62
Full Changelog: v0.3.0...v1.0.0