Skip to content

client: consume MX.Api.Client 2.3.77 SharedCacheConfiguration, remove reflection - #849

Merged
frasermolyneux merged 3 commits into
mainfrom
agents/root-cause-fix-repo-client-4221
Aug 3, 2026
Merged

client: consume MX.Api.Client 2.3.77 SharedCacheConfiguration, remove reflection#849
frasermolyneux merged 3 commits into
mainfrom
agents/root-cause-fix-repo-client-4221

Conversation

@frasermolyneux

@frasermolyneux frasermolyneux commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Summary

Fix the startup ArgumentException seen by consumers that enabled .WithCaching(c => c.UseLibraryDefaults()) on AddRepositoryApiClient (regression exposed in 4.2.21 on MX.Api.Client 2.3.76). This revision replaces the earlier private-reflection cache-scoping fix with MX.Api.Client 2.3.77's new first-class SharedCacheConfiguration / WithSharedCaching API. All reflection into MX internals is gone.

Closes #4221

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Dependency bump (MX.Api.Client 2.3.76 → 2.3.77 across all consuming projects for solution-wide version consistency)

Root cause

ApiClientOptionsBuilder<TOptions,TBuilder>.WithCaching(Action<CacheBuilder>) (2.3.76) evaluates every cache expression the consumer supplies against the currently-scoped typed client's interface and throws ArgumentException: The expression must invoke a method declared by ... IAdminActionsApi or inherited interfaces when the expression's declaring interface isn't assignable to the scoped client. AddRepositoryApiClient invokes the same consumer configureOptions delegate for every one of ~34 AddTypedApiClient registrations, so a single consumer .WithCaching(...) call containing operations for multiple sub-APIs unavoidably crashes on the first mismatched sibling.

Fix

MX.Api.Client 2.3.77 shipped a purpose-built API for the unified-client scenario. Repository consumes it:

  1. RepositoryApiOptionsBuilder.WithCaching(Action<CacheBuilder>) now only captures the consumer delegate onto an internal CapturedCacheConfigure property. It never throws and never applies operations itself. All private reflection into MX (_configuredClientType, SetCachePolicyOperation) is removed.
  2. ServiceCollectionExtensions.AddRepositoryApiClient runs configureOptions once against a throwaway probe builder to extract CapturedCacheConfigure, then constructs one SharedCacheConfiguration for the composition. A perClient wrapper invokes configureOptions(builder) and then builder.WithSharedCaching(sharedCache). WithSharedCaching applies only the operations whose declaring interface is assignable from the currently-scoped typed client and skips siblings instead of throwing. After all typed clients are registered, sharedCache?.ValidateAllOperationsMatched() is invoked exactly once — this surfaces genuine consumer typos (a cache expression targeting an interface that is not a registered Repository sub-API) as a clear InvalidOperationException.
  3. Existing AddDefaultCachePolicies<TClient> calls and the safe policy matrix are unchanged.

The consumer-facing surface is unchanged: .WithCaching(c => c.UseLibraryDefaults()....) continues to work exactly as documented. SharedCacheConfiguration is a Repository-side implementation detail.

Consumer impact

  • Public API unchanged. Consumers still call .WithCaching(c => c.UseLibraryDefaults()....) — no code change required in downstream consumers.
  • Consumers on the fixed package will additionally get a clear InvalidOperationException at startup if their cache expression targets an interface that is not a registered Repository sub-API (previously such typos would be silently mapped to the wrong client). This is intentional and matches the shipping MX.Api.Client 2.3.77 contract.
  • MX.Api.Client and MX.Api.Abstractions bumped 2.3.76 → 2.3.77 across V1/V2 Client, Abstractions V1/V2, Api V1/V2, and Client.Testing csprojs. No functional caching change in V2 — the bump is version-consistency only.
  • Api.Client.Testing package: no in-memory fake or DTO factory changed; version bump is transitive-consistency only.

Validation evidence

Build (dotnet build src/XtremeIdiots.Portal.Repository.sln):

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

Time Elapsed 00:00:16.15

Tests (dotnet test src/XtremeIdiots.Portal.Repository.sln --filter "FullyQualifiedName!~IntegrationTests") — all 12 non-integration assemblies pass on net9.0 and net10.0:

Passed!  - Failed: 0, Passed:  86, Skipped:  0, Total:  86  XtremeIdiots.Portal.Repository.Api.Client.Tests.V1.dll (net10.0)
Passed!  - Failed: 0, Passed:  86, Skipped:  0, Total:  86  XtremeIdiots.Portal.Repository.Api.Client.Tests.V1.dll (net9.0)
Passed!  - Failed: 0, Passed:   9, Skipped:  0, Total:   9  XtremeIdiots.Portal.Repository.Api.Tests.V2.dll (net9.0)
Passed!  - Failed: 0, Passed:  44, Skipped:  0, Total:  44  XtremeIdiots.Portal.Settings.Contracts.V1.Tests.dll (net10.0)
Passed!  - Failed: 0, Passed:  44, Skipped:  0, Total:  44  XtremeIdiots.Portal.Settings.Contracts.V1.Tests.dll (net9.0)
Passed!  - Failed: 0, Passed:  72, Skipped:  0, Total:  72  XtremeIdiots.Portal.Repository.Api.Client.Testing.Tests.dll (net10.0)
Passed!  - Failed: 0, Passed:   9, Skipped:  0, Total:   9  XtremeIdiots.Portal.Repository.Api.Tests.V2.dll (net10.0)
Passed!  - Failed: 0, Passed:  14, Skipped:  0, Total:  14  XtremeIdiots.Portal.Repository.Api.Client.Tests.V2.dll (net9.0)
Passed!  - Failed: 0, Passed:  14, Skipped:  0, Total:  14  XtremeIdiots.Portal.Repository.Api.Client.Tests.V2.dll (net10.0)
Passed!  - Failed: 0, Passed:  72, Skipped:  0, Total:  72  XtremeIdiots.Portal.Repository.Api.Client.Testing.Tests.dll (net9.0)
Passed!  - Failed: 0, Passed: 462, Skipped: 10, Total: 472  XtremeIdiots.Portal.Repository.Api.Tests.V1.dll (net9.0)
Passed!  - Failed: 0, Passed: 462, Skipped: 10, Total: 472  XtremeIdiots.Portal.Repository.Api.Tests.V1.dll (net10.0)

The Repository client test suite covers:

  • All 34 sub-API interfaces resolve after AddRepositoryApiClient + BuildServiceProvider() (theory over AllSubApiInterfaces).
  • A consumer WithCaching override targeting one sub-API lands only on that typed client's registered options (no cross-client bleed).
  • New: a cache expression targeting an interface that is not a registered Repository sub-API (INotARegisteredRepositoryApi) triggers ValidateAllOperationsMatched()InvalidOperationException at startup.

Format (dotnet format src/XtremeIdiots.Portal.Repository.sln --verify-no-changes): clean (no changes required).

Code-review sub-agent: no significant issues. Confirmed (a) probe pattern is side-effect free because the overridden WithCaching only stores the delegate, (b) ValidateAllOperationsMatched() runs after every typed client has been visited because AddTypedApiClient invokes the configure delegate eagerly at registration time, (c) WithSharedCaching composes correctly with the pre-existing AddDefaultCachePolicies<T> singleton lookup.

Risk and rollout

  • Blast radius: bug fix in the V1 Repository client NuGet package + solution-wide MX.Api.Client version bump 2.3.76 → 2.3.77. No API host runtime behavior change beyond consuming the newer MX library.
  • Auto-deploy: standard NBGV / release workflow. Expected version 4.2.22 (patch bump from git height — version.json untouched).
  • Manual steps post-merge: none for API hosts. Consumers of XtremeIdiots.Portal.Repository.Api.Client.V1 should upgrade to the new package to pick up the fix.
  • Rollback: revert this commit. Consumers can pin to 4.2.20 or earlier if they need to roll back to the pre-regression behavior.

Agent attestation

  • I read the required files listed in AGENTS.md before starting work.
  • I ran the pre-PR check commands locally and pasted their real output above.
  • No secrets, connection strings, or credentials were introduced.
  • No changes to version.json, Directory.Build.props, or .github/workflows/.
  • No hand edits to DataLib (no schema change; regeneration not required).
  • APIM API definitions are not managed by Terraform in this change.
  • All published contract surfaces (.WithCaching(Action<CacheBuilder>), RepositoryApiOptionsBuilder) remain source-compatible.
  • code-review sub-agent run; no High/Medium findings.
  • Zero reflection into MX.Api.Client internals remains in src/XtremeIdiots.Portal.Repository.Api.Client.V1/ (verified by grep).

…ent expression leaks

The V1 Repository client registers ~30 typed sub-API clients via a single consumer configureOptions delegate. When consumers invoked .WithCaching(c => c.UseLibraryDefaults()) plus per-sub-API cache expressions (e.g. c => c.GetGameServer(...)), MX.Api.Client 2.3.76 scoped the CacheBuilder to each typed client's _configuredClientType, causing ArgumentException at startup for every non-matching typed client (e.g. IAdminActionsApi rejecting an IGameServersApi expression).

Fix: override WithCaching(Action<CacheBuilder>) on RepositoryApiOptionsBuilder. The consumer delegate is captured once, executed against an unscoped shadow builder per typed client, and only the CachePolicyOperations whose declaring types are assignable from the current typed client are replayed onto the real builder. UseLibraryDefaults() is re-applied through the public API. Access to the internal SetCachePolicyOperation and _configuredClientType members is via pinned reflection with clear fail-fast messages if MX.Api.Client contract changes.

No change to the shipped public API, safe policy matrix, or AddDefaultCachePolicies registrations. Not an MX.Api.Client bug; per-client scoping is intentional.

Tests: RepositoryClientRegistrationTests covers resolution of the unified client, all 34 registered sub-APIs (with and without caching), per-typed-client override placement, and confirms no cross-client bleed for both single and multi-sub-API consumer overrides.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 2, 2026 21:25
@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
  • ⚠️ 9 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.77NullUnknown License

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

PackageVersionLicenseIssue Type
MX.Api.Abstractions2.3.77NullUnknown License

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

PackageVersionLicenseIssue Type
MX.Api.Abstractions2.3.77NullUnknown License

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

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

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

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

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

PackageVersionLicenseIssue Type
MX.Api.Abstractions2.3.77NullUnknown License

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

PackageVersionLicenseIssue Type
MX.Api.Abstractions2.3.77NullUnknown License

OpenSSF Scorecard

PackageVersionScoreDetails
nuget/MX.Api.Abstractions 2.3.77 UnknownUnknown
nuget/MX.Api.Abstractions 2.3.77 UnknownUnknown
nuget/MX.Api.Abstractions 2.3.77 UnknownUnknown
nuget/MX.Api.Abstractions 2.3.77 UnknownUnknown
nuget/MX.Api.Client 2.3.77 UnknownUnknown
nuget/MX.Api.Abstractions 2.3.77 UnknownUnknown
nuget/MX.Api.Client 2.3.77 UnknownUnknown
nuget/MX.Api.Abstractions 2.3.77 UnknownUnknown
nuget/MX.Api.Abstractions 2.3.77 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.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

Fixes a DI startup ArgumentException in the V1 Repository API client when consumers combine .WithCaching(c => c.UseLibraryDefaults()) with per-sub-API cache expressions, by scoping cache policy operations to the currently-configured typed sub-API during registration.

Changes:

  • Added a scope-aware WithCaching(Action<CacheBuilder>) implementation on RepositoryApiOptionsBuilder that captures cache intent once and replays only compatible operations per typed sub-API.
  • Added new DI resolution + scoping regression tests covering the crash repro and cross-client isolation guarantees.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/XtremeIdiots.Portal.Repository.Api.Client.V1/RepositoryApiOptionsBuilder.cs Introduces scope-isolated caching configuration to prevent cross-sub-API cache expression failures during DI registration.
src/XtremeIdiots.Portal.Repository.Api.Client.Tests.V1/RepositoryClientRegistrationTests.cs Adds regression tests to reproduce the prior startup crash and verify per-typed-client cache policy isolation.

@github-actions

github-actions Bot commented Aug 2, 2026

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

🏗️ Terraform Plan

🌍 Environment: dev

✅ Validate — Passed

✅ Plan

No changes. Your infrastructure matches the configuration.

Address PR review feedback: consolidate the accidental duplicate using block in RepositoryClientRegistrationTests and drop the unused System.Collections.Generic / System.Linq / System.Reflection usings.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 2, 2026 21:42

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

Suppressed comments (1)

src/XtremeIdiots.Portal.Repository.Api.Client.V1/RepositoryApiOptionsBuilder.cs:71

  • WithCaching(Action<CacheBuilder>) never sets Options.EnableCaching to true. If a consumer has previously called WithCaching(false) (or the option is otherwise false), the cache policy operations you replay here will be configured but never used because caching remains disabled. Consider explicitly enabling caching when this overload is used to match the expected semantics of “WithCaching(…)”.
        public new RepositoryApiOptionsBuilder WithCaching(Action<CacheBuilder> configure)
        {
            ArgumentNullException.ThrowIfNull(configure);

@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 — Passed

✅ Plan

No changes. Your infrastructure matches the configuration.

…rop reflection

Replaces the private-reflection cache-scoping hack introduced in the prior commit with MX.Api.Client 2.3.77's new first-class API surface:

- RepositoryApiOptionsBuilder.WithCaching(Action<CacheBuilder>) now only CAPTURES the consumer delegate onto an internal CapturedCacheConfigure property; no application, no reflection, no throws.

- ServiceCollectionExtensions.AddRepositoryApiClient runs configureOptions once on a throwaway probe builder to extract the captured cache delegate, constructs one SharedCacheConfiguration per composition, and wraps every AddTypedApiClient call with a perClient delegate that applies WithSharedCaching(sharedCache) after configureOptions. After all typed clients are registered, ValidateAllOperationsMatched() is invoked exactly once to surface consumer typos as InvalidOperationException.

- Consumer-facing API unchanged: .WithCaching(c => c.UseLibraryDefaults()...) on RepositoryApiOptionsBuilder continues to work exactly as before.

- Bumped MX.Api.Client + MX.Api.Abstractions from 2.3.76 to 2.3.77 across V1/V2 Client, Abstractions V1/V2, Api V1/V2, and Client.Testing csprojs for solution-wide version consistency.

- Added guard test AddRepositoryApiClient_ConsumerOverrideTargetingUnregisteredInterface_ThrowsValidationError proving orphaned cache expressions surface ValidateAllOperationsMatched().

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 3, 2026 06:54
@frasermolyneux frasermolyneux changed the title client: scope WithCaching per typed sub-API to fix startup ArgumentException client: consume MX.Api.Client 2.3.77 SharedCacheConfiguration, remove reflection Aug 3, 2026

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

Suppressed comments (3)

src/XtremeIdiots.Portal.Repository.Api.Client.V1/RepositoryApiOptionsBuilder.cs:56

  • The XML doc cref targets Abstractions.Interfaces.V1.*, but the interfaces live in XtremeIdiots.Portal.Repository.Abstractions.Interfaces.V1. As written, these <see cref> links won’t resolve in generated docs / IDE tooltips.
        /// <see cref="ServiceCollectionExtensions.AddRepositoryApiClient"/> re-invokes the consumer's configuration
        /// delegate once per typed sub-API (<see cref="Abstractions.Interfaces.V1.IAdminActionsApi"/>,
        /// <see cref="Abstractions.Interfaces.V1.IGameServersApi"/>, etc.). The base

src/XtremeIdiots.Portal.Repository.Api.Client.Tests.V1/RepositoryClientRegistrationTests.cs:116

  • GetRequiredService<RepositoryApiClientOptions>() returns the last registered RepositoryApiClientOptions when multiple registrations exist, so the variable name gameServersOptions is misleading here and may confuse future readers of the test.
            var gameServersOptions = provider.GetRequiredService<RepositoryApiClientOptions>();
            Assert.True(gameServersOptions.UseLibraryCacheDefaults);

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

  • PR description/risk section says the blast radius is the V1 client only and that the V2 client is untouched, but this PR also bumps MX.Api.* dependencies in the V2 client package. Either update the PR narrative/risk assessment to include V2, or revert/split these version bumps if they’re not required for the V1 fix.
    <PackageReference Include="MX.Api.Client" Version="2.3.77" />
    <PackageReference Include="MX.Api.Abstractions" Version="2.3.77" />

@sonarqubecloud

sonarqubecloud Bot commented Aug 3, 2026

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Aug 3, 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