Repository navigation
Conversation
…ons:KeyPrefix Every key the framework writes to Redis was unprefixed (cache entries, HybridCache tag invalidations, the Data Protection key ring, idempotency locks, quota counters) and the SignalR backplane used a fixed `fsh-signalr` channel. Applications or environments sharing one Redis server collided; a defaultDatabase index does not help the backplane, because pub/sub ignores the database. CachingOptions:KeyPrefix (normalized by Core's RedisKeyPrefix) now namespaces all of them: RedisCacheOptions.InstanceName, the Data Protection list key, the idempotency reservation, quota counters (QuotaOptions:KeyPrefix overrides) and the SignalR channel prefix. Empty keeps today's layout. The template ships FSH.Starter / FSH.Starter:prod, renamed per project by sourceName. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
iammukeshm
left a comment
There was a problem hiding this comment.
Thanks @maxiar, this is a good find. The pub/sub point is the part most people miss: defaultDatabase doesn't separate the SignalR backplane, and your tag-invalidation test shows the cache leaking across apps on main today. The BuildingBlocks change is justified here, because every Redis touchpoint lives there. I checked the coverage: health checks, AppHub and the SSE tokens go through IDistributedCache, so InstanceName covers them, and nothing else talks to Redis. It merges cleanly with main, builds with zero warnings, and Framework (360), Caching (31), Architecture (55) and the Valkey HybridCacheRedisTests (7) all pass.
The code is backward compatible with an empty prefix, but the shipped config isn't, and that's the blocker.
- The shipped default rotates the Data Protection key ring (critical). With
KeyPrefixset inappsettings.json:69andappsettings.Production.json:44, an existing deployment moves fromDataProtection-KeystoFSH.Starter:prod:DataProtection-Keyson upgrade, and the new ring is empty. That's worse than cookies and reset tokens failing once.WebhookSecretProtectorstores subscription signing secrets in the database protected with DP, so after the upgradeUnprotectthrows and every existing webhook subscription can never sign again. Please shipKeyPrefixempty, make it opt-in, and document the one-line migration (COPY DataProtection-Keys <prefix>:DataProtection-Keys) for anyone turning it on. That also means dropping or flippingEnvironments_Should_ShipDistinctRedisKeyPrefixes. - The migrator and the API get different prefixes in Production.
FSH.Starter.DbMigrator.csprojlinksappsettings.jsonandappsettings.Development.jsonbut notappsettings.Production.json. Under compose or Terraform, the migrator would run asFSH.Starterand the API asFSH.Starter:prod. If the default is empty this mostly goes away, but please make sure both read the same value wherever it's set (compose, Terraform,AppHost.cs). - Add a test for the invariant that protects existing deployments. An empty
KeyPrefixmust giveInstanceName == "", the DP key exactlyDataProtection-Keys, and thefsh-signalrchannel prefix. - Rolling deploys split the backplane. While old pods are on
fsh-signalrand new pods are on{prefix}:signalr, cross-pod broadcasts are lost until the rollout finishes. One line in the docs is enough. - Validate the prefix at startup. A
{or}becomes a Redis Cluster hash tag and pins every key to one slot, and whitespace is accepted too. Reject both.
One nit: Quota/Extensions.cs:27-29 patches the singleton QuotaOptions but not the bound IOptions<QuotaOptions>, so the two disagree. It's harmless today, but it's easy to trip on later.
Last thing: this needs a companion PR in fullstackhero/docs with a changelog entry and the setting documented on the caching page, plus a mention in deploy/docker/README.md. With the default flipped to empty, this is close.
Every key the framework writes to Redis is unprefixed: cache entries (
perm:u:{id},theme:t:root,theme:default), HybridCache tag invalidations, theDataProtection-Keysring, idempotency locks andquota:*counters. The SignalR backplane uses a fixedfsh-signalrchannel prefix. Two applications built on the kit, or the QA and Production deployments of one app, therefore collide as soon as they point at the same Redis server.A
defaultDatabase=Nindex in the connection string, which is the usual workaround, only partly helps. It separates keys, but Redis pub/sub is server-wide and ignores the database index. So every app on the server still shares one SignalR backplane.AppHubbroadcasts totenant:{id}anduser:{id}groups, andtenant:rootexists in every app. A presence or notification broadcast in one app therefore reaches clients connected to another.Within a shared database, the cache leaks too. An integration test in this PR shows it: when app A calls
RemoveByTagAsync("permissions"), app B's permission entries on the same server are evicted as well. Entries for the same logical key (theme:t:root) are read across apps.Change
This PR adds one setting,
CachingOptions:KeyPrefix, and applies it everywhere the framework talks to Redis:KeyPrefix = "MyApp:prod"RedisCacheOptions.InstanceName)theme:t:rootMyApp:prod:theme:t:rootDataProtection-KeysMyApp:prod:DataProtection-Keyslock:idem:…MyApp:prod:lock:idem:…QuotaOptions:KeyPrefixoverrides, since quota may use its own Redis)quota:…MyApp:prod:quota:…fsh-signalrMyApp:prod:signalrFSH.Framework.Core.Common.RedisKeyPrefix, normalizes the value to empty or exactly one trailing:. It lives in Core because Caching, Web and Quota all reference Core, while Quota does not reference Caching.CacheKeysis unchanged, because the prefix is applied underneath it."KeyPrefix": "FSH.Starter"inappsettings.jsonand"FSH.Starter:prod"inappsettings.Production.json.sourceNamerenames these per project, so a new scaffold is isolated per app and per environment out of the box..agents/rules/caching.mdandrealtime.mdare updated. The docs-site page and changelog entry (golden rule 10) will follow as a PR tofullstackhero/docsonce the shape here is agreed.Verification
New tests:
HybridCacheRedisTests(real Valkey).maintoday.RedisKeyPrefixWiringTests.fsh-signalrwithoutKeyPrefixand{prefix}:signalrwith it.IdempotencyEndpointFilterReplayTests. The reservation key starts with the prefix.ShippedConfigurationTests. Development and Production ship distinct, non-empty prefixes.RedisKeyPrefixTests. Normalization cases.The runs used
-p:NuGetAudit=false, becausemaincurrently fails restore on the crypto advisories that #1447 fixes.🤖 Generated with Claude Code