caching: Phase 2 Part 1 — client defaults, server-side cache-aside, shared table storage, connected players search/sort - #846
Conversation
…e, shared table storage, connected players search/sort)
Upgrade MX.Api.Client to 2.3.76 and add MX.Caching 0.1.5 wiring.
Client-side defaults (V1/V2) register AddDefaultCachePolicies per sub-API interface: GetGameServer/GetGameServers 60s, IMapsApi GetMap overloads + GetMaps 10min, all mutations and user-profile/auth/info/health NotCached.
Server-side cache-aside via service-seam decorators below controllers: GameServer (gameserver:{id}), Dashboard (dashboard:{metric}:{window}), Configuration (settings:server/global/ns tags, alias-normalised). Cross-instance eviction via IMxCache.RemoveByTagAsync from a RepositoryCacheInvalidator on mutations. Meter-based hit/miss/eviction metrics.
Add NoStoreCacheAttribute; apply to V1+V2 ApiInfo and Health controllers with focused tests.
Repoint ILiveStatusStore to shared cache Table endpoint from portal-core (fallback to legacy appdata endpoint) using managed identity, with MI-safe CreateIfNotExists for GameServerLiveStatus/GameServerLivePlayers at startup.
Terraform: consume portal_core cache_storage output via try(); wire shared_cache_storage_table_endpoint + MxCaching:{Backend,TableStorage:Endpoint,TableName} into V1 app settings, with graceful InMemory fallback.
ConnectedPlayers published contract: add server-side search (Username/PlayerId/UserProfileId/LinkMethod/GameType) and typed ConnectedPlayersOrder enum (default LinkedAtUtcDesc). Update Abstractions.V1, Api.V1 controller, Api.Client.V1, Api.Client.Testing fake, and tests. Paged shape returns filteredCount for DataTables semantics.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Dependency ReviewThe following issues were found:
License Issuessrc/XtremeIdiots.Portal.Repository.Abstractions.V1/XtremeIdiots.Portal.Repository.Abstractions.V1.csproj
src/XtremeIdiots.Portal.Repository.Abstractions.V2/XtremeIdiots.Portal.Repository.Abstractions.V2.csproj
src/XtremeIdiots.Portal.Repository.Api.Client.Testing/XtremeIdiots.Portal.Repository.Api.Client.Testing.csproj
src/XtremeIdiots.Portal.Repository.Api.Client.V1/XtremeIdiots.Portal.Repository.Api.Client.V1.csproj
src/XtremeIdiots.Portal.Repository.Api.Client.V2/XtremeIdiots.Portal.Repository.Api.Client.V2.csproj
src/XtremeIdiots.Portal.Repository.Api.Tests.V1/XtremeIdiots.Portal.Repository.Api.Tests.V1.csproj
src/XtremeIdiots.Portal.Repository.Api.V1/XtremeIdiots.Portal.Repository.Api.V1.csproj
src/XtremeIdiots.Portal.Repository.Api.V2/XtremeIdiots.Portal.Repository.Api.V2.csproj
OpenSSF ScorecardScorecard details
Scanned Files
|
There was a problem hiding this comment.
Pull request overview
This PR implements Phase 2 Part 1 caching work in the portal-repository scope by introducing client-side default cache policies, adding server-side cache-aside decorators with cross-instance tag eviction, switching LiveStatus + caching to prefer a shared Table Storage endpoint, and extending the Connected Players contract with server-side search + ordering.
Changes:
- Added server-side read-service seams (game servers, dashboard, configurations) and cache-aside decorators backed by
IMxCache, with tag-based invalidation and surface-level metrics. - Added “never-cache” response headers (
Cache-Control: no-storeetc.) for V1/V2/infoand/health/*, with unit tests. - Extended
GetConnectedPlayerswithsearchString+order, implemented SQL-side filtering/sorting/paging, and updated published contracts + clients + fakes + tests accordingly.
Reviewed changes
Copilot reviewed 54 out of 54 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| terraform/web_app_v1.tf | Adds app settings to wire shared Table Storage endpoint and MX.Caching backend selection. |
| terraform/locals.tf | Reads optional cache_storage output from portal-core remote state with try(...) fallbacks. |
| src/XtremeIdiots.Portal.Repository.Api.V2/XtremeIdiots.Portal.Repository.Api.V2.csproj | Bumps MX API packages to 2.3.76. |
| src/XtremeIdiots.Portal.Repository.Api.V2/Extensions/NoStoreCacheAttribute.cs | Adds V2 action filter to stamp never-cache response headers. |
| src/XtremeIdiots.Portal.Repository.Api.V2/Controllers/V2/HealthController.cs | Applies [NoStoreCache] to V2 health endpoints. |
| src/XtremeIdiots.Portal.Repository.Api.V2/Controllers/V2/ApiInfoController.cs | Applies [NoStoreCache] to V2 info endpoint. |
| src/XtremeIdiots.Portal.Repository.Api.V1/XtremeIdiots.Portal.Repository.Api.V1.csproj | Bumps MX packages and adds MX.Caching packages. |
| src/XtremeIdiots.Portal.Repository.Api.V1/Services/ServiceCollectionExtensions.cs | Registers read services + optional caching decorators and invalidator. |
| src/XtremeIdiots.Portal.Repository.Api.V1/Services/IGameServerReadService.cs | Introduces seam for game server reads to support caching decorator. |
| src/XtremeIdiots.Portal.Repository.Api.V1/Services/IDashboardService.cs | Introduces seam for dashboard aggregations to support caching decorator. |
| src/XtremeIdiots.Portal.Repository.Api.V1/Services/IConfigurationReadService.cs | Introduces seam for configuration reads to support caching decorator. |
| src/XtremeIdiots.Portal.Repository.Api.V1/Services/GameServerReadService.cs | Moves uncached game server read logic into a service. |
| src/XtremeIdiots.Portal.Repository.Api.V1/Services/DashboardService.cs | Moves uncached dashboard aggregation logic into a service. |
| src/XtremeIdiots.Portal.Repository.Api.V1/Services/ConfigurationReadService.cs | Moves uncached configuration read logic into a service (incl. server-list global compatibility behavior). |
| src/XtremeIdiots.Portal.Repository.Api.V1/Services/Caching/RepositoryCacheMetrics.cs | Adds meter/counters for repository cache-aside hit/miss/eviction metrics. |
| src/XtremeIdiots.Portal.Repository.Api.V1/Services/Caching/RepositoryCacheKeys.cs | Centralizes cache key/tag conventions for server-side caching surfaces. |
| src/XtremeIdiots.Portal.Repository.Api.V1/Services/Caching/RepositoryCacheInvalidator.cs | Implements tag-based eviction across instances via IMxCache.RemoveByTagAsync. |
| src/XtremeIdiots.Portal.Repository.Api.V1/Services/Caching/NoOpRepositoryCacheInvalidator.cs | Provides no-op invalidator for when shared caching is not configured. |
| src/XtremeIdiots.Portal.Repository.Api.V1/Services/Caching/IRepositoryCacheInvalidator.cs | Defines a surface-aware invalidation seam for mutation controllers. |
| src/XtremeIdiots.Portal.Repository.Api.V1/Services/Caching/CachingGameServerReadService.cs | Adds cache-aside behavior for GetGameServer with tag-based eviction support. |
| src/XtremeIdiots.Portal.Repository.Api.V1/Services/Caching/CachingDashboardService.cs | Adds cache-aside behavior for dashboard aggregations with a shared “dashboard” tag. |
| src/XtremeIdiots.Portal.Repository.Api.V1/Services/Caching/CachingConfigurationReadService.cs | Adds cache-aside behavior for server/global configuration reads with namespace tags. |
| src/XtremeIdiots.Portal.Repository.Api.V1/Program.cs | Prefers shared Table Storage endpoint, wires MX.Caching + decorators, and bootstraps LiveStatus tables via MI. |
| src/XtremeIdiots.Portal.Repository.Api.V1/Extensions/NoStoreCacheAttribute.cs | Adds V1 action filter to stamp never-cache response headers. |
| src/XtremeIdiots.Portal.Repository.Api.V1/Controllers/V1/HealthController.cs | Applies [NoStoreCache] to V1 health endpoints. |
| src/XtremeIdiots.Portal.Repository.Api.V1/Controllers/V1/GlobalConfigurationsController.cs | Uses configuration read service and evicts cache tags on mutations. |
| src/XtremeIdiots.Portal.Repository.Api.V1/Controllers/V1/GameServersController.cs | Uses game server read service and evicts game server + dashboard tags on mutations. |
| src/XtremeIdiots.Portal.Repository.Api.V1/Controllers/V1/GameServerConfigurationsController.cs | Uses configuration read service and evicts server-settings tags on mutations. |
| src/XtremeIdiots.Portal.Repository.Api.V1/Controllers/V1/DashboardController.cs | Delegates dashboard endpoints to IDashboardService (enables caching decorator). |
| src/XtremeIdiots.Portal.Repository.Api.V1/Controllers/V1/ConnectedPlayersController.cs | Adds searchString + order, implements SQL-side filtering/sorting/paging and returns total vs filtered counts. |
| src/XtremeIdiots.Portal.Repository.Api.V1/Controllers/V1/ApiInfoController.cs | Applies [NoStoreCache] to V1 info endpoint. |
| src/XtremeIdiots.Portal.Repository.Api.Tests.V2/Extensions/NoStoreCacheAttributeTests.cs | Adds tests verifying V2 never-cache headers and attribute application. |
| src/XtremeIdiots.Portal.Repository.Api.Tests.V1/XtremeIdiots.Portal.Repository.Api.Tests.V1.csproj | Adds MX.Caching.Testing dependency for cache-aside tests. |
| src/XtremeIdiots.Portal.Repository.Api.Tests.V1/Services/Caching/CachingReadServicesTests.cs | Adds unit tests for cache-aside decorators and tag eviction using FakeMxCache. |
| src/XtremeIdiots.Portal.Repository.Api.Tests.V1/Extensions/NoStoreCacheAttributeTests.cs | Adds tests verifying V1 never-cache headers and attribute application. |
| src/XtremeIdiots.Portal.Repository.Api.Tests.V1/Controllers/V1/GlobalConfigurationsControllerTests.cs | Updates controller tests for new DI seams (read service + invalidator). |
| src/XtremeIdiots.Portal.Repository.Api.Tests.V1/Controllers/V1/GameServersControllerTests.cs | Updates controller tests for new DI seams (read service + invalidator). |
| src/XtremeIdiots.Portal.Repository.Api.Tests.V1/Controllers/V1/GameServerConfigurationsControllerTests.cs | Updates controller tests for new DI seams (read service + invalidator). |
| src/XtremeIdiots.Portal.Repository.Api.Tests.V1/Controllers/V1/ConnectedPlayersControllerTests.cs | Adds tests for connected players search/sort/paging and total vs filtered counts. |
| src/XtremeIdiots.Portal.Repository.Api.Client.V2/XtremeIdiots.Portal.Repository.Api.Client.V2.csproj | Bumps MX.Api.Client/MX.Api.Abstractions to 2.3.76. |
| src/XtremeIdiots.Portal.Repository.Api.Client.V2/ServiceCollectionExtensions.cs | Registers default cache policies per V2 sub-API interface. |
| src/XtremeIdiots.Portal.Repository.Api.Client.V2/Caching/RepositoryApiCacheDefaults.cs | Defines V2 client defaults (info/health explicitly NotCached). |
| src/XtremeIdiots.Portal.Repository.Api.Client.V1/XtremeIdiots.Portal.Repository.Api.Client.V1.csproj | Bumps MX.Api.Client/MX.Api.Abstractions to 2.3.76. |
| src/XtremeIdiots.Portal.Repository.Api.Client.V1/ServiceCollectionExtensions.cs | Registers default cache policies per V1 sub-API interface. |
| src/XtremeIdiots.Portal.Repository.Api.Client.V1/Caching/RepositoryApiCacheDefaults.cs | Defines V1 client defaults (game servers/maps cached, probes + mutations NotCached). |
| src/XtremeIdiots.Portal.Repository.Api.Client.V1/Api/V1/ConnectedPlayersApi.cs | Adds query serialization for searchString and order. |
| src/XtremeIdiots.Portal.Repository.Api.Client.Tests.V2/RepositoryApiCacheDefaultsTests.cs | Tests V2 cache default policy registrations. |
| src/XtremeIdiots.Portal.Repository.Api.Client.Tests.V1/RepositoryApiCacheDefaultsTests.cs | Tests V1 cache default policy registrations (TTLs, overload disambiguation, never-cache guards). |
| src/XtremeIdiots.Portal.Repository.Api.Client.Testing/XtremeIdiots.Portal.Repository.Api.Client.Testing.csproj | Bumps MX.Api.Abstractions to 2.3.76. |
| src/XtremeIdiots.Portal.Repository.Api.Client.Testing/Fakes/FakeConnectedPlayersApi.cs | Updates fake to honor searchString + order. |
| src/XtremeIdiots.Portal.Repository.Abstractions.V2/XtremeIdiots.Portal.Repository.Abstractions.V2.csproj | Bumps MX.Api.Abstractions to 2.3.76. |
| src/XtremeIdiots.Portal.Repository.Abstractions.V1/XtremeIdiots.Portal.Repository.Abstractions.V1.csproj | Bumps MX.Api.Abstractions to 2.3.76. |
| src/XtremeIdiots.Portal.Repository.Abstractions.V1/Interfaces/V1/IConnectedPlayersApi.cs | Extends GetConnectedPlayers signature with searchString + order. |
| src/XtremeIdiots.Portal.Repository.Abstractions.V1/Constants/V1/ConnectedPlayersOrder.cs | Adds ordering enum for connected players query ordering. |
Suppressed comments (2)
src/XtremeIdiots.Portal.Repository.Api.V1/Services/Caching/CachingConfigurationReadService.cs:81
- Same as the server configuration path: the caching decorator should mirror the inner service’s 128-char namespace guard to avoid constructing cache keys/tags for invalid input.
src/XtremeIdiots.Portal.Repository.Api.V1/Services/Caching/RepositoryCacheInvalidator.cs:48 InvalidateGlobalNamespaceAsynchas the same alias-normalization mismatch as the per-server invalidation. Normalizing here ensures global upserts/deletes evict the same canonical tags that the read-side caching layer uses.
⛔ Superseded — A newer run has replaced this result.🏗️ Terraform Plan
❌ Validate — Failed
|
- terraform: use storage_account_id on azurerm_storage_table (v5 provider); coalesce shared cache endpoint app settings to avoid null strings - caching: normalize legacy namespace aliases inside RepositoryCacheInvalidator so alias-based writes evict canonical-tagged cache entries - caching: extend defence-in-depth length guard (>128) on CachingConfigurationReadService read paths - controllers: deterministic ConnectedPlayers pagination via ThenBy(ConnectedPlayerProfileId) - tests: add symmetric invalidator alias-normalization regression Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Addressed all 4 Copilot review threads plus the failing
Validation:
All 4 review threads resolved. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 55 out of 55 changed files in this pull request and generated no new comments.
Suppressed comments (4)
src/XtremeIdiots.Portal.Repository.Api.Client.Testing/Fakes/FakeConnectedPlayersApi.cs:251
- FakeConnectedPlayersApi.GetConnectedPlayers doesn’t clamp
skipEntries/takeEntriesor return pagination totals, but the real API now clamps (skip>=0, 1<=take<=500) and returnsTotalCount/FilteredCountin the pagination envelope. This mismatch can cause consumer tests to pass against the fake but fail in production (e.g.takeEntries=0returning an empty set in tests).
var items = query.Skip(skipEntries).Take(takeEntries).ToList();
var collection = new CollectionModel<ConnectedPlayerDto> { Items = items };
return Task.FromResult(new ApiResult<CollectionModel<ConnectedPlayerDto>>(
HttpStatusCode.OK,
src/XtremeIdiots.Portal.Repository.Api.V1/Services/ConfigurationReadService.cs:54
- GetServerConfigurationAsync queries GameServerConfigurations using the raw
nsstring. When caching is enabled, CachingConfigurationReadService normalizes legacy aliases (e.g.serverList-> canonical) before delegating to this service, but mutation endpoints can still persist rows under the legacy alias (they validate via TryValidate but do not normalize before saving). This combination will cause server configuration rows stored under the alias to become unreachable (404) when caching is enabled. Align server-configuration reads with the global-config compatibility behavior by normalizing and, for the server-list namespace, querying both canonical and legacy namespaces.
src/XtremeIdiots.Portal.Repository.Api.V1/Controllers/V1/ConnectedPlayersController.cs:664 - The search predicate lowercases database columns (
Username.ToLower(),LinkMethod.ToLower()) and usesLIKEwith unescaped user input. Lowercasing the column forces SQL to apply a function to the field (hurting index usage), and unescaped%/_in the user-provided search string will be treated as wildcards rather than literal characters (e.g. searching for "%" will match everything). Build an escaped LIKE pattern from the input and applyEF.Functions.Liketo the raw columns.
query = query.Where(cp =>
(cp.Player.Username != null && EF.Functions.Like(cp.Player.Username.ToLower(), "%" + lowered + "%"))
|| EF.Functions.Like(cp.LinkMethod.ToLower(), "%" + lowered + "%")
|| (isGuid && (cp.PlayerId == searchGuid || cp.UserProfileId == searchGuid))
|| (isGameType && cp.Player.GameType == (int)searchGameType));
src/XtremeIdiots.Portal.Repository.Api.V1/Program.cs:173
- The comment says that when the
MxCachingsection is absent, "the caching decorators remain functional", butmxCachingConfiguredgates decorator registration and will be false when the section is absent. Either enable the decorators unconditionally (if that’s the intended behavior) or update the comment to reflect that decorators are only enabled when the section exists.
|
🏗️ Terraform Plan
✅ Validate — Passed ✅ Plan
📋 Resource Details
|



Summary
Implements the portal-repository-owned scope of the Phase 2 Part 1 caching work package: bumps MX.Api.Client to 2.3.76, registers V1/V2 client-side default cache policies per sub-API, introduces server-side cache-aside decorators (game server / dashboard / configurations) with cross-instance tag eviction via
IMxCache, addsCache-Control: no-storeon info/health, repointsILiveStatusStoreat the shared Table Storage cache endpoint from portal-core with MI-safe table creation, wires the Terraform slice, and extends the connected-players published contract with server-side search and typed ordering.Type of change
What changed
Package bumps
MX.Api.Client,MX.Api.Abstractions,MX.Api.Web.Extensions→2.3.76across the two hosts, Abstractions.V1/V2, Client.V1/V2, and Client.Testing.MX.Caching0.1.5+MX.Caching.TableStorage+MX.Caching.Testingwhere needed.Client library defaults (V1 + V2)
Registers
AddDefaultCachePolicies<TSubApi>(…)per sub-API interface (aligned with eachAddTypedApiClient<TSubApi>) soDefaultCachePolicies<TSubApi>resolves correctly:IGameServersApi.GetGameServer(id)/GetGameServers(...)cached 60s;IMapsApi.GetMapoverloads +GetMapscached 10 min (invocation expressions disambiguate overloads);IUserProfileApi,IApiInfoApi,IApiHealthApiand all mutationsNotCached.IApiInfoApi/IApiHealthApiNotCached. V2 currently exposes only info/health; no resource surfaces to cache.New tests:
RepositoryApiCacheDefaultsTestson both client test projects prove effective policy set.Server-side cache-aside (below controllers)
Introduces clean service seams and caching decorators — controllers no longer receive
IMxCachedirectly:GameServerReadService/CachingGameServerReadService— 60s, taggameserver:{id}.DashboardService/CachingDashboardService— 60–120s, tagsdashboard:{metric}:{window}(dashboard summary, admin leaderboard 30d, moderation trend 30d, server utilization).ConfigurationReadService/CachingConfigurationReadService— 5 min, tagssettings:server:{gameServerId}:{ns},settings:global:{ns},settings:ns:{ns}. Legacy namespace aliases (e.g.serverList) are normalised before key/tag computation so alias-scoped entries evict on canonical-scoped writes.RepositoryCacheInvalidator— cross-instance eviction viaIMxCache.RemoveByTagAsyncon successful create/update/delete/bulk mutations.RepositoryCacheMetrics—System.Diagnostics.Metrics.Metercounters for hit / miss / eviction.Never-cache guards
NoStoreCacheAttribute(V1 + V2) applied toApiInfoControllerandHealthController, stampingCache-Control: no-store,Pragma: no-cache,Expires: 0. Focused unit tests on both hosts. Deployment version-verification poll behaviour preserved.Shared Table Storage cache
Program.csnow prefersshared_cache_storage_table_endpoint(portal-core) forILiveStatusStoreand MX.Caching, falling back to legacyappdata_storage_table_endpoint. Managed identity only; no keys/SAS.CreateTableIfNotExistsAsyncforGameServerLiveStatus/GameServerLivePlayersat startup (portal-core intentionally does not pre-create tables; MX.Caching self-creates its own configured table).AddMxCaching(Configuration)wiring.Terraform slice
locals.tfconsumesdata.terraform_remote_state.portal_core.outputs.cache_storageviatry(...)— graceful when the upstream output is not yet published.web_app_v1.tfadds V1 app settings:shared_cache_storage_table_endpoint,MxCaching__Backend(TableStoragewhen the shared endpoint is present, elseInMemory),MxCaching__TableStorage__Endpoint,MxCaching__TableStorage__TableName=RepositoryCache.table_storage+ LiveStatus tables + local role assignment retained for safe cutover / rollback; removal deferred until every environment’s portal-core state exposescache_storage.Connected players — published contract change
ConnectedPlayersOrderenum (GameType, Username, LinkMethod, IsActive, LinkedAtUtc, UnlinkedAtUtc × Asc/Desc; defaultLinkedAtUtcDesc).IConnectedPlayersApi.GetConnectedPlayersgainssearch(matches Username, PlayerId, UserProfileId, LinkMethod, GameType) andorderparameters.filteredCountdistinct from unfilteredtotalCount(DataTables semantics).FakeConnectedPlayersApi, and tests updated.Consumer impact
XtremeIdiots.Portal.Repository.Abstractions.V1— breaking:IConnectedPlayersApi.GetConnectedPlayerssignature addssearch+order; response paged shape addsfilteredCount. New publicConnectedPlayersOrderenum.XtremeIdiots.Portal.Repository.Api.Client.V1— matches Abstractions.V1 above; also gains client-side default cache policies viaUseLibraryDefaults(). Consumers usingWithoutLibraryDefaults()see no behavioural change.XtremeIdiots.Portal.Repository.Api.Client.V2— new default cache policies for info/health only.XtremeIdiots.Portal.Repository.Api.Client.Testing—FakeConnectedPlayersApihonours the new parameters.GetConnectedPlayerscall sites to passsearch/orderand readfilteredCount.cache_storageoutput (shape{ id, name, table_endpoint }) is consumed viatry(...). When absent, the host and MxCaching fall back to legacy Table Storage / in-memory — rollback-safe.Validation evidence
Code-review sub-agent run: one Medium finding (config decorator did not normalise legacy namespace aliases → alias-scoped entries lingered until TTL). Fixed in-scope with a regression test that proves canonical-scoped invalidation evicts alias reads.
Risk and rollout
/v1.0/infoand/v2.0/infowhich now carryno-store(verified with unit tests).GetConnectedPlayerssignature before its next release. Once every environment's portal-core state exposescache_storage, we can drop the repo-local Table Storage + role assignment in a follow-up.try(...)and Program.cs fallback mean absence of the portal-core output degrades gracefully to legacy Table Storage / in-memory; reverting this PR restores prior behaviour without data loss (LiveStatus tables live in shared storage but the schema is unchanged).Known limitations / upstream dependencies
gameserver:{id}at the client layer is impossible. Instance-scoped tagging is handled by the server-side decorator where the actual GUID is bound at runtime.cache_storageoutput — consumed viatry(...); behaviour degrades gracefully if not yet published in a given environment.azurerm_storage_table.storage_account_name(azurerm v5) is untouched by this PR and out of scope for the caching work.Agent attestation
ApiResponse/CollectionResult), routing, managed identity, settings validation, and compatibility shims.dotnet build, unit tests,dotnet format --verify-no-changes,terraform fmt -check -recursive— all clean.