Skip to content

caching: Phase 2 Part 1 — client defaults, server-side cache-aside, shared table storage, connected players search/sort - #846

Merged
frasermolyneux merged 2 commits into
mainfrom
agents/portal-caching-phase2-part1-implementation
Aug 2, 2026
Merged

caching: Phase 2 Part 1 — client defaults, server-side cache-aside, shared table storage, connected players search/sort#846
frasermolyneux merged 2 commits into
mainfrom
agents/portal-caching-phase2-part1-implementation

Conversation

@frasermolyneux

Copy link
Copy Markdown
Owner

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, adds Cache-Control: no-store on info/health, repoints ILiveStatusStore at 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

  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected) — see Consumer impact
  • Bug fix
  • Refactor / hardening (no behavioural change)
  • Docs only
  • Chore / dependency bump

What changed

Package bumps

  • MX.Api.Client, MX.Api.Abstractions, MX.Api.Web.Extensions2.3.76 across the two hosts, Abstractions.V1/V2, Client.V1/V2, and Client.Testing.
  • Add MX.Caching 0.1.5 + MX.Caching.TableStorage + MX.Caching.Testing where needed.

Client library defaults (V1 + V2)

Registers AddDefaultCachePolicies<TSubApi>(…) per sub-API interface (aligned with each AddTypedApiClient<TSubApi>) so DefaultCachePolicies<TSubApi> resolves correctly:

  • V1IGameServersApi.GetGameServer(id) / GetGameServers(...) cached 60s; IMapsApi.GetMap overloads + GetMaps cached 10 min (invocation expressions disambiguate overloads); IUserProfileApi, IApiInfoApi, IApiHealthApi and all mutations NotCached.
  • V2IApiInfoApi / IApiHealthApi NotCached. V2 currently exposes only info/health; no resource surfaces to cache.

New tests: RepositoryApiCacheDefaultsTests on 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 IMxCache directly:

  • GameServerReadService / CachingGameServerReadService — 60s, tag gameserver:{id}.
  • DashboardService / CachingDashboardService — 60–120s, tags dashboard:{metric}:{window} (dashboard summary, admin leaderboard 30d, moderation trend 30d, server utilization).
  • ConfigurationReadService / CachingConfigurationReadService — 5 min, tags settings: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 via IMxCache.RemoveByTagAsync on successful create/update/delete/bulk mutations.
  • RepositoryCacheMetricsSystem.Diagnostics.Metrics.Meter counters for hit / miss / eviction.

Never-cache guards

  • New NoStoreCacheAttribute (V1 + V2) applied to ApiInfoController and HealthController, stamping Cache-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.cs now prefers shared_cache_storage_table_endpoint (portal-core) for ILiveStatusStore and MX.Caching, falling back to legacy appdata_storage_table_endpoint. Managed identity only; no keys/SAS.
  • MI-safe CreateTableIfNotExistsAsync for GameServerLiveStatus / GameServerLivePlayers at startup (portal-core intentionally does not pre-create tables; MX.Caching self-creates its own configured table).
  • Adds AddMxCaching(Configuration) wiring.

Terraform slice

  • locals.tf consumes data.terraform_remote_state.portal_core.outputs.cache_storage via try(...) — graceful when the upstream output is not yet published.
  • web_app_v1.tf adds V1 app settings: shared_cache_storage_table_endpoint, MxCaching__Backend (TableStorage when the shared endpoint is present, else InMemory), MxCaching__TableStorage__Endpoint, MxCaching__TableStorage__TableName=RepositoryCache.
  • Repository-local table_storage + LiveStatus tables + local role assignment retained for safe cutover / rollback; removal deferred until every environment’s portal-core state exposes cache_storage.

Connected players — published contract change

  • New ConnectedPlayersOrder enum (GameType, Username, LinkMethod, IsActive, LinkedAtUtc, UnlinkedAtUtc × Asc/Desc; default LinkedAtUtcDesc).
  • IConnectedPlayersApi.GetConnectedPlayers gains search (matches Username, PlayerId, UserProfileId, LinkMethod, GameType) and order parameters.
  • Controller filters/orders/pages in SQL; response includes filteredCount distinct from unfiltered totalCount (DataTables semantics).
  • Client.V1 query serialization, FakeConnectedPlayersApi, and tests updated.

Consumer impact

  • XtremeIdiots.Portal.Repository.Abstractions.V1breaking: IConnectedPlayersApi.GetConnectedPlayers signature adds search + order; response paged shape adds filteredCount. New public ConnectedPlayersOrder enum.
  • XtremeIdiots.Portal.Repository.Api.Client.V1 — matches Abstractions.V1 above; also gains client-side default cache policies via UseLibraryDefaults(). Consumers using WithoutLibraryDefaults() see no behavioural change.
  • XtremeIdiots.Portal.Repository.Api.Client.V2 — new default cache policies for info/health only.
  • XtremeIdiots.Portal.Repository.Api.Client.TestingFakeConnectedPlayersApi honours the new parameters.
  • portal-web must update its GetConnectedPlayers call sites to pass search / order and read filteredCount.
  • portal-core dependency: cache_storage output (shape { id, name, table_endpoint }) is consumed via try(...). When absent, the host and MxCaching fall back to legacy Table Storage / in-memory — rollback-safe.

Validation evidence

dotnet build src\XtremeIdiots.Portal.Repository.sln
  Build succeeded. 0 Warning(s) 0 Error(s)

dotnet test src\XtremeIdiots.Portal.Repository.sln --no-build --nologo --filter "FullyQualifiedName!~IntegrationTests"
  Api.Client.Tests.V1        46 passed × 2 TFMs
  Api.Client.Tests.V2        14 passed × 2 TFMs
  Api.Client.Testing.Tests   72 passed × 2 TFMs
  Api.Tests.V1              446 passed, 10 skipped × 2 TFMs
  Api.Tests.V2                9 passed × 2 TFMs
  Settings.Contracts.V1      44 passed × 2 TFMs
  TOTAL: 631 passed / TFM, 0 failed

dotnet format src\XtremeIdiots.Portal.Repository.sln --verify-no-changes
  clean (exit 0)

terraform -chdir=terraform fmt -check -recursive
  clean (exit 0)

terraform -chdir=terraform validate
  FAILS — pre-existing azurerm v5 schema drift on storage_account.tf
  (azurerm_storage_table.storage_account_name deprecated). Untouched by this
  diff and unrelated to the caching scope.

terraform init/plan against Azure
  not attempted — no dev backend credentials available in this environment.

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

  • Blast radius — V1 API host (game servers / dashboard / configurations paths) + LiveStatus wiring + published Client.V1 contract for connected players.
  • Auto-deploy — via existing dev/prd workflows; deploy verification polls /v1.0/info and /v2.0/info which now carry no-store (verified with unit tests).
  • Manual steps post-merge — portal-web must be updated to the new GetConnectedPlayers signature before its next release. Once every environment's portal-core state exposes cache_storage, we can drop the repo-local Table Storage + role assignment in a follow-up.
  • Rollback — Terraform 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

  • MX.Api.Client 2.3.76 does not support parameter-templated dynamic tags on client-side policies — literal 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.
  • portal-core cache_storage output — consumed via try(...); behaviour degrades gracefully if not yet published in a given environment.
  • Pre-existing terraform validate failure on azurerm_storage_table.storage_account_name (azurerm v5) is untouched by this PR and out of scope for the caching work.

Agent attestation

  • Followed AGENTS.md and repository copilot instructions.
  • Preserved API envelope (ApiResponse / CollectionResult), routing, managed identity, settings validation, and compatibility shims.
  • Did not edit generated DataLib or the Database schema.
  • Did not introduce secrets or connection strings; managed identity only.
  • Did not add controller-level cache hacks, broad catches, or silent fallbacks.
  • Ran dotnet build, unit tests, dotnet format --verify-no-changes, terraform fmt -check -recursive — all clean.
  • Ran the code-review sub-agent; the Medium finding was resolved with a regression test.
  • Published contract changes (Abstractions.V1 / Client.V1 / Client.Testing) are documented under Consumer impact.

…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>
Copilot AI review requested due to automatic review settings August 2, 2026 09:12
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Dependency Review

The following issues were found:
  • ✅ 0 vulnerable package(s)
  • ✅ 0 package(s) with incompatible licenses
  • ✅ 0 package(s) with invalid SPDX license definitions
  • ⚠️ 15 package(s) with unknown licenses.
See the Details below.

License Issues

src/XtremeIdiots.Portal.Repository.Abstractions.V1/XtremeIdiots.Portal.Repository.Abstractions.V1.csproj

PackageVersionLicenseIssue Type
MX.Api.Abstractions2.3.76NullUnknown License

src/XtremeIdiots.Portal.Repository.Abstractions.V2/XtremeIdiots.Portal.Repository.Abstractions.V2.csproj

PackageVersionLicenseIssue Type
MX.Api.Abstractions2.3.76NullUnknown License

src/XtremeIdiots.Portal.Repository.Api.Client.Testing/XtremeIdiots.Portal.Repository.Api.Client.Testing.csproj

PackageVersionLicenseIssue Type
MX.Api.Abstractions2.3.76NullUnknown License

src/XtremeIdiots.Portal.Repository.Api.Client.V1/XtremeIdiots.Portal.Repository.Api.Client.V1.csproj

PackageVersionLicenseIssue Type
MX.Api.Abstractions2.3.76NullUnknown License
MX.Api.Client2.3.76NullUnknown License

src/XtremeIdiots.Portal.Repository.Api.Client.V2/XtremeIdiots.Portal.Repository.Api.Client.V2.csproj

PackageVersionLicenseIssue Type
MX.Api.Abstractions2.3.76NullUnknown License
MX.Api.Client2.3.76NullUnknown License

src/XtremeIdiots.Portal.Repository.Api.Tests.V1/XtremeIdiots.Portal.Repository.Api.Tests.V1.csproj

PackageVersionLicenseIssue Type
MX.Caching.Testing0.1.5NullUnknown License

src/XtremeIdiots.Portal.Repository.Api.V1/XtremeIdiots.Portal.Repository.Api.V1.csproj

PackageVersionLicenseIssue Type
MX.Api.Abstractions2.3.76NullUnknown License
MX.Api.Web.Extensions2.3.76NullUnknown License
MX.Caching0.1.5NullUnknown License
MX.Caching.Abstractions0.1.5NullUnknown License
MX.Caching.TableStorage0.1.5NullUnknown License

src/XtremeIdiots.Portal.Repository.Api.V2/XtremeIdiots.Portal.Repository.Api.V2.csproj

PackageVersionLicenseIssue Type
MX.Api.Abstractions2.3.76NullUnknown License
MX.Api.Web.Extensions2.3.76NullUnknown License

OpenSSF Scorecard

Scorecard details
PackageVersionScoreDetails
nuget/MX.Api.Abstractions 2.3.76 UnknownUnknown
nuget/MX.Api.Abstractions 2.3.76 UnknownUnknown
nuget/MX.Api.Abstractions 2.3.76 UnknownUnknown
nuget/MX.Api.Abstractions 2.3.76 UnknownUnknown
nuget/MX.Api.Client 2.3.76 UnknownUnknown
nuget/MX.Api.Abstractions 2.3.76 UnknownUnknown
nuget/MX.Api.Client 2.3.76 UnknownUnknown
nuget/MX.Caching.Testing 0.1.5 UnknownUnknown
nuget/MX.Api.Abstractions 2.3.76 UnknownUnknown
nuget/MX.Api.Web.Extensions 2.3.76 UnknownUnknown
nuget/MX.Caching 0.1.5 UnknownUnknown
nuget/MX.Caching.Abstractions 0.1.5 UnknownUnknown
nuget/MX.Caching.TableStorage 0.1.5 UnknownUnknown
nuget/MX.Api.Abstractions 2.3.76 UnknownUnknown
nuget/MX.Api.Web.Extensions 2.3.76 UnknownUnknown

Scanned Files

  • src/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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-store etc.) for V1/V2 /info and /health/*, with unit tests.
  • Extended GetConnectedPlayers with searchString + 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
  • InvalidateGlobalNamespaceAsync has 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.

Comment thread terraform/web_app_v1.tf Outdated
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor
Superseded — A newer run has replaced this result.

🏗️ Terraform Plan

🌍 Environment: dev

❌ Validate — Failed

❌ Validation failed. Check the workflow logs for details.

- 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>
Copilot AI review requested due to automatic review settings August 2, 2026 09:36
@frasermolyneux

Copy link
Copy Markdown
Owner Author

Addressed all 4 Copilot review threads plus the failing terraform-plan-dev job in adefa00:

  • terraform/storage_account.tf — switched both azurerm_storage_table resources to storage_account_id (azurerm v5 schema; storage_account_name was removed and was blocking terraform-plan-dev).
  • terraform/web_app_v1.tf — wrapped the shared cache endpoint app settings in coalesce(..., "") so a null upstream output never produces a null map value; MxCaching__Backend still branches on the raw value.
  • RepositoryCacheInvalidator.cs — normalizes ns via NamespaceSchemaValidationRegistry.NormalizeNamespace before tag computation in both invalidator methods, matching the read-decorator's normalization. Alias-based writes now evict canonical-tagged cache entries; symmetric regression test added in CachingReadServicesTests.
  • CachingConfigurationReadService.cs — extended the short-circuit guard on both read paths from IsNullOrWhiteSpace(ns) to also cover ns.Length > 128 as defence-in-depth for direct callers.
  • ConnectedPlayersController.cs — appended .ThenBy(cp => cp.ConnectedPlayerProfileId) before Skip/Take on the ordered query so pagination is deterministic when the primary sort key ties.

Validation:

  • dotnet build — clean (0 warnings / 0 errors)
  • dotnet test --filter "FullyQualifiedName!~IntegrationTests" — all suites green across net9.0 and net10.0 (V1 host: 447 passed / 10 skipped per TFM; total incl. client + testing suites all Passed)
  • dotnet format --verify-no-changes — clean
  • terraform fmt -check -recursive — clean
  • terraform validate — Success

All 4 review threads resolved.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/takeEntries or return pagination totals, but the real API now clamps (skip>=0, 1<=take<=500) and returns TotalCount/FilteredCount in the pagination envelope. This mismatch can cause consumer tests to pass against the fake but fail in production (e.g. takeEntries=0 returning 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 ns string. 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 uses LIKE with 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 apply EF.Functions.Like to 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 MxCaching section is absent, "the caching decorators remain functional", but mxCachingConfigured gates 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.

@sonarqubecloud

sonarqubecloud Bot commented Aug 2, 2026

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

🏗️ Terraform Plan

🌍 Environment: dev

✅ Validate — Passed

✅ Plan

Count
➕ Add 23
📋 Resource Details
Action Resource
➕ Create azurerm_api_management_api_version_set.api_version_set
➕ Create azurerm_api_management_product.api_product
➕ Create azurerm_api_management_product_policy.api_product_policy
➕ Create azurerm_linux_web_app.app_v1
➕ Create azurerm_linux_web_app.app_v2
➕ Create azurerm_monitor_activity_log_alert.rg_resource_health
➕ Create azurerm_mssql_database.database
➕ Create azurerm_portal_dashboard.app
➕ Create azurerm_portal_dashboard.staging_dashboard[0]
➕ Create azurerm_role_assignment.app-to-storage
➕ Create azurerm_role_assignment.app-to-storage-table
➕ Create azurerm_role_assignment.workflow-sp-to-backup-storage
➕ Create azurerm_storage_account.sql_backup_storage
➕ Create azurerm_storage_account.table_storage
➕ Create azurerm_storage_account.web_api_storage
➕ Create azurerm_storage_container.demos_container
➕ Create azurerm_storage_container.gametracker_container
➕ Create azurerm_storage_container.map_images_container
➕ Create azurerm_storage_container.sql_backups_container
➕ Create azurerm_storage_management_policy.sql_backup_lifecycle
➕ Create azurerm_storage_table.live_players
➕ Create azurerm_storage_table.live_status
➕ Create random_id.environment_id

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants