Skip to content

feat: roll Repository client to 4.2.22 and re-enable consumer L1 caching - #54

Merged
frasermolyneux merged 2 commits into
mainfrom
agents/roll-portal-server-events-onto-the-fixed-portal
Aug 3, 2026
Merged

feat: roll Repository client to 4.2.22 and re-enable consumer L1 caching#54
frasermolyneux merged 2 commits into
mainfrom
agents/roll-portal-server-events-onto-the-fixed-portal

Conversation

@frasermolyneux

Copy link
Copy Markdown
Owner

Summary

Roll the portal-server-events processor onto Portal Repository client 4.2.22 (MX.Api.Client 2.3.77) and re-enable the consumer-side L1 caching that hotfix PR #53 removed, now that the root-cause bug is fixed upstream. Adds a startup-composition regression test that would have caught the original crash.

Closes N/A (agent-driven rollout task; no tracking issue).

Type of change

  • New feature / enhancement (re-enables the caching capability originally rolled out in PR Implement caching rollout for portal-repo 4.2.21 #52)
  • Dependency / SDK bump (Repository client 4.2.21 → 4.2.22, MX.Api.Client 2.3.76 → 2.3.77)
  • Test coverage improvement (host-boot smoke test now mirrors production registration end-to-end)

Details

  • XtremeIdiots.Portal.Repository.Api.Client.V1: 4.2.214.2.22
  • MX.Api.Client: 2.3.762.3.77
  • Program.cs: re-applies .WithCaching(c => c.UseLibraryDefaults()) on AddRepositoryApiClient(...) and adds the new mandatory .WithCachePartition("portal-server-events") (2.3.77 requires an explicit non-secret partition when caching is enabled).
  • RepositoryApiClientRegistrationTests: the DI smoke test now mirrors production exactly (.WithBaseUrl.WithEntraIdAuthentication.WithCachePartition.WithCaching(UseLibraryDefaults)). It resolves IRepositoryApiClient and the 12 typed .V1 sub-APIs the processor uses — the exact composition path that threw on 4.2.21. Runs under the default CI filter (Startup namespace, no IntegrationTests marker).

Cached surface — safety rationale

Library defaults in 4.2.22 apply cache policies only to GET-shaped read operations on stable reference data. The processor's Repository call surface is:

  • Reads (potentially cached): GetConfiguration, GetConfigurations, GetProtectedNames, GetAdminActions, GetPlayer, GetPlayerByGameType, HeadPlayerByGameType, GetMap
  • Writes (never cached): CreatePlayer, UpdatePlayerIpAddress, CreateAdminAction, UpdateAdminAction, CreateChatMessage, CreateGameServerEvent, CreateGameServerStats, CreateRecentPlayers, EnsureAutomatedAction, RecordPlayerSession, SetGameServerLiveStatus, SetVpnDetectedTag, ClaimForumTopicPublication, CompleteForumTopicPublication, ConsumeConnectedPlayerActivationCode, UpsertMapVote

Verified no read-after-write on the same entity in a single Service Bus message handler that could return stale cached data:

  • PlayerConnectedProcessor / BanAppliedProcessor / BanFileChangedProcessor: HeadPlayerByGameType (404) → CreatePlayerGetPlayerByGameType. HEAD returning 404 is a not-found response, not a cacheable success payload, so the follow-up GET is a fresh call.
  • ChatModerationPipeline, ProtectedNameService, VPN/command providers: configuration / protected-names / admin-action reads all occur before their respective writes in the same handler.
  • App-owned IMemoryCache player-context is unaffected and still explicitly invalidated after writes (e.g. PlayerIpResolvedProcessor.InvalidatePlayerCache).

.WithCachePartition("portal-server-events") is a stable, non-secret literal that isolates this consumer's cache keys inside its isolated Functions worker process. No collision or leakage risk.

Why the 4.2.21 crash is fixed

Repository 4.2.22 consumes MX.Api.Client 2.3.77's reflection-free SharedCacheConfiguration, which scopes each cache policy to its matching typed sub-API instead of fanning one delegate expression across all ~34 typed sub-API registrations. The exact .WithCaching(c => c.UseLibraryDefaults()) expression from PR #52 is now safe to compose.

Validation evidence

> dotnet build src/XtremeIdiots.Portal.Server.Events.slnx --nologo
  XtremeIdiots.Portal.Server.Events.Processor.App -> ...\XtremeIdiots.Portal.Server.Events.Processor.App.dll
  XtremeIdiots.Portal.Server.Events.Processor.App.Tests -> ...\XtremeIdiots.Portal.Server.Events.Processor.App.Tests.dll

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:02.45
> dotnet test src/XtremeIdiots.Portal.Server.Events.slnx --nologo --no-build
Passed!  - Failed:     0, Passed:   380, Skipped:     0, Total:   380, Duration: 427 ms - XtremeIdiots.Portal.Server.Events.Processor.App.Tests.dll (net9.0)
> dotnet format src/XtremeIdiots.Portal.Server.Events.slnx --verify-no-changes
(no output)
FORMAT EXIT: 0

Local Windows note: the initial dotnet build failed with an MSB3030 file-copy error under Microsoft.NET.Sdk.Functions WorkerExtensions caused by the local worktree path exceeding Windows MAX_PATH (269 chars > 260). Reproducible on unmodified origin/main, so it is not a regression from this change. Worked around locally with subst P: <worktree>; CI does not hit this limit.

The code-review sub-agent was run against the full diff — no High or Medium findings.

Risk and rollout

  • Blast radius: portal-server-events processor Function App only. Terraform / Service Bus contracts / Abstractions NuGet unchanged.
  • Auto-deploy? Follows the standard devprd pipeline in .github/workflows/. Recommend soaking on dev to confirm no cache-hit anomalies before promoting.
  • Manual steps post-merge: None.
  • Rollback: Revert this commit — restores the current 4.2.21 registration without .WithCaching. Same shape as PR fix: remove consumer-side Repository client WithCaching to prevent startup crash #53. Repository server-side caching remains active regardless.

Consumer impact

None. Abstractions package, Service Bus queue contracts and DTOs are unchanged. No published contract affected.

Agent attestation

  • Ran dotnet build, dotnet test (default filter), and dotnet format --verify-no-changes — all clean.
  • Verified the smoke test resolves IRepositoryApiClient plus every typed .V1 sub-API the processor calls.
  • Verified the re-enabled cached read surface is disjoint from the write surface, with no in-handler read-after-write on the same entity.
  • Ran the code-review sub-agent; no High/Medium findings.
  • Did not modify version.json, .github/workflows/, or Directory.*.props.
  • No client secrets introduced; .WithCachePartition("portal-server-events") is a stable non-secret literal.

Bumps XtremeIdiots.Portal.Repository.Api.Client.V1 from 4.2.21 to 4.2.22 and
MX.Api.Client from 2.3.76 to 2.3.77. 4.2.22 consumes MX.Api 2.3.77's
reflection-free SharedCacheConfiguration which scopes each cache policy to its
matching typed sub-API, fixing the cross-sub-API expression fanning bug that
crashed startup with 'The expression must invoke a method declared by
...IAdminActionsApi ...' on 4.2.21 (hotfix removed via PR #53).

Re-applies '.WithCaching(c => c.UseLibraryDefaults())' on AddRepositoryApiClient
that was removed by PR #53. Adds the new mandatory '.WithCachePartition("portal-server-events")'
introduced in MX.Api 2.3.77 to isolate this consumer's cache keys.

Cache safety verified: the processor's cached surface (library defaults cover
GET-shaped reads on GetConfiguration/GetConfigurations, GetProtectedNames,
GetAdminActions and Get/HeadPlayer variants) is disjoint from its write surface
(Create*/Update*/Set*/Upsert*/Record*/Ensure*/Claim*/Complete*/Consume*). No
read-after-write on the same entity in a single Service Bus message handler:
HeadPlayerByGameType 404 -> CreatePlayer -> GetPlayerByGameType is a fresh GET
(HEAD 404 is not a cached success payload). Configuration/ProtectedNames/AdminAction
reads all happen before their respective writes in the same handler. App-owned
IMemoryCache player-context is unaffected and still explicitly invalidated after
writes.

Updates Startup/RepositoryApiClientRegistrationTests to mirror the production
registration exactly (both '.WithCachePartition' and '.WithCaching' present) so
the smoke test resolves IRepositoryApiClient plus every typed sub-API .V1 the
processor uses under the same DI composition path that failed on 4.2.21. Runs
under the default CI test filter.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 3, 2026 08:13
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

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
  • ⚠️ 2 package(s) with unknown licenses.
See the Details below.

License Issues

src/XtremeIdiots.Portal.Server.Events.Processor.App/XtremeIdiots.Portal.Server.Events.Processor.App.csproj

PackageVersionLicenseIssue Type
MX.Api.Client2.3.77NullUnknown License
XtremeIdiots.Portal.Repository.Api.Client.V14.2.22NullUnknown License

OpenSSF Scorecard

PackageVersionScoreDetails
nuget/MX.Api.Client 2.3.77 UnknownUnknown
nuget/XtremeIdiots.Portal.Repository.Api.Client.V1 4.2.22 UnknownUnknown

Scanned Files

  • src/XtremeIdiots.Portal.Server.Events.Processor.App/XtremeIdiots.Portal.Server.Events.Processor.App.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 pull request rolls the Processor Function App forward to Portal Repository client 4.2.22 / MX.Api.Client 2.3.77 and re-enables consumer-side L1 caching for Repository reads, restoring the optimization that was temporarily removed after the 4.2.21 startup crash.

Changes:

  • Bump XtremeIdiots.Portal.Repository.Api.Client.V1 to 4.2.22 and MX.Api.Client to 2.3.77.
  • Re-enable Repository client caching in the Processor App DI registration, adding the required explicit cache partition.
  • Update the startup DI smoke test to mirror the production Repository client registration including caching and partitioning.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
src/XtremeIdiots.Portal.Server.Events.Processor.App/XtremeIdiots.Portal.Server.Events.Processor.App.csproj Dependency bumps for Repository client and MX.Api.Client to the versions that support safe caching composition.
src/XtremeIdiots.Portal.Server.Events.Processor.App/Program.cs Re-introduces .WithCaching(UseLibraryDefaults) and adds mandatory .WithCachePartition("portal-server-events") to the Repository client registration.
src/XtremeIdiots.Portal.Server.Events.Processor.App.Tests/Startup/RepositoryApiClientRegistrationTests.cs Updates the DI registration smoke test to include cache partition + caching so the full production composition path is exercised.

@github-actions

github-actions Bot commented Aug 3, 2026

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

🏗️ Terraform Plan

🌍 Environment: dev

✅ Validate — Passed

✅ Plan

Count
➕ Add 29
📋 Resource Details
Action Resource
➕ Create azurerm_api_management_api.cod4x_ingest[0]
➕ Create azurerm_api_management_api_operation.cod4x_ingest_active_bans_get[0]
➕ Create azurerm_api_management_api_operation.cod4x_ingest_events_post["ban_file_changed"]
➕ Create azurerm_api_management_api_operation.cod4x_ingest_events_post["chat_message"]
➕ Create azurerm_api_management_api_operation.cod4x_ingest_events_post["map_change"]
➕ Create azurerm_api_management_api_operation.cod4x_ingest_events_post["player_connected"]
➕ Create azurerm_api_management_api_operation.cod4x_ingest_events_post["player_disconnected"]
➕ Create azurerm_api_management_api_operation.cod4x_ingest_events_post["player_ip_resolved"]
➕ Create azurerm_api_management_api_operation.cod4x_ingest_events_post["server_connected"]
➕ Create azurerm_api_management_api_operation.cod4x_ingest_events_post["server_status"]
➕ Create azurerm_api_management_api_operation.cod4x_ingest_vpn_protection_evaluate_post[0]
➕ Create azurerm_api_management_api_operation_policy.cod4x_ingest_active_bans_get[0]
➕ Create azurerm_api_management_api_operation_policy.cod4x_ingest_events_post["ban_file_changed"]
➕ Create azurerm_api_management_api_operation_policy.cod4x_ingest_events_post["chat_message"]
➕ Create azurerm_api_management_api_operation_policy.cod4x_ingest_events_post["map_change"]
➕ Create azurerm_api_management_api_operation_policy.cod4x_ingest_events_post["player_connected"]
➕ Create azurerm_api_management_api_operation_policy.cod4x_ingest_events_post["player_disconnected"]
➕ Create azurerm_api_management_api_operation_policy.cod4x_ingest_events_post["player_ip_resolved"]
➕ Create azurerm_api_management_api_operation_policy.cod4x_ingest_events_post["server_connected"]
➕ Create azurerm_api_management_api_operation_policy.cod4x_ingest_events_post["server_status"]
➕ Create azurerm_api_management_api_operation_policy.cod4x_ingest_vpn_protection_evaluate_post[0]
➕ Create azurerm_api_management_product_api.cod4x_ingest[0]
➕ Create azurerm_linux_function_app.function_app
➕ Create azurerm_monitor_activity_log_alert.rg_resource_health
➕ Create azurerm_monitor_metric_alert.dead_letter_messages
➕ Create azurerm_monitor_metric_alert.queue_backlog
➕ Create azurerm_role_assignment.app_to_storage
➕ Create azurerm_storage_account.function_app_storage
➕ Create random_id.environment_id

PR #54 review feedback: the boot smoke test previously duplicated the

production Repository client option chain inline, so Program.cs and the test

could drift (especially the cache partition literal and the option order).

Introduce internal AddPortalServerEventsRepositoryApiClient extension in the

App project as the single source of truth for BaseUrl / EntraId audience /

cache partition / L1 caching policy. Program.cs and

RepositoryApiClientRegistrationTests both call it, so the test is now a real

regression guard: any future change to the production chain is exercised by

the DI resolution asserts automatically.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 3, 2026 08:22
@frasermolyneux

Copy link
Copy Markdown
Owner Author

Addressed in c067139.

Extracted the Repository client registration into a new internal AddPortalServerEventsRepositoryApiClient extension in the App project (RepositoryApiClientRegistration.cs). The cache partition literal ("portal-server-events") and the full option chain (WithBaseUrl / WithEntraIdAuthentication / WithCachePartition / WithCaching(UseLibraryDefaults)) now live in exactly one place. Program.cs and RepositoryApiClientRegistrationTests.BuildProductionServiceProvider both call it, so any future tweak to the production registration is automatically exercised by the DI resolution asserts — no more drift risk.

Validation refreshed:

  • dotnet build: 0 Warning(s), 0 Error(s)
  • dotnet test (default filter): Passed! - Failed: 0, Passed: 380, Skipped: 0, Total: 380
  • dotnet format --verify-no-changes: exit 0

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 4 out of 4 changed files in this pull request and generated no new comments.

@sonarqubecloud

sonarqubecloud Bot commented Aug 3, 2026

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

🏗️ Terraform Plan

🌍 Environment: dev

✅ Validate — Passed

✅ Plan

Count
➕ Add 29
📋 Resource Details
Action Resource
➕ Create azurerm_api_management_api.cod4x_ingest[0]
➕ Create azurerm_api_management_api_operation.cod4x_ingest_active_bans_get[0]
➕ Create azurerm_api_management_api_operation.cod4x_ingest_events_post["ban_file_changed"]
➕ Create azurerm_api_management_api_operation.cod4x_ingest_events_post["chat_message"]
➕ Create azurerm_api_management_api_operation.cod4x_ingest_events_post["map_change"]
➕ Create azurerm_api_management_api_operation.cod4x_ingest_events_post["player_connected"]
➕ Create azurerm_api_management_api_operation.cod4x_ingest_events_post["player_disconnected"]
➕ Create azurerm_api_management_api_operation.cod4x_ingest_events_post["player_ip_resolved"]
➕ Create azurerm_api_management_api_operation.cod4x_ingest_events_post["server_connected"]
➕ Create azurerm_api_management_api_operation.cod4x_ingest_events_post["server_status"]
➕ Create azurerm_api_management_api_operation.cod4x_ingest_vpn_protection_evaluate_post[0]
➕ Create azurerm_api_management_api_operation_policy.cod4x_ingest_active_bans_get[0]
➕ Create azurerm_api_management_api_operation_policy.cod4x_ingest_events_post["ban_file_changed"]
➕ Create azurerm_api_management_api_operation_policy.cod4x_ingest_events_post["chat_message"]
➕ Create azurerm_api_management_api_operation_policy.cod4x_ingest_events_post["map_change"]
➕ Create azurerm_api_management_api_operation_policy.cod4x_ingest_events_post["player_connected"]
➕ Create azurerm_api_management_api_operation_policy.cod4x_ingest_events_post["player_disconnected"]
➕ Create azurerm_api_management_api_operation_policy.cod4x_ingest_events_post["player_ip_resolved"]
➕ Create azurerm_api_management_api_operation_policy.cod4x_ingest_events_post["server_connected"]
➕ Create azurerm_api_management_api_operation_policy.cod4x_ingest_events_post["server_status"]
➕ Create azurerm_api_management_api_operation_policy.cod4x_ingest_vpn_protection_evaluate_post[0]
➕ Create azurerm_api_management_product_api.cod4x_ingest[0]
➕ Create azurerm_linux_function_app.function_app
➕ Create azurerm_monitor_activity_log_alert.rg_resource_health
➕ Create azurerm_monitor_metric_alert.dead_letter_messages
➕ Create azurerm_monitor_metric_alert.queue_backlog
➕ Create azurerm_role_assignment.app_to_storage
➕ Create azurerm_storage_account.function_app_storage
➕ Create random_id.environment_id

@frasermolyneux
frasermolyneux merged commit 69b154e into main Aug 3, 2026
19 checks passed
@frasermolyneux
frasermolyneux deleted the agents/roll-portal-server-events-onto-the-fixed-portal branch August 3, 2026 10:08
frasermolyneux added a commit that referenced this pull request Aug 3, 2026
…ng (#55)

Bump three client packages to their latest crash-safe releases and turn on
the library-default in-memory read-only caching now that MX.Api.Client 2.3.77
scopes each captured policy via SharedCacheConfiguration:

- MX.GeoLocation.Api.Client.V1 1.2.96 -> 1.2.98 (caching on)
- MX.InvisionCommunity.Api.Client 1.0.61 -> 1.0.63 (caching on)
- XtremeIdiots.Portal.Integrations.Servers.Api.Client.V1 4.1.9 -> 4.1.14
  (currency + crash-safety; no defaults to enable)
- XtremeIdiots.Portal.Integrations.Servers.Abstractions.V1 4.1.9 -> 4.1.14
  (transitive dependency lockstep)

Registration is extracted into GeoLocationApiClientRegistration,
InvisionApiClientRegistration, and ServersApiClientRegistration helpers
so Program.cs and the boot-composition tests share a single source of
truth for each client's option chain — matching the RepositoryApiClient
pattern re-enabled in #54.

Add xUnit startup-composition tests that build the real ServiceProvider
with caching enabled and resolve every typed sub-API the Processor App
touches at runtime (Geo v1 + v1.1 GeoLookup, Invision Core/Downloads/
Forums, Servers Cod2/Cod4/Cod5/CoD4x Rcon + Maps). A dedicated Geo test
also resolves IMxCache to guard the L1 auto-wiring. Any future regression
of the 2.3.76-era 'The expression must invoke a method declared by ...'
ArgumentException now fails these tests instead of the Functions host.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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