Architecture Decision Record
ADR-040: Authenticated output caching for public reads
Status
Accepted (2026-07-10). Amended (2026-07-10): explicit query-string variance parity with the
built-in default policy (the initial release accidentally dropped it, collapsing every query
variant of a path onto one cache entry), plus an opt-in bypassRoles escape hatch for endpoints
whose payload is elevated for one privileged role. Amended (2026-07-25): a shared Redis-backed
output-cache store is now the expected posture for any multi-replica deployment; the per-replica
in-memory store is the single-replica case, not the accepted default (see Trade-offs). Amended
(2026-08-01): the bypass is a shared, singly-declared privileged read AUDIENCE, not one role (ADC
names two, Organizer and ContentEditor, and the API-layer visibility check reads the same
list), and it now backs almost every public policy rather than a lone exception; five minutes is
the usual TTL, not the rule, since a clock-dependent or cross-service payload takes a shorter one
(see Decision and Trade-offs). Amended (2026-08-14): ADC's public-policy set grew to ten with
SponsorsCache, and nine of the ten now pass the bypass audience; NowNextCache remains the only
policy without it. Amended (2026-08-19): the cross-service eviction limit in
Trade-offs is narrowed by ADR-026's Revision (2026-08-18): a mutation in
another service can now request this host's tag eviction over the outbox, broker and inbox path via
OutputCacheEvictionRequested, best-effort and per tag, and ADC's bookmark counts now pair that
event with their short TTL rather than relying on the TTL alone (see Trade-offs). Amended
(2026-08-31): ADC's public-policy set gained ActivitiesCache, and NowNextCache remains the only
policy without the bypass audience. Revised 2026-09-10: Store's CustomerErasedHandler publishes
OutputCacheEvictionRequested and the Catalog host registers both eviction halves (see the
Revision (2026-09-10)). Revised 2026-09-25: ADC's public-policy set is twelve and
eleven pass the bypass audience (the site-wide ConferencePublicCache and BookmarkCountsCache
among them); Store's ProductsCache runs a 60-second TTL because a variant's effective price moves
on the clock; both hosts register the Redis output-cache store unconditionally through the
framework wrapper; anchors refreshed (see the Revision (2026-09-25) at the end). Revised
2026-10-01: anchors only (see the Revision (2026-10-01)). Revised 2026-10-06: ADC bookmark eviction
is raised by a paced hosted processor, the admin-freshness driver is broker-round-trip latency rather
than unevicted writes, and ADC Conference scales to four replicas in conference mode (see the
Revision (2026-10-06)). Revised 2026-10-07: the Decision now records the policy's tenant vary
rule (the key varies by the resolved tenant) and its wide HasRole bypass check, and anchors are
refreshed after the v1.233.0 release (see the Revision (2026-10-07)).
Context
The framework's read-scaling design leans on ASP.NET Core output caching: anonymous-readable
endpoints ([AllowAnonymous] GETs like event/session/speaker catalogs) carry named policies with
tag-based eviction, primed by startup warmup and load-tested by k6. Five minutes is the usual TTL,
but it is a default, not a rule: a payload that cannot wait five minutes takes a shorter one. Three
of Store Catalog's four policies run five minutes
(MMCA.Store/Source/Services/MMCA.Store.Catalog.Service/Program.cs:152, :153, :162), while
ProductsCache runs 60 seconds because each variant's effective price moves on the clock when a
discount window opens or closes, with no mutation to evict on (:159, the reasoning at
:154-158). ADC runs two 60-second policies (NowNextCache, a clock-dependent now-and-next
snapshot, and BookmarkCountsCache, written by another service;
MMCA.ADC/Source/Services/MMCA.ADC.Conference.Service/Program.cs:286,300).
That design was silently inert for the traffic that matters. The shared UI HttpClient pipeline
attaches the stored Bearer token to every outgoing API request via AuthDelegatingHandler
(MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Auth/AuthDelegatingHandler.cs:34-40; a
request that sets the SkipBearer option, which the token-refresh POST does, passes through
untouched, :18-22, :29), including reads of public endpoints whose payload is identical for
every caller. ASP.NET Core's
built-in default output-cache policy refuses both cache lookup and cache storage for any request
carrying an Authorization header (or an authenticated identity). The result: every logged-in
user bypassed the output cache on every read, and on conference day (when every attendee is
logged in) 100% of agenda/session/speaker reads landed on Basic-tier SQL. The gap was invisible
in load evidence because the k6 scripts of the time and the warmup requests were anonymous, which
is exactly the traffic slice the default policy still cached.
Two ways out were considered:
- Route public reads through a second, unauthenticated named HttpClient in the UI.
- Replace the default policy server-side for the affected endpoints.
Option 1 bifurcates the UI's HTTP stack (two clients, two resilience pipelines, per-call-site decisions that silently regress when someone picks the wrong client) and still leaves any other authenticated caller (mobile hosts, cross-service calls, curl with a token) uncached.
Decision
MMCA.Common.API ships PublicEndpointOutputCachePolicy, an IOutputCachePolicy that mirrors
the built-in default policy with two deliberate departures, one relaxation and one tightening. The
relaxation: it does not disable cache lookup or storage when the request carries an
Authorization header or an authenticated identity
(MMCA.Common/Source/Presentation/MMCA.Common.API/Caching/PublicEndpointOutputCachePolicy.cs:83-90). It
enforces the same response-side guards (GET/HEAD only; never store Set-Cookie responses or
non-200s), varies the cache key by every query-string parameter (CacheVaryByRules.QueryKeys = "*", the same rule as the default policy; a raw IOutputCachePolicy registration replaces the
whole default chain, so the policy must restate it), and takes the expiration and eviction tags
as constructor arguments. The tightening, which the default policy lacks: when a tenant has
been resolved for the request, the key varies by ITenantContext.TenantId under
VaryByValues["t"], so one tenant's rows are never served to another from the shared entry
(PublicEndpointOutputCachePolicy.cs:98-106,
the key constant at :50). An unresolved tenant (a single-tenant host, a background call) adds
nothing, and the vary rule is a backstop, not a license: a caller-dependent payload still must not
use this policy (:37-42).
Hosts register it per named policy via the OutputCacheOptions.AddPublicEndpointPolicy(name, expiration, tags) extension and reference it from [OutputCache(PolicyName = ...)] exactly like
any built-in policy. Tag-based eviction from mutating commands is unchanged.
The contract for applying it is strict: ONLY endpoints that are [AllowAnonymous] AND whose
response does not vary by caller identity. A cached response is served verbatim to every
subsequent caller, so a user-dependent payload behind this policy is an information-disclosure
bug, not a perf tweak.
One bounded relaxation exists for role-elevated payloads: the AddPublicEndpointPolicy(name, expiration, bypassRoles, tags) overload makes callers in a bypass role skip the cache entirely
(no lookup, no storage), so they always read fresh and their elevated responses are never stored.
Use it when the payload is identical for every caller EXCEPT a privileged read audience (ADC's
audience is two roles, Organizer and ContentEditor, who see unpublished rows per BR-108).
Per-user payloads remain out of scope: bypass roles handle role-shaped variance, not
identity-shaped variance. Membership is checked with the framework's wide HasRole extension
(MMCA.Common/Source/Core/MMCA.Common.Shared/Auth/ClaimsPrincipalExtensions.cs:75-77, which reads
ClaimTypes.Role plus bare role/roles claims and compares case-insensitively, :59-67), not
the BCL ClaimsPrincipal.IsInRole, which matches only the identity's own role claim type: against an identity provider
that emits bare role/roles claims, IsInRole would miss the bypass while the permission
handler granted the elevated payload, and that response would be stored under the shared public
key (PublicEndpointOutputCachePolicy.cs:137-142).
That audience is declared ONCE and shared, never restated per policy. ADC keeps it in
ConferenceReadAudience.PrivilegedRoles
(MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Shared/Authorization/ConferenceReadAudience.cs:34-38)
and the API-layer visibility check reads the same list
(CurrentUserServiceExtensions.IsPrivilegedConferenceReader,
MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.API/Authorization/CurrentUserServiceExtensions.cs:24).
Two lists naming different roles would put a privileged payload in the shared public entries and
serve it to everyone, so the single declaration is the guard, not a convention. Nor is the bypass a
narrow exception in practice: eleven of ADC's twelve public policies pass it
(MMCA.ADC/Source/Services/MMCA.ADC.Conference.Service/Program.cs:269-300, the array at :268),
the exception being NowNextCache (:286), whose published-data payload is identical for every
role. Breadth has a second driver that role-shaped variance does not cover: the admin surfaces read
back right after mutating, and a write that never touches a controller (the BR-207 speaker
auto-link, an Engagement bookmark) evicts only by publishing OutputCacheEvictionRequested, a
broker round trip that is prompt but not instant (Program.cs:253-259), so a cached stale row
version makes the next save throw DbUpdateConcurrencyException (Program.cs:260-262).
Rationale
- The response payload, not the request's auth state, is what determines cacheability. For a
user-independent payload,
Authorizationis noise; refusing to cache on it turns the whole read-scaling mechanism off for the real workload. - A server-side policy fixes every caller (UI, MAUI hosts, service-to-service, tooling) at the single point that owns the endpoint's caching semantics, instead of asking every client to special-case its transport.
- Constructor-parameterized expiration/tags keep the policy self-contained and testable; it does not depend on builder-chain internals that assume the default policy runs first.
Trade-offs
Consumers must audit which named policies move to
AddPublicEndpointPolicy. Policies on permission-gated endpoints (e.g. an organizer dashboard) must NOT move; if such an endpoint needs caching, that is a separate decision with per-user vary rules.The output-cache store must be Redis-backed wherever the service runs more than one replica. This supersedes the original trade-off, which accepted a per-replica in-memory store and its bounded staleness window. That acceptance did not survive contact with the deployed topology: ADC's Conference service and Store's Catalog service both run
minReplicas: 1with an HTTP scale rule at 50 concurrent requests and a ceiling of at least two replicas (StoremaxReplicas: 2,MMCA.Store/infra/main.bicep:1715; ADCconferenceScaledMaxReplicas,MMCA.ADC/infra/main.bicep:2067, which is 2 normally and 4 withconferenceModeon,:192), so everyEvictByTagAsyncreached only the replica that handled the mutation and the others kept serving the pre-edit payload for the full 5-minute TTL.Be precise about when this bites: it is a LATENT defect, not a continuously active one. Both services sat at one live replica at ordinary traffic when the running apps were checked on 2026-07-25. That is a runtime observation, not a repo fact: the committed Bicep pins only the allowed range (
minReplicas: 1up to the ceilings above), so re-checking the live count means looking at Azure again. At one replica there is nothing to propagate. The staleness appears only once the scale rule adds the second replica, which is to say under exactly the load the cache exists to absorb: ADC conference day (~67 peak concurrent) and a Store traffic spike. At that point an organizer renaming a session, or an admin repricing a product, sees the change apply to roughly half of subsequent reads at random. Each replica also fills its own copy, doubling cold-database traffic on the Basic-tier databases this policy protects, and in Store's case a few multi-megabyte image entries per replica crowd out the product and category JSON that matters most.A defect that only surfaces at peak is a worse one to carry, not a lesser one: it cannot be reproduced in the steady state and it arrives when there is least room to diagnose it.
Both apps had Redis provisioned and already wired as
IDistributedCache, so closing this was a registration rather than new infrastructure. Both hosts make it through the framework wrapperAddRedisOutputCaching()(MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Caching/RedisCachingExtensions.cs:91), which holds the framework's singleAddStackExchangeRedisOutputCachecall (:99) and no-ops when the connection string is blank (:94), so each host calls it unconditionally, top-level, beforeAddOutputCache(MMCA.ADC/Source/Services/MMCA.ADC.Conference.Service/Program.cs:195,MMCA.Store/Source/Services/MMCA.Store.Catalog.Service/Program.cs:99), leaning on the framework behavior their comments state:AddOutputCacheregisters its store withTryAdd(ADCProgram.cs:191-192, StoreProgram.cs:96-97), so an explicit Redis registration wins regardless of call order (only the Store comment spells out that consequence). Read that as framework behavior per those host comments; nothing in these repos verifies it.A single-replica service may still use the in-memory store: with one replica there is no propagation problem to solve. The rule is about replica count, not about environment.
Tag eviction reaches directly only the caches the mutating process can address; crossing a service boundary takes an explicit eviction event. Since ADR-026's Revision (2026-08-18), a mutation owned by a DIFFERENT service can request eviction through
OutputCacheEvictionRequestedover the existing outbox, broker and inbox path, best-effort and per tag. ADC's bookmark counts, written by Engagement and read through Conference, now use exactly that:UserSessionBookmarkCacheEvictionHandler(MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.Application/UserSessionBookmarks/DomainEventHandlers/UserSessionBookmarkCacheEvictionHandler.cs:37) only signals a request (:56), and the hostedBookmarkCacheEvictionProcessorraises the event (MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.Infrastructure/Caching/BookmarkCacheEvictionProcessor.cs:71), paced to at most one broadcast per 10-secondPacingWindowper replica (:46) so a burst of stars cannot keep the tag permanently cold. The Conference host consumes it (AddOutputCacheEvictionHandler()atMMCA.ADC/Source/Services/MMCA.ADC.Conference.Service/Program.cs:306, andRegisterOutputCacheEvictionConsumer()atMMCA.ADC/Source/Services/MMCA.ADC.Conference.Service/ConferenceBrokerConsumers.cs:37, wired throughAddBrokerMessagingatProgram.cs:419), keeping the short TTL as the backstop for a lost or late event rather than the only defense (BookmarkCountsCache, 60 seconds,Program.cs:300). A payload that changes on the clock still has no mutation to evict on, so a short TTL remains its whole answer (NowNextCache, 60 seconds, ADCProgram.cs:286; Store'sProductsCache, 60 seconds,MMCA.Store/Source/Services/MMCA.Store.Catalog.Service/Program.cs:159, whose discount edits still evict thecatalog:productstag at once but whose discount windows open and close with no write at all). When adding a cached endpoint, check which process owns every write that can change its payload, and whether time alone changes it.Cache hit rate becomes meaningful for authenticated load tests; k6 scripts that log in now exercise the same cache path as anonymous ones.
Revision (2026-09-10)
Store now uses the cross-service eviction path too, and the first case is an Application-layer handler rather than a controller. The trade-off above says to check which process owns every write that can change a cached payload. Store's anonymous review list failed that check in a way the controller-side eviction could not reach.
Catalog's CustomerErasedHandler consumes Identity's CustomerErased and clears the reviewer's
name, title and body while keeping the star rating (ADR-005 erasure across a database boundary),
looping the reviews at
MMCA.Store/Source/Modules/Catalog/MMCA.Store.Catalog.Application/Reviews/IntegrationEventHandlers/CustomerErasedHandler.cs:89-96
and saving at :98. Those three fields are exactly what the cached public list returns
(MMCA.Store/Source/Modules/Catalog/MMCA.Store.Catalog.Shared/Reviews/PublicProductReviewDTO.cs:45,
:48, :51), so before this change an erasure kept being served under the pre-erasure name for up
to the full five-minute TTL, on every replica. Store's eviction lived entirely in API controllers,
and an Application-layer handler has no IOutputCacheStore to call.
The answer is the one ADC already uses: publish the eviction rather than perform it. The handler
raises OutputCacheEvictionRequested for the catalog:products tag after the save
(CustomerErasedHandler.cs:113, the tag constant at :59, inside the publish block at :108-115),
wrapped in BestEffort.ExecuteAsync so a broker fault degrades the erasure to TTL-bounded staleness
instead of failing the consume and re-delivering it.
Both halves are registered, because one alone is silently inert.
MMCA.Store/Source/Services/MMCA.Store.Catalog.Service/Program.cs calls
AddOutputCacheEvictionHandler() at :179 and RegisterOutputCacheEvictionConsumer() at :286.
The first registers the handler that evicts; the second subscribes the host to the message. A host
with only the handler never receives the event, and a host with only the consumer receives it and
does nothing, and neither mistake produces an error, only a cache that quietly stops being evicted.
The mechanism itself is framework code and needed no change
(MMCA.Common/Source/Core/MMCA.Common.Domain/IntegrationEvents/OutputCacheEvictionRequested.cs:29,
MMCA.Common/Source/Presentation/MMCA.Common.API/Caching/OutputCacheEvictionExtensions.cs:111,
MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Messaging/Consumers/IntegrationEventConsumerExtensions.cs:108-110).
Unchanged and deliberate: Store registers no bypassRoles overload. Its four public policies take
the no-bypass form (MMCA.Store/Source/Services/MMCA.Store.Catalog.Service/Program.cs:153-164),
which is correct rather than an omission. The bypass exists for a privileged audience that reads the
same cached endpoint and needs to see more than it caches; Store has no such audience, because its
anonymous review list serves published rows only and moderator visibility is a separate,
permission-gated, uncached surface. Adding a bypass would be adding a partition nothing reads.
Revision (2026-09-25)
Store's ProductsCache TTL is 60 seconds, because its payload moves on the clock. A product
payload carries each variant's effective price, and a discount window opening or closing changes
that price with no write to evict on, so under a five-minute TTL a cached product kept the old
price for up to five minutes past the window's edge. This record already gives a clock-dependent
payload a short TTL as its whole answer (Status, and the eviction trade-off above, with ADC's
NowNextCache as the precedent), and Store now applies it: ProductsCache expires after 60
seconds (MMCA.Store/Source/Services/MMCA.Store.Catalog.Service/Program.cs:161, the reasoning at
:156-160). Discount edits still evict catalog:products immediately; CatalogCache,
CategoriesCache and ProductImagesCache keep five minutes (:154, :155, :164). The Context
now states the TTL mix.
Both hosts register the shared output-cache store the same way. ADC Conference and Store
Catalog each call the framework's AddRedisOutputCaching() unconditionally, top-level
(MMCA.ADC/Source/Services/MMCA.ADC.Conference.Service/Program.cs:193,
MMCA.Store/Source/Services/MMCA.Store.Catalog.Service/Program.cs:98), relying on the wrapper's own
blank-connection-string no-op
(MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Caching/RedisCachingExtensions.cs:94). The
multi-replica trade-off above now cites the wrapper rather than a per-host branch.
ADC's public-policy count is twelve, and eleven pass the bypass audience. The set is
ConferencePublicCache and EventsCache (:268, :269), eight more five-minute policies
(:275-282), NowNextCache (:285) and BookmarkCountsCache (:297); only NowNextCache omits
adminBypassRoles. The Decision's count and every ADC Conference anchor are refreshed. The
bypassRoles asymmetry between the two apps is unchanged and stays sanctioned, for the reason the
2026-09-10 revision gives.
Revision (2026-10-01)
No decision or rationale changed. Citations in Context, Decision and Trade-offs are re-anchored to
the current hosts: ADC Conference Program.cs (the adminBypassRoles array at :265, the twelve
public policies at :266-295 with NowNextCache at :283 and BookmarkCountsCache at :295,
the DbUpdateConcurrencyException reasoning at :257-258, AddRedisOutputCaching() at :194,
AddOutputCacheEvictionHandler() at :301, RegisterOutputCacheEvictionConsumer() at :413) and
Store Catalog Program.cs (CatalogCache :151, CategoriesCache :152, ProductsCache :158
with its reasoning at :153-157, ProductImagesCache :161). The twelve/eleven bypass count and
Store's four no-bypass policies are unchanged. The Trade-offs wording on the TryAdd comments is
narrowed: the ADC comment (Program.cs:190-191) states only that AddOutputCache registers its
store with TryAdd, and only the Store comment (Program.cs:95-96) adds that the explicit
registration wins regardless of order. One observation, not a change: Store's CatalogCache
(Program.cs:151) is registered but no [OutputCache(PolicyName = "CatalogCache")] references it
in Store Source, so of the four Store policies three back endpoints. The earlier Revision sections
keep the anchors they were written with.
Revision (2026-10-06)
- ADC bookmark eviction is raised by a paced processor, not the handler.
UserSessionBookmarkCacheEvictionHandleronly signals a request (MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.Application/UserSessionBookmarks/DomainEventHandlers/UserSessionBookmarkCacheEvictionHandler.cs:56); the hostedBookmarkCacheEvictionProcessorpublishesOutputCacheEvictionRequested(MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.Infrastructure/Caching/BookmarkCacheEvictionProcessor.cs:71) at most once per 10-secondPacingWindowper replica (:46). Trade-offs updated. - The admin-freshness driver for the broad bypass is restated. Application-layer writes now do
request eviction, so the reason is that their eviction is a broker round trip (prompt, not
instant), not that some writes never evict
(
MMCA.ADC/Source/Services/MMCA.ADC.Conference.Service/Program.cs:251-259). Decision updated. - ADC Conference's replica ceiling is conditional.
conferenceScaledMaxReplicasis 2 normally and 4 withconferenceModeon (MMCA.ADC/infra/main.bicep:191, used at:2033); Store Catalog stays atmaxReplicas: 2(MMCA.Store/infra/main.bicep:1715). Trade-offs updated. - The anonymous-load-evidence sentence in Context is now past tense. One k6 script now logs in
and sends a Bearer token (
MMCA.ADC/Tests/Load/k6/conference-day-attendee-load.js:107,:157), so "the k6 scripts are anonymous" no longer holds for every script. - Correction to the 2026-09-10 revision. The cached anonymous review list does not return the
reviewer name:
PublicProductReviewDTOdeliberately carries none (MMCA.Store/Source/Modules/Catalog/MMCA.Store.Catalog.Shared/Reviews/PublicProductReviewDTO.cs:8,:16-18). Of the erased fields it returnsTitle(:45) andBody(:48);:51isStatus. The list ridesProductsCache(MMCA.Store/Source/Modules/Catalog/MMCA.Store.Catalog.API/Controllers/ReviewsController.cs:90), so the pre-erasure window is bounded by its 60-second TTL, not five minutes. The eviction publish that revision describes is unaffected. - The
RegisterOutputCacheEvictionConsumeranchors moved. The framework method is atMMCA.Common/Source/Core/MMCA.Common.Infrastructure/Messaging/Consumers/IntegrationEventConsumerExtensions.cs:158-160; Store Catalog callsAddOutputCacheEvictionHandler()atProgram.cs:177andRegisterOutputCacheEvictionConsumer()at:290. - Anchors in Context, Decision and Trade-offs were re-verified against current source (ADC
Conference
Program.csarray:266, policies:267-298,NowNextCache:284,BookmarkCountsCache:298,AddRedisOutputCaching():195with its comment:191-192; Store CatalogProgram.cspolicies:152,:153,:159(reasoning:154-158),:162,AddRedisOutputCaching():99with its comment:96-97;IsPrivilegedConferenceReaderatCurrentUserServiceExtensions.cs:24). Store'sCatalogCache(:152) is still unreferenced.
Revision (2026-10-07)
Re-verified against current source. The decision, the strict apply-only-to-identity-independent
contract, the twelve/eleven ADC bypass count, Store's four no-bypass policies and the three
60-second TTLs (ADC NowNextCache and BookmarkCountsCache, Store ProductsCache) are unchanged. What moved: the Decision now records two properties of the policy it did not
mention, and the ADC Conference and Bicep anchors shifted after the v1.233.0 release.
- The cache key varies by the resolved tenant.
PublicEndpointOutputCachePolicystampsITenantContext.TenantIdintoVaryByValues["t"]when a tenant is resolved (MMCA.Common/Source/Presentation/MMCA.Common.API/Caching/PublicEndpointOutputCachePolicy.cs:98-106, the key constant at:50, the class-level SECURITY (tenancy) note at:37-42), so the policy is not only a relaxation of the default: it adds a tenancy tightening the default lacks. Decision updated. - The bypass check is the wide
HasRoleread, notIsInRole(:137-142), so an identity provider emitting barerole/rolesclaims cannot get an elevated response stored under the shared key. Decision updated. AuthDelegatingHandlerhas aSkipBeareropt-out. It attaches the stored Bearer only when noAuthorizationheader is set (MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Auth/AuthDelegatingHandler.cs:34-40), and a request carrying theSkipBeareroption (the token-refresh POST) passes through untouched (:18-22,:29). Context updated; the reads the policy targets still carry the token.- Anchors re-verified against current source: ADC Conference
Program.cs(adminBypassRolesarray:268, the twelve public policies:269-300,NowNextCache:286,BookmarkCountsCache:300, the broker-round-trip reasoning:253-259, theDbUpdateConcurrencyExceptionsentence:260-262,AddRedisOutputCaching():195with itsTryAddcomment:191-192,AddOutputCacheEvictionHandler():306,AddBrokerMessagingwithConferenceBrokerConsumers.Register:419);RegisterOutputCacheEvictionConsumer()atConferenceBrokerConsumers.cs:37;MMCA.ADC/infra/main.bicep(conferenceScaledMaxReplicasdefined at:192, the Conference scale rule at:2067); Store CatalogProgram.cs(policies:152,:153,:159with reasoning:154-158,:162,AddRedisOutputCaching():99) andMMCA.Store/infra/main.bicep:1715;RedisCachingExtensions.cs:91,:94,:99. The earlier Revision sections keep the anchors they were written with.