Skip to content

v1.0.0

Latest

Choose a tag to compare

@matthewdevenny matthewdevenny released this 11 Aug 03:19
v1.0.0
77bf29e

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.Metrics and ActivitySource, with no OpenTelemetry dependency taken by the package — nothing is timed or allocated per operation until a listener subscribes. Register NatsCacheTelemetryNames.MeterName / .ActivitySourceName to opt in. Instruments are nats.cache.operation.duration (histogram, seconds) and nats.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 on KV_<bucket> beyond the cache's ordinary KV access. See Multi-tenant Key Prefixes and Bulk Purge.
  • Automatic bucket creation (#53). CreateBucketIfNotExists = true creates a missing bucket on first use with the settings per-key TTL requires (History = 1, non-zero LimitMarkerTTL); ConfigureBucketOnCreate customizes 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 as operation=get in 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 from RegexOptions.Compiled to 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 BucketName throws OptionsValidationException at host startup instead of surfacing at first cache use.
  • Direct new NatsCache(...) throws ArgumentException ("BucketName must be set") instead of NullReferenceException. Any catch keyed on NullReferenceException needs 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 BCL MemoryCache.
  • The public NatsCache constructor is unchanged (the clock is an internal init-only property), so this is not an ABI break.

Also worth knowing (not breaking)

  • DI registration shape changed (#60). AddNatsDistributedCache now registers the concrete NatsCache as the singleton and forwards both IDistributedCache and the new INatsCacheMaintenance to that one instance, so a purge and a cache read share the same KV store, key prefix, and key encoder. Resolving IDistributedCache behaves as before.
  • New direct dependency: System.Diagnostics.DiagnosticSource 10.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

Full Changelog: v0.3.0...v1.0.0