From 9ffeaf5976ffa8e2b852d132a1b0f0e3255956e7 Mon Sep 17 00:00:00 2001 From: Joshua Zillwood Date: Sat, 29 Aug 2026 16:25:41 -0500 Subject: [PATCH 1/2] chore(release): prepare 0.1.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Release prep for the first multi-package release, and the first since 0.0.9 (February 2025). * chore(deps): bump Microsoft.Azure.Cosmos to 3.62.1 Dependabot opened this as #36, but that branch predates #37 and #38 and its unit-test failure came from the staleness, not the SDK. Applied on current develop instead: unit 549/549 and integration 72/72 on both frameworks. * docs(packaging): a README for every package Only ActionCache set PackageReadmeFile, so the four packages this release introduces would each have shown an empty readme tab on nuget.org — on the very release that introduces them. Each backend keeps its own README.md beside its csproj, packed from Directory.Build.props so a future backend gets a landing page by adding the file. ActionCache is the exception and still packs the repository root README, declared in its own csproj. * docs: add CHANGELOG.md Everything relative to 0.0.9, breaking changes first. Two of those are only discoverable by upgrading and hitting them: net9.0 is no longer targeted, and entries written by 0.0.9 are unreadable because both the key format and the stored payload changed — a cold cache on first deploy, not an error. The upgrade itself needs no code change; the Use...Cache extensions still live in ActionCache.Common.Extensions, so it is only adding the backend package. PackageReleaseNotes points at the changelog rather than restating it: notes are baked into the nuspec at pack time and would freeze at the version they shipped with. Claude-Session: https://claude.ai/code/session_01VUPTqPh5w79mfa18fLnMLz --- CHANGELOG.md | 124 ++++++++++++++++++ Directory.Build.props | 20 +++ README.md | 1 + src/ActionCache.Abstractions/README.md | 37 ++++++ .../ActionCache.AzureCosmos.csproj | 2 +- src/ActionCache.AzureCosmos/README.md | 50 +++++++ src/ActionCache.Redis/README.md | 52 ++++++++ src/ActionCache.SqlServer/README.md | 50 +++++++ 8 files changed, 335 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md create mode 100644 src/ActionCache.Abstractions/README.md create mode 100644 src/ActionCache.AzureCosmos/README.md create mode 100644 src/ActionCache.Redis/README.md create mode 100644 src/ActionCache.SqlServer/README.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..aa27d14 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,124 @@ +# Changelog + +All notable changes to this project are documented here. + +The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this +project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). While the +version stays below 1.0, breaking changes may land in a minor release — they are always +listed first in the entry. + +## [0.1.0] — 2026-08-29 + +The first release of ActionCache as a set of packages rather than one, and the first since +`0.0.9` (February 2025). Everything below is relative to `0.0.9`. + +### Breaking changes + +- **The backends ship as separate packages.** `ActionCache` now contains the attributes, + filters, key building, DI and the in-memory backend only. Redis, SQL Server and Azure + Cosmos DB each moved to their own package. A consumer who caches in memory no longer + inherits StackExchange.Redis, `Microsoft.Data.SqlClient`, the Cosmos SDK and + Newtonsoft.Json — which `0.0.9` pulled in unconditionally. See *Upgrading* below; the + registration code itself does not change. +- **`net9.0` is no longer targeted.** `0.0.9` shipped `net8.0` and `net9.0`; `0.1.0` ships + `net8.0` and `net10.0`. +- **Cached entries from `0.0.9` are not readable.** Both the key format (now hashed) and the + stored payload (now a rendered response envelope rather than a serialized result graph) + changed. Entries left in a distributed backend by an older version are ignored and + overwritten as they are re-cached — a cold cache after upgrade, not an error. +- **Stored values are no longer polymorphic.** Entries serialize through a source-generated + `System.Text.Json` context, and nothing in a payload names a type to construct. +- **Responses vary by the authenticated user by default.** `VaryByUserMode.Auto` means two + users hitting one `[Authorize]` endpoint no longer share a cache entry. Set + `VaryByUser = VaryByUserMode.Never` to restore the previous behavior. +- **An endpoint may cache or have cache side effects, never both.** Combinations such as + `[ActionCache]` with `[ActionCacheEviction]` on one endpoint now throw + `ConflictingCacheAttributesException` at startup, listing every offending route. + +### Added + +- **Azure Cosmos DB backend** as `ActionCache.AzureCosmos`, with TTL-based expiry and lazy + container initialization. +- **`ActionCache.Abstractions`**, for implementing a cache backend without depending on an + implementation. +- **Minimal API support end to end** — `WithActionCache`, `WithActionCacheEviction` and + `WithActionCacheRefresh`, including refresh, which previously worked for controller + actions only. +- **Per-endpoint options for `WithActionCache`**, matching what `[ActionCache]` offers: + expiration, vary-by and single-flight, stated as `TimeSpan` rather than milliseconds. +- **Stampede protection.** `IActionCacheSingleFlight` coalesces concurrent misses for one + key so the origin action runs once. On by default; opt out with `SingleFlight = false`. + `options.UseDistributedSingleFlight()` coalesces across processes over the Redis or SQL + Server lock. +- **Vary-by keys** — `VaryByUser`, `VaryByHeader`, `VaryByQuery` and `VaryByClaim`, plus + `IActionCacheKeyContributor` for anything else. +- **Layered backends.** Registering more than one chains them: a deeper-layer hit is + promoted into the first layer, and key enumeration unions every layer. +- **Graceful degradation.** A backend outage degrades to a cache miss and logs a warning + rather than failing the request. Configurable through `ActionCacheResilienceOptions`, + including fail-closed and an operation timeout. +- **Cancellation throughout.** Every `IActionCache` method takes a `CancellationToken`, and + the filters pass `HttpContext.RequestAborted`. +- **Observability** — structured logging and a documented telemetry contract across cache + hits, misses, evictions, refreshes and degradation. +- **A documentation site** at , and XML + documentation on every public API. + +### Changed + +- **Refresh replays the recorded request** against the matching endpoint in its own DI + scope, rather than reflecting over the action. Replays are marked so a refresh cannot + recurse into itself or trip the eviction filter, and a refreshed entry keeps the + expiration its endpoint declared instead of silently inheriting the global options. +- **Refresh skips entries that vary by the request** — replaying another caller's request + would mean impersonating them. +- **Cache keys are hashed**, bounding key length regardless of argument size. +- **Distributed locking is production-grade** — `sp_getapplock` on SQL Server, and Lua + scripts on Redis so its operations are atomic without a lock at all. +- **Backends connect lazily.** Redis and Cosmos initialize on first use, so an application + no longer fails to start because a cache backend is unreachable. +- **Inter-package dependencies are pinned to an exact version.** These assemblies share + internals and release in lockstep, so a mismatched pair could fail at runtime rather than + at build time. +- Microsoft.Azure.Cosmos updated to 3.62.1. + +### Fixed + +- Only successful (2xx) results are cached. A `NotFound()` or `BadRequest()` body was + previously cached and replayed for the whole lifetime of the entry. +- A recorded request body is replayed with the content type it arrived as, so an endpoint + with `[Consumes]` no longer answers 415 on every refresh pass. +- A request body that cannot be faithfully replayed (XML, form data) no longer produces a + broken replay: the entry is still cached, and refresh skips it and logs why. +- The in-memory namespace index is guarded by a singleton lock — caches are created per + request, so a per-instance lock guarded nothing, and the read-modify-write it protects is + not atomic in `IMemoryCache`. +- Namespace eviction in the memory backend disposed a `CancellationTokenSource` that + in-flight requests still held, so a concurrent write threw `ObjectDisposedException` — a + 500 when fail-closed, a silently dropped cache write when fail-open. Entries written + afterwards also carried a token no later eviction would cancel. +- The Redis expiry listener targets the database named in the connection string rather than + database 0. +- Namespace injection through route-parameter templates. +- All build warnings; the build now treats warnings as errors. + +### Upgrading from 0.0.9 + +Add the package for each backend you register. The registration API is unchanged — the +`Use…Cache` extensions still live in `ActionCache.Common.Extensions`, the namespace you +already import for `AddActionCache` — so no `using` and no call site needs to change: + +```bash +dotnet add package ActionCache.Redis # if you call UseRedisCache +dotnet add package ActionCache.SqlServer # if you call UseSqlServerCache +dotnet add package ActionCache.AzureCosmos # if you call UseAzureCosmosCache +``` + +`ActionCache` on its own still covers `UseMemoryCache`. Each backend package references +`ActionCache`, so you do not need to list both. + +Then review the breaking changes above — in particular, expect a cold cache on first +deploy, and check whether any endpoint carries a combination of cache attributes that +startup validation now rejects. + +[0.1.0]: https://github.com/jzills/action-cache/releases/tag/v0.1.0 diff --git a/Directory.Build.props b/Directory.Build.props index 9c01e91..2de4856 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -26,6 +26,10 @@ Icon.jpg MIT https://github.com/jzills/action-cache + + See https://github.com/jzills/action-cache/blob/main/CHANGELOG.md https://github.com/jzills/action-cache.git git @@ -68,4 +72,20 @@ + + + README.md + + + + + + diff --git a/README.md b/README.md index 5e58e13..a211fd4 100644 --- a/README.md +++ b/README.md @@ -125,4 +125,5 @@ observability. ## Resources - [Documentation](https://jzills.github.io/action-cache/) +- [Changelog](./CHANGELOG.md) - [Samples](./samples/) diff --git a/src/ActionCache.Abstractions/README.md b/src/ActionCache.Abstractions/README.md new file mode 100644 index 0000000..582ffa3 --- /dev/null +++ b/src/ActionCache.Abstractions/README.md @@ -0,0 +1,37 @@ +# ActionCache.Abstractions + +The core contracts for [ActionCache](https://www.nuget.org/packages/ActionCache/) — +namespaced response caching for ASP.NET Core. + +```bash +dotnet add package ActionCache.Abstractions +``` + +**Most applications do not need this package.** Install +[`ActionCache`](https://www.nuget.org/packages/ActionCache/) for the attributes, filters and +the in-memory backend, or one of the backend packages +([`ActionCache.Redis`](https://www.nuget.org/packages/ActionCache.Redis/), +[`ActionCache.SqlServer`](https://www.nuget.org/packages/ActionCache.SqlServer/), +[`ActionCache.AzureCosmos`](https://www.nuget.org/packages/ActionCache.AzureCosmos/)) — +each of them brings this one with it. + +Reference it directly when you are **writing a cache backend of your own**, or when a +library needs to depend on the abstractions without pulling in an implementation. + +## What is here + +| Type | Purpose | +|---|---| +| `IActionCache` | The core contract: `GetAsync`, `SetAsync`, `RemoveAsync`, `RefreshAsync`, `GetKeysAsync` | +| `IActionCacheFactory` | Creates an `IActionCache` per namespace | +| `ActionCacheBase` | Base class carrying the locking strategy a backend opts into | +| `Namespace` | The namespace primitive, including route-parameter templates | +| `ActionCacheEntryOptions` | Absolute and sliding expiration for a single entry | +| `CachedResponse` | The stored value: status code, content type, rendered body, and the request refresh replays | + +This package takes no third-party dependencies — no Redis client, no SqlClient, no Cosmos +SDK, no Newtonsoft. + +## Documentation + +Full documentation: diff --git a/src/ActionCache.AzureCosmos/ActionCache.AzureCosmos.csproj b/src/ActionCache.AzureCosmos/ActionCache.AzureCosmos.csproj index 0f79da1..07e7cc5 100644 --- a/src/ActionCache.AzureCosmos/ActionCache.AzureCosmos.csproj +++ b/src/ActionCache.AzureCosmos/ActionCache.AzureCosmos.csproj @@ -11,7 +11,7 @@ - +