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/site/content/docs/getting-started/installation.md b/site/content/docs/getting-started/installation.md index c3a5469..d4d7d88 100644 --- a/site/content/docs/getting-started/installation.md +++ b/site/content/docs/getting-started/installation.md @@ -52,6 +52,36 @@ a `using`. `AddActionCache` detects whether the application uses MVC or Minimal APIs and registers the matching filter and descriptor providers, so nothing further is needed for either style. +## Upgrading from 0.0.9 + +`0.0.9` shipped as a single package containing every backend. `0.1.0` is the first release +split across several, so an existing application needs the package for each backend it +registers: + +```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 +``` + +**No code changes.** The `Use…Cache` extensions still live in +`ActionCache.Common.Extensions`, so no call site and no `using` moves. `ActionCache` on its +own still covers `UseMemoryCache`. + +Three things to know before deploying: + +- **`net9.0` is no longer targeted.** `0.0.9` shipped `net8.0` and `net9.0`; this release + ships `net8.0` and `net10.0`. +- **Expect a cold cache.** Both the key format and the stored payload changed, so entries + left in a distributed backend by `0.0.9` are ignored and rewritten as they are re-cached. + A drop in hit rate on first deploy is expected; nothing errors. +- **Responses now vary by the authenticated user by default.** Two users on one + `[Authorize]` endpoint no longer share an entry — see [Vary-by](../../caching/vary-by). + Set `VaryByUser = VaryByUserMode.Never` to keep one shared entry. + +The [changelog](https://github.com/jzills/action-cache/blob/main/CHANGELOG.md) lists every +breaking change. + ## Next {{< cards >}} diff --git a/site/content/docs/reference/configuration.md b/site/content/docs/reference/configuration.md index 610ca1c..f1c6166 100644 --- a/site/content/docs/reference/configuration.md +++ b/site/content/docs/reference/configuration.md @@ -96,6 +96,30 @@ See [Resilience](../../reliability/resilience). `[ActionCacheEviction]` and `[ActionCacheRefresh]` take `Namespace` only. +## ActionCacheEndpointOptions + +The Minimal API equivalent, configured through `WithActionCache(ns, options => ...)`. + +| Property | Type | Default | +|---|---|---| +| `AbsoluteExpiration` | `TimeSpan?` | `null` — none | +| `SlidingExpiration` | `TimeSpan?` | `null` — none | +| `VaryByUser` | `VaryByUserMode` | `Auto` | +| `VaryByHeader` | `string?` | `null` | +| `VaryByQuery` | `string?` | `null` | +| `VaryByClaim` | `string?` | `null` | +| `SingleFlight` | `bool` | `true` | + +The expirations are `TimeSpan?` where the attribute takes `long` milliseconds: an attribute +argument must be a compile-time constant, and a builder argument need not be. + +The delegate runs **once at registration**, not per request — the options describe the +endpoint, so re-running caller code on every request would only make an expensive lambda an +expensive endpoint. + +`WithActionCacheEviction` and `WithActionCacheRefresh` take a namespace only, matching their +attributes. See [Attributes](../../caching/attributes#minimal-apis). + ## Key contributors ```csharp 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 @@ - +