Add SharedCacheConfiguration for unified API client registrations - #248
Merged
Merged
Conversation
Introduces first-class, reflection-free support for the `unified API client` scenario where a single configureOptions delegate is applied across many typed sub-API clients that share one TOptions/TBuilder type. Previously such a delegate threw at DI-composition time whenever a cache expression referenced a sibling sub-API interface, aborting host startup. - Adds `SharedCacheConfiguration` (public sealed): captures cache intent once against an unscoped `CacheBuilder` and applies operations per typed client filtered by declaring-type assignability. Exposes `ValidateAllOperationsMatched` to surface real typos (operations whose declaring interface never matched a registered sub-API). - Adds `ApiClientOptionsBuilder<TOptions, TBuilder>.WithSharedCaching(...)`: the per-registration entry point that applies a shared configuration to the current typed client, skipping sibling sub-APIs instead of throwing. - Preserves the existing `WithCaching(...)` behavior for single-client registrations (still throws on real scope mismatch). - Adds a DI-composition regression test (runs under the default CI filter) that reproduces the production incident with the old API and proves the new API composes many sub-APIs cleanly with no cross-client bleed, plus a negative test for unregistered-interface typos. - Adds focused unit tests for capture, filter-and-apply, opt-in library defaults, and validation semantics. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
There was a problem hiding this comment.
Pull request overview
This PR adds a first-class, reflection-free way to share a single caching configuration across multiple typed sub-API client registrations, preventing DI-time failures when the shared delegate includes cache expressions for sibling sub-APIs. It introduces SharedCacheConfiguration plus a new WithSharedCaching(...) builder method, and locks the scenario with DI-composition regression tests.
Changes:
- Added
SharedCacheConfigurationto capture cache operations once and apply only operations that match the current typed client during registration. - Added
ApiClientOptionsBuilder<,>.WithSharedCaching(SharedCacheConfiguration)to apply shared caching safely per sub-API registration. - Added focused unit tests and DI-composition regression tests mirroring the unified-client registration pattern.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/MX.Api.Client/Configuration/SharedCacheConfiguration.cs | New stateful shared-cache capture/apply + validation surface for unified typed-client registrations. |
| src/MX.Api.Client/Configuration/ApiClientOptionsBuilder.cs | Adds WithSharedCaching(...) to apply shared cache operations filtered by configured client type. |
| src/MX.Api.Client.Tests/TestClients/UnifiedSubApis.cs | Adds fake sub-API interfaces/implementations for unified registration/caching tests. |
| src/MX.Api.Client.Tests/SharedCacheConfigurationTests.cs | Unit tests for capture, filtering, defaults propagation, and validation semantics. |
| src/MX.Api.Client.Tests/Extensions/UnifiedApiClientRegistrationTests.cs | DI-composition regression tests reproducing the incident and validating the new shared-caching path. |
Suppressed comments (1)
src/MX.Api.Client/Configuration/SharedCacheConfiguration.cs:116
- The error text says to verify the declaring interface is registered as a typed client, but the matching logic allows operations to match on inherited interfaces (DeclaringType.IsAssignableFrom(configuredClientType)). The message should reflect that to avoid sending consumers down the wrong path.
throw new InvalidOperationException(
"The following shared cache operations did not match any typed API client registered via WithSharedCaching. " +
"Verify that each declaring interface is registered as a typed API client and that the shared configuration " +
"is passed to every registration:" + Environment.NewLine + summary);
- Document stateful/single-use nature and thread-safety expectations in remarks. - Include full method signature (return type + parameter types) in the unmatched-operations summary so overloaded methods are unambiguous. - Clarify ValidateAllOperationsMatched exception docs and message to reflect the actual matching rule (declaring interface assignable from a registered typed client), not strict registration of the declaring interface. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
This was referenced Aug 3, 2026
This was referenced Aug 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Summary
Add a first-class, reflection-free
SharedCacheConfigurationcapability so a single cache-configuration delegate can be applied across many typed sub-API registrations without throwing for sibling sub-APIs. Locks the fix with a DI-composition regression test that would have caught the production incident.Closes the production outage where a downstream unified client (XtremeIdiots Portal Repository client) crashed Functions hosts at startup with
ArgumentException: The expression must invoke a method declared by ...IAdminActionsApi ...when the sharedconfigureOptionsdelegate contained cache expressions targeting multiple sub-APIs.Type of change
Root cause (recap)
ApiClientOptionsBuilder<>.WithCaching(Action<CacheBuilder>)constructs aCacheBuilderscoped to the current typed client type. When the same delegate is replayed across N typed clients (one call per sub-API), an expression declared onIGameServersApithrows the moment the delegate runs while the builder is scoped to a different sub-API (e.g.IAdminActionsApi). The throw happens inside DI registration, before the host is built.Fix approach (Option A: capture-once, apply-filtered): keep the existing single-client
WithCaching(...)scope safety unchanged, and add a new siblingWithSharedCaching(SharedCacheConfiguration)that captures the shared cache intent once and applies only the operations whose declaring interface is assignable from the current typed client. A finalValidateAllOperationsMatched()call surfaces genuine typos where an operation targets an interface that was never registered.Public API added
Nothing in the existing public surface changes.
WithCaching(...)continues to throw on real single-client scope mismatch, preserving typo safety.Consumer impact
Published contract change (MX.Api.Client NuGet): additive only — two new public members. No breaking changes.
Downstream migration (XtremeIdiots Portal Repository unified client — currently using private-reflection stopgap):
Before (unsupported, uses private reflection):
After (supported, no reflection):
The unified client's private reflection into
_configuredClientTypeandApiClientOptionsBase.SetCachePolicyOperationcan then be deleted.Validation evidence
Build
Tests (default CI filter — the DI-composition regression test is included here)
New tests (all under default filter):
MX.Api.Client.Tests.Extensions.UnifiedApiClientRegistrationTestsSharedWithCachingDelegate_AcrossMultipleTypedClients_StillThrows_ProvingRepro— proves the incident under the old API.SharedWithCachingDelegate_ViaSharedCacheConfiguration_ComposesAllTypedClientsCleanly— DI composition succeeds via the new API.SharedCacheConfiguration_OperationsLandOnMatchingClientOnly_NoCrossClientBleed— per-client landing correctness.ValidateAllOperationsMatched_UnregisteredSubApi_SurfacesClearError— real typo still surfaces.MX.Api.Client.Tests.SharedCacheConfigurationTests— 11 focused unit tests for capture, filter/apply, opt-in defaults, and validation semantics (including the regression test thatWithCachingsingle-client scope mismatch still throws).Format check
Code review
Ran the
code-reviewsub-agent on the change set. Findings:IsAssignableFrom) — correct; handles inheritance and skips siblings.ValidateAllOperationsMatched— no false-positive onUseLibraryDefaults()-only usage.ApplyTo— safe:AddTypedApiClientinvokesconfigureOptionssynchronously during DI composition on a single thread (seeApiClientExtensions.csline 146). Flagged as a note if the delegate ever migrates to a lazyIOptionsFactorypath in future.WithCachingscope safety — preserved.No High or Medium findings requiring resolution.
Risk and rollout
MX.Api.Client— two new public members. No existing behavior changes. Downstream consumers not usingSharedCacheConfigurationsee zero effect.SharedCacheConfigurationper the "Consumer impact" snippet above.Agent attestation
.github/copilot-instructions.md, personal working preferences, org catalog, stack-specific instructions).dotnet build src/MX.Api.Abstractions.sln— succeeded, 0 warnings, 0 errors.dotnet test src/MX.Api.Abstractions.sln --filter "FullyQualifiedName!~IntegrationTests"— all 480 tests passed across net9.0 + net10.0 TFMs.dotnet format src/MX.Api.Abstractions.sln --verify-no-changes— clean.code-reviewsub-agent — no High/Medium findings requiring resolution..github/workflows/,version.json,Directory.*.props, or cross-repo contracts.